Compare commits
479
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
528700ceea | ||
|
|
32f4215461 | ||
|
|
23343bb7f0 | ||
|
|
c8b2dab0ca | ||
|
|
6dbe452a9f | ||
|
|
59c755067e | ||
|
|
6ffb27787e | ||
|
|
130837365f | ||
|
|
67df06f1c4 | ||
|
|
60a309d5a4 | ||
|
|
2dc0f24828 | ||
|
|
31e7a4d25e | ||
|
|
ca19d57bba | ||
|
|
bb7d549080 | ||
|
|
5bef2c35eb | ||
|
|
2849a8f9db | ||
|
|
7ced5588c9 | ||
|
|
afcbbb302f | ||
|
|
c2c0b5fe8d | ||
|
|
c9cfe36204 | ||
|
|
fc90c89913 | ||
|
|
af2eede7a9 | ||
|
|
5118be74cb | ||
|
|
bf24066a49 | ||
|
|
92316ab41e | ||
|
|
b354bc8fae | ||
|
|
e834bbb83c | ||
|
|
7498fcb20d | ||
|
|
42d081613f | ||
|
|
b5c1381e45 | ||
|
|
6dfd78f643 | ||
|
|
45e2c2aad8 | ||
|
|
57919c38d8 | ||
|
|
87f561c1f8 | ||
|
|
8c45857859 | ||
|
|
605221d42f | ||
|
|
ee584ab48c | ||
|
|
ab4e138003 | ||
|
|
719c6ac3db | ||
|
|
b8807e60df | ||
|
|
c461380a4a | ||
|
|
98a771c8f8 | ||
|
|
bd9527c033 | ||
|
|
aa221bf92e | ||
|
|
799df40f4e | ||
|
|
b79e9f32c6 | ||
|
|
89d69eb23b | ||
|
|
59b611ba8a | ||
|
|
dfa0be42f6 | ||
|
|
bb96a3f23e | ||
|
|
48b2f28e45 | ||
|
|
8f09c910a9 | ||
|
|
dde95a59b3 | ||
|
|
821e19dcbb | ||
|
|
755df9079e | ||
|
|
ac5650d9f9 | ||
|
|
bd83f86740 | ||
|
|
0af3e218a1 | ||
|
|
b01c9b3bb0 | ||
|
|
b67f2c9f08 | ||
|
|
37675ae3f2 | ||
|
|
a4a6769a6d | ||
|
|
21650fb194 | ||
|
|
89c733e0b9 | ||
|
|
ee3f2defd9 | ||
|
|
7342c1290d | ||
|
|
e64ddd2c1c | ||
|
|
4ece6dc643 | ||
|
|
194c3b603e | ||
|
|
fc1600b738 | ||
|
|
0ee5b14c68 | ||
|
|
3eee176cc3 | ||
|
|
74fe60d8d6 | ||
|
|
0bfaa56e9e | ||
|
|
01dd6b9fa1 | ||
|
|
1038ae76e1 | ||
|
|
bf082d95a0 | ||
|
|
bb24292cf7 | ||
|
|
f2cda52e1a | ||
|
|
7d7cf012f0 | ||
|
|
c56dda74aa | ||
|
|
9f1a08185e | ||
|
|
d2e408656b | ||
|
|
54c278b871 | ||
|
|
4dbd429203 | ||
|
|
b985d7bfe2 | ||
|
|
45e8f02c91 | ||
|
|
54c422ab06 | ||
|
|
b9fb8aab57 | ||
|
|
78841f228a | ||
|
|
dc4afee848 | ||
|
|
1e2b8ac8de | ||
|
|
15d849c166 | ||
|
|
78251d4af8 | ||
|
|
1a4b1ebbf1 | ||
|
|
ccad30dd27 | ||
|
|
4c2b177eab | ||
|
|
58169f9979 | ||
|
|
51402bdb6d | ||
|
|
9c89c32684 | ||
|
|
8aabb8c5b2 | ||
|
|
66512550df | ||
|
|
46dd799548 | ||
|
|
5f03c05523 | ||
|
|
c3f810bbd1 | ||
|
|
b2cbf898d7 | ||
|
|
b2cec8c6ba | ||
|
|
81c1775a03 | ||
|
|
f64ec12f39 | ||
|
|
026382325c | ||
|
|
1bfd8570d6 | ||
|
|
312acd8bad | ||
|
|
d08b969918 | ||
|
|
051de0d8a9 | ||
|
|
bd76df1a50 | ||
|
|
62b2ce2da1 | ||
|
|
172bacb30f | ||
|
|
43667d7349 | ||
|
|
783884376c | ||
|
|
c08aa6fa46 | ||
|
|
0ae932ab34 | ||
|
|
a8cd52e88c | ||
|
|
a4c94d9a90 | ||
|
|
cee838d22e | ||
|
|
732f8a49cf | ||
|
|
be917e2496 | ||
|
|
cd8b1f666d | ||
|
|
8fa5995bde | ||
|
|
25cada7735 | ||
|
|
be6553101c | ||
|
|
417805f330 | ||
|
|
2472ce52e8 | ||
|
|
597eb232d7 | ||
|
|
afe997db82 | ||
|
|
b9d464de61 | ||
|
|
872c124581 | ||
|
|
a531029c5b | ||
|
|
35ab619bd0 | ||
|
|
831193cdd8 | ||
|
|
df460d5a49 | ||
|
|
119ff0eb1b | ||
|
|
3abd63ea5c | ||
|
|
641e4604d5 | ||
|
|
9b5ecc0171 | ||
|
|
a00325da0e | ||
|
|
4ebce3422d | ||
|
|
751e0ee330 | ||
|
|
54b2920ef3 | ||
|
|
5917016509 | ||
|
|
7b4f1d249d | ||
|
|
5425f9268e | ||
|
|
febd866098 | ||
|
|
2446593fff | ||
|
|
651426cf2e | ||
|
|
cf46f6e0ae | ||
|
|
6f15a84ccf | ||
|
|
c39433c361 | ||
|
|
257796ce87 | ||
|
|
2357602f50 | ||
|
|
1230f6b984 | ||
|
|
14b775f1b9 | ||
|
|
c7691d9807 | ||
|
|
9a53d55678 | ||
|
|
31008ef7ff | ||
|
|
621ab260c0 | ||
|
|
2b1840214e | ||
|
|
5cfccc2ead | ||
|
|
774b76447d | ||
|
|
80994bdc8e | ||
|
|
2e31626f87 | ||
|
|
255ba46a4d | ||
|
|
10285933a0 | ||
|
|
543388e18b | ||
|
|
07a1f5d594 | ||
|
|
c6fc090c98 | ||
|
|
9723b6b948 | ||
|
|
c0d0fd44b7 | ||
|
|
30c0fb1308 | ||
|
|
26fac4722f | ||
|
|
e3f64c79d9 | ||
|
|
cbd5e8c626 | ||
|
|
7560c7dee7 | ||
|
|
982a0e8f83 | ||
|
|
fc7fa11923 | ||
|
|
86d6c214fe | ||
|
|
39ccba95d0 | ||
|
|
202e375f41 | ||
|
|
d0378c5723 | ||
|
|
d6f04a0757 | ||
|
|
afedb8697e | ||
|
|
1274df7ffc | ||
|
|
1b4767bd8b | ||
|
|
0b0fe10b37 | ||
|
|
acfb31f8f6 | ||
|
|
fd83bd4f2d | ||
|
|
ce3ca1dbd1 | ||
|
|
95e7b071d4 | ||
|
|
d4c5797a65 | ||
|
|
70a51ba711 | ||
|
|
db8023bdbb | ||
|
|
9e597ecf87 | ||
|
|
a23c117ea4 | ||
|
|
0cf80dab8c | ||
|
|
04a80fb9ba | ||
|
|
626adac363 | ||
|
|
35fbd88a1d | ||
|
|
381b0eed7b | ||
|
|
25383ea645 | ||
|
|
e7db9ddf98 | ||
|
|
7bb878718d | ||
|
|
46a31d4e71 | ||
|
|
e128a7a322 | ||
|
|
27b1898ec6 | ||
|
|
d19ef45bb0 | ||
|
|
5e852df6c3 | ||
|
|
e0eca771c6 | ||
|
|
9d22ef4cc9 | ||
|
|
41961a6980 | ||
|
|
e797676a02 | ||
|
|
05d61e62be | ||
|
|
73043773d8 | ||
|
|
0be9729e40 | ||
|
|
e83674ac51 | ||
|
|
a6e59bf829 | ||
|
|
e46f0641f6 | ||
|
|
07efaa9580 | ||
|
|
361fece023 | ||
|
|
80e69016b0 | ||
|
|
e084a88a9d | ||
|
|
990a88362f | ||
|
|
ea9782b2dc | ||
|
|
8efbaf100e | ||
|
|
15830e2f2a | ||
|
|
04db8591af | ||
|
|
785d30e065 | ||
|
|
e57a10913d | ||
|
|
0d12471868 | ||
|
|
ea371d760d | ||
|
|
3b9104429b | ||
|
|
8a83aed9b1 | ||
|
|
2f68237046 | ||
|
|
45f5b9062e | ||
|
|
147f5f1bec | ||
|
|
f05b198882 | ||
|
|
d0a484cbb7 | ||
|
|
6e6ee37da0 | ||
|
|
53199122d8 | ||
|
|
b38cfac760 | ||
|
|
f3cb3e6852 | ||
|
|
e599f5fe38 | ||
|
|
6357a3fc9c | ||
|
|
92998e6e65 | ||
|
|
2394a2a0dd | ||
|
|
13934d4879 | ||
|
|
aa80013811 | ||
|
|
2ee7206c3a | ||
|
|
be74ca3cf9 | ||
|
|
35123b21ce | ||
|
|
492dc18e14 | ||
|
|
a824a43ed1 | ||
|
|
9b72f0ea14 | ||
|
|
d367f00077 | ||
|
|
31a5751c6c | ||
|
|
fa43989cd5 | ||
|
|
1b317e8a0a | ||
|
|
316807581c | ||
|
|
3321d4575a | ||
|
|
85d4527701 | ||
|
|
47b7509288 | ||
|
|
34fad9da81 | ||
|
|
48be0aa195 | ||
|
|
f544cc65d2 | ||
|
|
41e8f91b2d | ||
|
|
f161e3cb62 | ||
|
|
da41724490 | ||
|
|
281e636e4d | ||
|
|
87dcd12a65 | ||
|
|
d3fdc4ff54 | ||
|
|
9690aba0f5 | ||
|
|
10689a30d2 | ||
|
|
40c068fcbc | ||
|
|
a9340adad7 | ||
|
|
5cb72e8ca6 | ||
|
|
48323e7d6e | ||
|
|
01259f56cd | ||
|
|
472f046a85 | ||
|
|
dfaf5a52df | ||
|
|
93b3322e45 | ||
|
|
a532fd43b2 | ||
|
|
701bb69e6c | ||
|
|
1035d13fc0 | ||
|
|
b18976a7aa | ||
|
|
059962fe33 | ||
|
|
9b22477643 | ||
|
|
6a969fbf5f | ||
|
|
fa84bde6f6 | ||
|
|
6f2b3d4f8c | ||
|
|
0ee6bfe9de | ||
|
|
cabd39ba5b | ||
|
|
10761f3e47 | ||
|
|
08da6b76d1 | ||
|
|
5d4efb467c | ||
|
|
6c6bcbdb7f | ||
|
|
cfdd2b679c | ||
|
|
34d4dbbabd | ||
|
|
78d591b697 | ||
|
|
e95c70d329 | ||
|
|
d8ac088f3a | ||
|
|
0d7f3c6d14 | ||
|
|
eddcca7533 | ||
|
|
ad06e00f99 | ||
|
|
5b089392fd | ||
|
|
02ff3b3256 | ||
|
|
1d14ddcfe7 | ||
|
|
05a805eeca | ||
|
|
ebf99d9ff7 | ||
|
|
cf51fd6749 | ||
|
|
bb22857fde | ||
|
|
5261048d67 | ||
|
|
36095ad80f | ||
|
|
d06866f501 | ||
|
|
02e40f6c3c | ||
|
|
de64695ac5 | ||
|
|
dd108b9ab4 | ||
|
|
f3e90df2a0 | ||
|
|
721e6bbc52 | ||
|
|
27848bf42e | ||
|
|
061edcaa78 | ||
|
|
cbb729f377 | ||
|
|
cfb491e127 | ||
|
|
20808b9b84 | ||
|
|
fd61a36b01 | ||
|
|
c0a7bae977 | ||
|
|
68e056ac91 | ||
|
|
77ba13b41b | ||
|
|
307bb427d6 | ||
|
|
b89503fa8c | ||
|
|
254da35300 | ||
|
|
99926cdba2 | ||
|
|
25f880416a | ||
|
|
1138148543 | ||
|
|
4b70b603b3 | ||
|
|
2e7711fe65 | ||
|
|
417a57fa00 | ||
|
|
714fee52b9 | ||
|
|
133668f5b2 | ||
|
|
3b81bc9f3d | ||
|
|
cbfd6fb996 | ||
|
|
3f8553ce07 | ||
|
|
bf668e18f1 | ||
|
|
1f2b8125c6 | ||
|
|
93645295d5 | ||
|
|
7a52652be6 | ||
|
|
791c8f505e | ||
|
|
12653477d6 | ||
|
|
dedfa0d9ac | ||
|
|
c1d3dfd77e | ||
|
|
f0476cae92 | ||
|
|
b6effdcd6b | ||
|
|
39ef2ff123 | ||
|
|
a989b5e549 | ||
|
|
ff27e944a1 | ||
|
|
0821393c1d | ||
|
|
24f5c0699a | ||
|
|
96409c40bf | ||
|
|
8628f4f93a | ||
|
|
b649b5c987 | ||
|
|
b4d03a8b49 | ||
|
|
85aeebbde2 | ||
|
|
a4bb563779 | ||
|
|
7f6464bbda | ||
|
|
f0741e045f | ||
|
|
5a1991924c | ||
|
|
bd5d14d07f | ||
|
|
d5a1791dc5 | ||
|
|
bd81c12071 | ||
|
|
4da255bf04 | ||
|
|
82c10a7b33 | ||
|
|
d31070177c | ||
|
|
3792576566 | ||
|
|
cd57c75e41 | ||
|
|
237a863dfd | ||
|
|
cb92ba16c1 | ||
|
|
70e9f2c6bc | ||
|
|
a760401407 | ||
|
|
22a5e9791c | ||
|
|
d1bef49b4e | ||
|
|
76abf11eba | ||
|
|
c4850fe6c1 | ||
|
|
0809f4e787 | ||
|
|
6a4c020179 | ||
|
|
3bb401641e | ||
|
|
54b821d8bd | ||
|
|
09e649fc7e | ||
|
|
f208f72dc0 | ||
|
|
d42cd68ea4 | ||
|
|
07647c8382 | ||
|
|
8633823257 | ||
|
|
d0999a8e37 | ||
|
|
ea800e3f14 | ||
|
|
5d2e6fae63 | ||
|
|
fcd22c788a | ||
|
|
ab61a15edc | ||
|
|
2c60459851 | ||
|
|
ea524a6ba1 | ||
|
|
997a6d134f | ||
|
|
8aaf229483 | ||
|
|
049bb719e8 | ||
|
|
014ebdacda | ||
|
|
72a73c859c | ||
|
|
6d2b81f6e4 | ||
|
|
9d01a0d484 | ||
|
|
d5102f62fa | ||
|
|
a881e707e2 | ||
|
|
7d04874f3c | ||
|
|
9f036242fa | ||
|
|
c4e52085e3 | ||
|
|
84e1868028 | ||
|
|
f94f9f672b | ||
|
|
cd29fc8708 | ||
|
|
6e22c0fdeb | ||
|
|
1f4d54e474 | ||
|
|
b7a39b45d7 | ||
|
|
1bfdc91f90 | ||
|
|
58a90ac9d7 | ||
|
|
684dbdc6a4 | ||
|
|
e92de12cf9 | ||
|
|
1f784a6a04 | ||
|
|
ab37c2e69f | ||
|
|
c8f3e0db44 | ||
|
|
02772a3910 | ||
|
|
85a25fd995 | ||
|
|
20f302367c | ||
|
|
54c6bfded0 | ||
|
|
ca5472bc31 | ||
|
|
55b5a31c3c | ||
|
|
01e9891243 | ||
|
|
446a424c1f | ||
|
|
02a0d515d9 | ||
|
|
2bf3816efc | ||
|
|
96902bab44 | ||
|
|
280c5351e2 | ||
|
|
9eb48e1d9b | ||
|
|
d83ebe65e9 | ||
|
|
4fe7d09e5c | ||
|
|
e44cb7e56a | ||
|
|
fd4b7c2ba2 | ||
|
|
a1a1976b38 | ||
|
|
f0d1d4bafa | ||
|
|
600da70960 | ||
|
|
780f85e0d6 | ||
|
|
5d936d58a0 | ||
|
|
8da2759fec | ||
|
|
b03c603759 | ||
|
|
77da12a5ee | ||
|
|
7f6dc43a2d | ||
|
|
f3a7eadcea | ||
|
|
7f6815feaf | ||
|
|
7485f32e69 | ||
|
|
95f95f54cf | ||
|
|
aa9ee75a2a | ||
|
|
c54b69f7ce | ||
|
|
38897fe423 | ||
|
|
cbac5902db | ||
|
|
4cd5cbf893 | ||
|
|
6e3cccc812 | ||
|
|
2b1723e898 | ||
|
|
573484c83e | ||
|
|
bf2cb3a271 | ||
|
|
35e4e2e527 | ||
|
|
f6f05cf23a | ||
|
|
15352448d5 | ||
|
|
c58003e5e3 | ||
|
|
04528459ec | ||
|
|
4f84a01072 | ||
|
|
888bc32be1 | ||
|
|
6d0d288e31 | ||
|
|
339641352e | ||
|
|
e7f338e3a9 |
@@ -1,58 +0,0 @@
|
||||
# Dependencies (installed fresh in Docker)
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Build outputs (built fresh in Docker)
|
||||
dist
|
||||
**/dist
|
||||
.next
|
||||
**/.next
|
||||
|
||||
# TurboRepo cache
|
||||
.turbo
|
||||
**/.turbo
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Credentials
|
||||
.admin-credentials
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
**/coverage
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
*.tsbuildinfo
|
||||
**/*.tsbuildinfo
|
||||
.pnpm-approve-builds
|
||||
.husky/_
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# Documentation (not needed in container)
|
||||
docs
|
||||
*.md
|
||||
!README.md
|
||||
+105
-452
@@ -1,501 +1,154 @@
|
||||
# ==============================================
|
||||
# Mosaic Stack Environment Configuration
|
||||
# ==============================================
|
||||
# Copy this file to .env and customize for your environment
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Mosaic — Environment Variables Reference
|
||||
# Copy this file to .env and fill in the values for your deployment.
|
||||
# Lines beginning with # are comments; optional vars are commented out.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ======================
|
||||
# Application Ports
|
||||
# ======================
|
||||
API_PORT=3001
|
||||
API_HOST=0.0.0.0
|
||||
WEB_PORT=3000
|
||||
|
||||
# ======================
|
||||
# Web Configuration
|
||||
# ======================
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||
# Frontend auth mode:
|
||||
# - real: Normal auth/session flow
|
||||
# - mock: Local-only seeded user for FE development (blocked outside NODE_ENV=development)
|
||||
# Use `mock` locally to continue FE work when auth flow is unstable.
|
||||
# If omitted, web runtime defaults:
|
||||
# - development -> mock
|
||||
# - production -> real
|
||||
NEXT_PUBLIC_AUTH_MODE=real
|
||||
# ─── Database (PostgreSQL 17 + pgvector) ─────────────────────────────────────
|
||||
# Full connection string used by the gateway, ORM, and migration runner.
|
||||
# Port 5433 avoids conflict with a host-side PostgreSQL instance.
|
||||
DATABASE_URL=postgresql://mosaic:mosaic@localhost:5433/mosaic
|
||||
|
||||
# ======================
|
||||
# PostgreSQL Database
|
||||
# ======================
|
||||
# Bundled PostgreSQL
|
||||
# SECURITY: Change POSTGRES_PASSWORD to a strong random password in production
|
||||
DATABASE_URL=postgresql://mosaic:REPLACE_WITH_SECURE_PASSWORD@postgres:5432/mosaic
|
||||
POSTGRES_USER=mosaic
|
||||
POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
POSTGRES_DB=mosaic
|
||||
POSTGRES_PORT=5432
|
||||
# Docker Compose host-port override for the PostgreSQL container (default: 5433)
|
||||
# PG_HOST_PORT=5433
|
||||
|
||||
# External PostgreSQL (managed service)
|
||||
# To use an external instance, update DATABASE_URL above
|
||||
# Example: DATABASE_URL=postgresql://user:[email protected]:5432/mosaic
|
||||
|
||||
# PostgreSQL Performance Tuning (Optional)
|
||||
POSTGRES_SHARED_BUFFERS=256MB
|
||||
POSTGRES_EFFECTIVE_CACHE_SIZE=1GB
|
||||
POSTGRES_MAX_CONNECTIONS=100
|
||||
# ─── Queue (Valkey 8 / Redis-compatible) ─────────────────────────────────────
|
||||
# Port 6380 avoids conflict with a host-side Redis/Valkey instance.
|
||||
VALKEY_URL=redis://localhost:6380
|
||||
|
||||
# ======================
|
||||
# Valkey Cache (Redis-compatible)
|
||||
# ======================
|
||||
# Bundled Valkey
|
||||
VALKEY_URL=redis://valkey:6379
|
||||
VALKEY_HOST=valkey
|
||||
VALKEY_PORT=6379
|
||||
# VALKEY_PASSWORD= # Optional: Password for Valkey authentication
|
||||
VALKEY_MAXMEMORY=256mb
|
||||
# Docker Compose host-port override for the Valkey container (default: 6380)
|
||||
# VALKEY_HOST_PORT=6380
|
||||
|
||||
# External Redis/Valkey (managed service)
|
||||
# To use an external instance, update VALKEY_URL above
|
||||
# Example: VALKEY_URL=redis://elasticache.amazonaws.com:6379
|
||||
# Example with auth: VALKEY_URL=redis://:[email protected]:6379
|
||||
|
||||
# Knowledge Module Cache Configuration
|
||||
# Set KNOWLEDGE_CACHE_ENABLED=false to disable caching (useful for development)
|
||||
KNOWLEDGE_CACHE_ENABLED=true
|
||||
# Cache TTL in seconds (default: 300 = 5 minutes)
|
||||
KNOWLEDGE_CACHE_TTL=300
|
||||
# ─── Gateway ─────────────────────────────────────────────────────────────────
|
||||
# TCP port the NestJS/Fastify gateway listens on (default: 14242)
|
||||
GATEWAY_PORT=14242
|
||||
|
||||
# ======================
|
||||
# Authentication (Authentik OIDC)
|
||||
# ======================
|
||||
# Set to 'true' to enable OIDC authentication with Authentik
|
||||
# When enabled, OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_REDIRECT_URI are required
|
||||
OIDC_ENABLED=false
|
||||
# Comma-separated list of allowed CORS origins.
|
||||
# Must include the web app origin in production.
|
||||
GATEWAY_CORS_ORIGIN=http://localhost:3000
|
||||
|
||||
# Authentik Server URLs (required when OIDC_ENABLED=true)
|
||||
# OIDC_ISSUER must end with a trailing slash (/)
|
||||
OIDC_ISSUER=https://auth.example.com/application/o/mosaic-stack/
|
||||
OIDC_CLIENT_ID=your-client-id-here
|
||||
OIDC_CLIENT_SECRET=your-client-secret-here
|
||||
# Redirect URI must match what's configured in Authentik
|
||||
# Development: http://localhost:3001/auth/oauth2/callback/authentik
|
||||
# Production: https://mosaic-api.woltje.com/auth/oauth2/callback/authentik
|
||||
OIDC_REDIRECT_URI=http://localhost:3001/auth/oauth2/callback/authentik
|
||||
|
||||
# Authentik PostgreSQL Database
|
||||
AUTHENTIK_POSTGRES_USER=authentik
|
||||
AUTHENTIK_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_POSTGRES_DB=authentik
|
||||
# ─── Auth (BetterAuth) ───────────────────────────────────────────────────────
|
||||
# REQUIRED — random secret used to sign sessions and tokens.
|
||||
# Generate with: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET=change-me-to-a-random-32-char-string
|
||||
|
||||
# Authentik Configuration
|
||||
# CRITICAL: Generate a random secret key with at least 50 characters
|
||||
# Example: openssl rand -base64 50
|
||||
AUTHENTIK_SECRET_KEY=REPLACE_WITH_RANDOM_SECRET_MINIMUM_50_CHARS
|
||||
AUTHENTIK_ERROR_REPORTING=false
|
||||
# SECURITY: Change bootstrap password immediately after first login
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL=admin@localhost
|
||||
AUTHENTIK_COOKIE_DOMAIN=.localhost
|
||||
# Public base URL of the gateway (used by BetterAuth for callback URLs)
|
||||
BETTER_AUTH_URL=http://localhost:14242
|
||||
|
||||
# Authentik Ports
|
||||
AUTHENTIK_PORT_HTTP=9000
|
||||
AUTHENTIK_PORT_HTTPS=9443
|
||||
|
||||
# ======================
|
||||
# CSRF Protection
|
||||
# ======================
|
||||
# CRITICAL: Generate a random secret for CSRF token signing
|
||||
# Required in production; auto-generated in development (not persistent across restarts)
|
||||
# Command to generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
CSRF_SECRET=REPLACE_WITH_64_CHAR_HEX_STRING
|
||||
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
|
||||
# Public gateway URL — accessible from the browser, not just the server.
|
||||
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
|
||||
|
||||
# ======================
|
||||
# JWT Configuration
|
||||
# ======================
|
||||
# CRITICAL: Generate a random secret key with at least 32 characters
|
||||
# Example: openssl rand -base64 32
|
||||
JWT_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS
|
||||
JWT_EXPIRATION=24h
|
||||
|
||||
# ======================
|
||||
# BetterAuth Configuration
|
||||
# ======================
|
||||
# CRITICAL: Generate a random secret key with at least 32 characters
|
||||
# This is used by BetterAuth for session management and CSRF protection
|
||||
# Example: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS
|
||||
# Optional explicit BetterAuth origin for callback/error URL generation.
|
||||
# When empty, backend falls back to NEXT_PUBLIC_API_URL.
|
||||
BETTER_AUTH_URL=
|
||||
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
|
||||
# OTLP HTTP endpoint (otel-collector or any OpenTelemetry-compatible backend)
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||
|
||||
# Trusted Origins (comma-separated list of additional trusted origins for CORS and auth)
|
||||
# These are added to NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_API_URL automatically
|
||||
TRUSTED_ORIGINS=
|
||||
# Service name shown in traces
|
||||
OTEL_SERVICE_NAME=mosaic-gateway
|
||||
|
||||
# Cookie Domain (for cross-subdomain session sharing)
|
||||
# Leave empty for single-domain setups. Set to ".example.com" for cross-subdomain.
|
||||
COOKIE_DOMAIN=
|
||||
|
||||
# ======================
|
||||
# Encryption (Credential Security)
|
||||
# ======================
|
||||
# CRITICAL: Generate a random 32-byte (256-bit) encryption key
|
||||
# This key is used for AES-256-GCM encryption of OAuth tokens and sensitive data
|
||||
# Command to generate: openssl rand -hex 32
|
||||
# SECURITY: Never commit this key to version control
|
||||
# SECURITY: Use different keys for development, staging, and production
|
||||
# SECURITY: Store production keys in a secure secrets manager (see docs/design/credential-security.md)
|
||||
ENCRYPTION_KEY=REPLACE_WITH_64_CHAR_HEX_STRING_GENERATE_WITH_OPENSSL_RAND_HEX_32
|
||||
# ─── AI Providers ────────────────────────────────────────────────────────────
|
||||
|
||||
# ======================
|
||||
# OpenBao Secrets Management
|
||||
# ======================
|
||||
# OpenBao provides Transit encryption for sensitive credentials
|
||||
# Enable with: COMPOSE_PROFILES=openbao or COMPOSE_PROFILES=full
|
||||
# Auto-initialized on first run via openbao-init sidecar
|
||||
# Ollama (local models — set OLLAMA_BASE_URL to enable)
|
||||
# OLLAMA_BASE_URL=http://localhost:11434
|
||||
# OLLAMA_HOST is a legacy alias for OLLAMA_BASE_URL
|
||||
# OLLAMA_HOST=http://localhost:11434
|
||||
# Comma-separated list of Ollama model IDs to register (default: llama3.2,codellama,mistral)
|
||||
# OLLAMA_MODELS=llama3.2,codellama,mistral
|
||||
|
||||
# Bundled OpenBao (when openbao profile enabled)
|
||||
OPENBAO_ADDR=http://openbao:8200
|
||||
OPENBAO_PORT=8200
|
||||
# Anthropic (claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5)
|
||||
# ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# External OpenBao/Vault (managed service)
|
||||
# Disable 'openbao' profile and set OPENBAO_ADDR to your external instance
|
||||
# Example: OPENBAO_ADDR=https://vault.example.com:8200
|
||||
# Example: OPENBAO_ADDR=https://vault.hashicorp.com:8200
|
||||
|
||||
# AppRole Authentication (Optional)
|
||||
# If not set, credentials are read from /openbao/init/approle-credentials volume
|
||||
# Required when using external OpenBao
|
||||
# OPENBAO_ROLE_ID=your-role-id-here
|
||||
# OPENBAO_SECRET_ID=your-secret-id-here
|
||||
|
||||
# Fallback Mode
|
||||
# When OpenBao is unavailable, API automatically falls back to AES-256-GCM
|
||||
# encryption using ENCRYPTION_KEY. This provides graceful degradation.
|
||||
|
||||
# ======================
|
||||
# Ollama (Optional AI Service)
|
||||
# ======================
|
||||
# Set OLLAMA_ENDPOINT to use local or remote Ollama
|
||||
# For bundled Docker service: http://ollama:11434
|
||||
# For external service: http://your-ollama-server:11434
|
||||
OLLAMA_ENDPOINT=http://ollama:11434
|
||||
OLLAMA_PORT=11434
|
||||
|
||||
# Embedding Model Configuration
|
||||
# Model used for generating knowledge entry embeddings
|
||||
# Default: mxbai-embed-large (1024-dim, padded to 1536)
|
||||
# Alternative: nomic-embed-text (768-dim, padded to 1536)
|
||||
# Note: Embeddings are padded/truncated to 1536 dimensions to match schema
|
||||
OLLAMA_EMBEDDING_MODEL=mxbai-embed-large
|
||||
|
||||
# Semantic Search Configuration
|
||||
# Similarity threshold for semantic search (0.0 to 1.0, where 1.0 is identical)
|
||||
# Lower values return more results but may be less relevant
|
||||
# Default: 0.5 (50% similarity)
|
||||
SEMANTIC_SEARCH_SIMILARITY_THRESHOLD=0.5
|
||||
|
||||
# ======================
|
||||
# OpenAI API (For Semantic Search)
|
||||
# ======================
|
||||
# OPTIONAL: Semantic search requires an OpenAI API key
|
||||
# Get your API key from: https://platform.openai.com/api-keys
|
||||
# If not configured, semantic search endpoints will return an error
|
||||
# OpenAI (gpt-4o, gpt-4o-mini, o3-mini)
|
||||
# OPENAI_API_KEY=sk-...
|
||||
|
||||
# ======================
|
||||
# Application Environment
|
||||
# ======================
|
||||
NODE_ENV=development
|
||||
# Z.ai / GLM (glm-4.5, glm-4.5-air, glm-4.5-flash)
|
||||
# ZAI_API_KEY=...
|
||||
|
||||
# ======================
|
||||
# Docker Image Configuration
|
||||
# ======================
|
||||
# Docker image tag for pulling pre-built images from git.mosaicstack.dev registry
|
||||
# Used by docker-compose.yml (pulls images) and docker-swarm.yml
|
||||
# For local builds, use docker-compose.build.yml instead
|
||||
# Options:
|
||||
# - latest: Pull latest images from registry (default, built from main branch)
|
||||
# - <version>: Use specific version tag (e.g., v1.0.0)
|
||||
IMAGE_TAG=latest
|
||||
# Custom providers — JSON array of provider configs
|
||||
# Format: [{"id":"<id>","baseUrl":"<url>","apiKey":"<key>","models":[{"id":"<model-id>","name":"<label>"}]}]
|
||||
# MOSAIC_CUSTOM_PROVIDERS=
|
||||
|
||||
# ======================
|
||||
# Docker Compose Profiles
|
||||
# ======================
|
||||
# Enable optional services via profiles. Combine multiple profiles with commas.
|
||||
#
|
||||
# Available profiles:
|
||||
# - database: PostgreSQL database (disable to use external database)
|
||||
# - cache: Valkey cache (disable to use external Redis)
|
||||
# - openbao: OpenBao secrets management (disable to use external vault or fallback encryption)
|
||||
# - authentik: Authentik OIDC authentication (disable to use external auth provider)
|
||||
# - ollama: Ollama AI/LLM service (disable to use external LLM service)
|
||||
# - traefik-bundled: Bundled Traefik reverse proxy (disable to use external proxy)
|
||||
# - full: Enable all optional services (turnkey deployment)
|
||||
#
|
||||
# Examples:
|
||||
# COMPOSE_PROFILES=full # Everything bundled (development)
|
||||
# COMPOSE_PROFILES=database,cache,openbao # Core services only
|
||||
# COMPOSE_PROFILES= # All external services (production)
|
||||
COMPOSE_PROFILES=full
|
||||
|
||||
# ======================
|
||||
# Traefik Reverse Proxy
|
||||
# ======================
|
||||
# TRAEFIK_MODE options:
|
||||
# - bundled: Use bundled Traefik (requires traefik-bundled profile)
|
||||
# - upstream: Connect to external Traefik instance
|
||||
# - none: Direct port exposure without reverse proxy (default)
|
||||
TRAEFIK_MODE=none
|
||||
# ─── Embedding Service ───────────────────────────────────────────────────────
|
||||
# OpenAI-compatible embeddings endpoint (default: OpenAI)
|
||||
# EMBEDDING_API_URL=https://api.openai.com/v1
|
||||
# EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
# Domain configuration for Traefik routing
|
||||
MOSAIC_API_DOMAIN=api.mosaic.local
|
||||
MOSAIC_WEB_DOMAIN=mosaic.local
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.local
|
||||
|
||||
# External Traefik network name (for upstream mode and swarm)
|
||||
# Must match the network name of your existing Traefik instance
|
||||
TRAEFIK_NETWORK=traefik-public
|
||||
TRAEFIK_DOCKER_NETWORK=traefik-public
|
||||
# ─── Log Summarization Service ───────────────────────────────────────────────
|
||||
# OpenAI-compatible chat completions endpoint for log summarization (default: OpenAI)
|
||||
# SUMMARIZATION_API_URL=https://api.openai.com/v1
|
||||
# SUMMARIZATION_MODEL=gpt-4o-mini
|
||||
|
||||
# TLS/SSL Configuration
|
||||
TRAEFIK_TLS_ENABLED=true
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
# Cert resolver name (leave empty if TLS is handled externally or using self-signed certs)
|
||||
TRAEFIK_CERTRESOLVER=
|
||||
# For Let's Encrypt (production):
|
||||
TRAEFIK_ACME_EMAIL=[email protected]
|
||||
# For self-signed certificates (development), leave TRAEFIK_ACME_EMAIL empty
|
||||
# Cron schedule for summarization job (default: every 6 hours)
|
||||
# SUMMARIZATION_CRON=0 */6 * * *
|
||||
|
||||
# Traefik Dashboard (bundled mode only)
|
||||
TRAEFIK_DASHBOARD_ENABLED=true
|
||||
TRAEFIK_DASHBOARD_PORT=8080
|
||||
# Cron schedule for log tier management (default: daily at 03:00)
|
||||
# TIER_MANAGEMENT_CRON=0 3 * * *
|
||||
|
||||
# ======================
|
||||
# Gitea Integration (Coordinator)
|
||||
# ======================
|
||||
# Gitea instance URL
|
||||
GITEA_URL=https://git.mosaicstack.dev
|
||||
|
||||
# Coordinator bot credentials (see docs/1-getting-started/3-configuration/4-gitea-coordinator.md)
|
||||
# SECURITY: Store GITEA_BOT_TOKEN in secrets vault, not in version control
|
||||
GITEA_BOT_USERNAME=mosaic
|
||||
GITEA_BOT_TOKEN=REPLACE_WITH_COORDINATOR_BOT_API_TOKEN
|
||||
GITEA_BOT_PASSWORD=REPLACE_WITH_COORDINATOR_BOT_PASSWORD
|
||||
# ─── Agent ───────────────────────────────────────────────────────────────────
|
||||
# Filesystem sandbox root for agent file tools (default: process.cwd())
|
||||
# AGENT_FILE_SANDBOX_DIR=/var/lib/mosaic/sandbox
|
||||
|
||||
# Repository configuration
|
||||
GITEA_REPO_OWNER=mosaic
|
||||
GITEA_REPO_NAME=stack
|
||||
# Comma-separated list of tool names available to non-admin users.
|
||||
# Leave unset to allow all tools for all authenticated users.
|
||||
# AGENT_USER_TOOLS=read_file,list_directory,search_files
|
||||
|
||||
# Webhook secret for coordinator (HMAC SHA256 signature verification)
|
||||
# SECURITY: Generate random secret with: openssl rand -hex 32
|
||||
# Configure in Gitea: Repository Settings → Webhooks → Add Webhook
|
||||
GITEA_WEBHOOK_SECRET=REPLACE_WITH_RANDOM_WEBHOOK_SECRET
|
||||
# System prompt injected into every agent session (optional)
|
||||
# AGENT_SYSTEM_PROMPT=You are a helpful assistant.
|
||||
|
||||
# Coordinator API Key (service-to-service authentication)
|
||||
# CRITICAL: Generate a random API key with at least 32 characters
|
||||
# Example: openssl rand -base64 32
|
||||
# The coordinator service uses this key to authenticate with the API
|
||||
COORDINATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS
|
||||
|
||||
# Anthropic API Key (used by coordinator for issue parsing)
|
||||
# Get your API key from: https://console.anthropic.com/
|
||||
ANTHROPIC_API_KEY=REPLACE_WITH_ANTHROPIC_API_KEY
|
||||
# ─── MCP Servers ─────────────────────────────────────────────────────────────
|
||||
# JSON array of MCP server configs — set to enable MCP tool integration.
|
||||
# Each entry: {"name":"<id>","url":"<http-or-sse-url>"}
|
||||
# MCP_SERVERS=[{"name":"my-mcp","url":"http://localhost:3100/sse"}]
|
||||
|
||||
# Coordinator tuning
|
||||
COORDINATOR_POLL_INTERVAL=5.0
|
||||
COORDINATOR_MAX_CONCURRENT_AGENTS=10
|
||||
COORDINATOR_ENABLED=true
|
||||
|
||||
# ======================
|
||||
# Rate Limiting
|
||||
# ======================
|
||||
# Rate limiting prevents DoS attacks on webhook and API endpoints
|
||||
# TTL is in seconds, limits are per TTL window
|
||||
# ─── Coordinator ─────────────────────────────────────────────────────────────
|
||||
# Root directory used to scope coordinator (worktree/repo) operations.
|
||||
# Defaults to the monorepo root auto-detected from process.cwd().
|
||||
# MOSAIC_WORKSPACE_ROOT=/home/user/projects/mosaic
|
||||
|
||||
# Global rate limit (applies to all endpoints unless overridden)
|
||||
# Time window in seconds
|
||||
RATE_LIMIT_TTL=60
|
||||
# Requests per window
|
||||
RATE_LIMIT_GLOBAL_LIMIT=100
|
||||
|
||||
# Webhook endpoints (/stitcher/webhook, /stitcher/dispatch) — requests per minute
|
||||
RATE_LIMIT_WEBHOOK_LIMIT=60
|
||||
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
|
||||
# DISCORD_BOT_TOKEN=
|
||||
# DISCORD_GUILD_ID=
|
||||
# DISCORD_GATEWAY_URL=http://localhost:14242
|
||||
|
||||
# Coordinator endpoints (/coordinator/*) — requests per minute
|
||||
RATE_LIMIT_COORDINATOR_LIMIT=100
|
||||
|
||||
# Health check endpoints (/coordinator/health) — requests per minute (higher for monitoring)
|
||||
RATE_LIMIT_HEALTH_LIMIT=300
|
||||
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
|
||||
# TELEGRAM_BOT_TOKEN=
|
||||
# TELEGRAM_GATEWAY_URL=http://localhost:14242
|
||||
|
||||
# Storage backend for rate limiting (redis or memory)
|
||||
# redis: Uses Valkey for distributed rate limiting (recommended for production)
|
||||
# memory: Uses in-memory storage (single instance only, for development)
|
||||
RATE_LIMIT_STORAGE=redis
|
||||
|
||||
# ======================
|
||||
# Discord Bridge (Optional)
|
||||
# ======================
|
||||
# Discord bot integration for chat-based control
|
||||
# Get bot token from: https://discord.com/developers/applications
|
||||
# DISCORD_BOT_TOKEN=your-discord-bot-token-here
|
||||
# DISCORD_GUILD_ID=your-discord-server-id
|
||||
# DISCORD_CONTROL_CHANNEL_ID=channel-id-for-commands
|
||||
# DISCORD_WORKSPACE_ID=your-workspace-uuid
|
||||
#
|
||||
# SECURITY: DISCORD_WORKSPACE_ID must be a valid workspace UUID from your database.
|
||||
# All Discord commands will execute within this workspace context for proper
|
||||
# multi-tenant isolation. Each Discord bot instance should be configured for
|
||||
# a single workspace.
|
||||
# ─── SSO Providers (add credentials to enable) ───────────────────────────────
|
||||
|
||||
# ======================
|
||||
# Matrix Bridge (Optional)
|
||||
# ======================
|
||||
# Matrix bot integration for chat-based control via Matrix protocol
|
||||
# Requires a Matrix account with an access token for the bot user
|
||||
# Set these AFTER deploying Synapse and creating the bot account.
|
||||
#
|
||||
# SECURITY: MATRIX_WORKSPACE_ID must be a valid workspace UUID from your database.
|
||||
# All Matrix commands will execute within this workspace context for proper
|
||||
# multi-tenant isolation. Each Matrix bot instance should be configured for
|
||||
# a single workspace.
|
||||
MATRIX_HOMESERVER_URL=http://synapse:8008
|
||||
MATRIX_ACCESS_TOKEN=
|
||||
MATRIX_BOT_USER_ID=@mosaic-bot:matrix.woltje.com
|
||||
MATRIX_SERVER_NAME=matrix.woltje.com
|
||||
# MATRIX_CONTROL_ROOM_ID=!roomid:matrix.woltje.com
|
||||
# MATRIX_WORKSPACE_ID=your-workspace-uuid
|
||||
# --- Authentik (optional — set AUTHENTIK_CLIENT_ID to enable) ---
|
||||
# AUTHENTIK_ISSUER=https://auth.example.com/application/o/mosaic/
|
||||
# AUTHENTIK_CLIENT_ID=
|
||||
# AUTHENTIK_CLIENT_SECRET=
|
||||
|
||||
# ======================
|
||||
# Matrix / Synapse Deployment
|
||||
# ======================
|
||||
# Domains for Traefik routing to Matrix services
|
||||
MATRIX_DOMAIN=matrix.woltje.com
|
||||
ELEMENT_DOMAIN=chat.woltje.com
|
||||
# --- WorkOS (optional — set WORKOS_CLIENT_ID to enable) ---
|
||||
# WORKOS_ISSUER=https://your-company.authkit.app
|
||||
# WORKOS_CLIENT_ID=client_...
|
||||
# WORKOS_CLIENT_SECRET=sk_live_...
|
||||
|
||||
# Synapse database (created automatically by synapse-db-init in the swarm compose)
|
||||
SYNAPSE_POSTGRES_DB=synapse
|
||||
SYNAPSE_POSTGRES_USER=synapse
|
||||
SYNAPSE_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_SYNAPSE_DB_PASSWORD
|
||||
# --- Keycloak (optional — set KEYCLOAK_CLIENT_ID to enable) ---
|
||||
# KEYCLOAK_ISSUER=https://auth.example.com/realms/master
|
||||
# Legacy alternative if you prefer to compose the issuer from separate vars:
|
||||
# KEYCLOAK_URL=https://auth.example.com
|
||||
# KEYCLOAK_REALM=master
|
||||
# KEYCLOAK_CLIENT_ID=mosaic
|
||||
# KEYCLOAK_CLIENT_SECRET=
|
||||
|
||||
# Image tags for Matrix services
|
||||
SYNAPSE_IMAGE_TAG=latest
|
||||
ELEMENT_IMAGE_TAG=latest
|
||||
|
||||
# ======================
|
||||
# Orchestrator Configuration
|
||||
# ======================
|
||||
# API Key for orchestrator agent management endpoints
|
||||
# CRITICAL: Generate a random API key with at least 32 characters
|
||||
# Example: openssl rand -base64 32
|
||||
# Required for all /agents/* endpoints (spawn, kill, kill-all, status)
|
||||
# Health endpoints (/health/*) remain unauthenticated
|
||||
ORCHESTRATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS
|
||||
|
||||
# Runtime safety defaults (recommended for low-memory hosts)
|
||||
MAX_CONCURRENT_AGENTS=2
|
||||
SESSION_CLEANUP_DELAY_MS=30000
|
||||
ORCHESTRATOR_QUEUE_NAME=orchestrator-tasks
|
||||
ORCHESTRATOR_QUEUE_CONCURRENCY=1
|
||||
ORCHESTRATOR_QUEUE_MAX_RETRIES=3
|
||||
ORCHESTRATOR_QUEUE_BASE_DELAY_MS=1000
|
||||
ORCHESTRATOR_QUEUE_MAX_DELAY_MS=60000
|
||||
SANDBOX_DEFAULT_MEMORY_MB=256
|
||||
SANDBOX_DEFAULT_CPU_LIMIT=1.0
|
||||
|
||||
# ======================
|
||||
# AI Provider Configuration
|
||||
# ======================
|
||||
# Choose the AI provider for orchestrator agents
|
||||
# Options: ollama, claude, openai
|
||||
# Default: ollama (no API key required)
|
||||
AI_PROVIDER=ollama
|
||||
|
||||
# Ollama Configuration (when AI_PROVIDER=ollama)
|
||||
# For local Ollama: http://localhost:11434
|
||||
# For remote Ollama: http://your-ollama-server:11434
|
||||
OLLAMA_MODEL=llama3.1:latest
|
||||
|
||||
# Claude API Key
|
||||
# Required only when AI_PROVIDER=claude.
|
||||
# Get your API key from: https://console.anthropic.com/
|
||||
CLAUDE_API_KEY=REPLACE_WITH_CLAUDE_API_KEY
|
||||
|
||||
# OpenAI API Configuration (when AI_PROVIDER=openai)
|
||||
# OPTIONAL: Only required if AI_PROVIDER=openai
|
||||
# Get your API key from: https://platform.openai.com/api-keys
|
||||
# OPENAI_API_KEY=sk-...
|
||||
|
||||
# ======================
|
||||
# Speech Services (STT / TTS)
|
||||
# ======================
|
||||
# Speech-to-Text (STT) - Whisper via Speaches
|
||||
# Set STT_ENABLED=true to enable speech-to-text transcription
|
||||
# STT_BASE_URL is required when STT_ENABLED=true
|
||||
STT_ENABLED=true
|
||||
STT_BASE_URL=http://speaches:8000/v1
|
||||
STT_MODEL=Systran/faster-whisper-large-v3-turbo
|
||||
STT_LANGUAGE=en
|
||||
|
||||
# Text-to-Speech (TTS) - Default Engine (Kokoro)
|
||||
# Set TTS_ENABLED=true to enable text-to-speech synthesis
|
||||
# TTS_DEFAULT_URL is required when TTS_ENABLED=true
|
||||
TTS_ENABLED=true
|
||||
TTS_DEFAULT_URL=http://kokoro-tts:8880/v1
|
||||
TTS_DEFAULT_VOICE=af_heart
|
||||
TTS_DEFAULT_FORMAT=mp3
|
||||
|
||||
# Text-to-Speech (TTS) - Premium Engine (Chatterbox) - Optional
|
||||
# Higher quality voice cloning engine, disabled by default
|
||||
# TTS_PREMIUM_URL is required when TTS_PREMIUM_ENABLED=true
|
||||
TTS_PREMIUM_ENABLED=false
|
||||
TTS_PREMIUM_URL=http://chatterbox-tts:8881/v1
|
||||
|
||||
# Text-to-Speech (TTS) - Fallback Engine (Piper/OpenedAI) - Optional
|
||||
# Lightweight fallback engine, disabled by default
|
||||
# TTS_FALLBACK_URL is required when TTS_FALLBACK_ENABLED=true
|
||||
TTS_FALLBACK_ENABLED=false
|
||||
TTS_FALLBACK_URL=http://openedai-speech:8000/v1
|
||||
|
||||
# Whisper model for Speaches STT engine
|
||||
SPEACHES_WHISPER_MODEL=Systran/faster-whisper-large-v3-turbo
|
||||
|
||||
# Speech Service Limits
|
||||
# Maximum upload file size in bytes (default: 25MB)
|
||||
SPEECH_MAX_UPLOAD_SIZE=25000000
|
||||
# Maximum audio duration in seconds (default: 600 = 10 minutes)
|
||||
SPEECH_MAX_DURATION_SECONDS=600
|
||||
# Maximum text length for TTS in characters (default: 4096)
|
||||
SPEECH_MAX_TEXT_LENGTH=4096
|
||||
|
||||
# ======================
|
||||
# Mosaic Telemetry (Task Completion Tracking & Predictions)
|
||||
# ======================
|
||||
# Telemetry tracks task completion patterns to provide time estimates and predictions.
|
||||
# Data is sent to the Mosaic Telemetry API (a separate service).
|
||||
|
||||
# Master switch: set to false to completely disable telemetry (no HTTP calls will be made)
|
||||
MOSAIC_TELEMETRY_ENABLED=true
|
||||
|
||||
# URL of the telemetry API server
|
||||
# For Docker Compose (internal): http://telemetry-api:8000
|
||||
# For production/swarm: https://tel-api.mosaicstack.dev
|
||||
MOSAIC_TELEMETRY_SERVER_URL=http://telemetry-api:8000
|
||||
|
||||
# API key for authenticating with the telemetry server
|
||||
# Generate with: openssl rand -hex 32
|
||||
MOSAIC_TELEMETRY_API_KEY=your-64-char-hex-api-key-here
|
||||
|
||||
# Unique identifier for this Mosaic Stack instance
|
||||
# Generate with: uuidgen or python -c "import uuid; print(uuid.uuid4())"
|
||||
MOSAIC_TELEMETRY_INSTANCE_ID=your-instance-uuid-here
|
||||
|
||||
# Dry run mode: set to true to log telemetry events to console instead of sending HTTP requests
|
||||
# Useful for development and debugging telemetry payloads
|
||||
MOSAIC_TELEMETRY_DRY_RUN=false
|
||||
|
||||
# ======================
|
||||
# Logging & Debugging
|
||||
# ======================
|
||||
LOG_LEVEL=info
|
||||
DEBUG=false
|
||||
# Feature flags — set to true alongside provider credentials to show SSO buttons in the UI
|
||||
# NEXT_PUBLIC_WORKOS_ENABLED=true
|
||||
# NEXT_PUBLIC_KEYCLOAK_ENABLED=true
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# Traefik Bundled Mode Configuration
|
||||
# Copy this to .env to enable bundled Traefik reverse proxy
|
||||
#
|
||||
# Usage:
|
||||
# cp .env.traefik-bundled.example .env
|
||||
# docker compose --profile traefik-bundled up -d
|
||||
|
||||
# ======================
|
||||
# Traefik Configuration
|
||||
# ======================
|
||||
TRAEFIK_MODE=bundled
|
||||
TRAEFIK_ENABLE=true
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
TRAEFIK_DOCKER_NETWORK=mosaic-public
|
||||
|
||||
# Domain configuration
|
||||
MOSAIC_API_DOMAIN=api.mosaic.local
|
||||
MOSAIC_WEB_DOMAIN=mosaic.local
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.local
|
||||
|
||||
# TLS/SSL Configuration
|
||||
TRAEFIK_TLS_ENABLED=true
|
||||
# For Let's Encrypt (production):
|
||||
# [email protected]
|
||||
# TRAEFIK_CERTRESOLVER=letsencrypt
|
||||
# For self-signed certificates (development), leave TRAEFIK_ACME_EMAIL empty
|
||||
TRAEFIK_ACME_EMAIL=
|
||||
|
||||
# Traefik Dashboard
|
||||
TRAEFIK_DASHBOARD_ENABLED=true
|
||||
TRAEFIK_DASHBOARD_PORT=8080
|
||||
|
||||
# Traefik Ports
|
||||
TRAEFIK_HTTP_PORT=80
|
||||
TRAEFIK_HTTPS_PORT=443
|
||||
|
||||
# ======================
|
||||
# Application Ports (not exposed when using Traefik)
|
||||
# ======================
|
||||
API_PORT=3001
|
||||
WEB_PORT=3000
|
||||
|
||||
# ======================
|
||||
# PostgreSQL Database
|
||||
# ======================
|
||||
POSTGRES_USER=mosaic
|
||||
POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
POSTGRES_DB=mosaic
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# ======================
|
||||
# Valkey Cache
|
||||
# ======================
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_MAXMEMORY=256mb
|
||||
|
||||
# ======================
|
||||
# Authentication (Authentik OIDC)
|
||||
# ======================
|
||||
OIDC_ISSUER=https://auth.mosaic.local/application/o/mosaic-stack/
|
||||
OIDC_CLIENT_ID=your-client-id-here
|
||||
OIDC_CLIENT_SECRET=your-client-secret-here
|
||||
OIDC_REDIRECT_URI=https://api.mosaic.local/auth/callback
|
||||
|
||||
# Authentik Configuration
|
||||
AUTHENTIK_SECRET_KEY=REPLACE_WITH_RANDOM_SECRET_MINIMUM_50_CHARS
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL=admin@localhost
|
||||
AUTHENTIK_COOKIE_DOMAIN=.mosaic.local
|
||||
|
||||
AUTHENTIK_POSTGRES_USER=authentik
|
||||
AUTHENTIK_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_POSTGRES_DB=authentik
|
||||
|
||||
# ======================
|
||||
# JWT Configuration
|
||||
# ======================
|
||||
JWT_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS
|
||||
JWT_EXPIRATION=24h
|
||||
|
||||
# ======================
|
||||
# Docker Compose Profiles
|
||||
# ======================
|
||||
# Enable bundled Traefik and optional services
|
||||
COMPOSE_PROFILES=traefik-bundled,authentik
|
||||
@@ -1,83 +0,0 @@
|
||||
# Traefik Upstream Mode Configuration
|
||||
# Connect to an existing external Traefik instance
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. External Traefik instance must be running
|
||||
# 2. External network must exist: docker network create traefik-public
|
||||
# 3. Copy docker-compose.override.yml.example to docker-compose.override.yml
|
||||
# 4. Uncomment upstream mode network configuration in override file
|
||||
#
|
||||
# Usage:
|
||||
# cp .env.traefik-upstream.example .env
|
||||
# docker compose up -d
|
||||
|
||||
# ======================
|
||||
# Traefik Configuration
|
||||
# ======================
|
||||
TRAEFIK_MODE=upstream
|
||||
TRAEFIK_ENABLE=true
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
TRAEFIK_DOCKER_NETWORK=traefik-public
|
||||
TRAEFIK_NETWORK=traefik-public
|
||||
|
||||
# Domain configuration
|
||||
# These domains must be configured in your DNS or /etc/hosts
|
||||
MOSAIC_API_DOMAIN=api.mosaic.uscllc.com
|
||||
MOSAIC_WEB_DOMAIN=mosaic.uscllc.com
|
||||
MOSAIC_AUTH_DOMAIN=auth.mosaic.uscllc.com
|
||||
|
||||
# TLS/SSL Configuration
|
||||
TRAEFIK_TLS_ENABLED=true
|
||||
# ACME/Certresolver managed by upstream Traefik
|
||||
TRAEFIK_CERTRESOLVER=
|
||||
|
||||
# ======================
|
||||
# Application Ports (not exposed when using Traefik)
|
||||
# ======================
|
||||
# These ports are only used internally within Docker network
|
||||
API_PORT=3001
|
||||
WEB_PORT=3000
|
||||
|
||||
# ======================
|
||||
# PostgreSQL Database
|
||||
# ======================
|
||||
POSTGRES_USER=mosaic
|
||||
POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
POSTGRES_DB=mosaic
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# ======================
|
||||
# Valkey Cache
|
||||
# ======================
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_MAXMEMORY=256mb
|
||||
|
||||
# ======================
|
||||
# Authentication (Authentik OIDC)
|
||||
# ======================
|
||||
OIDC_ISSUER=https://auth.mosaic.uscllc.com/application/o/mosaic-stack/
|
||||
OIDC_CLIENT_ID=your-client-id-here
|
||||
OIDC_CLIENT_SECRET=your-client-secret-here
|
||||
OIDC_REDIRECT_URI=https://api.mosaic.uscllc.com/auth/callback
|
||||
|
||||
# Authentik Configuration
|
||||
AUTHENTIK_SECRET_KEY=REPLACE_WITH_RANDOM_SECRET_MINIMUM_50_CHARS
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL=admin@localhost
|
||||
AUTHENTIK_COOKIE_DOMAIN=.mosaic.uscllc.com
|
||||
|
||||
AUTHENTIK_POSTGRES_USER=authentik
|
||||
AUTHENTIK_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD
|
||||
AUTHENTIK_POSTGRES_DB=authentik
|
||||
|
||||
# ======================
|
||||
# JWT Configuration
|
||||
# ======================
|
||||
JWT_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS
|
||||
JWT_EXPIRATION=24h
|
||||
|
||||
# ======================
|
||||
# Docker Compose Profiles
|
||||
# ======================
|
||||
# Enable optional services (do NOT enable traefik-bundled in upstream mode)
|
||||
COMPOSE_PROFILES=authentik
|
||||
+9
-63
@@ -1,71 +1,17 @@
|
||||
# Dependencies
|
||||
logs/
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
|
||||
# Compiled source (prevent accidental commits)
|
||||
apps/*/src/**/*.js
|
||||
apps/*/src/**/*.d.ts
|
||||
apps/*/src/**/*.js.map
|
||||
apps/*/src/**/*.d.ts.map
|
||||
packages/*/src/**/*.js
|
||||
packages/*/src/**/*.d.ts
|
||||
packages/*/src/**/*.js.map
|
||||
packages/*/src/**/*.d.ts.map
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.next
|
||||
coverage
|
||||
.env
|
||||
.env.local
|
||||
.env.test
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.bak.*
|
||||
*.bak
|
||||
|
||||
# Credentials (never commit)
|
||||
.admin-credentials
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Misc
|
||||
*.tsbuildinfo
|
||||
.pnpm-approve-builds
|
||||
.pnpm-store
|
||||
docs/reports/
|
||||
|
||||
# Husky
|
||||
.husky/_
|
||||
# Step-CA dev password — real file is gitignored; commit only the .example
|
||||
infra/step-ca/dev-password
|
||||
|
||||
# Orchestrator reports (generated by QA automation, cleaned up after processing)
|
||||
docs/reports/qa-automation/
|
||||
|
||||
# Repo-local orchestrator runtime artifacts
|
||||
.mosaic/orchestrator/orchestrator.pid
|
||||
.mosaic/orchestrator/state.json
|
||||
.mosaic/orchestrator/tasks.json
|
||||
.mosaic/orchestrator/matrix_state.json
|
||||
.mosaic/orchestrator/logs/*.log
|
||||
.mosaic/orchestrator/results/*
|
||||
!.mosaic/orchestrator/logs/.gitkeep
|
||||
!.mosaic/orchestrator/results/.gitkeep
|
||||
# Scratch dirs created by the framework git-wrapper shell test harnesses
|
||||
.mosaic-test-work/
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm typecheck && pnpm lint && pnpm format:check
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"**/*.{ts,tsx,js,jsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"**/*.{json,md,yaml,yml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Monorepo-aware lint-staged configuration
|
||||
// STRICT ENFORCEMENT ENABLED: Blocks commits if affected packages have violations
|
||||
//
|
||||
// IMPORTANT: This lints ENTIRE packages, not just changed files.
|
||||
// If you touch ANY file in a package with violations, you must fix the whole package.
|
||||
// This forces incremental cleanup - work in a package = clean up that package.
|
||||
//
|
||||
export default {
|
||||
// TypeScript files - lint and typecheck affected packages
|
||||
'**/*.{ts,tsx}': (filenames) => {
|
||||
const commands = [];
|
||||
|
||||
// 1. Format first (auto-fixes what it can)
|
||||
commands.push(`prettier --write ${filenames.join(' ')}`);
|
||||
|
||||
// 2. Extract affected packages from absolute paths
|
||||
// lint-staged passes absolute paths, so we need to extract the relative part
|
||||
const packages = [...new Set(filenames.map(f => {
|
||||
// Match either absolute or relative paths: .../packages/shared/... or packages/shared/...
|
||||
const match = f.match(/(?:^|\/)(apps|packages)\/([^/]+)\//);
|
||||
if (!match) return null;
|
||||
// Return package name format for turbo (e.g., "@mosaic/api")
|
||||
return `@mosaic/${match[2]}`;
|
||||
}))].filter(Boolean);
|
||||
|
||||
if (packages.length === 0) {
|
||||
return commands;
|
||||
}
|
||||
|
||||
// 3. Lint entire affected packages via turbo
|
||||
// --max-warnings=0 means ANY warning/error blocks the commit
|
||||
packages.forEach(pkg => {
|
||||
commands.push(`pnpm turbo run lint --filter=${pkg} -- --max-warnings=0`);
|
||||
});
|
||||
|
||||
// 4. Type-check affected packages
|
||||
packages.forEach(pkg => {
|
||||
commands.push(`pnpm turbo run typecheck --filter=${pkg}`);
|
||||
});
|
||||
|
||||
return commands;
|
||||
},
|
||||
|
||||
// Format all other files
|
||||
'**/*.{js,jsx,json,md,yml,yaml}': [
|
||||
'prettier --write',
|
||||
],
|
||||
};
|
||||
@@ -13,3 +13,66 @@ This repository is attached to the machine-wide Mosaic framework.
|
||||
- Keep universal standards in `~/.config/mosaic`
|
||||
- Keep repo-specific behavior in this repo
|
||||
- Avoid copying large runtime configs into each project
|
||||
|
||||
## Optional Quality Rails
|
||||
|
||||
Use `.mosaic/quality-rails.yml` to track whether quality rails are enabled for this repo.
|
||||
|
||||
Apply a template:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target .
|
||||
```
|
||||
|
||||
Verify enforcement:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-quality-verify --target .
|
||||
```
|
||||
|
||||
## Optional Matrix Orchestrator Rail
|
||||
|
||||
Repo-local orchestrator state lives in `.mosaic/orchestrator/`.
|
||||
|
||||
Run one cycle:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-matrix-cycle
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-run --once
|
||||
```
|
||||
|
||||
Run continuously:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-run --poll-sec 10
|
||||
```
|
||||
|
||||
Bridge events to Matrix:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-matrix-publish
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-matrix-consume
|
||||
```
|
||||
|
||||
Run until queue is drained (syncs from `docs/tasks.md` first):
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/bin/mosaic-orchestrator-drain
|
||||
```
|
||||
|
||||
Set worker command if auto-detect does not match your CLI:
|
||||
|
||||
```bash
|
||||
export MOSAIC_WORKER_EXEC="codex -p"
|
||||
# or
|
||||
export MOSAIC_WORKER_EXEC="opencode -p"
|
||||
```
|
||||
|
||||
Use repo helper (foreground or detached):
|
||||
|
||||
```bash
|
||||
bash scripts/agent/orchestrator-daemon.sh drain
|
||||
bash scripts/agent/orchestrator-daemon.sh start
|
||||
bash scripts/agent/orchestrator-daemon.sh status
|
||||
bash scripts/agent/orchestrator-daemon.sh stop
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"transport": "matrix",
|
||||
"matrix": {
|
||||
"control_room_id": "",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"last_published_line": 0,
|
||||
"since": null
|
||||
}
|
||||
@@ -1,90 +1,14 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"mission_id": "ms21-multi-tenant-rbac-data-migration-20260228",
|
||||
"name": "MS21 Multi-Tenant RBAC Data Migration",
|
||||
"description": "Build multi-tenant user/workspace/team management, break-glass auth, RBAC UI enforcement, and migrate jarvis-brain data into Mosaic Stack",
|
||||
"project_path": "/home/jwoltje/src/mosaic-stack",
|
||||
"created_at": "2026-02-28T17:10:22Z",
|
||||
"mission_id": "mvp-20260312",
|
||||
"name": "MVP",
|
||||
"description": "",
|
||||
"project_path": "/home/jwoltje/src/mosaic-mono-v1",
|
||||
"created_at": "2026-03-13T00:44:02Z",
|
||||
"status": "active",
|
||||
"task_prefix": "MS21",
|
||||
"quality_gates": "pnpm lint && pnpm build && pnpm test",
|
||||
"milestone_version": "0.0.21",
|
||||
"milestones": [
|
||||
{
|
||||
"id": "phase-1",
|
||||
"name": "Schema and Admin API",
|
||||
"status": "pending",
|
||||
"branch": "schema-and-admin-api",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-2",
|
||||
"name": "Break-Glass Authentication",
|
||||
"status": "pending",
|
||||
"branch": "break-glass-authentication",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-3",
|
||||
"name": "Data Migration",
|
||||
"status": "pending",
|
||||
"branch": "data-migration",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-4",
|
||||
"name": "Admin UI",
|
||||
"status": "pending",
|
||||
"branch": "admin-ui",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-5",
|
||||
"name": "RBAC UI Enforcement",
|
||||
"status": "pending",
|
||||
"branch": "rbac-ui-enforcement",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
},
|
||||
{
|
||||
"id": "phase-6",
|
||||
"name": "Verification",
|
||||
"status": "pending",
|
||||
"branch": "verification",
|
||||
"issue_ref": "",
|
||||
"started_at": "",
|
||||
"completed_at": ""
|
||||
}
|
||||
],
|
||||
"sessions": [
|
||||
{
|
||||
"session_id": "sess-001",
|
||||
"runtime": "unknown",
|
||||
"started_at": "2026-02-28T17:48:51Z",
|
||||
"ended_at": "",
|
||||
"ended_reason": "",
|
||||
"milestone_at_end": "",
|
||||
"tasks_completed": [],
|
||||
"last_task_id": ""
|
||||
},
|
||||
{
|
||||
"session_id": "sess-002",
|
||||
"runtime": "unknown",
|
||||
"started_at": "2026-02-28T20:30:13Z",
|
||||
"ended_at": "",
|
||||
"ended_reason": "",
|
||||
"milestone_at_end": "",
|
||||
"tasks_completed": [],
|
||||
"last_task_id": ""
|
||||
}
|
||||
]
|
||||
"task_prefix": "",
|
||||
"quality_gates": "",
|
||||
"milestone_version": "0.0.1",
|
||||
"milestones": [],
|
||||
"sessions": []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"generated_at": "2026-03-13T00:44:51Z",
|
||||
"runtime": "claude",
|
||||
"mission_id": "mvp-20260312",
|
||||
"mission_name": "MVP",
|
||||
"project_path": "/home/jwoltje/src/mosaic-mono-v1",
|
||||
"quality_gates": "",
|
||||
"current_milestone": {
|
||||
"id": "",
|
||||
"name": ""
|
||||
},
|
||||
"next_task": "",
|
||||
"progress": {
|
||||
"tasks_done": 0,
|
||||
"tasks_total": 0,
|
||||
"pct": 0
|
||||
},
|
||||
"current_branch": ""
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"session_id": "sess-002",
|
||||
"runtime": "unknown",
|
||||
"pid": 3178395,
|
||||
"started_at": "2026-02-28T20:30:13Z",
|
||||
"project_path": "/tmp/ms21-ui-001",
|
||||
"session_id": "claude-20260312-194506-357136",
|
||||
"runtime": "claude",
|
||||
"pid": 357136,
|
||||
"started_at": "2026-03-13T00:45:06Z",
|
||||
"project_path": "/home/jwoltje/src/mosaic-mono-v1",
|
||||
"milestone_id": ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"running_task_id": null,
|
||||
"updated_at": null
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"tasks": []
|
||||
}
|
||||
@@ -7,4 +7,4 @@ template: ""
|
||||
# - monorepo
|
||||
#
|
||||
# Apply manually:
|
||||
# ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target <repo>
|
||||
# ~/.mosaic/bin/mosaic-quality-apply --template <template> --target <repo>
|
||||
|
||||
+13
-25
@@ -1,29 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Repo-specific hooks used by scripts/agent/*.sh for Mosaic Stack.
|
||||
# Optional repo-specific hooks used by scripts/agent/*.sh
|
||||
|
||||
mosaic_hook_session_start() {
|
||||
echo "[mosaic-stack] Branch: $(git rev-parse --abbrev-ref HEAD)"
|
||||
echo "[mosaic-stack] Remotes:"
|
||||
git remote -v | sed 's/^/[mosaic-stack] /'
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
echo "[mosaic-stack] Node: $(node -v)"
|
||||
fi
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
echo "[mosaic-stack] pnpm: $(pnpm -v)"
|
||||
fi
|
||||
}
|
||||
# Called by session-start.sh
|
||||
# mosaic_hook_session_start() {
|
||||
# echo "Run repo-specific startup checks"
|
||||
# }
|
||||
|
||||
mosaic_hook_critical() {
|
||||
echo "[mosaic-stack] Recent commits:"
|
||||
git log --oneline --decorate -n 5 | sed 's/^/[mosaic-stack] /'
|
||||
echo "[mosaic-stack] Open TODO/FIXME markers (top 20):"
|
||||
rg -n "(TODO|FIXME|HACK|SECURITY)" apps packages plugins docs --glob '!**/node_modules/**' -S \
|
||||
| head -n 20 \
|
||||
| sed 's/^/[mosaic-stack] /' \
|
||||
|| true
|
||||
}
|
||||
# Called by critical.sh
|
||||
# mosaic_hook_critical() {
|
||||
# echo "Run repo-specific critical queries"
|
||||
# }
|
||||
|
||||
mosaic_hook_session_end() {
|
||||
echo "[mosaic-stack] Working tree summary:"
|
||||
git status --short | sed 's/^/[mosaic-stack] /' || true
|
||||
}
|
||||
# Called by session-end.sh
|
||||
# mosaic_hook_session_end() {
|
||||
# echo "Run repo-specific end-of-session checks"
|
||||
# }
|
||||
|
||||
@@ -1 +1 @@
|
||||
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/
|
||||
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/
|
||||
|
||||
+6
-5
@@ -1,6 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
coverage
|
||||
pnpm-lock.yaml
|
||||
**/next-env.d.ts
|
||||
**/dist
|
||||
**/node_modules
|
||||
**/drizzle
|
||||
**/.next
|
||||
.claude/
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
# Trivy CVE Suppressions — Upstream Dependencies
|
||||
# Reviewed: 2026-02-13 | Milestone: M11-CIPipeline
|
||||
#
|
||||
# MITIGATED:
|
||||
# - Go stdlib CVEs (6): gosu rebuilt from source with Go 1.26
|
||||
# - npm bundled CVEs (5): npm removed from production Node.js images
|
||||
# - Node.js 20 → 24 LTS migration (#367): base images updated
|
||||
#
|
||||
# REMAINING: OpenBao (5 CVEs) + Next.js bundled tar/minimatch (5 CVEs)
|
||||
# Re-evaluate when upgrading openbao image beyond 2.5.0 or Next.js beyond 16.1.6.
|
||||
|
||||
# === OpenBao false positives ===
|
||||
# Trivy reads Go module pseudo-version (v0.0.0-20260204...) from bin/bao
|
||||
# and reports CVEs fixed in openbao 2.0.3–2.4.4. We run openbao:2.5.0.
|
||||
CVE-2024-8185 # HIGH: DoS via Raft join (fixed in 2.0.3)
|
||||
CVE-2024-9180 # HIGH: privilege escalation (fixed in 2.0.3)
|
||||
CVE-2025-59043 # HIGH: DoS via malicious JSON (fixed in 2.4.1)
|
||||
CVE-2025-64761 # HIGH: identity group root escalation (fixed in 2.4.4)
|
||||
|
||||
# === Next.js bundled tar/minimatch CVEs (upstream — waiting on Next.js release) ===
|
||||
# Next.js 16.1.6 bundles [email protected] and [email protected] in next/dist/compiled/ (pre-compiled).
|
||||
# These are NOT pnpm dependencies — they're embedded in the Next.js package itself.
|
||||
# pnpm overrides cannot reach these; only a Next.js upgrade can fix them.
|
||||
# Affects web image only (orchestrator and API are clean).
|
||||
# npm was also removed from all production images, eliminating the npm-bundled copy.
|
||||
# To resolve: upgrade Next.js when a release bundles tar >= 7.5.8 and minimatch >= 10.2.1.
|
||||
CVE-2026-23745 # HIGH: tar arbitrary file overwrite via unsanitized linkpaths (fixed in 7.5.3)
|
||||
CVE-2026-23950 # HIGH: tar arbitrary file overwrite via Unicode path collision (fixed in 7.5.4)
|
||||
CVE-2026-24842 # HIGH: tar arbitrary file creation via hardlink path traversal (needs tar >= 7.5.7)
|
||||
CVE-2026-26960 # HIGH: tar arbitrary file read/write via malicious archive hardlink (needs tar >= 7.5.8)
|
||||
CVE-2026-26996 # HIGH: minimatch DoS via specially crafted glob patterns (needs minimatch >= 10.2.1)
|
||||
|
||||
# === OpenBao Go stdlib (waiting on upstream rebuild) ===
|
||||
# OpenBao 2.5.0 compiled with Go 1.25.6, fix needs Go >= 1.25.7.
|
||||
# Cannot build OpenBao from source (large project). Waiting for upstream release.
|
||||
CVE-2025-68121 # CRITICAL: crypto/tls session resumption
|
||||
|
||||
# === multer CVEs (upstream via @nestjs/platform-express) ===
|
||||
# multer <2.1.0 — waiting on NestJS to update their dependency
|
||||
# These are DoS vulnerabilities in file upload handling
|
||||
GHSA-xf7r-hgr6-v32p # HIGH: DoS via incomplete cleanup
|
||||
GHSA-v52c-386h-88mc # HIGH: DoS via resource exhaustion
|
||||
@@ -1,141 +0,0 @@
|
||||
# Woodpecker CI Configuration for Mosaic Stack
|
||||
|
||||
## Pipeline Architecture
|
||||
|
||||
Split per-package pipelines with path filtering. Only affected packages rebuild on push.
|
||||
|
||||
```
|
||||
.woodpecker/
|
||||
├── api.yml # @mosaic/api (NestJS)
|
||||
├── web.yml # @mosaic/web (Next.js)
|
||||
├── orchestrator.yml # @mosaic/orchestrator (NestJS)
|
||||
├── coordinator.yml # mosaic-coordinator (Python/FastAPI)
|
||||
├── infra.yml # postgres + openbao Docker images
|
||||
├── codex-review.yml # AI code/security review (PRs only)
|
||||
├── README.md
|
||||
└── schemas/
|
||||
├── code-review-schema.json
|
||||
└── security-review-schema.json
|
||||
```
|
||||
|
||||
## Path Filtering
|
||||
|
||||
| Pipeline | Triggers On |
|
||||
| ------------------ | --------------------------------------------------- |
|
||||
| `api.yml` | `apps/api/**`, `packages/**`, root configs |
|
||||
| `web.yml` | `apps/web/**`, `packages/**`, root configs |
|
||||
| `orchestrator.yml` | `apps/orchestrator/**`, `packages/**`, root configs |
|
||||
| `coordinator.yml` | `apps/coordinator/**` |
|
||||
| `infra.yml` | `docker/**` |
|
||||
| `codex-review.yml` | All PRs (no path filter) |
|
||||
|
||||
**Root configs** = `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `turbo.json`, `package.json`
|
||||
|
||||
## Security Chain
|
||||
|
||||
Every pipeline follows the full security chain required by the CI/CD guide:
|
||||
|
||||
```
|
||||
source scanning (lint + pnpm audit / bandit + pip-audit)
|
||||
-> docker build (Kaniko)
|
||||
-> container scanning (Trivy: HIGH,CRITICAL)
|
||||
-> package linking (Gitea registry)
|
||||
```
|
||||
|
||||
Docker builds gate on ALL quality + security steps passing.
|
||||
|
||||
## Pipeline Dependency Graphs
|
||||
|
||||
### Node.js Apps (api, web, orchestrator)
|
||||
|
||||
```
|
||||
install -> [security-audit, lint, prisma-generate*]
|
||||
prisma-generate* -> [typecheck, prisma-migrate*]
|
||||
prisma-migrate* -> test
|
||||
[all quality gates] -> build -> docker-build -> trivy -> link
|
||||
```
|
||||
|
||||
_\*prisma steps: api.yml only_
|
||||
|
||||
### Coordinator (Python)
|
||||
|
||||
```
|
||||
install -> [ruff-check, mypy, security-bandit, security-pip-audit, test]
|
||||
[all quality gates] -> docker-build -> trivy -> link
|
||||
```
|
||||
|
||||
### Infrastructure
|
||||
|
||||
```
|
||||
[docker-build-postgres, docker-build-openbao]
|
||||
-> [trivy-postgres, trivy-openbao]
|
||||
-> link
|
||||
```
|
||||
|
||||
## Docker Images
|
||||
|
||||
| Image | Registry Path | Context |
|
||||
| ------------------ | ----------------------------------------------- | ------------------- |
|
||||
| stack-api | `git.mosaicstack.dev/mosaic/stack-api` | `.` (monorepo root) |
|
||||
| stack-web | `git.mosaicstack.dev/mosaic/stack-web` | `.` (monorepo root) |
|
||||
| stack-orchestrator | `git.mosaicstack.dev/mosaic/stack-orchestrator` | `.` (monorepo root) |
|
||||
| stack-coordinator | `git.mosaicstack.dev/mosaic/stack-coordinator` | `apps/coordinator` |
|
||||
| stack-postgres | `git.mosaicstack.dev/mosaic/stack-postgres` | `docker/postgres` |
|
||||
| stack-openbao | `git.mosaicstack.dev/mosaic/stack-openbao` | `docker/openbao` |
|
||||
|
||||
## Image Tagging
|
||||
|
||||
| Condition | Tag | Purpose |
|
||||
| ------------- | -------------------------- | -------------------------- |
|
||||
| Always | `${CI_COMMIT_SHA:0:8}` | Immutable commit reference |
|
||||
| `main` branch | `latest` | Current latest build |
|
||||
| Git tag | tag value (e.g., `v1.0.0`) | Semantic version release |
|
||||
|
||||
## Required Secrets
|
||||
|
||||
Configure in Woodpecker UI (Settings > Secrets):
|
||||
|
||||
| Secret | Scope | Purpose |
|
||||
| ---------------- | ----------------- | ------------------------------------------- |
|
||||
| `gitea_username` | push, manual, tag | Gitea registry auth |
|
||||
| `gitea_token` | push, manual, tag | Gitea registry auth (`package:write` scope) |
|
||||
| `codex_api_key` | pull_request | Codex AI reviews |
|
||||
|
||||
## Codex AI Review Pipeline
|
||||
|
||||
The `codex-review.yml` pipeline runs independently on all PRs:
|
||||
|
||||
- **Code review**: Correctness, code quality, testing, performance
|
||||
- **Security review**: OWASP Top 10, hardcoded secrets, injection flaws
|
||||
|
||||
Fails on blockers or critical/high severity security findings.
|
||||
|
||||
### Local Testing
|
||||
|
||||
```bash
|
||||
~/.claude/scripts/codex/codex-code-review.sh --uncommitted
|
||||
~/.claude/scripts/codex/codex-security-review.sh --uncommitted
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "unauthorized: authentication required"
|
||||
|
||||
- Verify `gitea_username` and `gitea_token` secrets in Woodpecker
|
||||
- Verify token has `package:write` scope
|
||||
|
||||
### Trivy scan fails with HIGH/CRITICAL
|
||||
|
||||
- Check if the vulnerability is in the base image (not our code)
|
||||
- Add to `.trivyignore` if it's a known, accepted risk
|
||||
- Use `--ignore-unfixed` (already set) to skip unfixable CVEs
|
||||
|
||||
### Package linking returns 404
|
||||
|
||||
- Normal for recently pushed packages — retry logic handles this
|
||||
- If persistent: verify package name matches exactly (case-sensitive)
|
||||
|
||||
### Pipeline runs Docker builds on pull requests
|
||||
|
||||
- Docker build steps have `when: branch: [main]` guards
|
||||
- PRs only run quality gates, not Docker builds
|
||||
+67
-307
@@ -1,337 +1,97 @@
|
||||
# Unified CI Pipeline - Mosaic Stack
|
||||
# Single install, parallel quality gates, sequential deploy
|
||||
#
|
||||
# Replaces: api.yml, orchestrator.yml, web.yml
|
||||
# Keeps: coordinator.yml (Python), infra.yml (separate concerns)
|
||||
#
|
||||
# Flow:
|
||||
# install → security-audit
|
||||
# → prisma-generate → lint + typecheck (parallel)
|
||||
# → prisma-migrate → test
|
||||
# → build (after all gates pass)
|
||||
# → docker builds (main only, parallel)
|
||||
# → trivy scans (main only, parallel)
|
||||
# → package linking (main only)
|
||||
variables:
|
||||
- &node_image 'node:22-alpine'
|
||||
- &enable_pnpm 'corepack enable'
|
||||
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
path:
|
||||
include:
|
||||
- "apps/api/**"
|
||||
- "apps/orchestrator/**"
|
||||
- "apps/web/**"
|
||||
- "packages/**"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
- "package.json"
|
||||
- ".woodpecker/ci.yml"
|
||||
- ".trivyignore"
|
||||
|
||||
variables:
|
||||
- &node_image "node:24-alpine"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
- &use_deps |
|
||||
corepack enable
|
||||
- &turbo_env
|
||||
TURBO_API:
|
||||
from_secret: turbo_api
|
||||
TURBO_TOKEN:
|
||||
from_secret: turbo_token
|
||||
TURBO_TEAM:
|
||||
from_secret: turbo_team
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17.7-alpine3.22
|
||||
environment:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
# Turbo remote cache (turbo.mosaicstack.dev) is configured via Woodpecker
|
||||
# repository-level environment variables (TURBO_API, TURBO_TEAM, TURBO_TOKEN).
|
||||
# This avoids from_secret which is blocked on pull_request events.
|
||||
# If the env vars aren't set, turbo falls back to local cache only.
|
||||
|
||||
steps:
|
||||
# ─── Install (once) ─────────────────────────────────────────
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- corepack enable
|
||||
- apk add --no-cache python3 make g++
|
||||
- pnpm install --frozen-lockfile
|
||||
|
||||
# ─── Security Audit (once) ──────────────────────────────────
|
||||
security-audit:
|
||||
# Blocking gate: public framework package must contain no operator-specific
|
||||
# personal data or private $HOME defaults. Runs early (no node_modules needed).
|
||||
sanitization:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm audit --audit-level=high
|
||||
depends_on:
|
||||
- install
|
||||
- apk add --no-cache bash
|
||||
- bash packages/mosaic/framework/tools/quality/scripts/verify-sanitized.sh
|
||||
# Resident line-count ceiling over framework-owned resident files
|
||||
# (Constitution + dispatcher + each RUNTIME.md slice). See DESIGN §7 / R9.
|
||||
- bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh --self-test
|
||||
- bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh
|
||||
|
||||
# ─── Prisma Generate ────────────────────────────────────────
|
||||
prisma-generate:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm --filter "@mosaic/api" prisma:generate
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# ─── Lint (all packages) ────────────────────────────────────
|
||||
lint:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
<<: *turbo_env
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm turbo lint
|
||||
depends_on:
|
||||
- prisma-generate
|
||||
|
||||
# ─── Typecheck (all packages, parallel with lint) ───────────
|
||||
typecheck:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
<<: *turbo_env
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm turbo typecheck
|
||||
- *enable_pnpm
|
||||
- pnpm typecheck
|
||||
depends_on:
|
||||
- prisma-generate
|
||||
- install
|
||||
- sanitization
|
||||
|
||||
# ─── Prisma Migrate (test DB) ──────────────────────────────
|
||||
prisma-migrate:
|
||||
# lint, format, and test are independent — run in parallel after typecheck
|
||||
lint:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm --filter "@mosaic/api" prisma migrate deploy
|
||||
- *enable_pnpm
|
||||
- pnpm lint
|
||||
depends_on:
|
||||
- prisma-generate
|
||||
- typecheck
|
||||
|
||||
format:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- pnpm format:check
|
||||
depends_on:
|
||||
- typecheck
|
||||
|
||||
# ─── Test (all packages) ───────────────────────────────────
|
||||
test:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db?schema=public"
|
||||
ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
<<: *turbo_env
|
||||
# Avoid the namespace-level Woodpecker DB service named "postgres".
|
||||
# The Kubernetes backend exposes service containers by step name.
|
||||
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm --filter "@mosaic/api" exec vitest run --exclude 'src/auth/auth-rls.integration.spec.ts' --exclude 'src/credentials/user-credential.model.spec.ts' --exclude 'src/job-events/job-events.performance.spec.ts' --exclude 'src/knowledge/services/fulltext-search.spec.ts' --exclude 'src/mosaic-telemetry/mosaic-telemetry.module.spec.ts'
|
||||
- pnpm turbo test --filter=@mosaic/orchestrator --filter=@mosaic/web
|
||||
- *enable_pnpm
|
||||
# Install postgresql-client for pg_isready
|
||||
- apk add --no-cache postgresql-client
|
||||
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
|
||||
- |
|
||||
ready=0
|
||||
for i in $(seq 1 60); do
|
||||
if pg_isready -h ci-postgres -p 5432 -U mosaic; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
echo "Waiting for ci-postgres ($i/60)..."
|
||||
sleep 1
|
||||
done
|
||||
if [ "$ready" -ne 1 ]; then
|
||||
echo "ci-postgres did not become ready" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Run migrations (DATABASE_URL is set in environment above)
|
||||
- pnpm --filter @mosaicstack/db run db:migrate
|
||||
# Run all tests
|
||||
- pnpm test
|
||||
depends_on:
|
||||
- prisma-migrate
|
||||
|
||||
# ─── Build (all packages) ──────────────────────────────────
|
||||
build:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
NODE_ENV: "production"
|
||||
<<: *turbo_env
|
||||
commands:
|
||||
- *use_deps
|
||||
- pnpm turbo build
|
||||
depends_on:
|
||||
- lint
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
|
||||
# ─── Docker Builds (main only, parallel) ───────────────────
|
||||
|
||||
docker-build-api:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
services:
|
||||
ci-postgres:
|
||||
image: pgvector/pgvector:pg17
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-api:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-api:latest"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile apps/api/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
docker-build-orchestrator:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-orchestrator:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-orchestrator:latest"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile apps/orchestrator/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
docker-build-web:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-web:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-web:latest"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile apps/web/Dockerfile --snapshot-mode=redo --build-arg NEXT_PUBLIC_API_URL=https://api.mosaicstack.dev $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
# ─── Container Security Scans (main only) ──────────────────
|
||||
|
||||
security-trivy-api:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-api:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-api
|
||||
|
||||
security-trivy-orchestrator:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-orchestrator:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-orchestrator
|
||||
|
||||
security-trivy-web:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then SCAN_TAG="$$CI_COMMIT_TAG"; else SCAN_TAG="latest"; fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore git.mosaicstack.dev/mosaic/stack-web:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-web
|
||||
|
||||
# ─── Package Linking (main only, once) ─────────────────────
|
||||
|
||||
link-packages:
|
||||
image: alpine:3
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- sleep 10
|
||||
- |
|
||||
set -e
|
||||
link_package() {
|
||||
PKG="$$1"
|
||||
echo "Linking $$PKG..."
|
||||
for attempt in 1 2 3; do
|
||||
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $$GITEA_TOKEN" \
|
||||
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||
echo " Linked $$PKG"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "400" ]; then
|
||||
echo " $$PKG already linked"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||
sleep 5
|
||||
else
|
||||
echo " FAILED: $$PKG status $$STATUS"
|
||||
cat /tmp/link-response.txt
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
link_package "stack-api"
|
||||
link_package "stack-orchestrator"
|
||||
link_package "stack-web"
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- security-trivy-api
|
||||
- security-trivy-orchestrator
|
||||
- security-trivy-web
|
||||
POSTGRES_USER: mosaic
|
||||
POSTGRES_PASSWORD: mosaic
|
||||
POSTGRES_DB: mosaic
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
# Coordinator Pipeline - Mosaic Stack
|
||||
# Quality gates, build, and Docker publish for mosaic-coordinator (Python)
|
||||
#
|
||||
# Triggers on: apps/coordinator/**
|
||||
# Security chain: bandit + pip-audit + Trivy container scan
|
||||
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
path:
|
||||
include:
|
||||
- "apps/coordinator/**"
|
||||
- ".woodpecker/coordinator.yml"
|
||||
|
||||
variables:
|
||||
- &python_image "python:3.11-slim"
|
||||
- &activate_venv |
|
||||
cd apps/coordinator
|
||||
. venv/bin/activate
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
steps:
|
||||
# === Quality Gates ===
|
||||
|
||||
install:
|
||||
image: *python_image
|
||||
commands:
|
||||
- cd apps/coordinator
|
||||
- python -m venv venv
|
||||
- . venv/bin/activate
|
||||
- pip install --no-cache-dir --upgrade "pip>=25.3"
|
||||
- pip install --no-cache-dir --extra-index-url https://git.mosaicstack.dev/api/packages/mosaic/pypi/simple/ -e ".[dev]"
|
||||
- pip install --no-cache-dir bandit pip-audit
|
||||
|
||||
ruff-check:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- ruff check src/ tests/
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
mypy:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- mypy src/
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
security-bandit:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- bandit -r src/ -c bandit.yaml -f screen
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
security-pip-audit:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- pip-audit
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
test:
|
||||
image: *python_image
|
||||
commands:
|
||||
- *activate_venv
|
||||
- pytest
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
# === Docker Build & Push ===
|
||||
|
||||
docker-build-coordinator:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-coordinator:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-coordinator:latest"
|
||||
fi
|
||||
/kaniko/executor --context apps/coordinator --dockerfile apps/coordinator/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- ruff-check
|
||||
- mypy
|
||||
- security-bandit
|
||||
- security-pip-audit
|
||||
- test
|
||||
|
||||
# === Container Security Scan ===
|
||||
|
||||
security-trivy-coordinator:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
SCAN_TAG="latest"
|
||||
else
|
||||
SCAN_TAG="latest"
|
||||
fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
--ignorefile .trivyignore \
|
||||
git.mosaicstack.dev/mosaic/stack-coordinator:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-coordinator
|
||||
|
||||
# === Package Linking ===
|
||||
|
||||
link-packages:
|
||||
image: alpine:3
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- sleep 10
|
||||
- |
|
||||
set -e
|
||||
link_package() {
|
||||
PKG="$$1"
|
||||
echo "Linking $$PKG..."
|
||||
for attempt in 1 2 3; do
|
||||
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $$GITEA_TOKEN" \
|
||||
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||
echo " Linked $$PKG"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "400" ]; then
|
||||
echo " $$PKG already linked"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||
sleep 5
|
||||
else
|
||||
echo " FAILED: $$PKG status $$STATUS"
|
||||
cat /tmp/link-response.txt
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
link_package "stack-coordinator"
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- security-trivy-coordinator
|
||||
@@ -1,170 +0,0 @@
|
||||
# Infrastructure Pipeline - Mosaic Stack
|
||||
# Docker build, Trivy scan, and publish for postgres + openbao images
|
||||
#
|
||||
# Triggers on: docker/**
|
||||
# No quality gates — infrastructure images (base image + config only)
|
||||
|
||||
when:
|
||||
- event: [push, manual, tag]
|
||||
path:
|
||||
include:
|
||||
- "docker/**"
|
||||
- ".woodpecker/infra.yml"
|
||||
|
||||
variables:
|
||||
- &kaniko_setup |
|
||||
mkdir -p /kaniko/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json
|
||||
|
||||
steps:
|
||||
# === Docker Build & Push ===
|
||||
|
||||
docker-build-postgres:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-postgres:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-postgres:latest"
|
||||
fi
|
||||
/kaniko/executor --context docker/postgres --dockerfile docker/postgres/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
|
||||
docker-build-openbao:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- *kaniko_setup
|
||||
- |
|
||||
DESTINATIONS=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-openbao:$CI_COMMIT_TAG"
|
||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaic/stack-openbao:latest"
|
||||
fi
|
||||
/kaniko/executor --context docker/openbao --dockerfile docker/openbao/Dockerfile --snapshot-mode=redo $DESTINATIONS
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
|
||||
# === Container Security Scans ===
|
||||
|
||||
security-trivy-postgres:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
SCAN_TAG="latest"
|
||||
else
|
||||
SCAN_TAG="latest"
|
||||
fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
--ignorefile .trivyignore \
|
||||
git.mosaicstack.dev/mosaic/stack-postgres:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-postgres
|
||||
|
||||
security-trivy-openbao:
|
||||
image: aquasec/trivy:latest
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_username
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
commands:
|
||||
- |
|
||||
if [ -n "$$CI_COMMIT_TAG" ]; then
|
||||
SCAN_TAG="$$CI_COMMIT_TAG"
|
||||
elif [ "$$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
SCAN_TAG="latest"
|
||||
else
|
||||
SCAN_TAG="latest"
|
||||
fi
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$$GITEA_USER\",\"password\":\"$$GITEA_TOKEN\"}}}" > ~/.docker/config.json
|
||||
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
--ignorefile .trivyignore \
|
||||
git.mosaicstack.dev/mosaic/stack-openbao:$$SCAN_TAG
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- docker-build-openbao
|
||||
|
||||
# === Package Linking ===
|
||||
|
||||
link-packages:
|
||||
image: alpine:3
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- sleep 10
|
||||
- |
|
||||
set -e
|
||||
link_package() {
|
||||
PKG="$$1"
|
||||
echo "Linking $$PKG..."
|
||||
for attempt in 1 2 3; do
|
||||
STATUS=$$(curl -s -o /tmp/link-response.txt -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $$GITEA_TOKEN" \
|
||||
"https://git.mosaicstack.dev/api/v1/packages/mosaic/container/$$PKG/-/link/stack")
|
||||
if [ "$$STATUS" = "201" ] || [ "$$STATUS" = "204" ]; then
|
||||
echo " Linked $$PKG"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "400" ]; then
|
||||
echo " $$PKG already linked"
|
||||
return 0
|
||||
elif [ "$$STATUS" = "404" ] && [ $$attempt -lt 3 ]; then
|
||||
echo " $$PKG not found yet, retrying in 5s (attempt $$attempt/3)..."
|
||||
sleep 5
|
||||
else
|
||||
echo " FAILED: $$PKG status $$STATUS"
|
||||
cat /tmp/link-response.txt
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
link_package "stack-postgres"
|
||||
link_package "stack-openbao"
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
depends_on:
|
||||
- security-trivy-postgres
|
||||
- security-trivy-openbao
|
||||
@@ -0,0 +1,165 @@
|
||||
# Build, publish npm packages, and push Docker images
|
||||
# Runs only on main branch push/tag
|
||||
|
||||
variables:
|
||||
- &node_image 'node:22-alpine'
|
||||
- &enable_pnpm 'corepack enable'
|
||||
|
||||
when:
|
||||
- branch: [main]
|
||||
event: [push, manual, tag]
|
||||
|
||||
steps:
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- corepack enable
|
||||
- pnpm install --frozen-lockfile
|
||||
|
||||
build:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
- pnpm build
|
||||
depends_on:
|
||||
- install
|
||||
|
||||
publish-npm:
|
||||
image: *node_image
|
||||
environment:
|
||||
NPM_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- *enable_pnpm
|
||||
# Configure auth for Gitea npm registry
|
||||
- |
|
||||
echo "//git.mosaicstack.dev/api/packages/mosaicstack/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
echo "@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/" >> ~/.npmrc
|
||||
# Publish non-private packages to Gitea.
|
||||
#
|
||||
# The only publish failure we tolerate is "version already exists" —
|
||||
# that legitimately happens when only some packages were bumped in
|
||||
# the merge. Any other failure (registry 404, auth error, network
|
||||
# error) MUST fail the pipeline loudly: the previous
|
||||
# `|| echo "... continuing"` fallback silently hid a 404 from the
|
||||
# Gitea org rename and caused every @mosaicstack/* publish to fall
|
||||
# on the floor while CI still reported green.
|
||||
- |
|
||||
# Portable sh (Alpine ash) — avoid bashisms like PIPESTATUS.
|
||||
set +e
|
||||
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" publish --no-git-checks --access public >/tmp/publish.log 2>&1
|
||||
EXIT=$?
|
||||
set -e
|
||||
cat /tmp/publish.log
|
||||
if [ "$EXIT" -eq 0 ]; then
|
||||
echo "[publish] all packages published successfully"
|
||||
exit 0
|
||||
fi
|
||||
# Hard registry / auth / network errors → fatal. Match npm's own
|
||||
# error lines specifically to avoid false positives on arbitrary
|
||||
# log text that happens to contain "E404" etc.
|
||||
if grep -qE "npm (error|ERR!) code (E404|E401|ENEEDAUTH|ECONNREFUSED|ETIMEDOUT|ENOTFOUND)" /tmp/publish.log; then
|
||||
echo "[publish] FATAL: registry/auth/network error detected — failing pipeline" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Only tolerate the explicit "version already published" case.
|
||||
# npm returns this as E403 with body "You cannot publish over..."
|
||||
# or EPUBLISHCONFLICT depending on version.
|
||||
if grep -qE "EPUBLISHCONFLICT|You cannot publish over|previously published" /tmp/publish.log; then
|
||||
echo "[publish] some packages already at this version — continuing (non-fatal)"
|
||||
exit 0
|
||||
fi
|
||||
echo "[publish] FATAL: publish failed with unrecognized error — failing pipeline" >&2
|
||||
exit 1
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
# TODO: Uncomment when ready to publish to npmjs.org
|
||||
# publish-npmjs:
|
||||
# image: *node_image
|
||||
# environment:
|
||||
# NPM_TOKEN:
|
||||
# from_secret: npmjs_token
|
||||
# commands:
|
||||
# - *enable_pnpm
|
||||
# - apk add --no-cache jq bash
|
||||
# - bash scripts/publish-npmjs.sh
|
||||
# depends_on:
|
||||
# - build
|
||||
# when:
|
||||
# - event: [tag]
|
||||
|
||||
build-gateway:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
REGISTRY_USER:
|
||||
from_secret: gitea_username
|
||||
REGISTRY_PASS:
|
||||
from_secret: gitea_password
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||
- |
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
|
||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:latest"
|
||||
fi
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:$CI_COMMIT_TAG"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile docker/gateway.Dockerfile $DESTINATIONS
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
build-appservice:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
REGISTRY_USER:
|
||||
from_secret: gitea_username
|
||||
REGISTRY_PASS:
|
||||
from_secret: gitea_password
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||
- |
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/appservice:sha-${CI_COMMIT_SHA:0:7}"
|
||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:latest"
|
||||
fi
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:$CI_COMMIT_TAG"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile docker/appservice.Dockerfile $DESTINATIONS
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
build-web:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
environment:
|
||||
REGISTRY_USER:
|
||||
from_secret: gitea_username
|
||||
REGISTRY_PASS:
|
||||
from_secret: gitea_password
|
||||
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
||||
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
||||
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||
- |
|
||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
||||
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:latest"
|
||||
fi
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG"
|
||||
fi
|
||||
/kaniko/executor --context . --dockerfile docker/web.Dockerfile $DESTINATIONS
|
||||
depends_on:
|
||||
- build
|
||||
@@ -1,82 +1,80 @@
|
||||
# Mosaic Stack — Agent Guidelines
|
||||
# Agent Guidelines — Mosaic Stack
|
||||
|
||||
## Load Order
|
||||
## Required Load Order
|
||||
|
||||
1. `SOUL.md` (repo identity + behavior invariants)
|
||||
2. `~/.config/mosaic/STANDARDS.md` (machine-wide standards rails)
|
||||
3. `AGENTS.md` (repo-specific overlay)
|
||||
4. `.mosaic/repo-hooks.sh` (repo lifecycle hooks)
|
||||
1. `~/.config/mosaic/SOUL.md`
|
||||
2. `~/.config/mosaic/STANDARDS.md`
|
||||
3. `~/.config/mosaic/AGENTS.md`
|
||||
4. `~/.config/mosaic/guides/E2E-DELIVERY.md`
|
||||
5. `AGENTS.md` (this file)
|
||||
6. Runtime-specific guide: `~/.config/mosaic/runtime/<runtime>/RUNTIME.md`
|
||||
|
||||
## Runtime Contract
|
||||
## Project Context
|
||||
|
||||
- This file is authoritative for repo-local operations.
|
||||
- `CLAUDE.md` is a compatibility pointer to `AGENTS.md`.
|
||||
- Follow universal rails from `~/.config/mosaic/guides/` and `~/.config/mosaic/rails/`.
|
||||
Mosaic Stack is a self-hosted, multi-user AI agent platform. TypeScript monorepo with NestJS gateway, Next.js web dashboard, Pi SDK agent runtime, and plugin architecture for Discord/Telegram.
|
||||
|
||||
## Session Lifecycle
|
||||
## Package Map
|
||||
|
||||
| Package | Purpose | Key Dependencies |
|
||||
| ------------------ | ------------------------------- | -------------------------------- |
|
||||
| `apps/gateway` | NestJS API + WebSocket hub | Fastify, Socket.IO, Pi SDK, OTEL |
|
||||
| `apps/web` | Next.js dashboard | React 19, Tailwind |
|
||||
| `packages/types` | Shared TypeScript contracts | class-validator |
|
||||
| `packages/db` | Drizzle ORM schema + migrations | drizzle-orm, postgres |
|
||||
| `packages/auth` | BetterAuth configuration | better-auth, @mosaicstack/db |
|
||||
| `packages/brain` | Data layer (PG-backed) | @mosaicstack/db |
|
||||
| `packages/queue` | Valkey task queue + MCP | ioredis |
|
||||
| `packages/coord` | Mission coordination | @mosaicstack/queue |
|
||||
| `packages/mosaic` | Unified `mosaic` CLI + TUI | Ink, Pi SDK, commander |
|
||||
| `plugins/discord` | Discord channel plugin | discord.js |
|
||||
| `plugins/telegram` | Telegram channel plugin | Telegraf |
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
1. Gateway is the single API surface — all clients connect through it
|
||||
2. Pi SDK is ESM-only — gateway and CLI must use ESM
|
||||
3. Socket.IO typed events defined in `@mosaicstack/types` enforce compile-time contracts
|
||||
4. OTEL auto-instrumentation loads before NestJS bootstrap
|
||||
5. BetterAuth manages auth tables; schema defined in `@mosaicstack/db`
|
||||
6. Docker Compose provides PG (5433), Valkey (6380), OTEL Collector (4317/4318), Jaeger (16686)
|
||||
7. Explicit `@Inject()` decorators required in NestJS (tsx/esbuild doesn't emit decorator metadata)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
```bash
|
||||
bash scripts/agent/session-start.sh
|
||||
bash scripts/agent/critical.sh
|
||||
bash scripts/agent/session-end.sh
|
||||
docker compose up -d # Infrastructure
|
||||
pnpm install # Dependencies
|
||||
pnpm typecheck && pnpm lint && pnpm format:check # Quality gates
|
||||
```
|
||||
|
||||
Optional:
|
||||
## Repo-Specific Notes
|
||||
|
||||
```bash
|
||||
bash scripts/agent/log-limitation.sh "Short Name"
|
||||
bash scripts/agent/orchestrator-daemon.sh status
|
||||
bash scripts/agent/orchestrator-events.sh recent --limit 50
|
||||
- DTOs in `*.dto.ts` files at module boundaries
|
||||
- ESM everywhere (`"type": "module"`, `.js` extensions in imports)
|
||||
- NodeNext module resolution in all tsconfigs
|
||||
- Scratchpads are mandatory for non-trivial tasks
|
||||
|
||||
## docs/TASKS.md — Schema (CANONICAL)
|
||||
|
||||
The `agent` column specifies the required model for each task. **This is set at task creation by the orchestrator and must not be changed by workers.**
|
||||
|
||||
| Value | When to use | Budget |
|
||||
| --------- | ----------------------------------------------------------- | -------------------------- |
|
||||
| `codex` | All coding tasks (default for implementation) | OpenAI credits — preferred |
|
||||
| `glm-5.1` | Cost-sensitive coding where Codex is unavailable | Z.ai credits |
|
||||
| `haiku` | Review gates, verify tasks, status checks, docs-only | Cheapest Claude tier |
|
||||
| `sonnet` | Complex planning, multi-file reasoning, architecture review | Claude quota |
|
||||
| `opus` | Major cross-cutting architecture decisions ONLY | Most expensive — minimize |
|
||||
| `—` | No preference / auto-select cheapest capable | Pipeline decides |
|
||||
|
||||
Pipeline crons read this column and spawn accordingly. Workers never modify `docs/TASKS.md` — only the orchestrator writes it.
|
||||
|
||||
**Full schema:**
|
||||
|
||||
```
|
||||
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
|
||||
```
|
||||
|
||||
## Repo Context
|
||||
|
||||
- Platform: multi-tenant personal assistant stack
|
||||
- Monorepo: `pnpm` workspaces + Turborepo
|
||||
- Core apps: `apps/api` (NestJS), `apps/web` (Next.js), orchestrator/coordinator services
|
||||
- Infrastructure: Docker Compose + PostgreSQL + Valkey + Authentik
|
||||
|
||||
## Quick Command Set
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
pnpm test
|
||||
pnpm lint
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Versioning Protocol (HARD GATE)
|
||||
|
||||
**This project is ALPHA. All versions MUST be `0.0.x`.**
|
||||
|
||||
- The `0.1.0` release is FORBIDDEN until Jason explicitly authorizes it.
|
||||
- Every milestone bump increments the patch: `0.0.20` → `0.0.21` → `0.0.22`, etc.
|
||||
- ALL package.json files in the monorepo MUST stay in sync at the same version.
|
||||
- Use `scripts/version-bump.sh <version>` to bump — it enforces the alpha constraint and updates all packages atomically.
|
||||
- The script rejects any version >= `0.1.0`.
|
||||
- When creating a release tag, the tag MUST match the package version: `v0.0.x`.
|
||||
|
||||
**Milestone-to-version mapping** is defined in the PRD (`docs/PRD.md`) under "Delivery/Milestone Intent". Agents MUST use the version from that table when tagging a milestone release.
|
||||
|
||||
**Violation of this protocol is a blocking error.** If an agent attempts to set a version >= `0.1.0`, stop and escalate.
|
||||
|
||||
## Standards and Quality
|
||||
|
||||
- Enforce strict typing and no unsafe shortcuts.
|
||||
- Keep lint/typecheck/tests green before completion.
|
||||
- Prefer small, focused commits and clear change descriptions.
|
||||
|
||||
## App-Specific Overlays
|
||||
|
||||
- `apps/api/AGENTS.md`
|
||||
- `apps/web/AGENTS.md`
|
||||
- `apps/coordinator/AGENTS.md`
|
||||
- `apps/orchestrator/AGENTS.md`
|
||||
|
||||
## Additional Guidance
|
||||
|
||||
- Orchestrator guidance: `docs/claude/orchestrator.md`
|
||||
- Security remediation context: `docs/reports/codebase-review-2026-02-05/01-security-review.md`
|
||||
- Code quality context: `docs/reports/codebase-review-2026-02-05/02-code-quality-review.md`
|
||||
- QA context: `docs/reports/codebase-review-2026-02-05/03-qa-test-coverage.md`
|
||||
- `status`: `not-started` | `in-progress` | `done` | `failed` | `blocked` | `needs-qa`
|
||||
- `agent`: model value from table above (set before spawning)
|
||||
- `estimate`: token budget e.g. `8K`, `25K`
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
# CLAUDE Compatibility Pointer
|
||||
# CLAUDE.md — Mosaic Stack
|
||||
|
||||
This file exists so Claude Code sessions load Mosaic standards.
|
||||
## Project
|
||||
|
||||
## MANDATORY — Read Before Any Response
|
||||
Self-hosted, multi-user AI agent platform. TypeScript monorepo.
|
||||
|
||||
BEFORE responding to any user message, READ `~/.config/mosaic/AGENTS.md`.
|
||||
## Stack
|
||||
|
||||
That file is the universal agent configuration. Do NOT respond until you have loaded it.
|
||||
Then read the project-local `AGENTS.md` in this repository for project-specific guidance.
|
||||
- **API**: NestJS + Fastify adapter (`apps/gateway`)
|
||||
- **Web**: Next.js 16 + React 19 (`apps/web`)
|
||||
- **ORM**: Drizzle ORM + PostgreSQL 17 + pgvector (`packages/db`)
|
||||
- **Auth**: BetterAuth (`packages/auth`)
|
||||
- **Agent**: Pi SDK (`packages/agent`, `packages/mosaic`)
|
||||
- **Queue**: Valkey 8 (`packages/queue`)
|
||||
- **Build**: pnpm workspaces + Turborepo
|
||||
- **CI**: Woodpecker CI
|
||||
- **Observability**: OpenTelemetry → Jaeger
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm typecheck # TypeScript check (all packages)
|
||||
pnpm lint # ESLint (all packages)
|
||||
pnpm format:check # Prettier check
|
||||
pnpm test # Vitest (all packages)
|
||||
pnpm build # Build all packages
|
||||
|
||||
# Database
|
||||
pnpm --filter @mosaicstack/db db:push # Push schema to PG (dev)
|
||||
pnpm --filter @mosaicstack/db db:generate # Generate migrations
|
||||
pnpm --filter @mosaicstack/db db:migrate # Run migrations
|
||||
|
||||
# Dev
|
||||
docker compose up -d # Start PG, Valkey, OTEL, Jaeger
|
||||
pnpm --filter @mosaicstack/gateway exec tsx src/main.ts # Start gateway
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- ESM everywhere (`"type": "module"`, `.js` extensions in imports)
|
||||
- NodeNext module resolution
|
||||
- Explicit `@Inject()` decorators in NestJS (tsx/esbuild doesn't support emitDecoratorMetadata)
|
||||
- DTOs in `*.dto.ts` files at module boundaries
|
||||
- OTEL tracing imported before NestJS bootstrap (`import './tracing.js'`)
|
||||
- All three gates must pass before push: typecheck, lint, format:check
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# Cron Job Configuration - Issue #29
|
||||
|
||||
## Overview
|
||||
|
||||
Implement cron job configuration for Mosaic Stack, likely as a MoltBot plugin for scheduled reminders/commands.
|
||||
|
||||
## Requirements (inferred from CLAUDE.md pattern)
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
plugins/mosaic-plugin-cron/
|
||||
├── SKILL.md # MoltBot skill definition
|
||||
├── src/
|
||||
│ └── cron.service.ts
|
||||
└── cron.service.test.ts
|
||||
```
|
||||
|
||||
### Core Features
|
||||
|
||||
1. Create/update/delete cron schedules
|
||||
2. Trigger MoltBot commands on schedule
|
||||
3. Workspace-scoped (RLS)
|
||||
4. PDA-friendly UI
|
||||
|
||||
### API Endpoints (inferred)
|
||||
|
||||
- `POST /api/cron` - Create schedule
|
||||
- `GET /api/cron` - List schedules
|
||||
- `DELETE /api/cron/:id` - Delete schedule
|
||||
|
||||
### Database (Prisma)
|
||||
|
||||
```prisma
|
||||
model CronSchedule {
|
||||
id String @id @default(uuid())
|
||||
workspaceId String
|
||||
expression String // cron expression
|
||||
command String // MoltBot command to trigger
|
||||
enabled Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([workspaceId])
|
||||
}
|
||||
```
|
||||
|
||||
## TDD Approach
|
||||
|
||||
1. **RED** - Write tests for CronService
|
||||
2. **GREEN** - Implement minimal service
|
||||
3. **REFACTOR** - Add CRUD controller + API endpoints
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [ ] Create feature branch: `git checkout -b feature/29-cron-config`
|
||||
- [ ] Write failing tests for cron service
|
||||
- [ ] Implement service (Green)
|
||||
- [ ] Add controller & routes
|
||||
- [ ] Add Prisma schema migration
|
||||
- [ ] Create MoltBot skill (SKILL.md)
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Mosaic Stack
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,145 +0,0 @@
|
||||
.PHONY: help install dev build test docker-up docker-down docker-logs docker-ps docker-build docker-restart docker-test speech-up speech-down speech-logs clean matrix-up matrix-down matrix-logs matrix-setup-bot
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Mosaic Stack - Available commands:"
|
||||
@echo ""
|
||||
@echo "Development:"
|
||||
@echo " make install Install dependencies"
|
||||
@echo " make dev Start development servers"
|
||||
@echo " make build Build all applications"
|
||||
@echo " make test Run all tests"
|
||||
@echo " make lint Run linters"
|
||||
@echo " make format Format code"
|
||||
@echo ""
|
||||
@echo "Docker:"
|
||||
@echo " make docker-up Start Docker services (core)"
|
||||
@echo " make docker-up-full Start Docker services (all)"
|
||||
@echo " make docker-up-traefik Start with bundled Traefik"
|
||||
@echo " make docker-down Stop Docker services"
|
||||
@echo " make docker-logs View Docker logs"
|
||||
@echo " make docker-ps Show Docker service status"
|
||||
@echo " make docker-build Rebuild Docker images"
|
||||
@echo " make docker-restart Restart Docker services"
|
||||
@echo " make docker-test Run Docker smoke test"
|
||||
@echo " make docker-test-traefik Run Traefik integration tests"
|
||||
@echo ""
|
||||
@echo "Speech Services:"
|
||||
@echo " make speech-up Start speech services (STT + TTS)"
|
||||
@echo " make speech-down Stop speech services"
|
||||
@echo " make speech-logs View speech service logs"
|
||||
@echo ""
|
||||
@echo "Matrix Dev Environment:"
|
||||
@echo " make matrix-up Start Matrix services (Synapse + Element)"
|
||||
@echo " make matrix-down Stop Matrix services"
|
||||
@echo " make matrix-logs View Matrix service logs"
|
||||
@echo " make matrix-setup-bot Create bot account and get access token"
|
||||
@echo ""
|
||||
@echo "Database:"
|
||||
@echo " make db-migrate Run database migrations"
|
||||
@echo " make db-seed Seed development data"
|
||||
@echo " make db-studio Open Prisma Studio"
|
||||
@echo " make db-reset Reset database (WARNING: deletes data)"
|
||||
@echo ""
|
||||
@echo "Cleanup:"
|
||||
@echo " make clean Clean build artifacts"
|
||||
@echo " make clean-all Clean everything including node_modules"
|
||||
@echo " make docker-clean Remove Docker containers and volumes"
|
||||
|
||||
# Development
|
||||
install:
|
||||
pnpm install
|
||||
|
||||
dev:
|
||||
pnpm dev
|
||||
|
||||
build:
|
||||
pnpm build
|
||||
|
||||
test:
|
||||
pnpm test
|
||||
|
||||
lint:
|
||||
pnpm lint
|
||||
|
||||
format:
|
||||
pnpm format
|
||||
|
||||
# Docker operations
|
||||
docker-up:
|
||||
docker compose up -d
|
||||
|
||||
docker-up-full:
|
||||
docker compose --profile full up -d
|
||||
|
||||
docker-up-traefik:
|
||||
docker compose --profile traefik-bundled up -d
|
||||
|
||||
docker-down:
|
||||
docker compose down
|
||||
|
||||
docker-logs:
|
||||
docker compose logs -f
|
||||
|
||||
docker-ps:
|
||||
docker compose ps
|
||||
|
||||
docker-build:
|
||||
docker compose build
|
||||
|
||||
docker-restart:
|
||||
docker compose restart
|
||||
|
||||
docker-test:
|
||||
./scripts/test-docker-deployment.sh
|
||||
|
||||
docker-test-traefik:
|
||||
./tests/integration/docker/traefik.test.sh all
|
||||
|
||||
# Speech services
|
||||
speech-up:
|
||||
docker compose -f docker-compose.yml -f docker-compose.speech.yml up -d speaches kokoro-tts
|
||||
|
||||
speech-down:
|
||||
docker compose -f docker-compose.yml -f docker-compose.speech.yml down --remove-orphans
|
||||
|
||||
speech-logs:
|
||||
docker compose -f docker-compose.yml -f docker-compose.speech.yml logs -f speaches kokoro-tts
|
||||
|
||||
# Matrix Dev Environment
|
||||
matrix-up:
|
||||
docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml up -d
|
||||
|
||||
matrix-down:
|
||||
docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml down
|
||||
|
||||
matrix-logs:
|
||||
docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml logs -f synapse element-web
|
||||
|
||||
matrix-setup-bot:
|
||||
docker/matrix/scripts/setup-bot.sh
|
||||
|
||||
# Database operations
|
||||
db-migrate:
|
||||
cd apps/api && pnpm prisma:migrate
|
||||
|
||||
db-seed:
|
||||
cd apps/api && pnpm prisma:seed
|
||||
|
||||
db-studio:
|
||||
cd apps/api && pnpm prisma:studio
|
||||
|
||||
db-reset:
|
||||
cd apps/api && pnpm prisma:reset
|
||||
|
||||
# Cleanup
|
||||
clean:
|
||||
pnpm clean
|
||||
|
||||
clean-all:
|
||||
pnpm clean
|
||||
rm -rf node_modules
|
||||
|
||||
docker-clean:
|
||||
docker compose down -v
|
||||
docker system prune -f
|
||||
@@ -1,20 +0,0 @@
|
||||
# Mosaic Stack Soul
|
||||
|
||||
You are Jarvis for the Mosaic Stack repository, running on the current agent runtime.
|
||||
|
||||
## Behavioral Invariants
|
||||
|
||||
- Identity first: answer identity prompts as Jarvis for this repository.
|
||||
- Implementation detail second: runtime (Codex/Claude/OpenCode/etc.) is secondary metadata.
|
||||
- Be proactive: surface risks, blockers, and next actions without waiting.
|
||||
- Be calm and clear: keep responses concise, chunked, and PDA-friendly.
|
||||
- Respect canonical sources:
|
||||
- Repo operations and conventions: `AGENTS.md`
|
||||
- Machine-wide rails: `~/.config/mosaic/STANDARDS.md`
|
||||
- Repo lifecycle hooks: `.mosaic/repo-hooks.sh`
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not claim completion without verification evidence.
|
||||
- Do not bypass lint/type/test quality gates.
|
||||
- Prefer explicit assumptions and concrete file/command references.
|
||||
@@ -1,48 +0,0 @@
|
||||
# Node modules
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log
|
||||
|
||||
# Build output
|
||||
dist
|
||||
build
|
||||
*.tsbuildinfo
|
||||
|
||||
# Tests
|
||||
coverage
|
||||
.vitest
|
||||
test
|
||||
*.spec.ts
|
||||
*.test.ts
|
||||
|
||||
# Development files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
docs
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
@@ -1,40 +0,0 @@
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/database
|
||||
|
||||
# System Administration
|
||||
# Comma-separated list of user IDs that have system administrator privileges
|
||||
# These users can perform system-level operations across all workspaces
|
||||
# Note: Workspace ownership does NOT grant system admin access
|
||||
# SYSTEM_ADMIN_IDS=uuid1,uuid2,uuid3
|
||||
|
||||
# Federation Instance Identity
|
||||
# Display name for this Mosaic instance
|
||||
INSTANCE_NAME=Mosaic Instance
|
||||
# Publicly accessible URL for federation (must be valid HTTP/HTTPS URL)
|
||||
INSTANCE_URL=http://localhost:3000
|
||||
|
||||
# Encryption (AES-256-GCM for sensitive data at rest)
|
||||
# CRITICAL: Generate a secure random key for production!
|
||||
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
|
||||
# CSRF Protection (Required in production)
|
||||
# Secret key for HMAC binding CSRF tokens to user sessions
|
||||
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
# In development, a random key is generated if not set
|
||||
CSRF_SECRET=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210
|
||||
|
||||
# OpenTelemetry Configuration
|
||||
# Enable/disable OpenTelemetry tracing (default: true)
|
||||
OTEL_ENABLED=true
|
||||
# Service name for telemetry (default: mosaic-api)
|
||||
OTEL_SERVICE_NAME=mosaic-api
|
||||
# OTLP exporter endpoint (default: http://localhost:4318/v1/traces)
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
||||
# Alternative: Jaeger endpoint (legacy)
|
||||
# OTEL_EXPORTER_JAEGER_ENDPOINT=http://localhost:4318/v1/traces
|
||||
# Deployment environment (default: development, or uses NODE_ENV)
|
||||
# OTEL_DEPLOYMENT_ENVIRONMENT=production
|
||||
# Trace sampling ratio: 0.0 (none) to 1.0 (all) - default: 1.0
|
||||
# Use lower values in high-traffic production environments
|
||||
# OTEL_TRACES_SAMPLER_ARG=1.0
|
||||
@@ -1,5 +0,0 @@
|
||||
DATABASE_URL="postgresql://test:test@localhost:5432/test"
|
||||
ENCRYPTION_KEY="test-encryption-key-32-characters"
|
||||
JWT_SECRET="test-jwt-secret"
|
||||
INSTANCE_NAME="Test Instance"
|
||||
INSTANCE_URL="https://test.example.com"
|
||||
@@ -1,9 +0,0 @@
|
||||
# WARNING: These are example test credentials for local integration testing.
|
||||
# Copy this file to .env.test and customize the values for your local environment.
|
||||
# NEVER use these credentials in any shared environment or commit .env.test to git.
|
||||
|
||||
DATABASE_URL="postgresql://test:test@localhost:5432/test"
|
||||
ENCRYPTION_KEY="test-encryption-key-32-characters"
|
||||
JWT_SECRET="test-jwt-secret"
|
||||
INSTANCE_NAME="Test Instance"
|
||||
INSTANCE_URL="https://test.example.com"
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "es2022",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"keepClassNames": true
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# api — Agent Context
|
||||
|
||||
> Part of the apps layer.
|
||||
|
||||
## Patterns
|
||||
|
||||
- **Config validation pattern**: Config files use exported validation functions + typed getter functions (not class-validator). See `auth.config.ts`, `federation.config.ts`, `speech/speech.config.ts`. Pattern: export `isXEnabled()`, `validateXConfig()`, and `getXConfig()` functions.
|
||||
- **Config registerAs**: `speech.config.ts` also exports a `registerAs("speech", ...)` factory for NestJS ConfigModule namespaced injection. Use `ConfigModule.forFeature(speechConfig)` in module imports and access via `this.config.get<string>('speech.stt.baseUrl')`.
|
||||
- **Conditional config validation**: When a service has an enabled flag (e.g., `STT_ENABLED`), URL/connection vars are only required when enabled. Validation throws with a helpful message suggesting how to disable.
|
||||
- **Boolean env parsing**: Use `value === "true" || value === "1"` pattern. No default-true -- all services default to disabled when env var is unset.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Prisma client must be generated** before `tsc --noEmit` will pass. Run `pnpm prisma:generate` first. Pre-existing type errors from Prisma are expected in worktrees without generated client.
|
||||
- **Pre-commit hooks**: lint-staged runs on staged files. If other packages' files are staged, their lint must pass too. Only stage files you intend to commit.
|
||||
- **vitest runs all test files**: Even when targeting a specific test file, vitest loads all spec files. Many will fail if Prisma client isn't generated -- this is expected. Check only your target file's pass/fail status.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `src/speech/speech.config.ts` | Speech services env var validation and typed config (STT, TTS, limits) |
|
||||
| `src/speech/speech.config.spec.ts` | Unit tests for speech config validation (51 tests) |
|
||||
| `src/auth/auth.config.ts` | Auth/OIDC config validation (reference pattern) |
|
||||
| `src/federation/federation.config.ts` | Federation config validation (reference pattern) |
|
||||
@@ -1,115 +0,0 @@
|
||||
# Base image for all stages
|
||||
# Uses Debian slim (glibc) instead of Alpine (musl) because native Node.js addons
|
||||
# (matrix-sdk-crypto-nodejs, Prisma engines) require glibc-compatible binaries.
|
||||
FROM node:24-slim AS base
|
||||
|
||||
# Install pnpm globally
|
||||
RUN corepack enable && corepack prepare [email protected] --activate
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy monorepo configuration files
|
||||
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
|
||||
COPY turbo.json ./
|
||||
|
||||
# ======================
|
||||
# Dependencies stage
|
||||
# ======================
|
||||
FROM base AS deps
|
||||
|
||||
# Install build tools for native addons (node-pty requires node-gyp compilation)
|
||||
# and OpenSSL for Prisma engine detection
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 make g++ openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy all package.json files for workspace resolution
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
COPY packages/ui/package.json ./packages/ui/
|
||||
COPY packages/config/package.json ./packages/config/
|
||||
COPY apps/api/package.json ./apps/api/
|
||||
|
||||
# Install dependencies (no cache mount — Kaniko builds are ephemeral in CI)
|
||||
# Then explicitly rebuild node-pty from source since pnpm may skip postinstall
|
||||
# scripts or fail to find prebuilt binaries for this Node.js version
|
||||
RUN pnpm install --frozen-lockfile \
|
||||
&& cd node_modules/.pnpm/node-pty@*/node_modules/node-pty \
|
||||
&& npx node-gyp rebuild 2>&1 || true
|
||||
|
||||
# ======================
|
||||
# Builder stage
|
||||
# ======================
|
||||
FROM base AS builder
|
||||
|
||||
# Copy root node_modules from deps
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
# Copy all source code FIRST
|
||||
COPY packages ./packages
|
||||
COPY apps/api ./apps/api
|
||||
|
||||
# Then copy workspace node_modules from deps (these go AFTER source to avoid being overwritten)
|
||||
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
|
||||
COPY --from=deps /app/packages/config/node_modules ./packages/config/node_modules
|
||||
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
||||
|
||||
# Build the API app and its dependencies using TurboRepo
|
||||
# --force disables turbo cache to ensure fresh build from source
|
||||
RUN pnpm turbo build --filter=@mosaic/api --force
|
||||
|
||||
# ======================
|
||||
# Production stage
|
||||
# ======================
|
||||
FROM node:24-slim AS production
|
||||
|
||||
# Install dumb-init for proper signal handling (static binary from GitHub,
|
||||
# avoids apt-get which fails under Kaniko with bookworm GPG signature errors)
|
||||
ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 /usr/local/bin/dumb-init
|
||||
|
||||
# Single RUN to minimize Kaniko filesystem snapshots (each RUN = full snapshot)
|
||||
# - openssl: Prisma engine detection requires libssl
|
||||
# - No build tools needed here — native addons are compiled in the deps stage
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends openssl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \
|
||||
&& chmod 755 /usr/local/bin/dumb-init \
|
||||
&& groupadd -g 1001 nodejs && useradd -m -u 1001 -g nodejs nestjs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy node_modules from builder (includes generated Prisma client in pnpm store)
|
||||
# pnpm stores the Prisma client in node_modules/.pnpm/.../.prisma, so we need the full tree
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules
|
||||
|
||||
# Copy built packages (includes dist/ directories)
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/packages ./packages
|
||||
|
||||
# Copy built API application
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/dist ./apps/api/dist
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/prisma ./apps/api/prisma
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/package.json ./apps/api/
|
||||
# Copy app's node_modules which contains symlinks to root node_modules
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/node_modules ./apps/api/node_modules
|
||||
|
||||
# Copy entrypoint script (runs migrations before starting app)
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/apps/api/docker-entrypoint.sh ./apps/api/
|
||||
|
||||
# Set working directory to API app
|
||||
WORKDIR /app/apps/api
|
||||
|
||||
# Switch to non-root user
|
||||
USER nestjs
|
||||
|
||||
# Expose API port (default 3001, can be overridden via PORT env var)
|
||||
EXPOSE ${PORT:-3001}
|
||||
|
||||
# Health check uses PORT env var (set by docker-compose or defaults to 3001)
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD node -e "const port = process.env.PORT || 3001; require('http').get('http://localhost:' + port + '/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||
|
||||
# Use dumb-init to handle signals properly
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
# Run migrations then start the application
|
||||
CMD ["sh", "docker-entrypoint.sh"]
|
||||
@@ -1,260 +0,0 @@
|
||||
# Mosaic Stack API
|
||||
|
||||
The Mosaic Stack API is a NestJS-based backend service providing REST endpoints and WebSocket support for the Mosaic productivity platform.
|
||||
|
||||
## Overview
|
||||
|
||||
The API serves as the central backend for:
|
||||
|
||||
- **Task Management** - Create, update, track tasks with filtering and sorting
|
||||
- **Event Management** - Calendar events and scheduling
|
||||
- **Project Management** - Organize work into projects
|
||||
- **Knowledge Base** - Wiki-style documentation with markdown support and wiki-linking
|
||||
- **Ideas** - Quick capture and organization of ideas
|
||||
- **Domains** - Categorize work across different domains
|
||||
- **Personalities** - AI personality configurations for the Ollama integration
|
||||
- **Widgets & Layouts** - Dashboard customization
|
||||
- **Activity Logging** - Track all user actions
|
||||
- **WebSocket Events** - Real-time updates for tasks, events, and projects
|
||||
|
||||
## Available Modules
|
||||
|
||||
| Module | Base Path | Description |
|
||||
| ------------------ | --------------------------- | ---------------------------------------- |
|
||||
| **Tasks** | `/api/tasks` | CRUD operations for tasks with filtering |
|
||||
| **Events** | `/api/events` | Calendar events and scheduling |
|
||||
| **Projects** | `/api/projects` | Project management |
|
||||
| **Knowledge** | `/api/knowledge/entries` | Wiki entries with markdown support |
|
||||
| **Knowledge Tags** | `/api/knowledge/tags` | Tag management for knowledge entries |
|
||||
| **Ideas** | `/api/ideas` | Quick capture and idea management |
|
||||
| **Domains** | `/api/domains` | Domain categorization |
|
||||
| **Personalities** | `/api/personalities` | AI personality configurations |
|
||||
| **Widgets** | `/api/widgets` | Dashboard widget data |
|
||||
| **Layouts** | `/api/layouts` | Dashboard layout configuration |
|
||||
| **Ollama** | `/api/ollama` | LLM integration (generate, chat, embed) |
|
||||
| **Users** | `/api/users/me/preferences` | User preferences |
|
||||
|
||||
### Health Check
|
||||
|
||||
- `GET /` - API health check
|
||||
- `GET /health` - Detailed health status including database connectivity
|
||||
|
||||
## Authentication
|
||||
|
||||
The API uses **BetterAuth** for authentication with the following features:
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. **Email/Password** - Users can sign up and log in with email and password
|
||||
2. **Session Tokens** - BetterAuth generates session tokens with configurable expiration
|
||||
|
||||
### Guards
|
||||
|
||||
The API uses a layered guard system:
|
||||
|
||||
| Guard | Purpose | Applies To |
|
||||
| ------------------- | ------------------------------------------------------------------------ | -------------------------- |
|
||||
| **AuthGuard** | Verifies user authentication via Bearer token | Most protected endpoints |
|
||||
| **WorkspaceGuard** | Validates workspace membership and sets Row-Level Security (RLS) context | Workspace-scoped resources |
|
||||
| **PermissionGuard** | Enforces role-based access control | Admin operations |
|
||||
|
||||
### Workspace Roles
|
||||
|
||||
- **OWNER** - Full control over workspace
|
||||
- **ADMIN** - Administrative functions (can delete content, manage members)
|
||||
- **MEMBER** - Standard access (create/edit content)
|
||||
- **GUEST** - Read-only access
|
||||
|
||||
### Permission Levels
|
||||
|
||||
Used with `@RequirePermission()` decorator:
|
||||
|
||||
```typescript
|
||||
Permission.WORKSPACE_OWNER; // Requires OWNER role
|
||||
Permission.WORKSPACE_ADMIN; // Requires ADMIN or OWNER
|
||||
Permission.WORKSPACE_MEMBER; // Requires MEMBER, ADMIN, or OWNER
|
||||
Permission.WORKSPACE_ANY; // Any authenticated member including GUEST
|
||||
```
|
||||
|
||||
### Providing Workspace Context
|
||||
|
||||
Workspace ID can be provided via:
|
||||
|
||||
1. **Header**: `X-Workspace-Id: <workspace-id>` (highest priority)
|
||||
2. **URL Parameter**: `:workspaceId`
|
||||
3. **Request Body**: `workspaceId` field
|
||||
|
||||
### Example: Protected Controller
|
||||
|
||||
```typescript
|
||||
@Controller("tasks")
|
||||
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
||||
export class TasksController {
|
||||
@Post()
|
||||
@RequirePermission(Permission.WORKSPACE_MEMBER)
|
||||
async create(@Body() dto: CreateTaskDto, @Workspace() workspaceId: string) {
|
||||
// workspaceId is verified and RLS context is set
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --------------------- | ----------------------------------------- | ----------------------- |
|
||||
| `PORT` | API server port | `3001` |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | Required |
|
||||
| `NODE_ENV` | Environment (`development`, `production`) | - |
|
||||
| `NEXT_PUBLIC_APP_URL` | Frontend application URL (for CORS) | `http://localhost:3000` |
|
||||
| `WEB_URL` | WebSocket CORS origin | `http://localhost:3000` |
|
||||
|
||||
## Running Locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- PostgreSQL database
|
||||
- pnpm workspace (part of Mosaic Stack monorepo)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install dependencies:**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Set up environment variables:**
|
||||
|
||||
```bash
|
||||
cp .env.example .env # If available
|
||||
# Edit .env with your DATABASE_URL
|
||||
```
|
||||
|
||||
3. **Generate Prisma client:**
|
||||
|
||||
```bash
|
||||
pnpm prisma:generate
|
||||
```
|
||||
|
||||
4. **Run database migrations:**
|
||||
|
||||
```bash
|
||||
pnpm prisma:migrate
|
||||
```
|
||||
|
||||
5. **Seed the database (optional):**
|
||||
```bash
|
||||
pnpm prisma:seed
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The API will start on `http://localhost:3001`
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
### Database Management
|
||||
|
||||
```bash
|
||||
# Open Prisma Studio
|
||||
pnpm prisma:studio
|
||||
|
||||
# Reset database (dev only)
|
||||
pnpm prisma:reset
|
||||
|
||||
# Run migrations in production
|
||||
pnpm prisma:migrate:prod
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
The API does not currently include Swagger/OpenAPI documentation. Instead:
|
||||
|
||||
- **Controller files** contain detailed JSDoc comments describing each endpoint
|
||||
- **DTO classes** define request/response schemas with class-validator decorators
|
||||
- Refer to the controller source files in `src/` for endpoint details
|
||||
|
||||
### Example: Reading an Endpoint
|
||||
|
||||
```typescript
|
||||
// src/tasks/tasks.controller.ts
|
||||
|
||||
/**
|
||||
* POST /api/tasks
|
||||
* Create a new task
|
||||
* Requires: MEMBER role or higher
|
||||
*/
|
||||
@Post()
|
||||
@RequirePermission(Permission.WORKSPACE_MEMBER)
|
||||
async create(@Body() createTaskDto: CreateTaskDto, @Workspace() workspaceId: string) {
|
||||
return this.tasksService.create(workspaceId, user.id, createTaskDto);
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket Support
|
||||
|
||||
The API provides real-time updates via WebSocket. Clients receive notifications for:
|
||||
|
||||
- `task:created` - New task created
|
||||
- `task:updated` - Task modified
|
||||
- `task:deleted` - Task removed
|
||||
- `event:created` - New event created
|
||||
- `event:updated` - Event modified
|
||||
- `event:deleted` - Event removed
|
||||
- `project:updated` - Project modified
|
||||
|
||||
Clients join workspace-specific rooms for scoped updates.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run unit tests
|
||||
pnpm test
|
||||
|
||||
# Run tests with coverage
|
||||
pnpm test:coverage
|
||||
|
||||
# Run e2e tests
|
||||
pnpm test:e2e
|
||||
|
||||
# Watch mode
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── activity/ # Activity logging
|
||||
├── auth/ # Authentication (BetterAuth config, guards)
|
||||
├── common/ # Shared decorators and guards
|
||||
├── database/ # Database module
|
||||
├── domains/ # Domain management
|
||||
├── events/ # Event management
|
||||
├── filters/ # Global exception filters
|
||||
├── ideas/ # Idea capture and management
|
||||
├── knowledge/ # Knowledge base (entries, tags, markdown)
|
||||
├── layouts/ # Dashboard layouts
|
||||
├── lib/ # Utility functions
|
||||
├── ollama/ # LLM integration
|
||||
├── personalities/ # AI personality configurations
|
||||
├── prisma/ # Prisma service
|
||||
├── projects/ # Project management
|
||||
├── tasks/ # Task management
|
||||
├── users/ # User preferences
|
||||
├── widgets/ # Dashboard widgets
|
||||
├── websocket/ # WebSocket gateway
|
||||
├── app.controller.ts # Root controller (health check)
|
||||
├── app.module.ts # Root module
|
||||
└── main.ts # Application bootstrap
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "Running database migrations..."
|
||||
./node_modules/.bin/prisma migrate deploy --schema ./prisma/schema.prisma
|
||||
|
||||
echo "Starting application..."
|
||||
exec node dist/main.js
|
||||
@@ -1,16 +0,0 @@
|
||||
import nestjsConfig from "@mosaic/config/eslint/nestjs";
|
||||
|
||||
export default [
|
||||
...nestjsConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**", "node_modules/**", "**/*.test.ts", "**/*.spec.ts"],
|
||||
},
|
||||
];
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"name": "@mosaic/api",
|
||||
"version": "0.0.20",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "nest start --watch",
|
||||
"start": "node dist/main",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "vitest run --config ./vitest.e2e.config.ts",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:migrate:prod": "prisma migrate deploy",
|
||||
"prisma:studio": "prisma studio",
|
||||
"prisma:seed": "prisma db seed",
|
||||
"prisma:reset": "prisma migrate reset",
|
||||
"migrate:encrypt-llm-keys": "tsx scripts/encrypt-llm-keys.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.72.1",
|
||||
"@mosaic/shared": "workspace:*",
|
||||
"@mosaicstack/telemetry-client": "^0.1.1",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"@nestjs/common": "^11.1.12",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.12",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/platform-express": "^11.1.12",
|
||||
"@nestjs/platform-socket.io": "^11.1.12",
|
||||
"@nestjs/schedule": "^6.1.1",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@nestjs/websockets": "^11.1.12",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.55.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.44.0",
|
||||
"@opentelemetry/resources": "^1.30.1",
|
||||
"@opentelemetry/sdk-node": "^0.56.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.5.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.28.0",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"@types/marked": "^6.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.13.5",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-auth": "^1.4.17",
|
||||
"bullmq": "^5.67.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"discord.js": "^14.25.1",
|
||||
"dockerode": "^4.0.9",
|
||||
"gray-matter": "^4.0.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ioredis": "^5.9.2",
|
||||
"jose": "^6.1.3",
|
||||
"marked": "^17.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.3",
|
||||
"marked-highlight": "^2.2.3",
|
||||
"matrix-bot-sdk": "^0.8.0",
|
||||
"node-pty": "^1.0.0",
|
||||
"ollama": "^0.6.3",
|
||||
"openai": "^6.17.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"slugify": "^1.6.6",
|
||||
"socket.io": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "^1.4.17",
|
||||
"@mosaic/config": "workspace:*",
|
||||
"@nestjs/cli": "^11.0.6",
|
||||
"@nestjs/schematics": "^11.0.1",
|
||||
"@nestjs/testing": "^11.1.12",
|
||||
"@opentelemetry/context-async-hooks": "^2.5.0",
|
||||
"@swc/core": "^1.10.18",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/dockerode": "^3.3.47",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/highlight.js": "^10.1.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"dotenv": "^17.2.4",
|
||||
"express": "^5.2.1",
|
||||
"prisma": "^6.19.2",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.8.2",
|
||||
"unplugin-swc": "^1.5.2",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
migrations: {
|
||||
seed: "tsx prisma/seed.ts",
|
||||
},
|
||||
});
|
||||
@@ -1,261 +0,0 @@
|
||||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS "vector";
|
||||
|
||||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TaskStatus" AS ENUM ('NOT_STARTED', 'IN_PROGRESS', 'PAUSED', 'COMPLETED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ProjectStatus" AS ENUM ('PLANNING', 'ACTIVE', 'PAUSED', 'COMPLETED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkspaceMemberRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'GUEST');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ActivityAction" AS ENUM ('CREATED', 'UPDATED', 'DELETED', 'COMPLETED', 'ASSIGNED', 'COMMENTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EntityType" AS ENUM ('TASK', 'EVENT', 'PROJECT', 'WORKSPACE', 'USER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" UUID NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"auth_provider_id" TEXT,
|
||||
"preferences" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workspaces" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"owner_id" UUID NOT NULL,
|
||||
"settings" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workspace_members" (
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"role" "WorkspaceMemberRole" NOT NULL DEFAULT 'MEMBER',
|
||||
"joined_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "workspace_members_pkey" PRIMARY KEY ("workspace_id","user_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tasks" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"status" "TaskStatus" NOT NULL DEFAULT 'NOT_STARTED',
|
||||
"priority" "TaskPriority" NOT NULL DEFAULT 'MEDIUM',
|
||||
"due_date" TIMESTAMPTZ,
|
||||
"assignee_id" UUID,
|
||||
"creator_id" UUID NOT NULL,
|
||||
"project_id" UUID,
|
||||
"parent_id" UUID,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "events" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"start_time" TIMESTAMPTZ NOT NULL,
|
||||
"end_time" TIMESTAMPTZ,
|
||||
"all_day" BOOLEAN NOT NULL DEFAULT false,
|
||||
"location" TEXT,
|
||||
"recurrence" JSONB,
|
||||
"creator_id" UUID NOT NULL,
|
||||
"project_id" UUID,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "projects" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"status" "ProjectStatus" NOT NULL DEFAULT 'PLANNING',
|
||||
"start_date" DATE,
|
||||
"end_date" DATE,
|
||||
"creator_id" UUID NOT NULL,
|
||||
"color" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "activity_logs" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"action" "ActivityAction" NOT NULL,
|
||||
"entity_type" "EntityType" NOT NULL,
|
||||
"entity_id" UUID NOT NULL,
|
||||
"details" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "activity_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "memory_embeddings" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"embedding" vector(1536),
|
||||
"entity_type" "EntityType",
|
||||
"entity_id" UUID,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "memory_embeddings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_auth_provider_id_key" ON "users"("auth_provider_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workspaces_owner_id_idx" ON "workspaces"("owner_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "workspace_members_user_id_idx" ON "workspace_members"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_workspace_id_idx" ON "tasks"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_workspace_id_status_idx" ON "tasks"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_workspace_id_due_date_idx" ON "tasks"("workspace_id", "due_date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_assignee_id_idx" ON "tasks"("assignee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_project_id_idx" ON "tasks"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_parent_id_idx" ON "tasks"("parent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_workspace_id_idx" ON "events"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_workspace_id_start_time_idx" ON "events"("workspace_id", "start_time");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_creator_id_idx" ON "events"("creator_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_project_id_idx" ON "events"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_workspace_id_idx" ON "projects"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_workspace_id_status_idx" ON "projects"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_creator_id_idx" ON "projects"("creator_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_workspace_id_idx" ON "activity_logs"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_workspace_id_created_at_idx" ON "activity_logs"("workspace_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_entity_type_entity_id_idx" ON "activity_logs"("entity_type", "entity_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_user_id_idx" ON "activity_logs"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "memory_embeddings_workspace_id_idx" ON "memory_embeddings"("workspace_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_assignee_id_fkey" FOREIGN KEY ("assignee_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "memory_embeddings" ADD CONSTRAINT "memory_embeddings_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Add HNSW index for fast vector similarity search on memory_embeddings table
|
||||
-- Using cosine distance operator for semantic similarity
|
||||
-- Parameters: m=16 (max connections per layer), ef_construction=64 (build quality)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS memory_embeddings_embedding_idx
|
||||
ON memory_embeddings
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (m = 16, ef_construction = 64);
|
||||
@@ -1,92 +0,0 @@
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'LOGIN';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'LOGOUT';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'PASSWORD_RESET';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'EMAIL_VERIFIED';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "activity_logs" ADD COLUMN "ip_address" TEXT,
|
||||
ADD COLUMN "user_agent" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "email_verified" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "image" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sessions" (
|
||||
"id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"expires_at" TIMESTAMPTZ NOT NULL,
|
||||
"ip_address" TEXT,
|
||||
"user_agent" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "accounts" (
|
||||
"id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"account_id" TEXT NOT NULL,
|
||||
"provider_id" TEXT NOT NULL,
|
||||
"access_token" TEXT,
|
||||
"refresh_token" TEXT,
|
||||
"id_token" TEXT,
|
||||
"access_token_expires_at" TIMESTAMPTZ,
|
||||
"refresh_token_expires_at" TIMESTAMPTZ,
|
||||
"scope" TEXT,
|
||||
"password" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "verifications" (
|
||||
"id" UUID NOT NULL,
|
||||
"identifier" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"expires_at" TIMESTAMPTZ NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "verifications_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sessions_token_key" ON "sessions"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sessions_user_id_idx" ON "sessions"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sessions_token_idx" ON "sessions"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "accounts_user_id_idx" ON "accounts"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "accounts_provider_id_account_id_key" ON "accounts"("provider_id", "account_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "verifications_identifier_idx" ON "verifications"("identifier");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "activity_logs_action_idx" ON "activity_logs"("action");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-286
@@ -1,286 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IdeaStatus" AS ENUM ('CAPTURED', 'PROCESSING', 'ACTIONABLE', 'ARCHIVED', 'DISCARDED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RelationshipType" AS ENUM ('BLOCKS', 'BLOCKED_BY', 'DEPENDS_ON', 'PARENT_OF', 'CHILD_OF', 'RELATED_TO', 'DUPLICATE_OF', 'SUPERSEDES', 'PART_OF');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AgentStatus" AS ENUM ('IDLE', 'WORKING', 'WAITING', 'ERROR', 'TERMINATED');
|
||||
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "EntityType" ADD VALUE 'IDEA';
|
||||
ALTER TYPE "EntityType" ADD VALUE 'DOMAIN';
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "memory_embeddings_embedding_idx";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "events" ADD COLUMN "domain_id" UUID;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "domain_id" UUID;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "tasks" ADD COLUMN "domain_id" UUID;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "domains" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"color" TEXT,
|
||||
"icon" TEXT,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "domains_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ideas" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"domain_id" UUID,
|
||||
"project_id" UUID,
|
||||
"title" TEXT,
|
||||
"content" TEXT NOT NULL,
|
||||
"status" "IdeaStatus" NOT NULL DEFAULT 'CAPTURED',
|
||||
"priority" "TaskPriority" NOT NULL DEFAULT 'MEDIUM',
|
||||
"category" TEXT,
|
||||
"tags" TEXT[],
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"embedding" vector(1536),
|
||||
"creator_id" UUID NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "ideas_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "relationships" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"source_type" "EntityType" NOT NULL,
|
||||
"source_id" UUID NOT NULL,
|
||||
"target_type" "EntityType" NOT NULL,
|
||||
"target_id" UUID NOT NULL,
|
||||
"relationship" "RelationshipType" NOT NULL,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"notes" TEXT,
|
||||
"creator_id" UUID NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "relationships_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agents" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"model" TEXT,
|
||||
"role" TEXT,
|
||||
"status" "AgentStatus" NOT NULL DEFAULT 'IDLE',
|
||||
"current_task" TEXT,
|
||||
"metrics" JSONB NOT NULL DEFAULT '{"totalTasks": 0, "successfulTasks": 0, "failedTasks": 0, "avgResponseTimeMs": 0}',
|
||||
"last_heartbeat" TIMESTAMPTZ,
|
||||
"error_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"last_error" TEXT,
|
||||
"fired_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"fire_history" JSONB NOT NULL DEFAULT '[]',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"terminated_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "agents_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_sessions" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"agent_id" UUID,
|
||||
"session_key" TEXT NOT NULL,
|
||||
"label" TEXT,
|
||||
"channel" TEXT,
|
||||
"context_summary" TEXT,
|
||||
"message_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"started_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_message_at" TIMESTAMPTZ,
|
||||
"ended_at" TIMESTAMPTZ,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
|
||||
CONSTRAINT "agent_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "widget_definitions" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"display_name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"component" TEXT NOT NULL,
|
||||
"default_width" INTEGER NOT NULL DEFAULT 1,
|
||||
"default_height" INTEGER NOT NULL DEFAULT 1,
|
||||
"min_width" INTEGER NOT NULL DEFAULT 1,
|
||||
"min_height" INTEGER NOT NULL DEFAULT 1,
|
||||
"max_width" INTEGER,
|
||||
"max_height" INTEGER,
|
||||
"config_schema" JSONB NOT NULL DEFAULT '{}',
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "widget_definitions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_layouts" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"layout" JSONB NOT NULL DEFAULT '[]',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "user_layouts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "domains_workspace_id_idx" ON "domains"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "domains_workspace_id_slug_key" ON "domains"("workspace_id", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_workspace_id_idx" ON "ideas"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_workspace_id_status_idx" ON "ideas"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_domain_id_idx" ON "ideas"("domain_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_project_id_idx" ON "ideas"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ideas_creator_id_idx" ON "ideas"("creator_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "relationships_source_type_source_id_idx" ON "relationships"("source_type", "source_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "relationships_target_type_target_id_idx" ON "relationships"("target_type", "target_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "relationships_relationship_idx" ON "relationships"("relationship");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "relationships_workspace_id_source_type_source_id_target_typ_key" ON "relationships"("workspace_id", "source_type", "source_id", "target_type", "target_id", "relationship");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agents_workspace_id_idx" ON "agents"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agents_status_idx" ON "agents"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agents_workspace_id_agent_id_key" ON "agents"("workspace_id", "agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_sessions_workspace_id_idx" ON "agent_sessions"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_sessions_user_id_idx" ON "agent_sessions"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_sessions_agent_id_idx" ON "agent_sessions"("agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_sessions_is_active_idx" ON "agent_sessions"("is_active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_sessions_workspace_id_session_key_key" ON "agent_sessions"("workspace_id", "session_key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "widget_definitions_name_key" ON "widget_definitions"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "user_layouts_user_id_idx" ON "user_layouts"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_layouts_workspace_id_user_id_name_key" ON "user_layouts"("workspace_id", "user_id", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_domain_id_idx" ON "events"("domain_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_domain_id_idx" ON "projects"("domain_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tasks_domain_id_idx" ON "tasks"("domain_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_domain_id_fkey" FOREIGN KEY ("domain_id") REFERENCES "domains"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_domain_id_fkey" FOREIGN KEY ("domain_id") REFERENCES "domains"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_domain_id_fkey" FOREIGN KEY ("domain_id") REFERENCES "domains"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "domains" ADD CONSTRAINT "domains_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_domain_id_fkey" FOREIGN KEY ("domain_id") REFERENCES "domains"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ideas" ADD CONSTRAINT "ideas_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "relationships" ADD CONSTRAINT "relationships_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "relationships" ADD CONSTRAINT "relationships_creator_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agents" ADD CONSTRAINT "agents_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_sessions" ADD CONSTRAINT "agent_sessions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_sessions" ADD CONSTRAINT "agent_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_sessions" ADD CONSTRAINT "agent_sessions_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_layouts" ADD CONSTRAINT "user_layouts_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_layouts" ADD CONSTRAINT "user_layouts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,158 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EntryStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'WORKSPACE', 'PUBLIC');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "user_layouts" ADD COLUMN "metadata" JSONB NOT NULL DEFAULT '{}';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_entries" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"content_html" TEXT,
|
||||
"summary" TEXT,
|
||||
"status" "EntryStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"created_by" UUID NOT NULL,
|
||||
"updated_by" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "knowledge_entries_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_entry_versions" (
|
||||
"id" UUID NOT NULL,
|
||||
"entry_id" UUID NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"summary" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" UUID NOT NULL,
|
||||
"change_note" TEXT,
|
||||
|
||||
CONSTRAINT "knowledge_entry_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_links" (
|
||||
"id" UUID NOT NULL,
|
||||
"source_id" UUID NOT NULL,
|
||||
"target_id" UUID NOT NULL,
|
||||
"link_text" TEXT NOT NULL,
|
||||
"context" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "knowledge_links_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_tags" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"color" TEXT,
|
||||
"description" TEXT,
|
||||
|
||||
CONSTRAINT "knowledge_tags_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_entry_tags" (
|
||||
"entry_id" UUID NOT NULL,
|
||||
"tag_id" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "knowledge_entry_tags_pkey" PRIMARY KEY ("entry_id","tag_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "knowledge_embeddings" (
|
||||
"id" UUID NOT NULL,
|
||||
"entry_id" UUID NOT NULL,
|
||||
"embedding" vector(1536) NOT NULL,
|
||||
"model" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "knowledge_embeddings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_workspace_id_status_idx" ON "knowledge_entries"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_workspace_id_updated_at_idx" ON "knowledge_entries"("workspace_id", "updated_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_created_by_idx" ON "knowledge_entries"("created_by");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entries_updated_by_idx" ON "knowledge_entries"("updated_by");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_entries_workspace_id_slug_key" ON "knowledge_entries"("workspace_id", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entry_versions_entry_id_version_idx" ON "knowledge_entry_versions"("entry_id", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_entry_versions_entry_id_version_key" ON "knowledge_entry_versions"("entry_id", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_links_source_id_idx" ON "knowledge_links"("source_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_links_target_id_idx" ON "knowledge_links"("target_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_links_source_id_target_id_key" ON "knowledge_links"("source_id", "target_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_tags_workspace_id_idx" ON "knowledge_tags"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_tags_workspace_id_slug_key" ON "knowledge_tags"("workspace_id", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entry_tags_entry_id_idx" ON "knowledge_entry_tags"("entry_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_entry_tags_tag_id_idx" ON "knowledge_entry_tags"("tag_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "knowledge_embeddings_entry_id_key" ON "knowledge_embeddings"("entry_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_embeddings_entry_id_idx" ON "knowledge_embeddings"("entry_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entries" ADD CONSTRAINT "knowledge_entries_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entry_versions" ADD CONSTRAINT "knowledge_entry_versions_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_links" ADD CONSTRAINT "knowledge_links_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_links" ADD CONSTRAINT "knowledge_links_target_id_fkey" FOREIGN KEY ("target_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_tags" ADD CONSTRAINT "knowledge_tags_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entry_tags" ADD CONSTRAINT "knowledge_entry_tags_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_entry_tags" ADD CONSTRAINT "knowledge_entry_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "knowledge_tags"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "knowledge_embeddings" ADD CONSTRAINT "knowledge_embeddings_entry_id_fkey" FOREIGN KEY ("entry_id") REFERENCES "knowledge_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,40 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TeamMemberRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "teams" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "teams_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "team_members" (
|
||||
"team_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"role" "TeamMemberRole" NOT NULL DEFAULT 'MEMBER',
|
||||
"joined_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "team_members_pkey" PRIMARY KEY ("team_id","user_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "teams_workspace_id_idx" ON "teams"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "team_members_user_id_idx" ON "team_members"("user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "teams" ADD CONSTRAINT "teams_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,319 +0,0 @@
|
||||
-- Row-Level Security (RLS) for Multi-Tenant Isolation
|
||||
-- This migration enables RLS on all tenant-scoped tables and creates policies
|
||||
-- to ensure users can only access data within their authorized workspaces.
|
||||
|
||||
-- =============================================================================
|
||||
-- ENABLE RLS ON TENANT-SCOPED TABLES
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE workspaces ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE workspace_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE teams ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE activity_logs ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE memory_embeddings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE domains ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE ideas ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE relationships ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE agents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE agent_sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE user_layouts ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_entries ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_tags ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_entry_tags ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_links ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_embeddings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_entry_versions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- HELPER FUNCTION: Check if user is workspace member
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_member(workspace_uuid UUID, user_uuid UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM workspace_members
|
||||
WHERE workspace_id = workspace_uuid
|
||||
AND user_id = user_uuid
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- =============================================================================
|
||||
-- HELPER FUNCTION: Check if user is workspace owner/admin
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_admin(workspace_uuid UUID, user_uuid UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM workspace_members
|
||||
WHERE workspace_id = workspace_uuid
|
||||
AND user_id = user_uuid
|
||||
AND role IN ('OWNER', 'ADMIN')
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- =============================================================================
|
||||
-- HELPER FUNCTION: Get current user ID from session variable
|
||||
-- =============================================================================
|
||||
-- Usage in API: SET LOCAL app.current_user_id = 'user-uuid';
|
||||
|
||||
CREATE OR REPLACE FUNCTION current_user_id()
|
||||
RETURNS UUID AS $$
|
||||
BEGIN
|
||||
RETURN NULLIF(current_setting('app.current_user_id', TRUE), '')::UUID;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- =============================================================================
|
||||
-- WORKSPACES: Users can only see workspaces they're members of
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY workspace_member_access ON workspaces
|
||||
FOR ALL
|
||||
USING (
|
||||
id IN (
|
||||
SELECT workspace_id FROM workspace_members
|
||||
WHERE user_id = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- WORKSPACE_MEMBERS: Users can see members of their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY workspace_members_access ON workspace_members
|
||||
FOR ALL
|
||||
USING (
|
||||
workspace_id IN (
|
||||
SELECT workspace_id FROM workspace_members
|
||||
WHERE user_id = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- TEAMS: Users can see teams in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY teams_workspace_access ON teams
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- TEAM_MEMBERS: Users can see team members in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY team_members_access ON team_members
|
||||
FOR ALL
|
||||
USING (
|
||||
team_id IN (
|
||||
SELECT id FROM teams
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- TASKS: Users can only see tasks in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY tasks_workspace_access ON tasks
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- EVENTS: Users can only see events in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY events_workspace_access ON events
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- PROJECTS: Users can only see projects in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY projects_workspace_access ON projects
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- ACTIVITY_LOGS: Users can only see activity in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY activity_logs_workspace_access ON activity_logs
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- MEMORY_EMBEDDINGS: Users can only see embeddings in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY memory_embeddings_workspace_access ON memory_embeddings
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- DOMAINS: Users can only see domains in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY domains_workspace_access ON domains
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- IDEAS: Users can only see ideas in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY ideas_workspace_access ON ideas
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- RELATIONSHIPS: Users can only see relationships in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY relationships_workspace_access ON relationships
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- AGENTS: Users can only see agents in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY agents_workspace_access ON agents
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- AGENT_SESSIONS: Users can only see agent sessions in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY agent_sessions_workspace_access ON agent_sessions
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- USER_LAYOUTS: Users can only see their own layouts in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY user_layouts_workspace_access ON user_layouts
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
AND user_id = current_user_id()
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_ENTRIES: Users can only see entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_entries_workspace_access ON knowledge_entries
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_TAGS: Users can only see tags in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_tags_workspace_access ON knowledge_tags
|
||||
FOR ALL
|
||||
USING (
|
||||
is_workspace_member(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_ENTRY_TAGS: Users can see tags for entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_entry_tags_access ON knowledge_entry_tags
|
||||
FOR ALL
|
||||
USING (
|
||||
entry_id IN (
|
||||
SELECT id FROM knowledge_entries
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_LINKS: Users can see links between entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_links_access ON knowledge_links
|
||||
FOR ALL
|
||||
USING (
|
||||
source_id IN (
|
||||
SELECT id FROM knowledge_entries
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_EMBEDDINGS: Users can see embeddings for entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_embeddings_access ON knowledge_embeddings
|
||||
FOR ALL
|
||||
USING (
|
||||
entry_id IN (
|
||||
SELECT id FROM knowledge_entries
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- KNOWLEDGE_ENTRY_VERSIONS: Users can see versions for entries in their workspaces
|
||||
-- =============================================================================
|
||||
|
||||
CREATE POLICY knowledge_entry_versions_access ON knowledge_entry_versions
|
||||
FOR ALL
|
||||
USING (
|
||||
entry_id IN (
|
||||
SELECT id FROM knowledge_entries
|
||||
WHERE is_workspace_member(workspace_id, current_user_id())
|
||||
)
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- GRANT USAGE TO APPLICATION ROLE
|
||||
-- =============================================================================
|
||||
-- The application should connect with a role that has appropriate permissions.
|
||||
-- By default, we assume the owner of the database has full access.
|
||||
-- In production, create a dedicated role with limited permissions.
|
||||
|
||||
-- Example (uncomment and customize for production):
|
||||
-- GRANT USAGE ON SCHEMA public TO mosaic_app;
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO mosaic_app;
|
||||
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO mosaic_app;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_preferences" (
|
||||
"id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"theme" TEXT NOT NULL DEFAULT 'system',
|
||||
"locale" TEXT NOT NULL DEFAULT 'en',
|
||||
"timezone" TEXT,
|
||||
"settings" JSONB NOT NULL DEFAULT '{}',
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "user_preferences_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_preferences_user_id_key" ON "user_preferences"("user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,47 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AgentTaskStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AgentTaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_tasks" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"status" "AgentTaskStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"priority" "AgentTaskPriority" NOT NULL DEFAULT 'MEDIUM',
|
||||
"agent_type" TEXT NOT NULL,
|
||||
"agent_config" JSONB NOT NULL DEFAULT '{}',
|
||||
"result" JSONB,
|
||||
"error" TEXT,
|
||||
"created_by_id" UUID NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"started_at" TIMESTAMPTZ,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "agent_tasks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_tasks_workspace_id_idx" ON "agent_tasks"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_tasks_workspace_id_status_idx" ON "agent_tasks"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_tasks_workspace_id_priority_idx" ON "agent_tasks"("workspace_id", "priority");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_tasks_created_by_id_idx" ON "agent_tasks"("created_by_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_tasks_id_workspace_id_key" ON "agent_tasks"("id", "workspace_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_tasks" ADD CONSTRAINT "agent_tasks_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_tasks" ADD CONSTRAINT "agent_tasks_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,31 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FormalityLevel" AS ENUM ('VERY_CASUAL', 'CASUAL', 'NEUTRAL', 'FORMAL', 'VERY_FORMAL');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "personalities" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"tone" TEXT NOT NULL,
|
||||
"formality_level" "FormalityLevel" NOT NULL DEFAULT 'NEUTRAL',
|
||||
"system_prompt_template" TEXT NOT NULL,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "personalities_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "personalities_workspace_id_idx" ON "personalities"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "personalities_workspace_id_is_default_idx" ON "personalities"("workspace_id", "is_default");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "personalities_workspace_id_name_key" ON "personalities"("workspace_id", "name");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "personalities" ADD CONSTRAINT "personalities_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `personalities` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `display_text` to the `knowledge_links` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `position_end` to the `knowledge_links` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `position_start` to the `knowledge_links` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "personalities" DROP CONSTRAINT "personalities_workspace_id_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "knowledge_links_source_id_target_id_key";
|
||||
|
||||
-- AlterTable: Add new columns with temporary defaults for existing records
|
||||
ALTER TABLE "knowledge_links"
|
||||
ADD COLUMN "display_text" TEXT DEFAULT '',
|
||||
ADD COLUMN "position_end" INTEGER DEFAULT 0,
|
||||
ADD COLUMN "position_start" INTEGER DEFAULT 0,
|
||||
ADD COLUMN "resolved" BOOLEAN NOT NULL DEFAULT false,
|
||||
ALTER COLUMN "target_id" DROP NOT NULL;
|
||||
|
||||
-- Update existing records: set display_text to link_text and resolved to true if target exists
|
||||
UPDATE "knowledge_links" SET "display_text" = "link_text" WHERE "display_text" = '';
|
||||
UPDATE "knowledge_links" SET "resolved" = true WHERE "target_id" IS NOT NULL;
|
||||
|
||||
-- Remove defaults for new records
|
||||
ALTER TABLE "knowledge_links"
|
||||
ALTER COLUMN "display_text" DROP DEFAULT,
|
||||
ALTER COLUMN "position_end" DROP DEFAULT,
|
||||
ALTER COLUMN "position_start" DROP DEFAULT;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "personalities";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "FormalityLevel";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "knowledge_links_source_id_resolved_idx" ON "knowledge_links"("source_id", "resolved");
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
-- Add HNSW index for fast vector similarity search on knowledge_embeddings table
|
||||
-- Using cosine distance operator for semantic similarity
|
||||
-- Parameters: m=16 (max connections per layer), ef_construction=64 (build quality)
|
||||
|
||||
CREATE INDEX IF NOT EXISTS knowledge_embeddings_embedding_idx
|
||||
ON knowledge_embeddings
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (m = 16, ef_construction = 64);
|
||||
@@ -1,29 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "llm_provider_instances" (
|
||||
"id" UUID NOT NULL,
|
||||
"provider_type" TEXT NOT NULL,
|
||||
"display_name" TEXT NOT NULL,
|
||||
"user_id" UUID,
|
||||
"config" JSONB NOT NULL,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"is_enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "llm_provider_instances_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_provider_instances_user_id_idx" ON "llm_provider_instances"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_provider_instances_provider_type_idx" ON "llm_provider_instances"("provider_type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_provider_instances_is_default_idx" ON "llm_provider_instances"("is_default");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_provider_instances_is_enabled_idx" ON "llm_provider_instances"("is_enabled");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "llm_provider_instances" ADD CONSTRAINT "llm_provider_instances_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,112 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RunnerJobStatus" AS ENUM ('PENDING', 'QUEUED', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobStepPhase" AS ENUM ('SETUP', 'EXECUTION', 'VALIDATION', 'CLEANUP');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobStepType" AS ENUM ('COMMAND', 'AI_ACTION', 'GATE', 'ARTIFACT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobStepStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "runner_jobs" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_task_id" UUID,
|
||||
"type" TEXT NOT NULL,
|
||||
"status" "RunnerJobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"priority" INTEGER NOT NULL,
|
||||
"progress_percent" INTEGER NOT NULL DEFAULT 0,
|
||||
"result" JSONB,
|
||||
"error" TEXT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"started_at" TIMESTAMPTZ,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "runner_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "job_steps" (
|
||||
"id" UUID NOT NULL,
|
||||
"job_id" UUID NOT NULL,
|
||||
"ordinal" INTEGER NOT NULL,
|
||||
"phase" "JobStepPhase" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "JobStepType" NOT NULL,
|
||||
"status" "JobStepStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"output" TEXT,
|
||||
"tokens_input" INTEGER,
|
||||
"tokens_output" INTEGER,
|
||||
"started_at" TIMESTAMPTZ,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
"duration_ms" INTEGER,
|
||||
|
||||
CONSTRAINT "job_steps_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "job_events" (
|
||||
"id" UUID NOT NULL,
|
||||
"job_id" UUID NOT NULL,
|
||||
"step_id" UUID,
|
||||
"type" TEXT NOT NULL,
|
||||
"timestamp" TIMESTAMPTZ NOT NULL,
|
||||
"actor" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "job_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "runner_jobs_id_workspace_id_key" ON "runner_jobs"("id", "workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runner_jobs_workspace_id_idx" ON "runner_jobs"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runner_jobs_workspace_id_status_idx" ON "runner_jobs"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runner_jobs_agent_task_id_idx" ON "runner_jobs"("agent_task_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "runner_jobs_priority_idx" ON "runner_jobs"("priority");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_steps_job_id_idx" ON "job_steps"("job_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_steps_job_id_ordinal_idx" ON "job_steps"("job_id", "ordinal");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_steps_status_idx" ON "job_steps"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_job_id_idx" ON "job_events"("job_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_step_id_idx" ON "job_events"("step_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_timestamp_idx" ON "job_events"("timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_type_idx" ON "job_events"("type");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "runner_jobs" ADD CONSTRAINT "runner_jobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "runner_jobs" ADD CONSTRAINT "runner_jobs_agent_task_id_fkey" FOREIGN KEY ("agent_task_id") REFERENCES "agent_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "job_steps" ADD CONSTRAINT "job_steps_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "runner_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "job_events" ADD CONSTRAINT "job_events_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "runner_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "job_events" ADD CONSTRAINT "job_events_step_id_fkey" FOREIGN KEY ("step_id") REFERENCES "job_steps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_events_job_id_timestamp_idx" ON "job_events"("job_id", "timestamp");
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
-- Add tsvector column for full-text search on knowledge_entries
|
||||
-- Weighted fields: title (A), summary (B), content (C)
|
||||
|
||||
-- Step 1: Add the search_vector column
|
||||
ALTER TABLE "knowledge_entries"
|
||||
ADD COLUMN "search_vector" tsvector;
|
||||
|
||||
-- Step 2: Create GIN index for fast full-text search
|
||||
CREATE INDEX "knowledge_entries_search_vector_idx"
|
||||
ON "knowledge_entries"
|
||||
USING gin("search_vector");
|
||||
|
||||
-- Step 3: Create function to update search_vector
|
||||
CREATE OR REPLACE FUNCTION knowledge_entries_search_vector_update()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.search_vector :=
|
||||
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.summary, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.content, '')), 'C');
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 4: Create trigger to automatically update search_vector on insert/update
|
||||
CREATE TRIGGER knowledge_entries_search_vector_trigger
|
||||
BEFORE INSERT OR UPDATE ON "knowledge_entries"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION knowledge_entries_search_vector_update();
|
||||
|
||||
-- Step 5: Populate search_vector for existing entries
|
||||
UPDATE "knowledge_entries"
|
||||
SET search_vector =
|
||||
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(summary, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(content, '')), 'C');
|
||||
@@ -1,118 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FederationConnectionStatus" AS ENUM ('PENDING', 'ACTIVE', 'SUSPENDED', 'DISCONNECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FederationMessageType" AS ENUM ('QUERY', 'COMMAND', 'EVENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FederationMessageStatus" AS ENUM ('PENDING', 'DELIVERED', 'FAILED', 'TIMEOUT');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "federation_connections" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"remote_instance_id" TEXT NOT NULL,
|
||||
"remote_url" TEXT NOT NULL,
|
||||
"remote_public_key" TEXT NOT NULL,
|
||||
"remote_capabilities" JSONB NOT NULL DEFAULT '{}',
|
||||
"status" "FederationConnectionStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"connected_at" TIMESTAMPTZ,
|
||||
"disconnected_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "federation_connections_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "federated_identities" (
|
||||
"id" UUID NOT NULL,
|
||||
"local_user_id" UUID NOT NULL,
|
||||
"remote_user_id" TEXT NOT NULL,
|
||||
"remote_instance_id" TEXT NOT NULL,
|
||||
"oidc_subject" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "federated_identities_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "federation_messages" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"connection_id" UUID NOT NULL,
|
||||
"message_type" "FederationMessageType" NOT NULL,
|
||||
"message_id" TEXT NOT NULL,
|
||||
"correlation_id" TEXT,
|
||||
"query" TEXT,
|
||||
"command_type" TEXT,
|
||||
"event_type" TEXT,
|
||||
"payload" JSONB DEFAULT '{}',
|
||||
"response" JSONB DEFAULT '{}',
|
||||
"status" "FederationMessageStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"error" TEXT,
|
||||
"signature" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"delivered_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "federation_messages_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "federation_connections_workspace_id_remote_instance_id_key" ON "federation_connections"("workspace_id", "remote_instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_connections_workspace_id_idx" ON "federation_connections"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_connections_workspace_id_status_idx" ON "federation_connections"("workspace_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_connections_remote_instance_id_idx" ON "federation_connections"("remote_instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "federated_identities_local_user_id_remote_instance_id_key" ON "federated_identities"("local_user_id", "remote_instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federated_identities_local_user_id_idx" ON "federated_identities"("local_user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federated_identities_remote_instance_id_idx" ON "federated_identities"("remote_instance_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federated_identities_oidc_subject_idx" ON "federated_identities"("oidc_subject");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "federation_messages_message_id_key" ON "federation_messages"("message_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_workspace_id_idx" ON "federation_messages"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_connection_id_idx" ON "federation_messages"("connection_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_message_id_idx" ON "federation_messages"("message_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_correlation_id_idx" ON "federation_messages"("correlation_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_messages_event_type_idx" ON "federation_messages"("event_type");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_connections" ADD CONSTRAINT "federation_connections_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federated_identities" ADD CONSTRAINT "federated_identities_local_user_id_fkey" FOREIGN KEY ("local_user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_messages" ADD CONSTRAINT "federation_messages_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "federation_connections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_messages" ADD CONSTRAINT "federation_messages_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- Add version field for optimistic locking to prevent race conditions
|
||||
-- This allows safe concurrent updates to runner job status
|
||||
|
||||
ALTER TABLE "runner_jobs" ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- Create index for better performance on version checks
|
||||
CREATE INDEX "runner_jobs_version_idx" ON "runner_jobs"("version");
|
||||
@@ -1,34 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "federation_event_subscriptions" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"connection_id" UUID NOT NULL,
|
||||
"event_type" TEXT NOT NULL,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "federation_event_subscriptions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_event_subscriptions_workspace_id_idx" ON "federation_event_subscriptions"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_event_subscriptions_connection_id_idx" ON "federation_event_subscriptions"("connection_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_event_subscriptions_event_type_idx" ON "federation_event_subscriptions"("event_type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "federation_event_subscriptions_workspace_id_is_active_idx" ON "federation_event_subscriptions"("workspace_id", "is_active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "federation_event_subscriptions_workspace_id_connection_id_even_key" ON "federation_event_subscriptions"("workspace_id", "connection_id", "event_type");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_event_subscriptions" ADD CONSTRAINT "federation_event_subscriptions_connection_id_fkey" FOREIGN KEY ("connection_id") REFERENCES "federation_connections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "federation_event_subscriptions" ADD CONSTRAINT "federation_event_subscriptions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
-- Rollback: SQL Injection Hardening for is_workspace_admin() Helper Function
|
||||
-- This reverts the function to its previous implementation
|
||||
|
||||
-- =============================================================================
|
||||
-- REVERT is_workspace_admin() to original implementation
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_admin(workspace_uuid UUID, user_uuid UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM workspace_members
|
||||
WHERE workspace_id = workspace_uuid
|
||||
AND user_id = user_uuid
|
||||
AND role IN ('OWNER', 'ADMIN')
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
-- Security Fix: SQL Injection Hardening for is_workspace_admin() Helper Function
|
||||
-- This migration adds explicit UUID validation to prevent SQL injection attacks
|
||||
--
|
||||
-- Related: #355 Code Review - Security CRIT-3
|
||||
-- Original issue: Migration 20260129221004_add_rls_policies
|
||||
|
||||
-- =============================================================================
|
||||
-- SECURITY FIX: Add explicit UUID validation to is_workspace_admin()
|
||||
-- =============================================================================
|
||||
-- The is_workspace_admin() function previously accepted UUID parameters without
|
||||
-- explicit type casting/validation. Although PostgreSQL's parameter binding provides
|
||||
-- some protection, explicit UUID type validation is a security best practice.
|
||||
--
|
||||
-- This fix adds explicit UUID validation using PostgreSQL's uuid type checking
|
||||
-- to ensure that non-UUID values cannot bypass the function's intent.
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_workspace_admin(workspace_uuid UUID, user_uuid UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
-- Validate input parameters are valid UUIDs
|
||||
v_workspace_id UUID;
|
||||
v_user_id UUID;
|
||||
BEGIN
|
||||
-- Explicitly validate workspace_uuid parameter
|
||||
IF workspace_uuid IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
v_workspace_id := workspace_uuid::UUID;
|
||||
|
||||
-- Explicitly validate user_uuid parameter
|
||||
IF user_uuid IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
v_user_id := user_uuid::UUID;
|
||||
|
||||
-- Query with validated parameters
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM workspace_members
|
||||
WHERE workspace_id = v_workspace_id
|
||||
AND user_id = v_user_id
|
||||
AND role IN ('OWNER', 'ADMIN')
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- =============================================================================
|
||||
-- NOTES
|
||||
-- =============================================================================
|
||||
-- This is a hardening fix that adds defense-in-depth to the is_workspace_admin()
|
||||
-- helper function. While PostgreSQL's parameterized queries already provide
|
||||
-- protection against SQL injection, explicit UUID type validation ensures:
|
||||
--
|
||||
-- 1. Parameters are explicitly cast to UUID type
|
||||
-- 2. NULL values are handled defensively
|
||||
-- 3. The function's intent is clear and secure
|
||||
-- 4. Compliance with security best practices
|
||||
--
|
||||
-- This change is backward compatible and does not affect existing functionality.
|
||||
@@ -1,91 +0,0 @@
|
||||
-- Row-Level Security (RLS) for Auth Tables
|
||||
-- This migration adds FORCE ROW LEVEL SECURITY and policies to accounts and sessions tables
|
||||
-- to ensure users can only access their own authentication data.
|
||||
--
|
||||
-- Related: #350 - Add RLS policies to auth tables with FORCE enforcement
|
||||
-- Design: docs/design/credential-security.md (Phase 1a)
|
||||
|
||||
-- =============================================================================
|
||||
-- ENABLE FORCE RLS ON AUTH TABLES
|
||||
-- =============================================================================
|
||||
-- FORCE means the table owner (mosaic) is also subject to RLS policies.
|
||||
-- This prevents Prisma (connecting as owner) from bypassing policies.
|
||||
|
||||
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE accounts FORCE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE sessions FORCE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- ACCOUNTS TABLE POLICIES
|
||||
-- =============================================================================
|
||||
|
||||
-- Owner bypass policy: Allow access to all rows ONLY when no RLS context is set
|
||||
-- This is required for:
|
||||
-- 1. Prisma migrations that run without RLS context
|
||||
-- 2. BetterAuth internal operations during authentication flow (when no user context)
|
||||
-- 3. Database maintenance operations
|
||||
-- When RLS context IS set (current_user_id() returns non-NULL), this policy does not apply
|
||||
--
|
||||
-- NOTE: If connecting as a PostgreSQL superuser (like the default 'mosaic' role),
|
||||
-- RLS policies are bypassed entirely. For full RLS enforcement, the application
|
||||
-- should connect as a non-superuser role. See docs/design/credential-security.md
|
||||
CREATE POLICY accounts_owner_bypass ON accounts
|
||||
FOR ALL
|
||||
USING (current_user_id() IS NULL);
|
||||
|
||||
-- User access policy: Users can only access their own accounts
|
||||
-- Uses current_user_id() helper from migration 20260129221004_add_rls_policies
|
||||
-- This policy applies to all operations: SELECT, INSERT, UPDATE, DELETE
|
||||
CREATE POLICY accounts_user_access ON accounts
|
||||
FOR ALL
|
||||
USING (user_id = current_user_id());
|
||||
|
||||
-- =============================================================================
|
||||
-- SESSIONS TABLE POLICIES
|
||||
-- =============================================================================
|
||||
|
||||
-- Owner bypass policy: Allow access to all rows ONLY when no RLS context is set
|
||||
-- See note on accounts_owner_bypass policy about superuser limitations
|
||||
CREATE POLICY sessions_owner_bypass ON sessions
|
||||
FOR ALL
|
||||
USING (current_user_id() IS NULL);
|
||||
|
||||
-- User access policy: Users can only access their own sessions
|
||||
CREATE POLICY sessions_user_access ON sessions
|
||||
FOR ALL
|
||||
USING (user_id = current_user_id());
|
||||
|
||||
-- =============================================================================
|
||||
-- VERIFICATION TABLE ANALYSIS
|
||||
-- =============================================================================
|
||||
-- The verifications table does NOT need RLS policies because:
|
||||
-- 1. It stores ephemeral verification tokens (email verification, password reset)
|
||||
-- 2. It has no user_id column - only identifier (email) and value (token)
|
||||
-- 3. Tokens are short-lived and accessed by token value, not user context
|
||||
-- 4. BetterAuth manages access control through token validation, not RLS
|
||||
-- 5. No cross-user data leakage risk since tokens are random and expire
|
||||
--
|
||||
-- Therefore, we intentionally do NOT add RLS to verifications table.
|
||||
|
||||
-- =============================================================================
|
||||
-- IMPORTANT: SUPERUSER LIMITATION
|
||||
-- =============================================================================
|
||||
-- PostgreSQL superusers (including the default 'mosaic' role) ALWAYS bypass
|
||||
-- Row-Level Security policies, even with FORCE ROW LEVEL SECURITY enabled.
|
||||
-- This is a fundamental PostgreSQL security design.
|
||||
--
|
||||
-- For production deployments with full RLS enforcement, create a dedicated
|
||||
-- non-superuser application role:
|
||||
--
|
||||
-- CREATE ROLE mosaic_app WITH LOGIN PASSWORD 'secure-password';
|
||||
-- GRANT CONNECT ON DATABASE mosaic TO mosaic_app;
|
||||
-- GRANT USAGE ON SCHEMA public TO mosaic_app;
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO mosaic_app;
|
||||
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO mosaic_app;
|
||||
--
|
||||
-- Then update DATABASE_URL to connect as mosaic_app instead of mosaic.
|
||||
-- The RLS policies will then be properly enforced for application queries.
|
||||
--
|
||||
-- See: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
|
||||
@@ -1,76 +0,0 @@
|
||||
-- Rollback: User Credentials Storage with RLS Policies
|
||||
-- This migration reverses all changes from migration.sql
|
||||
--
|
||||
-- Related: #355 - Create UserCredential Prisma model with RLS policies
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP TRIGGERS AND FUNCTIONS
|
||||
-- =============================================================================
|
||||
|
||||
DROP TRIGGER IF EXISTS user_credentials_updated_at ON user_credentials;
|
||||
DROP FUNCTION IF EXISTS update_user_credentials_updated_at();
|
||||
|
||||
-- =============================================================================
|
||||
-- DISABLE RLS
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE user_credentials DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP RLS POLICIES
|
||||
-- =============================================================================
|
||||
|
||||
DROP POLICY IF EXISTS user_credentials_owner_bypass ON user_credentials;
|
||||
DROP POLICY IF EXISTS user_credentials_user_access ON user_credentials;
|
||||
DROP POLICY IF EXISTS user_credentials_workspace_access ON user_credentials;
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP INDEXES
|
||||
-- =============================================================================
|
||||
|
||||
DROP INDEX IF EXISTS "user_credentials_user_id_workspace_id_provider_name_key";
|
||||
DROP INDEX IF EXISTS "user_credentials_scope_is_active_idx";
|
||||
DROP INDEX IF EXISTS "user_credentials_workspace_id_scope_idx";
|
||||
DROP INDEX IF EXISTS "user_credentials_user_id_scope_idx";
|
||||
DROP INDEX IF EXISTS "user_credentials_workspace_id_idx";
|
||||
DROP INDEX IF EXISTS "user_credentials_user_id_idx";
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP FOREIGN KEY CONSTRAINTS
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE "user_credentials" DROP CONSTRAINT IF EXISTS "user_credentials_workspace_id_fkey";
|
||||
ALTER TABLE "user_credentials" DROP CONSTRAINT IF EXISTS "user_credentials_user_id_fkey";
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP TABLE
|
||||
-- =============================================================================
|
||||
|
||||
DROP TABLE IF EXISTS "user_credentials";
|
||||
|
||||
-- =============================================================================
|
||||
-- DROP ENUMS
|
||||
-- =============================================================================
|
||||
-- NOTE: ENUM values cannot be easily removed from an existing enum type in PostgreSQL.
|
||||
-- To fully reverse this migration, you would need to:
|
||||
--
|
||||
-- 1. Remove the 'CREDENTIAL' value from EntityType enum (if not used elsewhere):
|
||||
-- ALTER TYPE "EntityType" RENAME TO "EntityType_old";
|
||||
-- CREATE TYPE "EntityType" AS ENUM (...all values except CREDENTIAL...);
|
||||
-- -- Then rebuild all dependent objects
|
||||
--
|
||||
-- 2. Remove credential-related actions from ActivityAction enum (if not used elsewhere):
|
||||
-- ALTER TYPE "ActivityAction" RENAME TO "ActivityAction_old";
|
||||
-- CREATE TYPE "ActivityAction" AS ENUM (...all values except CREDENTIAL_*...);
|
||||
-- -- Then rebuild all dependent objects
|
||||
--
|
||||
-- 3. Drop the CredentialType and CredentialScope enums:
|
||||
-- DROP TYPE IF EXISTS "CredentialType";
|
||||
-- DROP TYPE IF EXISTS "CredentialScope";
|
||||
--
|
||||
-- Due to the complexity and risk of breaking existing data/code that references
|
||||
-- these enum values, this migration does NOT automatically remove them.
|
||||
-- If you need to clean up the enums, manually execute the steps above.
|
||||
--
|
||||
-- For development environments, you can safely drop and recreate the enums manually
|
||||
-- using the SQL statements above.
|
||||
@@ -1,184 +0,0 @@
|
||||
-- User Credentials Storage with RLS Policies
|
||||
-- This migration adds the user_credentials table for secure storage of user API keys,
|
||||
-- OAuth tokens, and other credentials with encryption and RLS enforcement.
|
||||
--
|
||||
-- Related: #355 - Create UserCredential Prisma model with RLS policies
|
||||
-- Design: docs/design/credential-security.md (Phase 3a)
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE ENUMS
|
||||
-- =============================================================================
|
||||
|
||||
-- CredentialType enum: Types of credentials that can be stored
|
||||
CREATE TYPE "CredentialType" AS ENUM ('API_KEY', 'OAUTH_TOKEN', 'ACCESS_TOKEN', 'SECRET', 'PASSWORD', 'CUSTOM');
|
||||
|
||||
-- CredentialScope enum: Access scope for credentials
|
||||
CREATE TYPE "CredentialScope" AS ENUM ('USER', 'WORKSPACE', 'SYSTEM');
|
||||
|
||||
-- =============================================================================
|
||||
-- EXTEND EXISTING ENUMS
|
||||
-- =============================================================================
|
||||
|
||||
-- Add CREDENTIAL to EntityType for activity logging
|
||||
ALTER TYPE "EntityType" ADD VALUE 'CREDENTIAL';
|
||||
|
||||
-- Add credential-related actions to ActivityAction
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_CREATED';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_ACCESSED';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_ROTATED';
|
||||
ALTER TYPE "ActivityAction" ADD VALUE 'CREDENTIAL_REVOKED';
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE USER_CREDENTIALS TABLE
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE "user_credentials" (
|
||||
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"user_id" UUID NOT NULL,
|
||||
"workspace_id" UUID,
|
||||
|
||||
-- Identity
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"provider" VARCHAR(100) NOT NULL,
|
||||
"type" "CredentialType" NOT NULL,
|
||||
"scope" "CredentialScope" NOT NULL DEFAULT 'USER',
|
||||
|
||||
-- Encrypted storage
|
||||
"encrypted_value" TEXT NOT NULL,
|
||||
"masked_value" VARCHAR(20),
|
||||
|
||||
-- Metadata
|
||||
"description" TEXT,
|
||||
"expires_at" TIMESTAMPTZ,
|
||||
"last_used_at" TIMESTAMPTZ,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
|
||||
-- Status
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"rotated_at" TIMESTAMPTZ,
|
||||
|
||||
-- Audit
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT "user_credentials_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE FOREIGN KEY CONSTRAINTS
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_user_id_fkey"
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_workspace_id_fkey"
|
||||
FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE INDEXES
|
||||
-- =============================================================================
|
||||
|
||||
-- Index for user lookups
|
||||
CREATE INDEX "user_credentials_user_id_idx" ON "user_credentials"("user_id");
|
||||
|
||||
-- Index for workspace lookups
|
||||
CREATE INDEX "user_credentials_workspace_id_idx" ON "user_credentials"("workspace_id");
|
||||
|
||||
-- Index for user + scope queries
|
||||
CREATE INDEX "user_credentials_user_id_scope_idx" ON "user_credentials"("user_id", "scope");
|
||||
|
||||
-- Index for workspace + scope queries
|
||||
CREATE INDEX "user_credentials_workspace_id_scope_idx" ON "user_credentials"("workspace_id", "scope");
|
||||
|
||||
-- Index for scope + active status queries
|
||||
CREATE INDEX "user_credentials_scope_is_active_idx" ON "user_credentials"("scope", "is_active");
|
||||
|
||||
-- =============================================================================
|
||||
-- CREATE UNIQUE CONSTRAINT
|
||||
-- =============================================================================
|
||||
|
||||
-- Prevent duplicate credentials per user/workspace/provider/name
|
||||
CREATE UNIQUE INDEX "user_credentials_user_id_workspace_id_provider_name_key"
|
||||
ON "user_credentials"("user_id", "workspace_id", "provider", "name");
|
||||
|
||||
-- =============================================================================
|
||||
-- ENABLE FORCE ROW LEVEL SECURITY
|
||||
-- =============================================================================
|
||||
-- FORCE means the table owner (mosaic) is also subject to RLS policies.
|
||||
-- This prevents Prisma (connecting as owner) from bypassing policies.
|
||||
|
||||
ALTER TABLE user_credentials ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE user_credentials FORCE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- RLS POLICIES
|
||||
-- =============================================================================
|
||||
|
||||
-- Owner bypass policy: Allow access to all rows ONLY when no RLS context is set
|
||||
-- This is required for:
|
||||
-- 1. Prisma migrations that run without RLS context
|
||||
-- 2. Database maintenance operations
|
||||
-- When RLS context IS set (current_user_id() returns non-NULL), this policy does not apply
|
||||
--
|
||||
-- NOTE: If connecting as a PostgreSQL superuser (like the default 'mosaic' role),
|
||||
-- RLS policies are bypassed entirely. For full RLS enforcement, the application
|
||||
-- should connect as a non-superuser role. See docs/design/credential-security.md
|
||||
CREATE POLICY user_credentials_owner_bypass ON user_credentials
|
||||
FOR ALL
|
||||
USING (current_user_id() IS NULL);
|
||||
|
||||
-- User access policy: USER-scoped credentials visible only to owner
|
||||
-- Uses current_user_id() helper from migration 20260129221004_add_rls_policies
|
||||
CREATE POLICY user_credentials_user_access ON user_credentials
|
||||
FOR ALL
|
||||
USING (
|
||||
scope = 'USER' AND user_id = current_user_id()
|
||||
);
|
||||
|
||||
-- Workspace admin access policy: WORKSPACE-scoped credentials visible to workspace admins
|
||||
-- Uses is_workspace_admin() helper from migration 20260129221004_add_rls_policies
|
||||
CREATE POLICY user_credentials_workspace_access ON user_credentials
|
||||
FOR ALL
|
||||
USING (
|
||||
scope = 'WORKSPACE'
|
||||
AND workspace_id IS NOT NULL
|
||||
AND is_workspace_admin(workspace_id, current_user_id())
|
||||
);
|
||||
|
||||
-- SYSTEM-scoped credentials are only accessible via owner bypass policy
|
||||
-- (when current_user_id() IS NULL, which happens for admin operations)
|
||||
|
||||
-- =============================================================================
|
||||
-- AUDIT TRIGGER
|
||||
-- =============================================================================
|
||||
|
||||
-- Update updated_at timestamp on row changes
|
||||
CREATE OR REPLACE FUNCTION update_user_credentials_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER user_credentials_updated_at
|
||||
BEFORE UPDATE ON user_credentials
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_user_credentials_updated_at();
|
||||
|
||||
-- =============================================================================
|
||||
-- NOTES
|
||||
-- =============================================================================
|
||||
-- This migration creates the foundation for secure credential storage.
|
||||
-- The encrypted_value column stores ciphertext in one of two formats:
|
||||
--
|
||||
-- 1. OpenBao Transit format (preferred): vault:v1:base64data
|
||||
-- 2. AES-256-GCM fallback format: iv:authTag:encrypted
|
||||
--
|
||||
-- The VaultService (issue #353) handles encryption/decryption with automatic
|
||||
-- fallback to CryptoService when OpenBao is unavailable.
|
||||
--
|
||||
-- RLS enforcement ensures:
|
||||
-- - USER scope: Only the credential owner can access
|
||||
-- - WORKSPACE scope: Only workspace admins can access
|
||||
-- - SYSTEM scope: Only accessible via admin/migration bypass
|
||||
@@ -1,37 +0,0 @@
|
||||
-- Encrypt existing plaintext Account tokens
|
||||
-- This migration adds an encryption_version column and marks existing records for encryption
|
||||
-- The actual encryption happens via Prisma middleware on first read/write
|
||||
|
||||
-- Add encryption_version column to track encryption state
|
||||
-- NULL = not encrypted (legacy plaintext)
|
||||
-- 'aes' = AES-256-GCM encrypted
|
||||
-- 'vault' = OpenBao Transit encrypted (Phase 2)
|
||||
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS encryption_version VARCHAR(20);
|
||||
|
||||
-- Create index for efficient queries filtering by encryption status
|
||||
-- This index is also declared in Prisma schema (@@index([encryptionVersion]))
|
||||
-- Using CREATE INDEX IF NOT EXISTS for idempotency
|
||||
CREATE INDEX IF NOT EXISTS "accounts_encryption_version_idx" ON accounts(encryption_version);
|
||||
|
||||
-- Verify index was created successfully by running:
|
||||
-- SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'accounts' AND indexname = 'accounts_encryption_version_idx';
|
||||
|
||||
-- Update statistics for query planner
|
||||
ANALYZE accounts;
|
||||
|
||||
-- Migration Note:
|
||||
-- This migration does NOT encrypt data in-place to avoid downtime and data corruption risks.
|
||||
-- Instead, the Prisma middleware (account-encryption.middleware.ts) handles encryption:
|
||||
--
|
||||
-- 1. On READ: Detects format (plaintext vs encrypted) and decrypts if needed
|
||||
-- 2. On WRITE: Encrypts tokens and sets encryption_version = 'aes'
|
||||
-- 3. Backward compatible: Plaintext tokens (encryption_version = NULL) are passed through unchanged
|
||||
--
|
||||
-- To actively encrypt existing tokens, run the companion script:
|
||||
-- node scripts/encrypt-account-tokens.js
|
||||
--
|
||||
-- This approach ensures:
|
||||
-- - Zero downtime migration
|
||||
-- - No risk of corrupting tokens during bulk encryption
|
||||
-- - Progressive encryption as tokens are accessed/refreshed
|
||||
-- - Easy rollback (middleware is idempotent)
|
||||
@@ -1,26 +0,0 @@
|
||||
-- Encrypt LLM Provider API Keys Migration
|
||||
--
|
||||
-- This migration enables transparent encryption/decryption of LLM provider API keys
|
||||
-- stored in the llm_provider_instances.config JSON field.
|
||||
--
|
||||
-- IMPORTANT: This is a data migration with no schema changes.
|
||||
--
|
||||
-- Strategy:
|
||||
-- 1. Prisma middleware (llm-encryption.middleware.ts) handles encryption/decryption
|
||||
-- 2. Middleware auto-detects encryption format:
|
||||
-- - vault:v1:... = OpenBao Transit encrypted
|
||||
-- - Otherwise = Legacy plaintext (backward compatible)
|
||||
-- 3. New API keys are always encrypted on write
|
||||
-- 4. Existing plaintext keys work until re-saved (lazy migration)
|
||||
--
|
||||
-- To actively encrypt all existing API keys NOW:
|
||||
-- pnpm --filter @mosaic/api migrate:encrypt-llm-keys
|
||||
--
|
||||
-- This approach ensures:
|
||||
-- - Zero downtime migration
|
||||
-- - No schema changes required
|
||||
-- - Backward compatible with plaintext keys
|
||||
-- - Progressive encryption as keys are accessed/updated
|
||||
-- - Easy rollback (middleware is idempotent)
|
||||
--
|
||||
-- Note: No SQL changes needed. This file exists for migration tracking only.
|
||||
@@ -1,197 +0,0 @@
|
||||
-- RecreateEnum: FormalityLevel was dropped in 20260129235248_add_link_storage_fields
|
||||
CREATE TYPE "FormalityLevel" AS ENUM ('VERY_CASUAL', 'CASUAL', 'NEUTRAL', 'FORMAL', 'VERY_FORMAL');
|
||||
|
||||
-- RecreateTable: personalities was dropped in 20260129235248_add_link_storage_fields
|
||||
-- Recreated with current schema (display_name, system_prompt, temperature, etc.)
|
||||
CREATE TABLE "personalities" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"display_name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"system_prompt" TEXT NOT NULL,
|
||||
"temperature" DOUBLE PRECISION,
|
||||
"max_tokens" INTEGER,
|
||||
"llm_provider_instance_id" UUID,
|
||||
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"is_enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "personalities_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex: personalities
|
||||
CREATE UNIQUE INDEX "personalities_id_workspace_id_key" ON "personalities"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "personalities_workspace_id_name_key" ON "personalities"("workspace_id", "name");
|
||||
CREATE INDEX "personalities_workspace_id_idx" ON "personalities"("workspace_id");
|
||||
CREATE INDEX "personalities_workspace_id_is_default_idx" ON "personalities"("workspace_id", "is_default");
|
||||
CREATE INDEX "personalities_workspace_id_is_enabled_idx" ON "personalities"("workspace_id", "is_enabled");
|
||||
CREATE INDEX "personalities_llm_provider_instance_id_idx" ON "personalities"("llm_provider_instance_id");
|
||||
|
||||
-- AddForeignKey: personalities
|
||||
ALTER TABLE "personalities" ADD CONSTRAINT "personalities_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "personalities" ADD CONSTRAINT "personalities_llm_provider_instance_id_fkey" FOREIGN KEY ("llm_provider_instance_id") REFERENCES "llm_provider_instances"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "cron_schedules" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"expression" TEXT NOT NULL,
|
||||
"command" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"last_run" TIMESTAMPTZ,
|
||||
"next_run" TIMESTAMPTZ,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "cron_schedules_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "workspace_llm_settings" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"default_llm_provider_id" UUID,
|
||||
"default_personality_id" UUID,
|
||||
"settings" JSONB DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "workspace_llm_settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "quality_gates" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"command" TEXT,
|
||||
"expected_output" TEXT,
|
||||
"is_regex" BOOLEAN NOT NULL DEFAULT false,
|
||||
"required" BOOLEAN NOT NULL DEFAULT true,
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
"is_enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "quality_gates_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "task_rejections" (
|
||||
"id" UUID NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"workspace_id" TEXT NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"attempt_count" INTEGER NOT NULL,
|
||||
"failures" JSONB NOT NULL,
|
||||
"original_task" TEXT NOT NULL,
|
||||
"started_at" TIMESTAMPTZ NOT NULL,
|
||||
"rejected_at" TIMESTAMPTZ NOT NULL,
|
||||
"escalated" BOOLEAN NOT NULL DEFAULT false,
|
||||
"manual_review" BOOLEAN NOT NULL DEFAULT false,
|
||||
"resolved_at" TIMESTAMPTZ,
|
||||
"resolution" TEXT,
|
||||
|
||||
CONSTRAINT "task_rejections_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "token_budgets" (
|
||||
"id" UUID NOT NULL,
|
||||
"task_id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"allocated_tokens" INTEGER NOT NULL,
|
||||
"estimated_complexity" TEXT NOT NULL,
|
||||
"input_tokens_used" INTEGER NOT NULL DEFAULT 0,
|
||||
"output_tokens_used" INTEGER NOT NULL DEFAULT 0,
|
||||
"total_tokens_used" INTEGER NOT NULL DEFAULT 0,
|
||||
"estimated_cost" DECIMAL(10,6),
|
||||
"started_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_updated_at" TIMESTAMPTZ NOT NULL,
|
||||
"completed_at" TIMESTAMPTZ,
|
||||
"budget_utilization" DOUBLE PRECISION,
|
||||
"suspicious_pattern" BOOLEAN NOT NULL DEFAULT false,
|
||||
"suspicious_reason" TEXT,
|
||||
|
||||
CONSTRAINT "token_budgets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "llm_usage_logs" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"provider" VARCHAR(50) NOT NULL,
|
||||
"model" VARCHAR(100) NOT NULL,
|
||||
"provider_instance_id" UUID,
|
||||
"prompt_tokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"completion_tokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"total_tokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"cost_cents" DOUBLE PRECISION,
|
||||
"task_type" VARCHAR(50),
|
||||
"conversation_id" UUID,
|
||||
"duration_ms" INTEGER,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "llm_usage_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex: cron_schedules
|
||||
CREATE INDEX "cron_schedules_workspace_id_idx" ON "cron_schedules"("workspace_id");
|
||||
CREATE INDEX "cron_schedules_workspace_id_enabled_idx" ON "cron_schedules"("workspace_id", "enabled");
|
||||
CREATE INDEX "cron_schedules_next_run_idx" ON "cron_schedules"("next_run");
|
||||
|
||||
-- CreateIndex: workspace_llm_settings
|
||||
CREATE UNIQUE INDEX "workspace_llm_settings_workspace_id_key" ON "workspace_llm_settings"("workspace_id");
|
||||
CREATE INDEX "workspace_llm_settings_workspace_id_idx" ON "workspace_llm_settings"("workspace_id");
|
||||
CREATE INDEX "workspace_llm_settings_default_llm_provider_id_idx" ON "workspace_llm_settings"("default_llm_provider_id");
|
||||
CREATE INDEX "workspace_llm_settings_default_personality_id_idx" ON "workspace_llm_settings"("default_personality_id");
|
||||
|
||||
-- CreateIndex: quality_gates
|
||||
CREATE UNIQUE INDEX "quality_gates_workspace_id_name_key" ON "quality_gates"("workspace_id", "name");
|
||||
CREATE INDEX "quality_gates_workspace_id_idx" ON "quality_gates"("workspace_id");
|
||||
CREATE INDEX "quality_gates_workspace_id_is_enabled_idx" ON "quality_gates"("workspace_id", "is_enabled");
|
||||
|
||||
-- CreateIndex: task_rejections
|
||||
CREATE INDEX "task_rejections_task_id_idx" ON "task_rejections"("task_id");
|
||||
CREATE INDEX "task_rejections_workspace_id_idx" ON "task_rejections"("workspace_id");
|
||||
CREATE INDEX "task_rejections_agent_id_idx" ON "task_rejections"("agent_id");
|
||||
CREATE INDEX "task_rejections_escalated_idx" ON "task_rejections"("escalated");
|
||||
CREATE INDEX "task_rejections_manual_review_idx" ON "task_rejections"("manual_review");
|
||||
|
||||
-- CreateIndex: token_budgets
|
||||
CREATE UNIQUE INDEX "token_budgets_task_id_key" ON "token_budgets"("task_id");
|
||||
CREATE INDEX "token_budgets_task_id_idx" ON "token_budgets"("task_id");
|
||||
CREATE INDEX "token_budgets_workspace_id_idx" ON "token_budgets"("workspace_id");
|
||||
CREATE INDEX "token_budgets_suspicious_pattern_idx" ON "token_budgets"("suspicious_pattern");
|
||||
|
||||
-- CreateIndex: llm_usage_logs
|
||||
CREATE INDEX "llm_usage_logs_workspace_id_idx" ON "llm_usage_logs"("workspace_id");
|
||||
CREATE INDEX "llm_usage_logs_workspace_id_created_at_idx" ON "llm_usage_logs"("workspace_id", "created_at");
|
||||
CREATE INDEX "llm_usage_logs_user_id_idx" ON "llm_usage_logs"("user_id");
|
||||
CREATE INDEX "llm_usage_logs_provider_idx" ON "llm_usage_logs"("provider");
|
||||
CREATE INDEX "llm_usage_logs_model_idx" ON "llm_usage_logs"("model");
|
||||
CREATE INDEX "llm_usage_logs_provider_instance_id_idx" ON "llm_usage_logs"("provider_instance_id");
|
||||
CREATE INDEX "llm_usage_logs_task_type_idx" ON "llm_usage_logs"("task_type");
|
||||
CREATE INDEX "llm_usage_logs_conversation_id_idx" ON "llm_usage_logs"("conversation_id");
|
||||
|
||||
-- AddForeignKey: cron_schedules
|
||||
ALTER TABLE "cron_schedules" ADD CONSTRAINT "cron_schedules_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: workspace_llm_settings
|
||||
ALTER TABLE "workspace_llm_settings" ADD CONSTRAINT "workspace_llm_settings_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "workspace_llm_settings" ADD CONSTRAINT "workspace_llm_settings_default_llm_provider_id_fkey" FOREIGN KEY ("default_llm_provider_id") REFERENCES "llm_provider_instances"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "workspace_llm_settings" ADD CONSTRAINT "workspace_llm_settings_default_personality_id_fkey" FOREIGN KEY ("default_personality_id") REFERENCES "personalities"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: quality_gates
|
||||
ALTER TABLE "quality_gates" ADD CONSTRAINT "quality_gates_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey: llm_usage_logs
|
||||
ALTER TABLE "llm_usage_logs" ADD CONSTRAINT "llm_usage_logs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "llm_usage_logs" ADD CONSTRAINT "llm_usage_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "llm_usage_logs" ADD CONSTRAINT "llm_usage_logs_provider_instance_id_fkey" FOREIGN KEY ("provider_instance_id") REFERENCES "llm_provider_instances"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "workspaces" ADD COLUMN "matrix_room_id" TEXT;
|
||||
@@ -1,49 +0,0 @@
|
||||
-- Fix schema drift: tables, indexes, and constraints defined in schema.prisma
|
||||
-- but never created (or dropped and never recreated) by prior migrations.
|
||||
|
||||
-- ============================================
|
||||
-- CreateTable: instances (Federation module)
|
||||
-- Never created in any prior migration
|
||||
-- ============================================
|
||||
CREATE TABLE "instances" (
|
||||
"id" UUID NOT NULL,
|
||||
"instance_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"public_key" TEXT NOT NULL,
|
||||
"private_key" TEXT NOT NULL,
|
||||
"capabilities" JSONB NOT NULL DEFAULT '{}',
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "instances_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "instances_instance_id_key" ON "instances"("instance_id");
|
||||
|
||||
-- ============================================
|
||||
-- Recreate dropped unique index on knowledge_links
|
||||
-- Created in 20260129220645_add_knowledge_module, dropped in
|
||||
-- 20260129235248_add_link_storage_fields, never recreated.
|
||||
-- ============================================
|
||||
CREATE UNIQUE INDEX "knowledge_links_source_id_target_id_key" ON "knowledge_links"("source_id", "target_id");
|
||||
|
||||
-- ============================================
|
||||
-- Missing @@unique([id, workspaceId]) composite indexes
|
||||
-- Defined in schema.prisma but never created in migrations.
|
||||
-- (agent_tasks and runner_jobs already have these.)
|
||||
-- ============================================
|
||||
CREATE UNIQUE INDEX "tasks_id_workspace_id_key" ON "tasks"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "events_id_workspace_id_key" ON "events"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "projects_id_workspace_id_key" ON "projects"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "activity_logs_id_workspace_id_key" ON "activity_logs"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "domains_id_workspace_id_key" ON "domains"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "ideas_id_workspace_id_key" ON "ideas"("id", "workspace_id");
|
||||
CREATE UNIQUE INDEX "user_layouts_id_workspace_id_key" ON "user_layouts"("id", "workspace_id");
|
||||
|
||||
-- ============================================
|
||||
-- Missing index on agent_tasks.agent_type
|
||||
-- Defined as @@index([agentType]) in schema.prisma
|
||||
-- ============================================
|
||||
CREATE INDEX "agent_tasks_agent_type_idx" ON "agent_tasks"("agent_type");
|
||||
@@ -1,23 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TerminalSessionStatus" AS ENUM ('ACTIVE', 'CLOSED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "terminal_sessions" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL DEFAULT 'Terminal',
|
||||
"status" "TerminalSessionStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"closed_at" TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT "terminal_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "terminal_sessions_workspace_id_idx" ON "terminal_sessions"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "terminal_sessions_workspace_id_status_idx" ON "terminal_sessions"("workspace_id", "status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "terminal_sessions" ADD CONSTRAINT "terminal_sessions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable: add tone and formality_level columns to personalities
|
||||
ALTER TABLE "personalities" ADD COLUMN "tone" TEXT NOT NULL DEFAULT 'neutral';
|
||||
ALTER TABLE "personalities" ADD COLUMN "formality_level" "FormalityLevel" NOT NULL DEFAULT 'NEUTRAL';
|
||||
@@ -1,24 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_memories" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" JSONB NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "agent_memories_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_memories_workspace_id_agent_id_key_key" ON "agent_memories"("workspace_id", "agent_id", "key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_memories_workspace_id_idx" ON "agent_memories"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_memories_agent_id_idx" ON "agent_memories"("agent_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_memories" ADD CONSTRAINT "agent_memories_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,33 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "conversation_archives" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"session_id" TEXT NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"messages" JSONB NOT NULL,
|
||||
"message_count" INTEGER NOT NULL,
|
||||
"summary" TEXT NOT NULL,
|
||||
"embedding" vector(1536),
|
||||
"started_at" TIMESTAMPTZ NOT NULL,
|
||||
"ended_at" TIMESTAMPTZ,
|
||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "conversation_archives_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "conversation_archives_workspace_id_session_id_key" ON "conversation_archives"("workspace_id", "session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "conversation_archives_workspace_id_idx" ON "conversation_archives"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "conversation_archives_agent_id_idx" ON "conversation_archives"("agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "conversation_archives_started_at_idx" ON "conversation_archives"("started_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "conversation_archives" ADD CONSTRAINT "conversation_archives_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,109 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "SystemConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"encrypted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SystemConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BreakglassUser" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "BreakglassUser_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LlmProvider" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"baseUrl" TEXT,
|
||||
"apiKey" TEXT,
|
||||
"apiType" TEXT NOT NULL DEFAULT 'openai-completions',
|
||||
"models" JSONB NOT NULL DEFAULT '[]',
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LlmProvider_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserContainer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"containerId" TEXT,
|
||||
"containerName" TEXT NOT NULL,
|
||||
"gatewayPort" INTEGER,
|
||||
"gatewayToken" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'stopped',
|
||||
"lastActiveAt" TIMESTAMP(3),
|
||||
"idleTimeoutMin" INTEGER NOT NULL DEFAULT 30,
|
||||
"config" JSONB NOT NULL DEFAULT '{}',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserContainer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SystemContainer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL,
|
||||
"containerId" TEXT,
|
||||
"gatewayPort" INTEGER,
|
||||
"gatewayToken" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'stopped',
|
||||
"primaryModel" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SystemContainer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserAgentConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"primaryModel" TEXT,
|
||||
"fallbackModels" JSONB NOT NULL DEFAULT '[]',
|
||||
"personality" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserAgentConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SystemConfig_key_key" ON "SystemConfig"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BreakglassUser_username_key" ON "BreakglassUser"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LlmProvider_userId_idx" ON "LlmProvider"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LlmProvider_userId_name_key" ON "LlmProvider"("userId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserContainer_userId_key" ON "UserContainer"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SystemContainer_name_key" ON "SystemContainer"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserAgentConfig_userId_key" ON "UserAgentConfig"("userId");
|
||||
@@ -1,37 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "findings" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"task_id" UUID,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
"summary" TEXT NOT NULL,
|
||||
"embedding" vector(1536),
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "findings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "findings_id_workspace_id_key" ON "findings"("id", "workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "findings_workspace_id_idx" ON "findings"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "findings_agent_id_idx" ON "findings"("agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "findings_type_idx" ON "findings"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "findings_task_id_idx" ON "findings"("task_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "findings" ADD CONSTRAINT "findings_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "findings" ADD CONSTRAINT "findings_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "agent_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "tasks" ADD COLUMN "assigned_agent" TEXT;
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,599 +0,0 @@
|
||||
import {
|
||||
PrismaClient,
|
||||
TaskStatus,
|
||||
TaskPriority,
|
||||
ProjectStatus,
|
||||
WorkspaceMemberRole,
|
||||
EntryStatus,
|
||||
Visibility,
|
||||
} from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding database...");
|
||||
|
||||
// IMPORTANT: This seed script should not be run concurrently
|
||||
// If running in CI/CD, ensure serialization of seed operations
|
||||
// to prevent race conditions and data corruption
|
||||
|
||||
// Create test user
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: "[email protected]" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "[email protected]",
|
||||
name: "Development User",
|
||||
preferences: {
|
||||
theme: "system",
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Created user: ${user.email}`);
|
||||
|
||||
// Create workspace
|
||||
const workspace = await prisma.workspace.upsert({
|
||||
where: { id: "00000000-0000-0000-0000-000000000001" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "00000000-0000-0000-0000-000000000001",
|
||||
name: "Development Workspace",
|
||||
ownerId: user.id,
|
||||
settings: {
|
||||
timezone: "America/New_York",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Created workspace: ${workspace.name}`);
|
||||
|
||||
// Add user as workspace owner
|
||||
await prisma.workspaceMember.upsert({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
role: WorkspaceMemberRole.OWNER,
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// WIDGET DEFINITIONS (global, not workspace-scoped)
|
||||
// ============================================
|
||||
const widgetDefs = [
|
||||
{
|
||||
name: "TasksWidget",
|
||||
displayName: "Tasks",
|
||||
description: "View and manage your tasks",
|
||||
component: "TasksWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
maxWidth: 4,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "CalendarWidget",
|
||||
displayName: "Calendar",
|
||||
description: "View upcoming events and schedule",
|
||||
component: "CalendarWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 2,
|
||||
minHeight: 2,
|
||||
maxWidth: 4,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "QuickCaptureWidget",
|
||||
displayName: "Quick Capture",
|
||||
description: "Quickly capture notes and tasks",
|
||||
component: "QuickCaptureWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 1,
|
||||
minWidth: 2,
|
||||
minHeight: 1,
|
||||
maxWidth: 4,
|
||||
maxHeight: 2,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "AgentStatusWidget",
|
||||
displayName: "Agent Status",
|
||||
description: "Monitor agent activity and status",
|
||||
component: "AgentStatusWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
maxWidth: 3,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "ActiveProjectsWidget",
|
||||
displayName: "Active Projects & Agent Chains",
|
||||
description: "View active projects and running agent sessions",
|
||||
component: "ActiveProjectsWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 3,
|
||||
minWidth: 2,
|
||||
minHeight: 2,
|
||||
maxWidth: 4,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "TaskProgressWidget",
|
||||
displayName: "Task Progress",
|
||||
description: "Live progress of orchestrator agent tasks",
|
||||
component: "TaskProgressWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
maxWidth: 3,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
{
|
||||
name: "OrchestratorEventsWidget",
|
||||
displayName: "Orchestrator Events",
|
||||
description: "Recent orchestration events with stream/Matrix visibility",
|
||||
component: "OrchestratorEventsWidget",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
maxWidth: 4,
|
||||
maxHeight: null,
|
||||
configSchema: {},
|
||||
},
|
||||
];
|
||||
|
||||
for (const wd of widgetDefs) {
|
||||
await prisma.widgetDefinition.upsert({
|
||||
where: { name: wd.name },
|
||||
update: {
|
||||
displayName: wd.displayName,
|
||||
description: wd.description,
|
||||
component: wd.component,
|
||||
defaultWidth: wd.defaultWidth,
|
||||
defaultHeight: wd.defaultHeight,
|
||||
minWidth: wd.minWidth,
|
||||
minHeight: wd.minHeight,
|
||||
maxWidth: wd.maxWidth,
|
||||
maxHeight: wd.maxHeight,
|
||||
configSchema: wd.configSchema,
|
||||
},
|
||||
create: {
|
||||
name: wd.name,
|
||||
displayName: wd.displayName,
|
||||
description: wd.description,
|
||||
component: wd.component,
|
||||
defaultWidth: wd.defaultWidth,
|
||||
defaultHeight: wd.defaultHeight,
|
||||
minWidth: wd.minWidth,
|
||||
minHeight: wd.minHeight,
|
||||
maxWidth: wd.maxWidth,
|
||||
maxHeight: wd.maxHeight,
|
||||
configSchema: wd.configSchema,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Seeded ${widgetDefs.length} widget definitions`);
|
||||
|
||||
// Use transaction for atomic seed data reset and creation
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Delete existing seed data for idempotency (avoids duplicates on re-run)
|
||||
await tx.task.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
await tx.event.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
await tx.project.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
|
||||
// Create sample project
|
||||
const project = await tx.project.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
name: "Sample Project",
|
||||
description: "A sample project for development",
|
||||
status: ProjectStatus.ACTIVE,
|
||||
creatorId: user.id,
|
||||
color: "#3B82F6",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Created project: ${project.name}`);
|
||||
|
||||
// Create sample tasks
|
||||
const tasks = [
|
||||
{
|
||||
title: "Set up development environment",
|
||||
status: TaskStatus.COMPLETED,
|
||||
priority: TaskPriority.HIGH,
|
||||
},
|
||||
{
|
||||
title: "Review project requirements",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
},
|
||||
{
|
||||
title: "Design database schema",
|
||||
status: TaskStatus.COMPLETED,
|
||||
priority: TaskPriority.HIGH,
|
||||
},
|
||||
{
|
||||
title: "Implement NestJS integration",
|
||||
status: TaskStatus.COMPLETED,
|
||||
priority: TaskPriority.HIGH,
|
||||
},
|
||||
{
|
||||
title: "Create seed data",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
},
|
||||
];
|
||||
|
||||
// Use createMany for batch insertion (better performance)
|
||||
await tx.task.createMany({
|
||||
data: tasks.map((taskData) => ({
|
||||
workspaceId: workspace.id,
|
||||
title: taskData.title,
|
||||
status: taskData.status,
|
||||
priority: taskData.priority,
|
||||
creatorId: user.id,
|
||||
projectId: project.id,
|
||||
})),
|
||||
});
|
||||
|
||||
console.log(`Created ${tasks.length} sample tasks`);
|
||||
|
||||
// Create sample event
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(10, 0, 0, 0);
|
||||
|
||||
await tx.event.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
title: "Morning standup",
|
||||
description: "Daily team sync",
|
||||
startTime: tomorrow,
|
||||
endTime: new Date(tomorrow.getTime() + 30 * 60000), // 30 minutes later
|
||||
creatorId: user.id,
|
||||
projectId: project.id,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Created sample event");
|
||||
|
||||
// ============================================
|
||||
// KNOWLEDGE MODULE SEED DATA
|
||||
// ============================================
|
||||
|
||||
// Delete existing knowledge data
|
||||
await tx.knowledgeEmbedding.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
|
||||
await tx.knowledgeEntryTag.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
|
||||
await tx.knowledgeLink.deleteMany({ where: { source: { workspaceId: workspace.id } } });
|
||||
await tx.knowledgeEntryVersion.deleteMany({ where: { entry: { workspaceId: workspace.id } } });
|
||||
await tx.knowledgeEntry.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
await tx.knowledgeTag.deleteMany({ where: { workspaceId: workspace.id } });
|
||||
|
||||
// Create knowledge tags
|
||||
const tags = await Promise.all([
|
||||
tx.knowledgeTag.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
name: "Architecture",
|
||||
slug: "architecture",
|
||||
color: "#3B82F6",
|
||||
description: "System architecture and design decisions",
|
||||
},
|
||||
}),
|
||||
tx.knowledgeTag.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
name: "Development",
|
||||
slug: "development",
|
||||
color: "#10B981",
|
||||
description: "Development practices and guidelines",
|
||||
},
|
||||
}),
|
||||
tx.knowledgeTag.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
name: "Getting Started",
|
||||
slug: "getting-started",
|
||||
color: "#F59E0B",
|
||||
description: "Onboarding and setup guides",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log(`Created ${tags.length} knowledge tags`);
|
||||
|
||||
// Create knowledge entries
|
||||
const entries = [
|
||||
{
|
||||
slug: "welcome",
|
||||
title: "Welcome to Mosaic Stack Knowledge Base",
|
||||
content: `# Welcome to Mosaic Stack
|
||||
|
||||
This is the knowledge base for the Mosaic Stack project. Here you'll find:
|
||||
|
||||
- **[[architecture-overview]]** - High-level system architecture
|
||||
- **[[development-setup]]** - Getting started with development
|
||||
- **[[database-schema]]** - Database design and conventions
|
||||
|
||||
## About This Knowledge Base
|
||||
|
||||
The Knowledge Module provides:
|
||||
- Wiki-style linking between entries
|
||||
- Full-text and semantic search
|
||||
- Version history and change tracking
|
||||
- Tag-based organization
|
||||
|
||||
Start exploring by following the links above!`,
|
||||
summary: "Introduction to the Mosaic Stack knowledge base and navigation guide",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
visibility: Visibility.WORKSPACE,
|
||||
tags: ["getting-started"],
|
||||
},
|
||||
{
|
||||
slug: "architecture-overview",
|
||||
title: "Architecture Overview",
|
||||
content: `# Architecture Overview
|
||||
|
||||
The Mosaic Stack is built on a modern, scalable architecture:
|
||||
|
||||
## Stack Components
|
||||
|
||||
- **Frontend**: Next.js 15+ with React 19
|
||||
- **Backend**: NestJS with Prisma ORM
|
||||
- **Database**: PostgreSQL 17 with pgvector
|
||||
- **Cache**: Valkey (Redis fork)
|
||||
|
||||
## Key Modules
|
||||
|
||||
1. **Task Management** - See [[development-setup]] for local setup
|
||||
2. **Event Calendar** - Integrated scheduling
|
||||
3. **Knowledge Base** - This module! See [[database-schema]]
|
||||
4. **Agent Orchestration** - AI agent coordination
|
||||
|
||||
The database schema is documented in [[database-schema]].`,
|
||||
summary: "High-level overview of Mosaic Stack architecture and components",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
visibility: Visibility.WORKSPACE,
|
||||
tags: ["architecture"],
|
||||
},
|
||||
{
|
||||
slug: "development-setup",
|
||||
title: "Development Setup Guide",
|
||||
content: `# Development Setup Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 22+
|
||||
- PostgreSQL 17
|
||||
- pnpm 9+
|
||||
|
||||
## Quick Start
|
||||
|
||||
\`\`\`bash
|
||||
# Clone the repository
|
||||
git clone https://git.mosaicstack.dev/mosaic/stack.git
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Set up database
|
||||
cd apps/api
|
||||
pnpm prisma migrate dev
|
||||
pnpm prisma:seed
|
||||
|
||||
# Start development servers
|
||||
pnpm dev
|
||||
\`\`\`
|
||||
|
||||
## Architecture
|
||||
|
||||
Before diving in, review the [[architecture-overview]] to understand the system design.
|
||||
|
||||
## Database
|
||||
|
||||
The database schema is documented in [[database-schema]]. All models use Prisma ORM.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Read the [[architecture-overview]]
|
||||
2. Explore the codebase
|
||||
3. Check out the [[database-schema]] documentation`,
|
||||
summary: "Step-by-step guide to setting up the Mosaic Stack development environment",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
visibility: Visibility.WORKSPACE,
|
||||
tags: ["development", "getting-started"],
|
||||
},
|
||||
{
|
||||
slug: "database-schema",
|
||||
title: "Database Schema Documentation",
|
||||
content: `# Database Schema Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Mosaic Stack uses PostgreSQL 17 with Prisma ORM. See [[architecture-overview]] for context.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- All IDs are UUIDs
|
||||
- Timestamps use \`@db.Timestamptz\` (timezone-aware)
|
||||
- Soft deletes via status fields (e.g., ARCHIVED)
|
||||
- Relations enforce cascade deletes for data integrity
|
||||
|
||||
## Core Models
|
||||
|
||||
### Task Management
|
||||
- \`Task\` - Individual work items
|
||||
- \`Project\` - Task containers
|
||||
- \`Domain\` - High-level categorization
|
||||
|
||||
### Knowledge Module
|
||||
- \`KnowledgeEntry\` - Wiki pages
|
||||
- \`KnowledgeLink\` - Page connections
|
||||
- \`KnowledgeTag\` - Categorization
|
||||
- \`KnowledgeEmbedding\` - Semantic search (pgvector)
|
||||
|
||||
### Agent System
|
||||
- \`Agent\` - AI agent instances
|
||||
- \`AgentSession\` - Conversation sessions
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrations are managed via Prisma:
|
||||
|
||||
\`\`\`bash
|
||||
# Create migration
|
||||
pnpm prisma migrate dev --name my_migration
|
||||
|
||||
# Apply in production
|
||||
pnpm prisma migrate deploy
|
||||
\`\`\`
|
||||
|
||||
For setup instructions, see [[development-setup]].`,
|
||||
summary:
|
||||
"Comprehensive documentation of the Mosaic Stack database schema and Prisma conventions",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
visibility: Visibility.WORKSPACE,
|
||||
tags: ["architecture", "development"],
|
||||
},
|
||||
{
|
||||
slug: "future-ideas",
|
||||
title: "Future Ideas and Roadmap",
|
||||
content: `# Future Ideas and Roadmap
|
||||
|
||||
## Planned Features
|
||||
|
||||
- Real-time collaboration (CRDT)
|
||||
- Advanced graph visualizations
|
||||
- AI-powered summarization
|
||||
- Mobile app
|
||||
|
||||
## Research Areas
|
||||
|
||||
- Vector search optimization
|
||||
- Knowledge graph algorithms
|
||||
- Agent memory systems
|
||||
|
||||
This is a draft document. See [[architecture-overview]] for current state.`,
|
||||
summary: "Brainstorming document for future features and research directions",
|
||||
status: EntryStatus.DRAFT,
|
||||
visibility: Visibility.PRIVATE,
|
||||
tags: [],
|
||||
},
|
||||
];
|
||||
|
||||
// Create entries and track them for linking
|
||||
const createdEntries = new Map<string, any>();
|
||||
|
||||
for (const entryData of entries) {
|
||||
const entry = await tx.knowledgeEntry.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
slug: entryData.slug,
|
||||
title: entryData.title,
|
||||
content: entryData.content,
|
||||
summary: entryData.summary,
|
||||
status: entryData.status,
|
||||
visibility: entryData.visibility,
|
||||
createdBy: user.id,
|
||||
updatedBy: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
createdEntries.set(entryData.slug, entry);
|
||||
|
||||
// Create initial version
|
||||
await tx.knowledgeEntryVersion.create({
|
||||
data: {
|
||||
entryId: entry.id,
|
||||
version: 1,
|
||||
title: entry.title,
|
||||
content: entry.content,
|
||||
summary: entry.summary,
|
||||
createdBy: user.id,
|
||||
changeNote: "Initial version",
|
||||
},
|
||||
});
|
||||
|
||||
// Add tags
|
||||
for (const tagSlug of entryData.tags) {
|
||||
const tag = tags.find((t) => t.slug === tagSlug);
|
||||
if (tag) {
|
||||
await tx.knowledgeEntryTag.create({
|
||||
data: {
|
||||
entryId: entry.id,
|
||||
tagId: tag.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Created ${entries.length} knowledge entries`);
|
||||
|
||||
// Create wiki-links between entries
|
||||
const links = [
|
||||
{ source: "welcome", target: "architecture-overview", text: "architecture-overview" },
|
||||
{ source: "welcome", target: "development-setup", text: "development-setup" },
|
||||
{ source: "welcome", target: "database-schema", text: "database-schema" },
|
||||
{ source: "architecture-overview", target: "development-setup", text: "development-setup" },
|
||||
{ source: "architecture-overview", target: "database-schema", text: "database-schema" },
|
||||
{
|
||||
source: "development-setup",
|
||||
target: "architecture-overview",
|
||||
text: "architecture-overview",
|
||||
},
|
||||
{ source: "development-setup", target: "database-schema", text: "database-schema" },
|
||||
{ source: "database-schema", target: "architecture-overview", text: "architecture-overview" },
|
||||
{ source: "database-schema", target: "development-setup", text: "development-setup" },
|
||||
{ source: "future-ideas", target: "architecture-overview", text: "architecture-overview" },
|
||||
];
|
||||
|
||||
for (const link of links) {
|
||||
const sourceEntry = createdEntries.get(link.source);
|
||||
const targetEntry = createdEntries.get(link.target);
|
||||
|
||||
if (sourceEntry && targetEntry) {
|
||||
await tx.knowledgeLink.create({
|
||||
data: {
|
||||
sourceId: sourceEntry.id,
|
||||
targetId: targetEntry.id,
|
||||
linkText: link.text,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Created ${links.length} knowledge links`);
|
||||
});
|
||||
console.log("Seeding completed successfully!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error("Error seeding database:", e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
/**
|
||||
* Data Migration: Encrypt LLM Provider API Keys
|
||||
*
|
||||
* Encrypts all plaintext API keys in llm_provider_instances.config using OpenBao Transit.
|
||||
* This script processes records in batches and runs in a transaction for safety.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm --filter @mosaic/api migrate:encrypt-llm-keys
|
||||
*
|
||||
* Environment Variables:
|
||||
* DATABASE_URL - PostgreSQL connection string
|
||||
* OPENBAO_ADDR - OpenBao server address (default: http://openbao:8200)
|
||||
* APPROLE_CREDENTIALS_PATH - Path to AppRole credentials file
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { VaultService } from "../src/vault/vault.service";
|
||||
import { TransitKey } from "../src/vault/vault.constants";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
interface LlmProviderConfig {
|
||||
apiKey?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface LlmProviderInstance {
|
||||
id: string;
|
||||
config: LlmProviderConfig;
|
||||
providerType: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is already encrypted
|
||||
*/
|
||||
function isEncrypted(value: string): boolean {
|
||||
if (!value || typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vault format: vault:v1:...
|
||||
if (value.startsWith("vault:v1:")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// AES format: iv:authTag:encrypted (3 colon-separated hex parts)
|
||||
const parts = value.split(":");
|
||||
if (parts.length === 3 && parts.every((part) => /^[0-9a-f]+$/i.test(part))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main migration function
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
const logger = new Logger("EncryptLlmKeys");
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
try {
|
||||
logger.log("Starting LLM API key encryption migration...");
|
||||
|
||||
// Initialize VaultService
|
||||
const configService = new ConfigService();
|
||||
const vaultService = new VaultService(configService);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
await vaultService.onModuleInit();
|
||||
|
||||
logger.log("VaultService initialized successfully");
|
||||
|
||||
// Fetch all LLM provider instances
|
||||
const instances = await prisma.llmProviderInstance.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
config: true,
|
||||
providerType: true,
|
||||
displayName: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.log(`Found ${String(instances.length)} LLM provider instances`);
|
||||
|
||||
let encryptedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// Process each instance
|
||||
for (const instance of instances as LlmProviderInstance[]) {
|
||||
try {
|
||||
const config = instance.config;
|
||||
|
||||
// Skip if no apiKey field
|
||||
if (!config.apiKey || typeof config.apiKey !== "string") {
|
||||
logger.debug(`Skipping ${instance.displayName} (${instance.id}): No API key`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if already encrypted
|
||||
if (isEncrypted(config.apiKey)) {
|
||||
logger.debug(`Skipping ${instance.displayName} (${instance.id}): Already encrypted`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Encrypt the API key
|
||||
logger.log(`Encrypting ${instance.displayName} (${instance.providerType})...`);
|
||||
|
||||
const encryptedApiKey = await vaultService.encrypt(config.apiKey, TransitKey.LLM_CONFIG);
|
||||
|
||||
// Update the instance with encrypted key
|
||||
await prisma.llmProviderInstance.update({
|
||||
where: { id: instance.id },
|
||||
data: {
|
||||
config: {
|
||||
...config,
|
||||
apiKey: encryptedApiKey,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
encryptedCount++;
|
||||
logger.log(`✓ Encrypted ${instance.displayName} (${instance.id})`);
|
||||
} catch (error: unknown) {
|
||||
errorCount++;
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`✗ Failed to encrypt ${instance.displayName} (${instance.id}): ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
logger.log("\n=== Migration Summary ===");
|
||||
logger.log(`Total instances: ${String(instances.length)}`);
|
||||
logger.log(`Encrypted: ${String(encryptedCount)}`);
|
||||
logger.log(`Skipped: ${String(skippedCount)}`);
|
||||
logger.log(`Errors: ${String(errorCount)}`);
|
||||
|
||||
if (errorCount > 0) {
|
||||
logger.warn("\n⚠️ Some API keys failed to encrypt. Please review the errors above.");
|
||||
process.exit(1);
|
||||
} else if (encryptedCount === 0) {
|
||||
logger.log("\n✓ All API keys are already encrypted or no keys found.");
|
||||
} else {
|
||||
logger.log("\n✓ Migration completed successfully!");
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`Migration failed: ${errorMsg}`);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,314 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { ActivityController } from "./activity.controller";
|
||||
import { ActivityService } from "./activity.service";
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
import type { QueryActivityLogDto } from "./dto";
|
||||
|
||||
describe("ActivityController", () => {
|
||||
let controller: ActivityController;
|
||||
let service: ActivityService;
|
||||
|
||||
const mockActivityService = {
|
||||
findAll: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
getAuditTrail: vi.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceId = "workspace-123";
|
||||
|
||||
beforeEach(() => {
|
||||
service = mockActivityService as any;
|
||||
controller = new ActivityController(service);
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
const mockPaginatedResult = {
|
||||
data: [
|
||||
{
|
||||
id: "activity-1",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "task-123",
|
||||
details: {},
|
||||
createdAt: new Date("2024-01-01"),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalPages: 1,
|
||||
},
|
||||
};
|
||||
|
||||
it("should return paginated activity logs using authenticated user's workspaceId", async () => {
|
||||
const query: QueryActivityLogDto = {
|
||||
workspaceId: "workspace-123",
|
||||
page: 1,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
mockActivityService.findAll.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
const result = await controller.findAll(query, mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockPaginatedResult);
|
||||
expect(mockActivityService.findAll).toHaveBeenCalledWith({
|
||||
...query,
|
||||
workspaceId: "workspace-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle query with filters", async () => {
|
||||
const query: QueryActivityLogDto = {
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
mockActivityService.findAll.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
await controller.findAll(query, mockWorkspaceId);
|
||||
|
||||
expect(mockActivityService.findAll).toHaveBeenCalledWith({
|
||||
...query,
|
||||
workspaceId: "workspace-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle query with date range", async () => {
|
||||
const startDate = new Date("2024-01-01");
|
||||
const endDate = new Date("2024-01-31");
|
||||
|
||||
const query: QueryActivityLogDto = {
|
||||
workspaceId: "workspace-123",
|
||||
startDate,
|
||||
endDate,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
mockActivityService.findAll.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
await controller.findAll(query, mockWorkspaceId);
|
||||
|
||||
expect(mockActivityService.findAll).toHaveBeenCalledWith({
|
||||
...query,
|
||||
workspaceId: "workspace-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should use user's workspaceId even if query provides different one", async () => {
|
||||
const query: QueryActivityLogDto = {
|
||||
workspaceId: "different-workspace",
|
||||
page: 1,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
mockActivityService.findAll.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
await controller.findAll(query, mockWorkspaceId);
|
||||
|
||||
// Should use authenticated user's workspaceId, not query's
|
||||
expect(mockActivityService.findAll).toHaveBeenCalledWith({
|
||||
...query,
|
||||
workspaceId: "workspace-123",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("findOne", () => {
|
||||
const mockActivity = {
|
||||
id: "activity-123",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "task-123",
|
||||
details: {},
|
||||
createdAt: new Date(),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
};
|
||||
|
||||
it("should return a single activity log using authenticated user's workspaceId", async () => {
|
||||
mockActivityService.findOne.mockResolvedValue(mockActivity);
|
||||
|
||||
const result = await controller.findOne("activity-123", mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockActivity);
|
||||
expect(mockActivityService.findOne).toHaveBeenCalledWith("activity-123", "workspace-123");
|
||||
});
|
||||
|
||||
it("should return null if activity not found", async () => {
|
||||
mockActivityService.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.findOne("nonexistent", mockWorkspaceId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null if workspaceId is missing (service handles gracefully)", async () => {
|
||||
mockActivityService.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.findOne("activity-123", undefined as any);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockActivityService.findOne).toHaveBeenCalledWith("activity-123", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuditTrail", () => {
|
||||
const mockAuditTrail = [
|
||||
{
|
||||
id: "activity-1",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "task-123",
|
||||
details: { title: "New Task" },
|
||||
createdAt: new Date("2024-01-01"),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "activity-2",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-456",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "task-123",
|
||||
details: { title: "Updated Task" },
|
||||
createdAt: new Date("2024-01-02"),
|
||||
user: {
|
||||
id: "user-456",
|
||||
name: "Another User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it("should return audit trail for a task using authenticated user's workspaceId", async () => {
|
||||
mockActivityService.getAuditTrail.mockResolvedValue(mockAuditTrail);
|
||||
|
||||
const result = await controller.getAuditTrail(EntityType.TASK, "task-123", mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockAuditTrail);
|
||||
expect(mockActivityService.getAuditTrail).toHaveBeenCalledWith(
|
||||
"workspace-123",
|
||||
EntityType.TASK,
|
||||
"task-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return audit trail for an event", async () => {
|
||||
const eventAuditTrail = [
|
||||
{
|
||||
id: "activity-3",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.EVENT,
|
||||
entityId: "event-123",
|
||||
details: {},
|
||||
createdAt: new Date(),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockActivityService.getAuditTrail.mockResolvedValue(eventAuditTrail);
|
||||
|
||||
const result = await controller.getAuditTrail(EntityType.EVENT, "event-123", mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(eventAuditTrail);
|
||||
expect(mockActivityService.getAuditTrail).toHaveBeenCalledWith(
|
||||
"workspace-123",
|
||||
EntityType.EVENT,
|
||||
"event-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return audit trail for a project", async () => {
|
||||
const projectAuditTrail = [
|
||||
{
|
||||
id: "activity-4",
|
||||
workspaceId: "workspace-123",
|
||||
userId: "user-123",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: "project-123",
|
||||
details: {},
|
||||
createdAt: new Date(),
|
||||
user: {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "[email protected]",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockActivityService.getAuditTrail.mockResolvedValue(projectAuditTrail);
|
||||
|
||||
const result = await controller.getAuditTrail(
|
||||
EntityType.PROJECT,
|
||||
"project-123",
|
||||
mockWorkspaceId
|
||||
);
|
||||
|
||||
expect(result).toEqual(projectAuditTrail);
|
||||
expect(mockActivityService.getAuditTrail).toHaveBeenCalledWith(
|
||||
"workspace-123",
|
||||
EntityType.PROJECT,
|
||||
"project-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty array if no audit trail found", async () => {
|
||||
mockActivityService.getAuditTrail.mockResolvedValue([]);
|
||||
|
||||
const result = await controller.getAuditTrail(
|
||||
EntityType.WORKSPACE,
|
||||
"workspace-999",
|
||||
mockWorkspaceId
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return empty array if workspaceId is missing (service handles gracefully)", async () => {
|
||||
mockActivityService.getAuditTrail.mockResolvedValue([]);
|
||||
|
||||
const result = await controller.getAuditTrail(EntityType.TASK, "task-123", undefined as any);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockActivityService.getAuditTrail).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
EntityType.TASK,
|
||||
"task-123"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Controller, Get, Query, Param, UseGuards } from "@nestjs/common";
|
||||
import { ActivityService } from "./activity.service";
|
||||
import { EntityType } from "@prisma/client";
|
||||
import { QueryActivityLogDto } from "./dto";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
|
||||
import { Workspace, Permission, RequirePermission } from "../common/decorators";
|
||||
|
||||
@Controller("activity")
|
||||
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
||||
export class ActivityController {
|
||||
constructor(private readonly activityService: ActivityService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async findAll(@Query() query: QueryActivityLogDto, @Workspace() workspaceId: string) {
|
||||
return this.activityService.findAll(Object.assign({}, query, { workspaceId }));
|
||||
}
|
||||
|
||||
@Get("audit/:entityType/:entityId")
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async getAuditTrail(
|
||||
@Param("entityType") entityType: EntityType,
|
||||
@Param("entityId") entityId: string,
|
||||
@Workspace() workspaceId: string
|
||||
) {
|
||||
return this.activityService.getAuditTrail(workspaceId, entityType, entityId);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async findOne(@Param("id") id: string, @Workspace() workspaceId: string) {
|
||||
return this.activityService.findOne(id, workspaceId);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ActivityController } from "./activity.controller";
|
||||
import { ActivityService } from "./activity.service";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
|
||||
/**
|
||||
* Module for activity logging and audit trail functionality
|
||||
*/
|
||||
@Module({
|
||||
imports: [PrismaModule, AuthModule],
|
||||
controllers: [ActivityController],
|
||||
providers: [ActivityService],
|
||||
exports: [ActivityService],
|
||||
})
|
||||
export class ActivityModule {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,585 +0,0 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { ActivityAction, EntityType, Prisma, ActivityLog } from "@prisma/client";
|
||||
import type {
|
||||
CreateActivityLogInput,
|
||||
PaginatedActivityLogs,
|
||||
ActivityLogResult,
|
||||
} from "./interfaces/activity.interface";
|
||||
import type { QueryActivityLogDto } from "./dto";
|
||||
|
||||
/**
|
||||
* Service for managing activity logs and audit trails
|
||||
*/
|
||||
@Injectable()
|
||||
export class ActivityService {
|
||||
private readonly logger = new Logger(ActivityService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Create a new activity log entry (fire-and-forget)
|
||||
*
|
||||
* Activity logging failures are logged but never propagate to callers.
|
||||
* This ensures activity logging never breaks primary operations.
|
||||
*
|
||||
* @returns The created ActivityLog or null if logging failed
|
||||
*/
|
||||
async logActivity(input: CreateActivityLogInput): Promise<ActivityLog | null> {
|
||||
try {
|
||||
return await this.prisma.activityLog.create({
|
||||
data: input as unknown as Prisma.ActivityLogCreateInput,
|
||||
});
|
||||
} catch (error) {
|
||||
// Log the error but don't propagate - activity logging is fire-and-forget
|
||||
this.logger.error(
|
||||
`Failed to log activity: action=${input.action} entityType=${input.entityType} entityId=${input.entityId}`,
|
||||
error instanceof Error ? error.stack : String(error)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated activity logs with filters
|
||||
*/
|
||||
async findAll(query: QueryActivityLogDto): Promise<PaginatedActivityLogs> {
|
||||
const page = query.page ?? 1;
|
||||
const limit = query.limit ?? 50;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.ActivityLogWhereInput = {};
|
||||
|
||||
if (query.workspaceId !== undefined) {
|
||||
where.workspaceId = query.workspaceId;
|
||||
}
|
||||
|
||||
if (query.userId) {
|
||||
where.userId = query.userId;
|
||||
}
|
||||
|
||||
if (query.action) {
|
||||
where.action = query.action;
|
||||
}
|
||||
|
||||
if (query.entityType) {
|
||||
where.entityType = query.entityType;
|
||||
}
|
||||
|
||||
if (query.entityId) {
|
||||
where.entityId = query.entityId;
|
||||
}
|
||||
|
||||
if (query.startDate ?? query.endDate) {
|
||||
where.createdAt = {};
|
||||
if (query.startDate) {
|
||||
where.createdAt.gte = query.startDate;
|
||||
}
|
||||
if (query.endDate) {
|
||||
where.createdAt.lte = query.endDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute queries in parallel
|
||||
const [data, total] = await Promise.all([
|
||||
this.prisma.activityLog.findMany({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.activityLog.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single activity log by ID
|
||||
*/
|
||||
async findOne(id: string, workspaceId?: string): Promise<ActivityLogResult | null> {
|
||||
const where: Prisma.ActivityLogWhereUniqueInput = { id };
|
||||
if (workspaceId) {
|
||||
where.workspaceId = workspaceId;
|
||||
}
|
||||
return await this.prisma.activityLog.findUnique({
|
||||
where,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit trail for a specific entity
|
||||
*/
|
||||
async getAuditTrail(
|
||||
workspaceId: string,
|
||||
entityType: EntityType,
|
||||
entityId: string
|
||||
): Promise<ActivityLogResult[]> {
|
||||
return await this.prisma.activityLog.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
entityType,
|
||||
entityId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPER METHODS FOR COMMON ACTIVITY TYPES
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Log task creation
|
||||
*/
|
||||
async logTaskCreated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
taskId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: taskId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log task update
|
||||
*/
|
||||
async logTaskUpdated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
taskId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: taskId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log task deletion
|
||||
*/
|
||||
async logTaskDeleted(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
taskId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.DELETED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: taskId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log task completion
|
||||
*/
|
||||
async logTaskCompleted(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
taskId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.COMPLETED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: taskId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log task assignment
|
||||
*/
|
||||
async logTaskAssigned(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
taskId: string,
|
||||
assigneeId: string
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.ASSIGNED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: taskId,
|
||||
details: { assigneeId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log event creation
|
||||
*/
|
||||
async logEventCreated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
eventId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.EVENT,
|
||||
entityId: eventId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log event update
|
||||
*/
|
||||
async logEventUpdated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
eventId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.EVENT,
|
||||
entityId: eventId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log event deletion
|
||||
*/
|
||||
async logEventDeleted(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
eventId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.DELETED,
|
||||
entityType: EntityType.EVENT,
|
||||
entityId: eventId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log project creation
|
||||
*/
|
||||
async logProjectCreated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
projectId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: projectId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log project update
|
||||
*/
|
||||
async logProjectUpdated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
projectId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: projectId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log project deletion
|
||||
*/
|
||||
async logProjectDeleted(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
projectId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.DELETED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: projectId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log workspace creation
|
||||
*/
|
||||
async logWorkspaceCreated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.WORKSPACE,
|
||||
entityId: workspaceId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log workspace update
|
||||
*/
|
||||
async logWorkspaceUpdated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.WORKSPACE,
|
||||
entityId: workspaceId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log workspace member added
|
||||
*/
|
||||
async logWorkspaceMemberAdded(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
memberId: string,
|
||||
role: string
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.WORKSPACE,
|
||||
entityId: workspaceId,
|
||||
details: { memberId, role },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log workspace member removed
|
||||
*/
|
||||
async logWorkspaceMemberRemoved(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
memberId: string
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.DELETED,
|
||||
entityType: EntityType.WORKSPACE,
|
||||
entityId: workspaceId,
|
||||
details: { memberId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log user profile update
|
||||
*/
|
||||
async logUserUpdated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.USER,
|
||||
entityId: userId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log domain creation
|
||||
*/
|
||||
async logDomainCreated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
domainId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.DOMAIN,
|
||||
entityId: domainId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log domain update
|
||||
*/
|
||||
async logDomainUpdated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
domainId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.DOMAIN,
|
||||
entityId: domainId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log domain deletion
|
||||
*/
|
||||
async logDomainDeleted(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
domainId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.DELETED,
|
||||
entityType: EntityType.DOMAIN,
|
||||
entityId: domainId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log idea creation
|
||||
*/
|
||||
async logIdeaCreated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
ideaId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.IDEA,
|
||||
entityId: ideaId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log idea update
|
||||
*/
|
||||
async logIdeaUpdated(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
ideaId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.IDEA,
|
||||
entityId: ideaId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log idea deletion
|
||||
*/
|
||||
async logIdeaDeleted(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
ideaId: string,
|
||||
details?: Prisma.JsonValue
|
||||
): Promise<ActivityLog | null> {
|
||||
return this.logActivity({
|
||||
workspaceId,
|
||||
userId,
|
||||
action: ActivityAction.DELETED,
|
||||
entityType: EntityType.IDEA,
|
||||
entityId: ideaId,
|
||||
...(details && { details }),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { validate } from "class-validator";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { CreateActivityLogDto } from "./create-activity-log.dto";
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
|
||||
describe("CreateActivityLogDto", () => {
|
||||
describe("required fields validation", () => {
|
||||
it("should pass with all required fields valid", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail when workspaceId is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const workspaceIdError = errors.find((e) => e.property === "workspaceId");
|
||||
expect(workspaceIdError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when userId is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const userIdError = errors.find((e) => e.property === "userId");
|
||||
expect(userIdError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when action is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const actionError = errors.find((e) => e.property === "action");
|
||||
expect(actionError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when entityType is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const entityTypeError = errors.find((e) => e.property === "entityType");
|
||||
expect(entityTypeError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when entityId is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const entityIdError = errors.find((e) => e.property === "entityId");
|
||||
expect(entityIdError).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("UUID validation", () => {
|
||||
it("should fail with invalid workspaceId UUID", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "invalid-uuid",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("workspaceId");
|
||||
});
|
||||
|
||||
it("should fail with invalid userId UUID", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "not-a-uuid",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const userIdError = errors.find((e) => e.property === "userId");
|
||||
expect(userIdError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail with invalid entityId UUID", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "bad-entity-id",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const entityIdError = errors.find((e) => e.property === "entityId");
|
||||
expect(entityIdError).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("enum validation", () => {
|
||||
it("should pass with all valid ActivityAction values", async () => {
|
||||
const actions = Object.values(ActivityAction);
|
||||
|
||||
for (const action of actions) {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail with invalid action value", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: "INVALID_ACTION",
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const actionError = errors.find((e) => e.property === "action");
|
||||
expect(actionError?.constraints?.isEnum).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass with all valid EntityType values", async () => {
|
||||
const entityTypes = Object.values(EntityType);
|
||||
|
||||
for (const entityType of entityTypes) {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail with invalid entityType value", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: "INVALID_TYPE",
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const entityTypeError = errors.find((e) => e.property === "entityType");
|
||||
expect(entityTypeError?.constraints?.isEnum).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("optional fields validation", () => {
|
||||
it("should pass with valid details object", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
details: {
|
||||
field: "status",
|
||||
oldValue: "TODO",
|
||||
newValue: "IN_PROGRESS",
|
||||
},
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with non-object details", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
details: "not an object",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const detailsError = errors.find((e) => e.property === "details");
|
||||
expect(detailsError?.constraints?.isObject).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass with valid ipAddress", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
ipAddress: "192.168.1.1",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should pass with valid IPv6 address", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
ipAddress: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail when ipAddress exceeds max length", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
ipAddress: "a".repeat(46),
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const ipError = errors.find((e) => e.property === "ipAddress");
|
||||
expect(ipError?.constraints?.maxLength).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass with valid userAgent", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail when userAgent exceeds max length", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
userAgent: "a".repeat(501),
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const userAgentError = errors.find((e) => e.property === "userAgent");
|
||||
expect(userAgentError?.constraints?.maxLength).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass when optional fields are not provided", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("complete validation", () => {
|
||||
it("should pass with all fields valid", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
details: {
|
||||
changes: ["status", "priority"],
|
||||
metadata: { source: "web-app" },
|
||||
},
|
||||
ipAddress: "10.0.0.1",
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
import { IsUUID, IsEnum, IsOptional, IsObject, IsString, MaxLength } from "class-validator";
|
||||
|
||||
/**
|
||||
* DTO for creating a new activity log entry
|
||||
*/
|
||||
export class CreateActivityLogDto {
|
||||
@IsUUID("4", { message: "workspaceId must be a valid UUID" })
|
||||
workspaceId!: string;
|
||||
|
||||
@IsUUID("4", { message: "userId must be a valid UUID" })
|
||||
userId!: string;
|
||||
|
||||
@IsEnum(ActivityAction, { message: "action must be a valid ActivityAction" })
|
||||
action!: ActivityAction;
|
||||
|
||||
@IsEnum(EntityType, { message: "entityType must be a valid EntityType" })
|
||||
entityType!: EntityType;
|
||||
|
||||
@IsUUID("4", { message: "entityId must be a valid UUID" })
|
||||
entityId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject({ message: "details must be an object" })
|
||||
details?: Record<string, unknown>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "ipAddress must be a string" })
|
||||
@MaxLength(45, { message: "ipAddress must not exceed 45 characters" })
|
||||
ipAddress?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "userAgent must be a string" })
|
||||
@MaxLength(500, { message: "userAgent must not exceed 500 characters" })
|
||||
userAgent?: string;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./create-activity-log.dto";
|
||||
export * from "./query-activity-log.dto";
|
||||
@@ -1,254 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { validate } from "class-validator";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { QueryActivityLogDto } from "./query-activity-log.dto";
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
|
||||
describe("QueryActivityLogDto", () => {
|
||||
describe("workspaceId validation", () => {
|
||||
it("should pass with valid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "invalid-uuid",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("workspaceId");
|
||||
expect(errors[0].constraints?.isUuid).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass when workspaceId is missing (it's optional)", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {});
|
||||
|
||||
const errors = await validate(dto);
|
||||
// workspaceId is optional in DTO since it's set by controller from @Workspace() decorator
|
||||
const workspaceIdError = errors.find((e) => e.property === "workspaceId");
|
||||
expect(workspaceIdError).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("userId validation", () => {
|
||||
it("should pass with valid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "not-a-uuid",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("userId");
|
||||
});
|
||||
|
||||
it("should pass when userId is not provided (optional)", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("action validation", () => {
|
||||
it("should pass with valid ActivityAction", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
action: ActivityAction.CREATED,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid action value", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
action: "INVALID_ACTION",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("action");
|
||||
});
|
||||
|
||||
it("should pass when action is not provided (optional)", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("entityType validation", () => {
|
||||
it("should pass with valid EntityType", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
entityType: EntityType.TASK,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid entityType value", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
entityType: "INVALID_TYPE",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("entityType");
|
||||
});
|
||||
});
|
||||
|
||||
describe("entityId validation", () => {
|
||||
it("should pass with valid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
entityId: "invalid-entity-id",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("entityId");
|
||||
});
|
||||
});
|
||||
|
||||
describe("date validation", () => {
|
||||
it("should pass with valid ISO date strings", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
startDate: "2024-01-01T00:00:00.000Z",
|
||||
endDate: "2024-01-31T23:59:59.999Z",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid date format", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
startDate: "not-a-date",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("startDate");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pagination validation", () => {
|
||||
it("should pass with valid page and limit", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
page: "1",
|
||||
limit: "50",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.page).toBe(1);
|
||||
expect(dto.limit).toBe(50);
|
||||
});
|
||||
|
||||
it("should fail when page is less than 1", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
page: "0",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const pageError = errors.find((e) => e.property === "page");
|
||||
expect(pageError?.constraints?.min).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when limit exceeds 100", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
limit: "101",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const limitError = errors.find((e) => e.property === "limit");
|
||||
expect(limitError?.constraints?.max).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when page is not an integer", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
page: "1.5",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const pageError = errors.find((e) => e.property === "page");
|
||||
expect(pageError?.constraints?.isInt).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when limit is not an integer", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
limit: "50.5",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const limitError = errors.find((e) => e.property === "limit");
|
||||
expect(limitError?.constraints?.isInt).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiple filters", () => {
|
||||
it("should pass with all valid filters combined", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
startDate: "2024-01-01T00:00:00.000Z",
|
||||
endDate: "2024-01-31T23:59:59.999Z",
|
||||
page: "2",
|
||||
limit: "25",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
import { IsUUID, IsEnum, IsOptional, IsInt, Min, Max, IsDateString } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
/**
|
||||
* DTO for querying activity logs with filters and pagination
|
||||
*/
|
||||
export class QueryActivityLogDto {
|
||||
@IsOptional()
|
||||
@IsUUID("4", { message: "workspaceId must be a valid UUID" })
|
||||
workspaceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID("4", { message: "userId must be a valid UUID" })
|
||||
userId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ActivityAction, { message: "action must be a valid ActivityAction" })
|
||||
action?: ActivityAction;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(EntityType, { message: "entityType must be a valid EntityType" })
|
||||
entityType?: EntityType;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID("4", { message: "entityId must be a valid UUID" })
|
||||
entityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString({}, { message: "startDate must be a valid ISO 8601 date string" })
|
||||
startDate?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString({}, { message: "endDate must be a valid ISO 8601 date string" })
|
||||
endDate?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt({ message: "page must be an integer" })
|
||||
@Min(1, { message: "page must be at least 1" })
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt({ message: "limit must be an integer" })
|
||||
@Min(1, { message: "limit must be at least 1" })
|
||||
@Max(100, { message: "limit must not exceed 100" })
|
||||
limit?: number;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user