diff --git a/apps/processor/src/api/avatar.ts b/apps/processor/src/api/avatar.ts index c168fadc78..f03c91dd2d 100644 --- a/apps/processor/src/api/avatar.ts +++ b/apps/processor/src/api/avatar.ts @@ -2,7 +2,7 @@ import { Router, Request, Response } from 'express'; import multer from 'multer'; import { promises as fs } from 'fs'; import path from 'path'; -import { hasServiceScope } from '../middleware/auth'; +import { hasAuthScope } from '../middleware/auth'; import { processorLogger } from '../logger'; import { DEFAULT_IMAGE_EXTENSION, @@ -42,7 +42,7 @@ if (!AVATAR_ROOT) { // Avatar upload endpoint router.post('/upload', upload.single('file'), async (req: Request, res: Response) => { try { - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { return res.status(401).json({ error: 'Service authentication required' }); } @@ -64,7 +64,7 @@ router.post('/upload', upload.single('file'), async (req: Request, res: Response if ( auth.userId && userId !== auth.userId && - !hasServiceScope(auth, 'avatars:write:any') + !hasAuthScope(auth, 'avatars:write:any') ) { return res.status(403).json({ error: 'Cannot modify avatar for another user' }); } @@ -127,7 +127,7 @@ router.post('/upload', upload.single('file'), async (req: Request, res: Response // Avatar deletion endpoint router.delete('/:userId', async (req: Request, res: Response) => { try { - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { return res.status(401).json({ error: 'Service authentication required' }); } @@ -144,7 +144,7 @@ router.delete('/:userId', async (req: Request, res: Response) => { if ( auth.userId && userId !== auth.userId && - !hasServiceScope(auth, 'avatars:write:any') + !hasAuthScope(auth, 'avatars:write:any') ) { return res.status(403).json({ error: 'Cannot delete avatar for another user' }); } diff --git a/apps/processor/src/api/ingest.ts b/apps/processor/src/api/ingest.ts index 8e896fbb1d..0f0c1f06ee 100644 --- a/apps/processor/src/api/ingest.ts +++ b/apps/processor/src/api/ingest.ts @@ -10,11 +10,11 @@ const router = Router(); // Enqueue ingestion job by pageId router.post('/by-page/:pageId', async (req, res) => { try { - if (!req.serviceAuth) { + if (!req.auth) { return res.status(401).json({ error: 'Service authentication required' }); } - const userId = req.serviceAuth.userId; + const userId = req.auth.userId; if (!userId) { return res.status(401).json({ error: 'Service authentication required' }); } diff --git a/apps/processor/src/api/optimize.ts b/apps/processor/src/api/optimize.ts index 26507b4ab8..02ecc0a2b9 100644 --- a/apps/processor/src/api/optimize.ts +++ b/apps/processor/src/api/optimize.ts @@ -11,7 +11,7 @@ const router = Router(); // Optimize image endpoint - synchronous if cached, async if not router.post('/', async (req, res) => { try { - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { return res.status(401).json({ error: 'Service authentication required' }); } @@ -107,7 +107,7 @@ router.post('/', async (req, res) => { // Batch optimize endpoint for multiple presets router.post('/batch', async (req, res) => { try { - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { return res.status(401).json({ error: 'Service authentication required' }); } @@ -190,7 +190,7 @@ router.post('/batch', async (req, res) => { // Prepare image for AI endpoint router.post('/prepare-for-ai', async (req, res) => { try { - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { return res.status(401).json({ error: 'Service authentication required' }); } diff --git a/apps/processor/src/api/serve.ts b/apps/processor/src/api/serve.ts index c8eb362f22..606cba3eba 100644 --- a/apps/processor/src/api/serve.ts +++ b/apps/processor/src/api/serve.ts @@ -12,7 +12,7 @@ const router = Router(); router.get('/:contentHash/original', async (req, res) => { try { const { contentHash } = req.params; - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { return res.status(401).json({ error: 'Service authentication required' }); @@ -127,7 +127,7 @@ router.get('/:contentHash/original', async (req, res) => { router.get('/:contentHash/:preset', async (req, res) => { try { const { contentHash, preset } = req.params; - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { return res.status(401).json({ error: 'Service authentication required' }); @@ -203,7 +203,7 @@ router.get('/:contentHash/:preset', async (req, res) => { router.get('/:contentHash/metadata', async (req, res) => { try { const { contentHash } = req.params; - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { return res.status(401).json({ error: 'Service authentication required' }); diff --git a/apps/processor/src/api/upload.ts b/apps/processor/src/api/upload.ts index 9856d23695..094549e219 100644 --- a/apps/processor/src/api/upload.ts +++ b/apps/processor/src/api/upload.ts @@ -8,7 +8,7 @@ import path from 'path'; import { contentStore, queueManager } from '../server'; import { processorLogger } from '../logger'; import { rateLimitUpload } from '../middleware/rate-limit'; -import { hasServiceScope } from '../middleware/auth'; +import { hasAuthScope } from '../middleware/auth'; import { resolvePathWithin, sanitizeExtension } from '../utils/security'; const router = Router(); @@ -83,8 +83,8 @@ const upload = multer({ }); router.use((req, res, next) => { - if (!req.serviceAuth) { - return res.status(401).json({ error: 'Service authentication required' }); + if (!req.auth) { + return res.status(401).json({ error: 'Authentication required' }); } return next(); }); @@ -96,14 +96,15 @@ router.post('/single', upload.single('file'), async (req, res) => { let tempFilePath: string | undefined; try { - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { - return res.status(401).json({ error: 'Service authentication required' }); + return res.status(401).json({ error: 'Authentication required' }); } - const resourcePageId = auth.claims.resource; + // Get resource binding (pageId) from session + const resourcePageId = auth.resourceBinding?.type === 'page' ? auth.resourceBinding.id : undefined; if (!resourcePageId) { - return res.status(403).json({ error: 'Service token missing page scope' }); + return res.status(403).json({ error: 'Token missing page resource binding' }); } const driveId = typeof req.body?.driveId === 'string' ? req.body.driveId : undefined; @@ -112,7 +113,7 @@ router.post('/single', upload.single('file'), async (req, res) => { } if (!auth.driveId || auth.driveId !== driveId) { - return res.status(403).json({ error: 'Service token drive does not match requested drive' }); + return res.status(403).json({ error: 'Token drive does not match requested drive' }); } const pageId = typeof req.body?.pageId === 'string' ? req.body.pageId : undefined; @@ -121,7 +122,7 @@ router.post('/single', upload.single('file'), async (req, res) => { } if (resourcePageId && resourcePageId !== pageId) { - return res.status(403).json({ error: 'Service token resource does not match requested page' }); + return res.status(403).json({ error: 'Token resource does not match requested page' }); } const providedUserId = typeof req.body?.userId === 'string' ? req.body.userId : undefined; @@ -129,13 +130,12 @@ router.post('/single', upload.single('file'), async (req, res) => { auth.userId && providedUserId && providedUserId !== auth.userId && - !hasServiceScope(auth, 'files:write:any') + !hasAuthScope(auth, 'files:write:any') ) { return res.status(403).json({ error: 'Cannot upload on behalf of another user' }); } const uploaderId = auth.userId ?? providedUserId; - const tenantId = auth.tenantId; if (!req.file) { return res.status(400).json({ error: 'No file provided' }); @@ -157,10 +157,10 @@ router.post('/single', upload.single('file'), async (req, res) => { const alreadyStored = await contentStore.originalExists(contentHash); if (alreadyStored) { await contentStore.appendUploadMetadata(contentHash, { - tenantId, + tenantId: auth.userId, driveId, userId: uploaderId, - service: auth.service + service: 'processor' }); processorLogger.info('Upload deduplicated', { @@ -194,10 +194,10 @@ router.post('/single', upload.single('file'), async (req, res) => { originalname, contentHash, { - tenantId, + tenantId: auth.userId, driveId, userId: uploaderId, - service: auth.service + service: 'processor' } ); processorLogger.info('Saved original upload', { @@ -233,7 +233,7 @@ router.post('/single', upload.single('file'), async (req, res) => { processorLogger.error('Upload error', error as Error, { tempFilePath, pageId: req.body?.pageId, - userId: req.serviceAuth?.userId ?? req.body?.userId + userId: req.auth?.userId ?? req.body?.userId }); // Clean up temporary file on error @@ -260,14 +260,15 @@ router.post('/multiple', upload.array('files', 10), async (req, res) => { const tempFilePaths: string[] = []; try { - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { - return res.status(401).json({ error: 'Service authentication required' }); + return res.status(401).json({ error: 'Authentication required' }); } - const resourcePageId = auth.claims.resource; + // Get resource binding (pageId) from session + const resourcePageId = auth.resourceBinding?.type === 'page' ? auth.resourceBinding.id : undefined; if (!resourcePageId) { - return res.status(403).json({ error: 'Service token missing page scope' }); + return res.status(403).json({ error: 'Token missing page resource binding' }); } const driveId = typeof req.body?.driveId === 'string' ? req.body.driveId : undefined; @@ -276,7 +277,7 @@ router.post('/multiple', upload.array('files', 10), async (req, res) => { } if (!auth.driveId || auth.driveId !== driveId) { - return res.status(403).json({ error: 'Service token drive does not match requested drive' }); + return res.status(403).json({ error: 'Token drive does not match requested drive' }); } const providedUserId = typeof req.body?.userId === 'string' ? req.body.userId : undefined; @@ -284,13 +285,12 @@ router.post('/multiple', upload.array('files', 10), async (req, res) => { auth.userId && providedUserId && providedUserId !== auth.userId && - !hasServiceScope(auth, 'files:write:any') + !hasAuthScope(auth, 'files:write:any') ) { return res.status(403).json({ error: 'Cannot upload on behalf of another user' }); } const uploaderId = auth.userId ?? providedUserId; - const tenantId = auth.tenantId; if (!req.files || !Array.isArray(req.files)) { return res.status(400).json({ error: 'No files provided' }); @@ -298,7 +298,7 @@ router.post('/multiple', upload.array('files', 10), async (req, res) => { const pageId = typeof req.body?.pageId === 'string' ? req.body.pageId : undefined; if (resourcePageId && pageId && resourcePageId !== pageId) { - return res.status(403).json({ error: 'Service token resource does not match requested page' }); + return res.status(403).json({ error: 'Token resource does not match requested page' }); } const results = []; @@ -319,18 +319,18 @@ router.post('/multiple', upload.array('files', 10), async (req, res) => { if (!alreadyStored) { await contentStore.saveOriginalFromFile(tempPath, originalname, contentHash, { - tenantId, + tenantId: auth.userId, driveId, userId: uploaderId, - service: auth.service + service: 'processor' }); } if (alreadyStored) { await contentStore.appendUploadMetadata(contentHash, { - tenantId, + tenantId: auth.userId, driveId, userId: uploaderId, - service: auth.service + service: 'processor' }); } diff --git a/apps/processor/src/middleware/auth.ts b/apps/processor/src/middleware/auth.ts index 6b0cb588aa..e0c3bfcd66 100644 --- a/apps/processor/src/middleware/auth.ts +++ b/apps/processor/src/middleware/auth.ts @@ -1,26 +1,6 @@ import type { NextFunction, Request, Response } from 'express'; -import { - authenticateServiceToken, - assertScope as assertServiceScope, - hasScope, - type ServiceScope, - type ServiceTokenClaims, -} from '@pagespace/lib'; -import { validateServiceUser } from '../services/user-validator'; - -export interface ProcessorServiceAuth { - userId: string; - service: string; - scopes: ServiceScope[]; - claims: ServiceTokenClaims; - resource?: string; - driveId?: string; - /** - * Compatibility shim for legacy code paths that still expect tenantId === userId - * or the resource identifier. This should be removed when RBAC integration is complete. - */ - tenantId?: string; -} +import { sessionService, type SessionClaims } from '@pagespace/lib/auth'; +import { EnforcedAuthContext } from '@pagespace/lib/permissions'; export const AUTH_REQUIRED = process.env.PROCESSOR_AUTH_REQUIRED !== 'false'; @@ -44,7 +24,7 @@ function collectCandidateUrls(req: Request): string[] { .map((value) => value.toLowerCase()); } -function inferScope(req: Request): ServiceScope | null { +function inferScope(req: Request): string | null { const method = req.method.toUpperCase(); const candidateUrls = collectCandidateUrls(req); @@ -95,26 +75,11 @@ function inferScope(req: Request): ServiceScope | null { return null; } -function buildAuthContext(claims: ServiceTokenClaims): ProcessorServiceAuth { - const scopes = Array.isArray(claims.scopes) ? (claims.scopes as ServiceScope[]) : []; - const userId = String(claims.sub); - - return { - userId, - service: claims.service, - scopes, - claims, - resource: claims.resource, - driveId: claims.driveId, - tenantId: claims.resource ?? userId, - }; -} - -function respondUnauthorized(res: Response, message = 'Service authentication required'): void { +function respondUnauthorized(res: Response, message = 'Authentication required'): void { res.status(401).json({ error: message }); } -function respondForbidden(res: Response, message: string, scope?: ServiceScope): void { +function respondForbidden(res: Response, message: string, scope?: string): void { res.status(403).json({ error: message, ...(scope ? { requiredScope: scope } : {}), @@ -136,122 +101,87 @@ export async function authenticateService(req: Request, res: Response, next: Nex const token = header.slice(7).trim(); try { - const { claims } = await authenticateServiceToken(token); - - if (!claims.service || typeof claims.service !== 'string') { - respondForbidden(res, 'Service token missing service identifier'); - return; - } + const claims = await sessionService.validateSession(token); - if (!claims.scopes || !Array.isArray(claims.scopes) || claims.scopes.length === 0) { - respondForbidden(res, 'Service token missing scopes'); + if (!claims) { + respondUnauthorized(res, 'Invalid or expired token'); return; } - // P1-T2: Validate that the user still exists - const userValidation = await validateServiceUser(String(claims.sub)); - - if (!userValidation.valid) { - if (process.env.NODE_ENV !== 'test') { - console.warn('Service token user validation failed', { - service: claims.service, - subject: claims.sub, - reason: userValidation.reason, - }); - } - respondUnauthorized(res, `User validation failed: ${userValidation.reason}`); + // Reject non-service session types - processor is for service-to-service only + if (claims.type !== 'service') { + respondForbidden(res, 'Service token required'); return; } - const context = buildAuthContext(claims); - req.serviceAuth = context; + // Build enforced auth context from validated session + const context = EnforcedAuthContext.fromSession(claims); + req.auth = context; + // Check inferred scope if applicable const inferredScope = inferScope(req); - if (inferredScope && !hasScope(claims, inferredScope)) { + if (inferredScope && !context.hasScope(inferredScope)) { if (process.env.NODE_ENV !== 'test') { - console.warn('Service scope inference failed', { - service: claims.service, + console.warn('Scope inference failed', { required: inferredScope, - provided: claims.scopes, - subject: claims.sub, - resource: claims.resource, + userId: claims.userId, + resourceBinding: context.resourceBinding, urls: collectCandidateUrls(req), }); } - respondForbidden(res, 'Insufficient service scopes', inferredScope); + respondForbidden(res, 'Insufficient scopes', inferredScope); return; } next(); } catch (error) { - const message = error instanceof Error ? error.message : 'Invalid service token'; - console.error('Service authentication failed:', message); - respondUnauthorized(res, 'Invalid service token'); + console.error('Authentication failed:', error); + respondUnauthorized(res, 'Invalid token'); } } -export function requireScope(scope: ServiceScope) { +export function requireScope(scope: string) { return function permissionMiddleware(req: Request, res: Response, next: NextFunction): void { if (!AUTH_REQUIRED) { next(); return; } - const auth = req.serviceAuth; + const auth = req.auth; if (!auth) { respondUnauthorized(res); return; } - try { - assertServiceScope(auth.claims, scope); - next(); - } catch (error) { - const message = error instanceof Error ? error.message : 'Insufficient service scopes'; + if (!auth.hasScope(scope)) { if (process.env.NODE_ENV !== 'test') { - console.warn('Service scope assertion failed', { - service: auth.service, + console.warn('Scope assertion failed', { required: scope, - provided: auth.scopes, - subject: auth.userId, - resource: auth.resource, + userId: auth.userId, urls: collectCandidateUrls(req), }); } - respondForbidden(res, message, scope); + respondForbidden(res, `Missing required scope: ${scope}`, scope); + return; } + + next(); }; } -export function hasServiceScope(auth: ProcessorServiceAuth | undefined, scope: ServiceScope): boolean { +export function hasAuthScope(auth: EnforcedAuthContext | undefined, scope: string): boolean { if (!auth) { return false; } - return hasScope(auth.claims, scope); + return auth.hasScope(scope); } -export function requireTenantContext(req: Request): string | null { - const auth = req.serviceAuth; +export function getUserId(req: Request): string | null { + const auth = req.auth; if (!auth) { return null; } - - const tokenTenant = auth.tenantId ?? null; - const bodyTenant = typeof req.body?.tenantId === 'string' ? req.body.tenantId : null; - - if (!tokenTenant) { - return bodyTenant; - } - - if (bodyTenant && bodyTenant !== tokenTenant) { - return null; - } - - if (!bodyTenant && typeof req.body === 'object' && req.body !== null) { - (req.body as Record).tenantId = tokenTenant; - } - - return tokenTenant; + return auth.userId; } -export type { ServiceScope }; +export { EnforcedAuthContext }; diff --git a/apps/processor/src/middleware/rate-limit.ts b/apps/processor/src/middleware/rate-limit.ts index 0c8f82f2fa..7d8a10a209 100644 --- a/apps/processor/src/middleware/rate-limit.ts +++ b/apps/processor/src/middleware/rate-limit.ts @@ -10,17 +10,12 @@ const DEFAULT_LIMIT = parseInt(process.env.PROCESSOR_UPLOAD_RATE_LIMIT ?? '100', const WINDOW_SECONDS = parseInt(process.env.PROCESSOR_UPLOAD_RATE_WINDOW ?? '3600', 10); function getBucketKey(req: Request): string { - const auth = req.serviceAuth; - // Use userId for rate limiting (not tenantId which may be per-resource) - // This ensures rate limits accumulate per-user across all their uploads + const auth = req.auth; + // Use userId for rate limiting - this ensures rate limits accumulate per-user if (auth?.userId) { return `user:${auth.userId}`; } - if (auth?.service) { - return `service:${auth.service}`; - } - // Fallback to IP-based limiting for unauthenticated requests const forwarded = req.headers['x-forwarded-for']; if (typeof forwarded === 'string' && forwarded.length > 0) { diff --git a/apps/processor/src/types/express.d.ts b/apps/processor/src/types/express.d.ts index f63de942e2..84178dbba7 100644 --- a/apps/processor/src/types/express.d.ts +++ b/apps/processor/src/types/express.d.ts @@ -1,9 +1,9 @@ -import type { ProcessorServiceAuth } from '../middleware/auth'; +import type { EnforcedAuthContext } from '../middleware/auth'; declare global { namespace Express { interface Request { - serviceAuth?: ProcessorServiceAuth; + auth?: EnforcedAuthContext; } } } diff --git a/packages/db/drizzle/0037_slimy_bishop.sql b/packages/db/drizzle/0037_slimy_bishop.sql new file mode 100644 index 0000000000..b65065e458 --- /dev/null +++ b/packages/db/drizzle/0037_slimy_bishop.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "drive_id" text; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0037_snapshot.json b/packages/db/drizzle/meta/0037_snapshot.json new file mode 100644 index 0000000000..f8d3ca3e56 --- /dev/null +++ b/packages/db/drizzle/meta/0037_snapshot.json @@ -0,0 +1,8899 @@ +{ + "id": "284b0731-46e5-46d1-9abb-dd2ef757bd7a", + "prevId": "1f92f28d-b62e-4da6-a2c2-2bed9aaa9950", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device_tokens": { + "name": "device_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenPrefix": { + "name": "tokenPrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deviceId": { + "name": "deviceId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "PlatformType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "deviceName": { + "name": "deviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastIpAddress": { + "name": "lastIpAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trustScore": { + "name": "trustScore", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "suspiciousActivityCount": { + "name": "suspiciousActivityCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revokedAt": { + "name": "revokedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revokedReason": { + "name": "revokedReason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_tokens_user_id_idx": { + "name": "device_tokens_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_tokens_token_idx": { + "name": "device_tokens_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_tokens_device_id_idx": { + "name": "device_tokens_device_id_idx", + "columns": [ + { + "expression": "deviceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_tokens_expires_at_idx": { + "name": "device_tokens_expires_at_idx", + "columns": [ + { + "expression": "expiresAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_tokens_token_hash_partial_idx": { + "name": "device_tokens_token_hash_partial_idx", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"device_tokens\".\"tokenHash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_tokens_active_device_idx": { + "name": "device_tokens_active_device_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deviceId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"device_tokens\".\"revokedAt\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_tokens_userId_users_id_fk": { + "name": "device_tokens_userId_users_id_fk", + "tableFrom": "device_tokens", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "device_tokens_token_unique": { + "name": "device_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.mcp_tokens": { + "name": "mcp_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenPrefix": { + "name": "tokenPrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsed": { + "name": "lastUsed", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revokedAt": { + "name": "revokedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mcp_tokens_user_id_idx": { + "name": "mcp_tokens_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_tokens_token_idx": { + "name": "mcp_tokens_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_tokens_token_hash_partial_idx": { + "name": "mcp_tokens_token_hash_partial_idx", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_tokens\".\"tokenHash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_tokens_userId_users_id_fk": { + "name": "mcp_tokens_userId_users_id_fk", + "tableFrom": "mcp_tokens", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mcp_tokens_token_unique": { + "name": "mcp_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenPrefix": { + "name": "tokenPrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "PlatformType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "deviceTokenId": { + "name": "deviceTokenId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "refresh_tokens_user_id_idx": { + "name": "refresh_tokens_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "refresh_tokens_token_hash_partial_idx": { + "name": "refresh_tokens_token_hash_partial_idx", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"refresh_tokens\".\"tokenHash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "refresh_tokens_userId_users_id_fk": { + "name": "refresh_tokens_userId_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.socket_tokens": { + "name": "socket_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "socket_tokens_user_id_idx": { + "name": "socket_tokens_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "socket_tokens_token_hash_idx": { + "name": "socket_tokens_token_hash_idx", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "socket_tokens_expires_at_idx": { + "name": "socket_tokens_expires_at_idx", + "columns": [ + { + "expression": "expiresAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "socket_tokens_userId_users_id_fk": { + "name": "socket_tokens_userId_users_id_fk", + "tableFrom": "socket_tokens", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "socket_tokens_tokenHash_unique": { + "name": "socket_tokens_tokenHash_unique", + "nullsNotDistinct": false, + "columns": [ + "tokenHash" + ] + } + } + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "googleId": { + "name": "googleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "AuthProvider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "tokenVersion": { + "name": "tokenVersion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "role": { + "name": "role", + "type": "UserRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "currentAiProvider": { + "name": "currentAiProvider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pagespace'" + }, + "currentAiModel": { + "name": "currentAiModel", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'glm-4.5-air'" + }, + "storageUsedBytes": { + "name": "storageUsedBytes", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "activeUploads": { + "name": "activeUploads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lastStorageCalculated": { + "name": "lastStorageCalculated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscriptionTier": { + "name": "subscriptionTier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "tosAcceptedAt": { + "name": "tosAcceptedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_googleId_unique": { + "name": "users_googleId_unique", + "nullsNotDistinct": false, + "columns": [ + "googleId" + ] + }, + "users_stripeCustomerId_unique": { + "name": "users_stripeCustomerId_unique", + "nullsNotDistinct": false, + "columns": [ + "stripeCustomerId" + ] + } + } + }, + "public.verification_tokens": { + "name": "verification_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokenPrefix": { + "name": "tokenPrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "usedAt": { + "name": "usedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_tokens_user_id_idx": { + "name": "verification_tokens_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_tokens_token_idx": { + "name": "verification_tokens_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_tokens_type_idx": { + "name": "verification_tokens_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_tokens_token_hash_partial_idx": { + "name": "verification_tokens_token_hash_partial_idx", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"verification_tokens\".\"tokenHash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "verification_tokens_userId_users_id_fk": { + "name": "verification_tokens_userId_users_id_fk", + "tableFrom": "verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verification_tokens_token_unique": { + "name": "verification_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "drive_id": { + "name": "drive_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_version": { + "name": "token_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by_service": { + "name": "created_by_service", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_ip": { + "name": "created_by_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_used_ip": { + "name": "last_used_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_active_idx": { + "name": "sessions_user_active_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_hash_unique": { + "name": "sessions_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + } + }, + "public.chat_messages": { + "name": "chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversationId": { + "name": "conversationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toolCalls": { + "name": "toolCalls", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "toolResults": { + "name": "toolResults", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "editedAt": { + "name": "editedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messageType": { + "name": "messageType", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + } + }, + "indexes": { + "chat_messages_page_id_idx": { + "name": "chat_messages_page_id_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_user_id_idx": { + "name": "chat_messages_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_conversation_id_idx": { + "name": "chat_messages_conversation_id_idx", + "columns": [ + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_page_id_conversation_id_idx": { + "name": "chat_messages_page_id_conversation_id_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_page_id_is_active_created_at_idx": { + "name": "chat_messages_page_id_is_active_created_at_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "isActive", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_messages_pageId_pages_id_fk": { + "name": "chat_messages_pageId_pages_id_fk", + "tableFrom": "chat_messages", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_userId_users_id_fk": { + "name": "chat_messages_userId_users_id_fk", + "tableFrom": "chat_messages", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.drives": { + "name": "drives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ownerId": { + "name": "ownerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isTrashed": { + "name": "isTrashed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trashedAt": { + "name": "trashedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "drivePrompt": { + "name": "drivePrompt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drives_owner_id_idx": { + "name": "drives_owner_id_idx", + "columns": [ + { + "expression": "ownerId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drives_owner_id_slug_key": { + "name": "drives_owner_id_slug_key", + "columns": [ + { + "expression": "ownerId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drives_ownerId_users_id_fk": { + "name": "drives_ownerId_users_id_fk", + "tableFrom": "drives", + "tableTo": "users", + "columnsFrom": [ + "ownerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.favorites": { + "name": "favorites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "favorites_user_id_page_id_key": { + "name": "favorites_user_id_page_id_key", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "favorites_userId_users_id_fk": { + "name": "favorites_userId_users_id_fk", + "tableFrom": "favorites", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "favorites_pageId_pages_id_fk": { + "name": "favorites_pageId_pages_id_fk", + "tableFrom": "favorites", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mentions": { + "name": "mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sourcePageId": { + "name": "sourcePageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "targetPageId": { + "name": "targetPageId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mentions_source_page_id_target_page_id_key": { + "name": "mentions_source_page_id_target_page_id_key", + "columns": [ + { + "expression": "sourcePageId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "targetPageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mentions_source_page_id_idx": { + "name": "mentions_source_page_id_idx", + "columns": [ + { + "expression": "sourcePageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mentions_target_page_id_idx": { + "name": "mentions_target_page_id_idx", + "columns": [ + { + "expression": "targetPageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mentions_sourcePageId_pages_id_fk": { + "name": "mentions_sourcePageId_pages_id_fk", + "tableFrom": "mentions", + "tableTo": "pages", + "columnsFrom": [ + "sourcePageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mentions_targetPageId_pages_id_fk": { + "name": "mentions_targetPageId_pages_id_fk", + "tableFrom": "mentions", + "tableTo": "pages", + "columnsFrom": [ + "targetPageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.page_tags": { + "name": "page_tags", + "schema": "", + "columns": { + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "page_tags_pageId_pages_id_fk": { + "name": "page_tags_pageId_pages_id_fk", + "tableFrom": "page_tags", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_tags_tagId_tags_id_fk": { + "name": "page_tags_tagId_tags_id_fk", + "tableFrom": "page_tags", + "tableTo": "tags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "page_tags_pageId_tagId_pk": { + "name": "page_tags_pageId_tagId_pk", + "columns": [ + "pageId", + "tagId" + ] + } + }, + "uniqueConstraints": {} + }, + "public.pages": { + "name": "pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "PageType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isPaginated": { + "name": "isPaginated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "position": { + "name": "position", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "isTrashed": { + "name": "isTrashed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "aiProvider": { + "name": "aiProvider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aiModel": { + "name": "aiModel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "systemPrompt": { + "name": "systemPrompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabledTools": { + "name": "enabledTools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "includeDrivePrompt": { + "name": "includeDrivePrompt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agentDefinition": { + "name": "agentDefinition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibleToGlobalAssistant": { + "name": "visibleToGlobalAssistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "includePageTree": { + "name": "includePageTree", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pageTreeScope": { + "name": "pageTreeScope", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'children'" + }, + "fileSize": { + "name": "fileSize", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "originalFileName": { + "name": "originalFileName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fileMetadata": { + "name": "fileMetadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processingStatus": { + "name": "processingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "processingError": { + "name": "processingError", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processedAt": { + "name": "processedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "extractionMethod": { + "name": "extractionMethod", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extractionMetadata": { + "name": "extractionMetadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contentHash": { + "name": "contentHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "trashedAt": { + "name": "trashedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stateHash": { + "name": "stateHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "originalParentId": { + "name": "originalParentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pages_drive_id_idx": { + "name": "pages_drive_id_idx", + "columns": [ + { + "expression": "driveId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pages_parent_id_idx": { + "name": "pages_parent_id_idx", + "columns": [ + { + "expression": "parentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pages_parent_id_position_idx": { + "name": "pages_parent_id_position_idx", + "columns": [ + { + "expression": "parentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pages_driveId_drives_id_fk": { + "name": "pages_driveId_drives_id_fk", + "tableFrom": "pages", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.storage_events": { + "name": "storage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eventType": { + "name": "eventType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sizeDelta": { + "name": "sizeDelta", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "totalSizeAfter": { + "name": "totalSizeAfter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "storage_events_user_id_idx": { + "name": "storage_events_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "storage_events_created_at_idx": { + "name": "storage_events_created_at_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "storage_events_userId_users_id_fk": { + "name": "storage_events_userId_users_id_fk", + "tableFrom": "storage_events", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "storage_events_pageId_pages_id_fk": { + "name": "storage_events_pageId_pages_id_fk", + "tableFrom": "storage_events", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tags_name_unique": { + "name": "tags_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + } + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "action": { + "name": "action", + "type": "PermissionAction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subjectType": { + "name": "subjectType", + "type": "SubjectType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subjectId": { + "name": "subjectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_page_id_idx": { + "name": "permissions_page_id_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_subject_id_subject_type_idx": { + "name": "permissions_subject_id_subject_type_idx", + "columns": [ + { + "expression": "subjectId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subjectType", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_page_id_subject_id_subject_type_idx": { + "name": "permissions_page_id_subject_id_subject_type_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subjectId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subjectType", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_pageId_pages_id_fk": { + "name": "permissions_pageId_pages_id_fk", + "tableFrom": "permissions", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.drive_invitations": { + "name": "drive_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invitedBy": { + "name": "invitedBy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "InvitationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "respondedAt": { + "name": "respondedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drive_invitations_drive_id_idx": { + "name": "drive_invitations_drive_id_idx", + "columns": [ + { + "expression": "driveId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drive_invitations_email_idx": { + "name": "drive_invitations_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drive_invitations_status_idx": { + "name": "drive_invitations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drive_invitations_token_idx": { + "name": "drive_invitations_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_invitations_driveId_drives_id_fk": { + "name": "drive_invitations_driveId_drives_id_fk", + "tableFrom": "drive_invitations", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "drive_invitations_userId_users_id_fk": { + "name": "drive_invitations_userId_users_id_fk", + "tableFrom": "drive_invitations", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "drive_invitations_invitedBy_users_id_fk": { + "name": "drive_invitations_invitedBy_users_id_fk", + "tableFrom": "drive_invitations", + "tableTo": "users", + "columnsFrom": [ + "invitedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "drive_invitations_token_unique": { + "name": "drive_invitations_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.drive_members": { + "name": "drive_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "MemberRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'MEMBER'" + }, + "customRoleId": { + "name": "customRoleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invitedBy": { + "name": "invitedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invitedAt": { + "name": "invitedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "acceptedAt": { + "name": "acceptedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastAccessedAt": { + "name": "lastAccessedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drive_members_drive_id_idx": { + "name": "drive_members_drive_id_idx", + "columns": [ + { + "expression": "driveId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drive_members_user_id_idx": { + "name": "drive_members_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drive_members_role_idx": { + "name": "drive_members_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drive_members_custom_role_id_idx": { + "name": "drive_members_custom_role_id_idx", + "columns": [ + { + "expression": "customRoleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_members_driveId_drives_id_fk": { + "name": "drive_members_driveId_drives_id_fk", + "tableFrom": "drive_members", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "drive_members_userId_users_id_fk": { + "name": "drive_members_userId_users_id_fk", + "tableFrom": "drive_members", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "drive_members_customRoleId_drive_roles_id_fk": { + "name": "drive_members_customRoleId_drive_roles_id_fk", + "tableFrom": "drive_members", + "tableTo": "drive_roles", + "columnsFrom": [ + "customRoleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "drive_members_invitedBy_users_id_fk": { + "name": "drive_members_invitedBy_users_id_fk", + "tableFrom": "drive_members", + "tableTo": "users", + "columnsFrom": [ + "invitedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "drive_members_drive_user_key": { + "name": "drive_members_drive_user_key", + "nullsNotDistinct": false, + "columns": [ + "driveId", + "userId" + ] + } + } + }, + "public.drive_roles": { + "name": "drive_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "drive_roles_drive_id_idx": { + "name": "drive_roles_drive_id_idx", + "columns": [ + { + "expression": "driveId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drive_roles_position_idx": { + "name": "drive_roles_position_idx", + "columns": [ + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_roles_driveId_drives_id_fk": { + "name": "drive_roles_driveId_drives_id_fk", + "tableFrom": "drive_roles", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "drive_roles_drive_name_key": { + "name": "drive_roles_drive_name_key", + "nullsNotDistinct": false, + "columns": [ + "driveId", + "name" + ] + } + } + }, + "public.page_permissions": { + "name": "page_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canView": { + "name": "canView", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canEdit": { + "name": "canEdit", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canShare": { + "name": "canShare", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDelete": { + "name": "canDelete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "grantedBy": { + "name": "grantedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grantedAt": { + "name": "grantedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_permissions_page_id_idx": { + "name": "page_permissions_page_id_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_permissions_user_id_idx": { + "name": "page_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_permissions_expires_at_idx": { + "name": "page_permissions_expires_at_idx", + "columns": [ + { + "expression": "expiresAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_permissions_pageId_pages_id_fk": { + "name": "page_permissions_pageId_pages_id_fk", + "tableFrom": "page_permissions", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_permissions_userId_users_id_fk": { + "name": "page_permissions_userId_users_id_fk", + "tableFrom": "page_permissions", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_permissions_grantedBy_users_id_fk": { + "name": "page_permissions_grantedBy_users_id_fk", + "tableFrom": "page_permissions", + "tableTo": "users", + "columnsFrom": [ + "grantedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "page_permissions_page_user_key": { + "name": "page_permissions_page_user_key", + "nullsNotDistinct": false, + "columns": [ + "pageId", + "userId" + ] + } + } + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatarUrl": { + "name": "avatarUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPublic": { + "name": "isPublic", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_profiles_is_public_idx": { + "name": "user_profiles_is_public_idx", + "columns": [ + { + "expression": "isPublic", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_profiles_userId_users_id_fk": { + "name": "user_profiles_userId_users_id_fk", + "tableFrom": "user_profiles", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.channel_messages": { + "name": "channel_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "channel_messages_page_id_idx": { + "name": "channel_messages_page_id_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "channel_messages_pageId_pages_id_fk": { + "name": "channel_messages_pageId_pages_id_fk", + "tableFrom": "channel_messages", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "channel_messages_userId_users_id_fk": { + "name": "channel_messages_userId_users_id_fk", + "tableFrom": "channel_messages", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user_ai_settings": { + "name": "user_ai_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encryptedApiKey": { + "name": "encryptedApiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "baseUrl": { + "name": "baseUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_ai_settings_userId_users_id_fk": { + "name": "user_ai_settings_userId_users_id_fk", + "tableFrom": "user_ai_settings", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_provider_unique": { + "name": "user_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "userId", + "provider" + ] + } + } + }, + "public.user_dashboards": { + "name": "user_dashboards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_dashboards_userId_users_id_fk": { + "name": "user_dashboards_userId_users_id_fk", + "tableFrom": "user_dashboards", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_dashboards_userId_unique": { + "name": "user_dashboards_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + } + } + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contextId": { + "name": "contextId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMessageAt": { + "name": "lastMessageAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "conversations_user_id_idx": { + "name": "conversations_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_user_id_type_idx": { + "name": "conversations_user_id_type_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_user_id_last_message_at_idx": { + "name": "conversations_user_id_last_message_at_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lastMessageAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_context_id_idx": { + "name": "conversations_context_id_idx", + "columns": [ + { + "expression": "contextId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_userId_users_id_fk": { + "name": "conversations_userId_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "conversationId": { + "name": "conversationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageType": { + "name": "messageType", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toolCalls": { + "name": "toolCalls", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "toolResults": { + "name": "toolResults", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "editedAt": { + "name": "editedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_conversation_id_created_at_idx": { + "name": "messages_conversation_id_created_at_idx", + "columns": [ + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_user_id_idx": { + "name": "messages_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversationId_conversations_id_fk": { + "name": "messages_conversationId_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_userId_users_id_fk": { + "name": "messages_userId_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "NotificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "isRead": { + "name": "isRead", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "readAt": { + "name": "readAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "triggeredByUserId": { + "name": "triggeredByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "notifications_user_id_idx": { + "name": "notifications_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_user_id_is_read_idx": { + "name": "notifications_user_id_is_read_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "isRead", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_at_idx": { + "name": "notifications_created_at_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_type_idx": { + "name": "notifications_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_userId_users_id_fk": { + "name": "notifications_userId_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_pageId_pages_id_fk": { + "name": "notifications_pageId_pages_id_fk", + "tableFrom": "notifications", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_driveId_drives_id_fk": { + "name": "notifications_driveId_drives_id_fk", + "tableFrom": "notifications", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_triggeredByUserId_users_id_fk": { + "name": "notifications_triggeredByUserId_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "triggeredByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.email_notification_log": { + "name": "email_notification_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notificationType": { + "name": "notificationType", + "type": "NotificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipientEmail": { + "name": "recipientEmail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sentAt": { + "name": "sentAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_notification_log_user_idx": { + "name": "email_notification_log_user_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_notification_log_sent_at_idx": { + "name": "email_notification_log_sent_at_idx", + "columns": [ + { + "expression": "sentAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_notification_log_notification_id_idx": { + "name": "email_notification_log_notification_id_idx", + "columns": [ + { + "expression": "notificationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_notification_log_userId_users_id_fk": { + "name": "email_notification_log_userId_users_id_fk", + "tableFrom": "email_notification_log", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.email_notification_preferences": { + "name": "email_notification_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notificationType": { + "name": "notificationType", + "type": "NotificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "emailEnabled": { + "name": "emailEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_notification_preferences_user_type_idx": { + "name": "email_notification_preferences_user_type_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "notificationType", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_notification_preferences_userId_users_id_fk": { + "name": "email_notification_preferences_userId_users_id_fk", + "tableFrom": "email_notification_preferences", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actorEmail": { + "name": "actorEmail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'legacy@unknown'" + }, + "actorDisplayName": { + "name": "actorDisplayName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isAiGenerated": { + "name": "isAiGenerated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "aiProvider": { + "name": "aiProvider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aiModel": { + "name": "aiModel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aiConversationId": { + "name": "aiConversationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "operation": { + "name": "operation", + "type": "activity_operation", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resourceType": { + "name": "resourceType", + "type": "activity_resource", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resourceId": { + "name": "resourceId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resourceTitle": { + "name": "resourceTitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contentSnapshot": { + "name": "contentSnapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contentFormat": { + "name": "contentFormat", + "type": "content_format", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "contentRef": { + "name": "contentRef", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contentSize": { + "name": "contentSize", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rollbackFromActivityId": { + "name": "rollbackFromActivityId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackSourceOperation": { + "name": "rollbackSourceOperation", + "type": "activity_operation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "rollbackSourceTimestamp": { + "name": "rollbackSourceTimestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rollbackSourceTitle": { + "name": "rollbackSourceTitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedFields": { + "name": "updatedFields", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "previousValues": { + "name": "previousValues", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "newValues": { + "name": "newValues", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "streamId": { + "name": "streamId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "streamSeq": { + "name": "streamSeq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changeGroupId": { + "name": "changeGroupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changeGroupType": { + "name": "changeGroupType", + "type": "activity_change_group_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "stateHashBefore": { + "name": "stateHashBefore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stateHashAfter": { + "name": "stateHashAfter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isArchived": { + "name": "isArchived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previousLogHash": { + "name": "previousLogHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logHash": { + "name": "logHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chainSeed": { + "name": "chainSeed", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_activity_logs_timestamp": { + "name": "idx_activity_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_activity_logs_user_timestamp": { + "name": "idx_activity_logs_user_timestamp", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_activity_logs_drive_timestamp": { + "name": "idx_activity_logs_drive_timestamp", + "columns": [ + { + "expression": "driveId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_activity_logs_page_timestamp": { + "name": "idx_activity_logs_page_timestamp", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_activity_logs_archived": { + "name": "idx_activity_logs_archived", + "columns": [ + { + "expression": "isArchived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_activity_logs_rollback_from": { + "name": "idx_activity_logs_rollback_from", + "columns": [ + { + "expression": "rollbackFromActivityId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_activity_logs_stream": { + "name": "idx_activity_logs_stream", + "columns": [ + { + "expression": "streamId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "streamSeq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"activity_logs\".\"streamId\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_activity_logs_change_group": { + "name": "idx_activity_logs_change_group", + "columns": [ + { + "expression": "changeGroupId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"activity_logs\".\"changeGroupId\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_activity_logs_log_hash": { + "name": "idx_activity_logs_log_hash", + "columns": [ + { + "expression": "logHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"activity_logs\".\"logHash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_logs_userId_users_id_fk": { + "name": "activity_logs_userId_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_logs_driveId_drives_id_fk": { + "name": "activity_logs_driveId_drives_id_fk", + "tableFrom": "activity_logs", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_logs_pageId_pages_id_fk": { + "name": "activity_logs_pageId_pages_id_fk", + "tableFrom": "activity_logs", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.ai_usage_logs": { + "name": "ai_usage_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'USD'" + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "streaming_duration": { + "name": "streaming_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "drive_id": { + "name": "drive_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion": { + "name": "completion", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "context_messages": { + "name": "context_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "context_size": { + "name": "context_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "system_prompt_tokens": { + "name": "system_prompt_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tool_definition_tokens": { + "name": "tool_definition_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "conversation_tokens": { + "name": "conversation_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "was_truncated": { + "name": "was_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "truncation_strategy": { + "name": "truncation_strategy", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ai_usage_timestamp": { + "name": "idx_ai_usage_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_user_id": { + "name": "idx_ai_usage_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_provider": { + "name": "idx_ai_usage_provider", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_cost": { + "name": "idx_ai_usage_cost", + "columns": [ + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_conversation": { + "name": "idx_ai_usage_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_context": { + "name": "idx_ai_usage_context", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_context_size": { + "name": "idx_ai_usage_context_size", + "columns": [ + { + "expression": "context_size", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.alert_history": { + "name": "alert_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "actual_value": { + "name": "actual_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "notified": { + "name": "notified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "notified_at": { + "name": "notified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notification_channel": { + "name": "notification_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acknowledged": { + "name": "acknowledged", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "acknowledged_by": { + "name": "acknowledged_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_alerts_timestamp": { + "name": "idx_alerts_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alerts_type": { + "name": "idx_alerts_type", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alerts_severity": { + "name": "idx_alerts_severity", + "columns": [ + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alerts_acknowledged": { + "name": "idx_alerts_acknowledged", + "columns": [ + { + "expression": "acknowledged", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.api_metrics": { + "name": "api_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "http_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "request_size": { + "name": "request_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_size": { + "name": "response_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_hit": { + "name": "cache_hit", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cache_key": { + "name": "cache_key", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_api_metrics_timestamp": { + "name": "idx_api_metrics_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_api_metrics_endpoint": { + "name": "idx_api_metrics_endpoint", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_api_metrics_user_id": { + "name": "idx_api_metrics_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_api_metrics_status": { + "name": "idx_api_metrics_status", + "columns": [ + { + "expression": "status_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_api_metrics_duration": { + "name": "idx_api_metrics_duration", + "columns": [ + { + "expression": "duration", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.daily_aggregates": { + "name": "daily_aggregates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_count": { + "name": "total_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "avg_duration": { + "name": "avg_duration", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "min_duration": { + "name": "min_duration", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "max_duration": { + "name": "max_duration", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "p50_duration": { + "name": "p50_duration", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "p95_duration": { + "name": "p95_duration", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "p99_duration": { + "name": "p99_duration", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "unique_users": { + "name": "unique_users", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unique_sessions": { + "name": "unique_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cost": { + "name": "total_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_aggregates_date": { + "name": "idx_aggregates_date", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_aggregates_category": { + "name": "idx_aggregates_category", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.error_logs": { + "name": "error_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "http_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line": { + "name": "line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "column": { + "name": "column", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resolved": { + "name": "resolved", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_errors_timestamp": { + "name": "idx_errors_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_errors_name": { + "name": "idx_errors_name", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_errors_user_id": { + "name": "idx_errors_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_errors_resolved": { + "name": "idx_errors_resolved", + "columns": [ + { + "expression": "resolved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_errors_endpoint": { + "name": "idx_errors_endpoint", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.performance_metrics": { + "name": "performance_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "drive_id": { + "name": "drive_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cpu_usage": { + "name": "cpu_usage", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "memory_usage": { + "name": "memory_usage", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "disk_usage": { + "name": "disk_usage", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_performance_timestamp": { + "name": "idx_performance_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_performance_metric": { + "name": "idx_performance_metric", + "columns": [ + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_performance_value": { + "name": "idx_performance_value", + "columns": [ + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_performance_user_id": { + "name": "idx_performance_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscriptionTier": { + "name": "subscriptionTier", + "type": "subscription_tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "retentionDays": { + "name": "retentionDays", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "retention_policies_subscriptionTier_unique": { + "name": "retention_policies_subscriptionTier_unique", + "nullsNotDistinct": false, + "columns": [ + "subscriptionTier" + ] + } + } + }, + "public.system_logs": { + "name": "system_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "level": { + "name": "level", + "type": "log_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "drive_id": { + "name": "drive_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "http_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_name": { + "name": "error_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "name": "pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_system_logs_timestamp": { + "name": "idx_system_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_system_logs_level": { + "name": "idx_system_logs_level", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_system_logs_category": { + "name": "idx_system_logs_category", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_system_logs_user_id": { + "name": "idx_system_logs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_system_logs_request_id": { + "name": "idx_system_logs_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_system_logs_error": { + "name": "idx_system_logs_error", + "columns": [ + { + "expression": "error_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user_activities": { + "name": "user_activities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "drive_id": { + "name": "drive_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_activities_timestamp": { + "name": "idx_user_activities_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_activities_user_id": { + "name": "idx_user_activities_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_activities_action": { + "name": "idx_user_activities_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_activities_resource": { + "name": "idx_user_activities_resource", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.drive_backup_files": { + "name": "drive_backup_files", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fileId": { + "name": "fileId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storagePath": { + "name": "storagePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sizeBytes": { + "name": "sizeBytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checksumVersion": { + "name": "checksumVersion", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drive_backup_files_backup_idx": { + "name": "drive_backup_files_backup_idx", + "columns": [ + { + "expression": "backupId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_backup_files_backupId_drive_backups_id_fk": { + "name": "drive_backup_files_backupId_drive_backups_id_fk", + "tableFrom": "drive_backup_files", + "tableTo": "drive_backups", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "drive_backup_files_backupId_fileId_pk": { + "name": "drive_backup_files_backupId_fileId_pk", + "columns": [ + "backupId", + "fileId" + ] + } + }, + "uniqueConstraints": {} + }, + "public.drive_backup_members": { + "name": "drive_backup_members", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customRoleId": { + "name": "customRoleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invitedBy": { + "name": "invitedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invitedAt": { + "name": "invitedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "acceptedAt": { + "name": "acceptedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drive_backup_members_backup_idx": { + "name": "drive_backup_members_backup_idx", + "columns": [ + { + "expression": "backupId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_backup_members_backupId_drive_backups_id_fk": { + "name": "drive_backup_members_backupId_drive_backups_id_fk", + "tableFrom": "drive_backup_members", + "tableTo": "drive_backups", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "drive_backup_members_backupId_userId_pk": { + "name": "drive_backup_members_backupId_userId_pk", + "columns": [ + "backupId", + "userId" + ] + } + }, + "uniqueConstraints": {} + }, + "public.drive_backup_pages": { + "name": "drive_backup_pages", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pageVersionId": { + "name": "pageVersionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "originalParentId": { + "name": "originalParentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "isTrashed": { + "name": "isTrashed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trashedAt": { + "name": "trashedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drive_backup_pages_backup_idx": { + "name": "drive_backup_pages_backup_idx", + "columns": [ + { + "expression": "backupId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_backup_pages_backupId_drive_backups_id_fk": { + "name": "drive_backup_pages_backupId_drive_backups_id_fk", + "tableFrom": "drive_backup_pages", + "tableTo": "drive_backups", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "drive_backup_pages_pageVersionId_page_versions_id_fk": { + "name": "drive_backup_pages_pageVersionId_page_versions_id_fk", + "tableFrom": "drive_backup_pages", + "tableTo": "page_versions", + "columnsFrom": [ + "pageVersionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "drive_backup_pages_backupId_pageId_pk": { + "name": "drive_backup_pages_backupId_pageId_pk", + "columns": [ + "backupId", + "pageId" + ] + } + }, + "uniqueConstraints": {} + }, + "public.drive_backup_permissions": { + "name": "drive_backup_permissions", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canView": { + "name": "canView", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "canEdit": { + "name": "canEdit", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canShare": { + "name": "canShare", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDelete": { + "name": "canDelete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "grantedBy": { + "name": "grantedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drive_backup_permissions_backup_idx": { + "name": "drive_backup_permissions_backup_idx", + "columns": [ + { + "expression": "backupId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_backup_permissions_backupId_drive_backups_id_fk": { + "name": "drive_backup_permissions_backupId_drive_backups_id_fk", + "tableFrom": "drive_backup_permissions", + "tableTo": "drive_backups", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "drive_backup_permissions_backupId_pageId_userId_pk": { + "name": "drive_backup_permissions_backupId_pageId_userId_pk", + "columns": [ + "backupId", + "pageId", + "userId" + ] + } + }, + "uniqueConstraints": {} + }, + "public.drive_backup_roles": { + "name": "drive_backup_roles", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drive_backup_roles_backup_idx": { + "name": "drive_backup_roles_backup_idx", + "columns": [ + { + "expression": "backupId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_backup_roles_backupId_drive_backups_id_fk": { + "name": "drive_backup_roles_backupId_drive_backups_id_fk", + "tableFrom": "drive_backup_roles", + "tableTo": "drive_backups", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "drive_backup_roles_backupId_roleId_pk": { + "name": "drive_backup_roles_backupId_roleId_pk", + "columns": [ + "backupId", + "roleId" + ] + } + }, + "uniqueConstraints": {} + }, + "public.drive_backups": { + "name": "drive_backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "createdBy": { + "name": "createdBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "drive_backup_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "status": { + "name": "status", + "type": "drive_backup_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changeGroupId": { + "name": "changeGroupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changeGroupType": { + "name": "changeGroupType", + "type": "activity_change_group_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "isPinned": { + "name": "isPinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failedAt": { + "name": "failedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failureReason": { + "name": "failureReason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "drive_backups_drive_created_at_idx": { + "name": "drive_backups_drive_created_at_idx", + "columns": [ + { + "expression": "driveId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "drive_backups_status_idx": { + "name": "drive_backups_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "drive_backups_driveId_drives_id_fk": { + "name": "drive_backups_driveId_drives_id_fk", + "tableFrom": "drive_backups", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "drive_backups_createdBy_users_id_fk": { + "name": "drive_backups_createdBy_users_id_fk", + "tableFrom": "drive_backups", + "tableTo": "users", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.page_versions": { + "name": "page_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "createdBy": { + "name": "createdBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "page_version_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'auto'" + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changeGroupId": { + "name": "changeGroupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changeGroupType": { + "name": "changeGroupType", + "type": "activity_change_group_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "contentRef": { + "name": "contentRef", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contentFormat": { + "name": "contentFormat", + "type": "content_format", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "contentSize": { + "name": "contentSize", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stateHash": { + "name": "stateHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pageRevision": { + "name": "pageRevision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "isPinned": { + "name": "isPinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_versions_page_created_at_idx": { + "name": "page_versions_page_created_at_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_versions_drive_created_at_idx": { + "name": "page_versions_drive_created_at_idx", + "columns": [ + { + "expression": "driveId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_versions_pinned_idx": { + "name": "page_versions_pinned_idx", + "columns": [ + { + "expression": "isPinned", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_versions_pageId_pages_id_fk": { + "name": "page_versions_pageId_pages_id_fk", + "tableFrom": "page_versions", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_versions_driveId_drives_id_fk": { + "name": "page_versions_driveId_drives_id_fk", + "tableFrom": "page_versions", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_versions_createdBy_users_id_fk": { + "name": "page_versions_createdBy_users_id_fk", + "tableFrom": "page_versions", + "tableTo": "users", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.connections": { + "name": "connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user1Id": { + "name": "user1Id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user2Id": { + "name": "user2Id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ConnectionStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "requestedBy": { + "name": "requestedBy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestMessage": { + "name": "requestMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requestedAt": { + "name": "requestedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "acceptedAt": { + "name": "acceptedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "blockedBy": { + "name": "blockedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blockedAt": { + "name": "blockedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "connections_user1_id_idx": { + "name": "connections_user1_id_idx", + "columns": [ + { + "expression": "user1Id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connections_user2_id_idx": { + "name": "connections_user2_id_idx", + "columns": [ + { + "expression": "user2Id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connections_status_idx": { + "name": "connections_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connections_user1_status_idx": { + "name": "connections_user1_status_idx", + "columns": [ + { + "expression": "user1Id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connections_user2_status_idx": { + "name": "connections_user2_status_idx", + "columns": [ + { + "expression": "user2Id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connections_user1Id_users_id_fk": { + "name": "connections_user1Id_users_id_fk", + "tableFrom": "connections", + "tableTo": "users", + "columnsFrom": [ + "user1Id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connections_user2Id_users_id_fk": { + "name": "connections_user2Id_users_id_fk", + "tableFrom": "connections", + "tableTo": "users", + "columnsFrom": [ + "user2Id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connections_requestedBy_users_id_fk": { + "name": "connections_requestedBy_users_id_fk", + "tableFrom": "connections", + "tableTo": "users", + "columnsFrom": [ + "requestedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connections_blockedBy_users_id_fk": { + "name": "connections_blockedBy_users_id_fk", + "tableFrom": "connections", + "tableTo": "users", + "columnsFrom": [ + "blockedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "connections_user_pair_key": { + "name": "connections_user_pair_key", + "nullsNotDistinct": false, + "columns": [ + "user1Id", + "user2Id" + ] + } + } + }, + "public.direct_messages": { + "name": "direct_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "conversationId": { + "name": "conversationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "senderId": { + "name": "senderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isRead": { + "name": "isRead", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "readAt": { + "name": "readAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "isEdited": { + "name": "isEdited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "editedAt": { + "name": "editedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "direct_messages_conversation_id_idx": { + "name": "direct_messages_conversation_id_idx", + "columns": [ + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "direct_messages_sender_id_idx": { + "name": "direct_messages_sender_id_idx", + "columns": [ + { + "expression": "senderId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "direct_messages_created_at_idx": { + "name": "direct_messages_created_at_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "direct_messages_conversation_created_idx": { + "name": "direct_messages_conversation_created_idx", + "columns": [ + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "direct_messages_conversation_is_read_idx": { + "name": "direct_messages_conversation_is_read_idx", + "columns": [ + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "isRead", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "direct_messages_unread_count_idx": { + "name": "direct_messages_unread_count_idx", + "columns": [ + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "senderId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "isRead", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "direct_messages_conversationId_dm_conversations_id_fk": { + "name": "direct_messages_conversationId_dm_conversations_id_fk", + "tableFrom": "direct_messages", + "tableTo": "dm_conversations", + "columnsFrom": [ + "conversationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "direct_messages_senderId_users_id_fk": { + "name": "direct_messages_senderId_users_id_fk", + "tableFrom": "direct_messages", + "tableTo": "users", + "columnsFrom": [ + "senderId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.dm_conversations": { + "name": "dm_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "participant1Id": { + "name": "participant1Id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "participant2Id": { + "name": "participant2Id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastMessageAt": { + "name": "lastMessageAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastMessagePreview": { + "name": "lastMessagePreview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "participant1LastRead": { + "name": "participant1LastRead", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "participant2LastRead": { + "name": "participant2LastRead", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dm_conversations_participant1_id_idx": { + "name": "dm_conversations_participant1_id_idx", + "columns": [ + { + "expression": "participant1Id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dm_conversations_participant2_id_idx": { + "name": "dm_conversations_participant2_id_idx", + "columns": [ + { + "expression": "participant2Id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dm_conversations_last_message_at_idx": { + "name": "dm_conversations_last_message_at_idx", + "columns": [ + { + "expression": "lastMessageAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dm_conversations_participant1_last_message_idx": { + "name": "dm_conversations_participant1_last_message_idx", + "columns": [ + { + "expression": "participant1Id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lastMessageAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dm_conversations_participant2_last_message_idx": { + "name": "dm_conversations_participant2_last_message_idx", + "columns": [ + { + "expression": "participant2Id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lastMessageAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dm_conversations_participant1Id_users_id_fk": { + "name": "dm_conversations_participant1Id_users_id_fk", + "tableFrom": "dm_conversations", + "tableTo": "users", + "columnsFrom": [ + "participant1Id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dm_conversations_participant2Id_users_id_fk": { + "name": "dm_conversations_participant2Id_users_id_fk", + "tableFrom": "dm_conversations", + "tableTo": "users", + "columnsFrom": [ + "participant2Id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dm_conversations_participant_pair_key": { + "name": "dm_conversations_participant_pair_key", + "nullsNotDistinct": false, + "columns": [ + "participant1Id", + "participant2Id" + ] + } + } + }, + "public.stripe_events": { + "name": "stripe_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processedAt": { + "name": "processedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "stripe_events_type_idx": { + "name": "stripe_events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "stripe_events_processed_at_idx": { + "name": "stripe_events_processed_at_idx", + "columns": [ + { + "expression": "processedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripePriceId": { + "name": "stripePriceId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currentPeriodStart": { + "name": "currentPeriodStart", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "currentPeriodEnd": { + "name": "currentPeriodEnd", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "cancelAtPeriodEnd": { + "name": "cancelAtPeriodEnd", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeScheduleId": { + "name": "stripeScheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduledPriceId": { + "name": "scheduledPriceId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduledChangeDate": { + "name": "scheduledChangeDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscriptions_user_id_idx": { + "name": "subscriptions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscriptions_stripe_subscription_id_idx": { + "name": "subscriptions_stripe_subscription_id_idx", + "columns": [ + { + "expression": "stripeSubscriptionId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscriptions_stripe_schedule_id_idx": { + "name": "subscriptions_stripe_schedule_id_idx", + "columns": [ + { + "expression": "stripeScheduleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_userId_users_id_fk": { + "name": "subscriptions_userId_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "subscriptions_stripeSubscriptionId_unique": { + "name": "subscriptions_stripeSubscriptionId_unique", + "nullsNotDistinct": false, + "columns": [ + "stripeSubscriptionId" + ] + } + } + }, + "public.contact_submissions": { + "name": "contact_submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_submissions_email_idx": { + "name": "contact_submissions_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_submissions_created_at_idx": { + "name": "contact_submissions_created_at_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.file_pages": { + "name": "file_pages", + "schema": "", + "columns": { + "fileId": { + "name": "fileId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "linkedBy": { + "name": "linkedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linkedAt": { + "name": "linkedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "linkSource": { + "name": "linkSource", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "file_pages_file_id_idx": { + "name": "file_pages_file_id_idx", + "columns": [ + { + "expression": "fileId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "file_pages_page_id_idx": { + "name": "file_pages_page_id_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_pages_fileId_files_id_fk": { + "name": "file_pages_fileId_files_id_fk", + "tableFrom": "file_pages", + "tableTo": "files", + "columnsFrom": [ + "fileId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_pages_pageId_pages_id_fk": { + "name": "file_pages_pageId_pages_id_fk", + "tableFrom": "file_pages", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "file_pages_linkedBy_users_id_fk": { + "name": "file_pages_linkedBy_users_id_fk", + "tableFrom": "file_pages", + "tableTo": "users", + "columnsFrom": [ + "linkedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_pages_fileId_pageId_pk": { + "name": "file_pages_fileId_pageId_pk", + "columns": [ + "fileId", + "pageId" + ] + } + }, + "uniqueConstraints": { + "file_pages_page_id_key": { + "name": "file_pages_page_id_key", + "nullsNotDistinct": false, + "columns": [ + "pageId" + ] + } + } + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "driveId": { + "name": "driveId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sizeBytes": { + "name": "sizeBytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storagePath": { + "name": "storagePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checksumVersion": { + "name": "checksumVersion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "createdBy": { + "name": "createdBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastAccessedAt": { + "name": "lastAccessedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "files_drive_id_idx": { + "name": "files_drive_id_idx", + "columns": [ + { + "expression": "driveId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "files_driveId_drives_id_fk": { + "name": "files_driveId_drives_id_fk", + "tableFrom": "files", + "tableTo": "drives", + "columnsFrom": [ + "driveId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_createdBy_users_id_fk": { + "name": "files_createdBy_users_id_fk", + "tableFrom": "files", + "tableTo": "users", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.task_items": { + "name": "task_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "taskListId": { + "name": "taskListId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigneeId": { + "name": "assigneeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigneeAgentId": { + "name": "assigneeAgentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dueDate": { + "name": "dueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_items_task_list_id_idx": { + "name": "task_items_task_list_id_idx", + "columns": [ + { + "expression": "taskListId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_items_task_list_status_idx": { + "name": "task_items_task_list_status_idx", + "columns": [ + { + "expression": "taskListId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_items_assignee_id_idx": { + "name": "task_items_assignee_id_idx", + "columns": [ + { + "expression": "assigneeId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_items_assignee_agent_id_idx": { + "name": "task_items_assignee_agent_id_idx", + "columns": [ + { + "expression": "assigneeAgentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_items_page_id_idx": { + "name": "task_items_page_id_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_items_taskListId_task_lists_id_fk": { + "name": "task_items_taskListId_task_lists_id_fk", + "tableFrom": "task_items", + "tableTo": "task_lists", + "columnsFrom": [ + "taskListId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_items_userId_users_id_fk": { + "name": "task_items_userId_users_id_fk", + "tableFrom": "task_items", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_items_assigneeId_users_id_fk": { + "name": "task_items_assigneeId_users_id_fk", + "tableFrom": "task_items", + "tableTo": "users", + "columnsFrom": [ + "assigneeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_items_assigneeAgentId_pages_id_fk": { + "name": "task_items_assigneeAgentId_pages_id_fk", + "tableFrom": "task_items", + "tableTo": "pages", + "columnsFrom": [ + "assigneeAgentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "task_items_pageId_pages_id_fk": { + "name": "task_items_pageId_pages_id_fk", + "tableFrom": "task_items", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.task_lists": { + "name": "task_lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pageId": { + "name": "pageId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conversationId": { + "name": "conversationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_lists_page_id_idx": { + "name": "task_lists_page_id_idx", + "columns": [ + { + "expression": "pageId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_lists_conversation_id_idx": { + "name": "task_lists_conversation_id_idx", + "columns": [ + { + "expression": "conversationId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_lists_user_id_idx": { + "name": "task_lists_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_lists_userId_users_id_fk": { + "name": "task_lists_userId_users_id_fk", + "tableFrom": "task_lists", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_lists_pageId_pages_id_fk": { + "name": "task_lists_pageId_pages_id_fk", + "tableFrom": "task_lists", + "tableTo": "pages", + "columnsFrom": [ + "pageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "public.AuthProvider": { + "name": "AuthProvider", + "schema": "public", + "values": [ + "email", + "google", + "both" + ] + }, + "public.PlatformType": { + "name": "PlatformType", + "schema": "public", + "values": [ + "web", + "desktop", + "ios", + "android" + ] + }, + "public.UserRole": { + "name": "UserRole", + "schema": "public", + "values": [ + "user", + "admin" + ] + }, + "public.PageType": { + "name": "PageType", + "schema": "public", + "values": [ + "FOLDER", + "DOCUMENT", + "CHANNEL", + "AI_CHAT", + "CANVAS", + "FILE", + "SHEET", + "TASK_LIST" + ] + }, + "public.PermissionAction": { + "name": "PermissionAction", + "schema": "public", + "values": [ + "VIEW", + "EDIT", + "SHARE", + "DELETE" + ] + }, + "public.SubjectType": { + "name": "SubjectType", + "schema": "public", + "values": [ + "USER" + ] + }, + "public.InvitationStatus": { + "name": "InvitationStatus", + "schema": "public", + "values": [ + "PENDING", + "ACCEPTED", + "REJECTED", + "EXPIRED" + ] + }, + "public.MemberRole": { + "name": "MemberRole", + "schema": "public", + "values": [ + "OWNER", + "ADMIN", + "MEMBER" + ] + }, + "public.NotificationType": { + "name": "NotificationType", + "schema": "public", + "values": [ + "PERMISSION_GRANTED", + "PERMISSION_REVOKED", + "PERMISSION_UPDATED", + "PAGE_SHARED", + "DRIVE_INVITED", + "DRIVE_JOINED", + "DRIVE_ROLE_CHANGED", + "CONNECTION_REQUEST", + "CONNECTION_ACCEPTED", + "CONNECTION_REJECTED", + "NEW_DIRECT_MESSAGE", + "EMAIL_VERIFICATION_REQUIRED", + "TOS_PRIVACY_UPDATED" + ] + }, + "public.activity_change_group_type": { + "name": "activity_change_group_type", + "schema": "public", + "values": [ + "user", + "ai", + "automation", + "system" + ] + }, + "public.activity_operation": { + "name": "activity_operation", + "schema": "public", + "values": [ + "create", + "update", + "delete", + "restore", + "reorder", + "permission_grant", + "permission_update", + "permission_revoke", + "trash", + "move", + "agent_config_update", + "member_add", + "member_remove", + "member_role_change", + "login", + "logout", + "signup", + "password_change", + "email_change", + "token_create", + "token_revoke", + "upload", + "convert", + "account_delete", + "profile_update", + "avatar_update", + "message_update", + "message_delete", + "role_reorder", + "ownership_transfer", + "rollback", + "conversation_undo", + "conversation_undo_with_changes" + ] + }, + "public.activity_resource": { + "name": "activity_resource", + "schema": "public", + "values": [ + "page", + "drive", + "permission", + "agent", + "user", + "member", + "role", + "file", + "token", + "device", + "message", + "conversation" + ] + }, + "public.content_format": { + "name": "content_format", + "schema": "public", + "values": [ + "text", + "html", + "json", + "tiptap" + ] + }, + "public.http_method": { + "name": "http_method", + "schema": "public", + "values": [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS" + ] + }, + "public.log_level": { + "name": "log_level", + "schema": "public", + "values": [ + "trace", + "debug", + "info", + "warn", + "error", + "fatal" + ] + }, + "public.subscription_tier": { + "name": "subscription_tier", + "schema": "public", + "values": [ + "free", + "pro", + "business", + "founder" + ] + }, + "public.drive_backup_source": { + "name": "drive_backup_source", + "schema": "public", + "values": [ + "manual", + "scheduled", + "pre_restore", + "system" + ] + }, + "public.drive_backup_status": { + "name": "drive_backup_status", + "schema": "public", + "values": [ + "pending", + "ready", + "failed" + ] + }, + "public.page_version_source": { + "name": "page_version_source", + "schema": "public", + "values": [ + "manual", + "auto", + "pre_ai", + "pre_restore", + "restore", + "system" + ] + }, + "public.ConnectionStatus": { + "name": "ConnectionStatus", + "schema": "public", + "values": [ + "PENDING", + "ACCEPTED", + "BLOCKED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 03731b9500..a0725f1e4e 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -260,6 +260,13 @@ "when": 1768228730754, "tag": "0036_amused_true_believers", "breakpoints": true + }, + { + "idx": 37, + "version": "7", + "when": 1768285686596, + "tag": "0037_slimy_bishop", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/__tests__/sessions-schema.test.ts b/packages/db/src/__tests__/sessions-schema.test.ts index 1b7ec6ef33..a8716fe5ca 100644 --- a/packages/db/src/__tests__/sessions-schema.test.ts +++ b/packages/db/src/__tests__/sessions-schema.test.ts @@ -6,33 +6,46 @@ */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { db, sessions, users } from '../index'; -import { eq, and, isNull } from 'drizzle-orm'; +import { eq, and, isNull, inArray } from 'drizzle-orm'; import { createId } from '@paralleldrive/cuid2'; describe('Sessions Schema', () => { let testUserId = ''; beforeEach(async () => { - testUserId = ''; + // Generate unique test ID first + const uniqueId = createId(); + + // Clean any orphaned test sessions (defensive cleanup) + await db.delete(sessions).where( + inArray(sessions.tokenHash, ['abc123hash', 'unique-hash', 'cascade-test', 'hash-1', 'hash-2']) + ); + const [user] = await db.insert(users).values({ - id: createId(), + id: uniqueId, name: 'Test Session User', - email: `test-session-${Date.now()}@example.com`, + email: `test-session-${uniqueId}@example.com`, password: 'hashed_password', provider: 'email', role: 'user', tokenVersion: 1, }).returning(); + testUserId = user.id; }); afterEach(async () => { - if (!testUserId) { - return; + if (!testUserId) return; + + try { + // Delete sessions before user (cascade also handles this, but explicit is safer) + await db.delete(sessions).where(eq(sessions.userId, testUserId)); + await db.delete(users).where(eq(users.id, testUserId)); + } catch { + // User may already be deleted by "cascades delete" test } - await db.delete(users).where(eq(users.id, testUserId)); - await db.delete(sessions).where(eq(sessions.userId, testUserId)); + testUserId = ''; }); it('creates session with required fields', async () => { diff --git a/packages/db/src/schema/sessions.ts b/packages/db/src/schema/sessions.ts index 201ea3db91..4ba8a6bb09 100644 --- a/packages/db/src/schema/sessions.ts +++ b/packages/db/src/schema/sessions.ts @@ -20,6 +20,7 @@ export const sessions = pgTable('sessions', { // Resource binding resourceType: text('resource_type'), resourceId: text('resource_id'), + driveId: text('drive_id'), // Security context tokenVersion: integer('token_version').notNull(), diff --git a/packages/lib/src/__tests__/notifications.test.ts b/packages/lib/src/__tests__/notifications.test.ts index e1f1e3f060..b1001d922b 100644 --- a/packages/lib/src/__tests__/notifications.test.ts +++ b/packages/lib/src/__tests__/notifications.test.ts @@ -7,7 +7,7 @@ import { deleteNotification, getUnreadCount } from '../notifications' -import { db, sql, notifications, users } from '@pagespace/db' +import { db, users } from '@pagespace/db' import { factories } from '@pagespace/db/test/factories' // Mock fetch for broadcast testing @@ -20,10 +20,11 @@ describe('notifications', () => { let testPage: Awaited> beforeEach(async () => { - // Clean up test data before each test - // Use DELETE to avoid TRUNCATE CASCADE deadlocks with connection pool + // Clean up ALL test data first (ensures clean slate) + // Deleting users cascades to drives, pages, notifications await db.delete(users) + // Create fresh test data (factories use unique IDs) testUser = await factories.createUser() otherUser = await factories.createUser() testDrive = await factories.createDrive(testUser.id) @@ -34,10 +35,6 @@ describe('notifications', () => { ;(global.fetch as any).mockResolvedValue({ ok: true, json: async () => ({}) }) }) - afterEach(() => { - vi.clearAllMocks() - }) - describe('createNotification', () => { it('creates a notification successfully', async () => { const notification = await createNotification({ diff --git a/packages/lib/src/auth/auth-utils.ts b/packages/lib/src/auth/auth-utils.ts index 489d94d294..28eae00146 100644 --- a/packages/lib/src/auth/auth-utils.ts +++ b/packages/lib/src/auth/auth-utils.ts @@ -1,11 +1,5 @@ import * as jose from 'jose'; import { createId } from '@paralleldrive/cuid2'; -import { - createServiceToken as createServiceTokenV2, - verifyServiceToken as verifyServiceTokenV2, - type ServiceScope, - type ServiceTokenClaims, -} from '../services/service-auth'; const JWT_ALGORITHM = 'HS256'; @@ -156,38 +150,3 @@ export function requireAdminPayload(userPayload: UserPayload | null): void { throw new Error('Admin access required'); } } - -// Service JWT functions (legacy interface maintained for backwards compatibility) -export async function createServiceToken( - service: string, - permissions: string[] = ['*'], - options?: { - tenantId?: string; - userId?: string; - driveIds?: string[]; - expirationTime?: string; - } -): Promise { - const subject = options?.userId ?? options?.tenantId ?? service; - const scopes = (permissions.length > 0 ? permissions : ['*']) as ServiceScope[]; - - return createServiceTokenV2({ - service, - subject, - scopes, - resource: options?.tenantId, - driveId: options?.driveIds?.[0], - expiresIn: options?.expirationTime ?? '1h', - }); -} - -export async function verifyServiceToken(token: string): Promise { - try { - return await verifyServiceTokenV2(token); - } catch (error) { - console.error('Invalid service token:', error); - return null; - } -} - -export type { ServiceScope, ServiceTokenClaims }; diff --git a/packages/lib/src/auth/session-service.ts b/packages/lib/src/auth/session-service.ts index 55784ad761..d711abb31e 100644 --- a/packages/lib/src/auth/session-service.ts +++ b/packages/lib/src/auth/session-service.ts @@ -14,6 +14,7 @@ export interface SessionClaims { scopes: string[]; resourceType?: string; resourceId?: string; + driveId?: string; } export interface CreateSessionOptions { @@ -23,6 +24,7 @@ export interface CreateSessionOptions { expiresInMs: number; resourceType?: string; resourceId?: string; + driveId?: string; createdByService?: string; createdByIp?: string; } @@ -56,6 +58,7 @@ export class SessionService { scopes: options.scopes, resourceType: options.resourceType, resourceId: options.resourceId, + driveId: options.driveId, tokenVersion: user.tokenVersion, createdByService: options.createdByService, createdByIp: options.createdByIp, @@ -110,6 +113,7 @@ export class SessionService { scopes: session.scopes, resourceType: session.resourceType ?? undefined, resourceId: session.resourceId ?? undefined, + driveId: session.driveId ?? undefined, }; } diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index d55b848768..795152cb29 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -49,19 +49,8 @@ export * from './sheets'; export * from './auth/auth-utils'; export * from './auth/device-auth-utils'; export { secureCompare } from './auth/secure-compare'; -export { - createServiceToken as createServiceTokenV2, - verifyServiceToken as verifyServiceTokenV2, - authenticateServiceToken, - decodeServiceTokenHeader, - hasScope, - assertScope, - hasScope as hasServiceScope, - assertScope as assertServiceScope, - type ServiceTokenClaims, - type ServiceTokenOptions, - type ServiceScope, -} from './services/service-auth'; +export { sessionService, type SessionClaims, type CreateSessionOptions } from './auth/session-service'; +export { EnforcedAuthContext, type ResourceBinding } from './permissions/enforced-context'; export { createValidatedServiceToken, createPageServiceToken, @@ -71,6 +60,7 @@ export { type ValidatedTokenResult, type PermissionSet, type ResourceType, + type ServiceScope, } from './services/validated-service-token'; export * from './auth/csrf-utils'; export * from './encryption'; diff --git a/packages/lib/src/notifications/notifications.ts b/packages/lib/src/notifications/notifications.ts index 5e392ddf85..1d00597c58 100644 --- a/packages/lib/src/notifications/notifications.ts +++ b/packages/lib/src/notifications/notifications.ts @@ -1,6 +1,7 @@ import { db, notifications, users, pages, drives, eq, and, desc, count, sql } from '@pagespace/db'; import { createId } from '@paralleldrive/cuid2'; import { sendNotificationEmail } from '../services/notification-email-service'; +import { createSignedBroadcastHeaders } from '../auth/broadcast-auth'; // Export types and guards export * from './types'; @@ -10,14 +11,16 @@ import type { NotificationType } from './types'; async function broadcastNotification(userId: string, notification: unknown) { try { const realtimeUrl = process.env.INTERNAL_REALTIME_URL || 'http://localhost:3001'; + const requestBody = JSON.stringify({ + channelId: `notifications:${userId}`, + event: 'notification:new', + payload: notification, + }); + await fetch(`${realtimeUrl}/api/broadcast`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - channelId: `notifications:${userId}`, - event: 'notification:new', - payload: notification, - }), + headers: createSignedBroadcastHeaders(requestBody), + body: requestBody, }); } catch (error) { console.error('Failed to broadcast notification:', error); diff --git a/packages/lib/src/permissions/__tests__/enforced-context.test.ts b/packages/lib/src/permissions/__tests__/enforced-context.test.ts new file mode 100644 index 0000000000..8138b13a69 --- /dev/null +++ b/packages/lib/src/permissions/__tests__/enforced-context.test.ts @@ -0,0 +1,178 @@ +import { describe, it, expect } from 'vitest'; +import { EnforcedAuthContext } from '../enforced-context'; +import type { SessionClaims } from '../../auth/session-service'; + +describe('EnforcedAuthContext', () => { + const createMockClaims = (overrides: Partial = {}): SessionClaims => ({ + sessionId: 'test-session-id', + userId: 'test-user-id', + userRole: 'user', + tokenVersion: 1, + type: 'user', + scopes: ['files:read'], + driveId: undefined, + ...overrides, + }); + + describe('construction', () => { + it('cannot be constructed directly (TypeScript enforced)', () => { + // TypeScript prevents direct construction via private constructor + // This test documents the design intent - TS compilation enforces it + // @ts-expect-error - Constructor is private and inaccessible + const attemptConstruction = () => new EnforcedAuthContext('u', 'user', [], undefined); + // At runtime JS allows it, but TS prevents compilation + expect(typeof attemptConstruction).toBe('function'); + }); + + it('fromSession creates valid context', () => { + const claims = createMockClaims(); + const context = EnforcedAuthContext.fromSession(claims); + + expect(context.userId).toBe('test-user-id'); + expect(context.userRole).toBe('user'); + }); + + it('context is immutable (frozen)', () => { + const claims = createMockClaims(); + const context = EnforcedAuthContext.fromSession(claims); + + expect(Object.isFrozen(context)).toBe(true); + + // Verify properties cannot be modified + expect(() => { + // @ts-expect-error - Testing immutability + context.userId = 'hacked'; + }).toThrow(); + }); + }); + + describe('hasScope', () => { + it('checks exact scope match', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ scopes: ['files:read', 'files:write'] }) + ); + + expect(context.hasScope('files:read')).toBe(true); + expect(context.hasScope('files:write')).toBe(true); + expect(context.hasScope('files:delete')).toBe(false); + }); + + it('supports wildcard (*) scope', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ scopes: ['*'] }) + ); + + expect(context.hasScope('files:read')).toBe(true); + expect(context.hasScope('files:write')).toBe(true); + expect(context.hasScope('admin:delete')).toBe(true); + }); + + it('supports namespace wildcard (files:*)', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ scopes: ['files:*'] }) + ); + + expect(context.hasScope('files:read')).toBe(true); + expect(context.hasScope('files:write')).toBe(true); + expect(context.hasScope('files:delete')).toBe(true); + expect(context.hasScope('admin:read')).toBe(false); + }); + + it('returns false for empty scopes', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ scopes: [] }) + ); + + expect(context.hasScope('files:read')).toBe(false); + }); + }); + + describe('isAdmin', () => { + it('returns true for admin role', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ userRole: 'admin' }) + ); + + expect(context.isAdmin()).toBe(true); + }); + + it('returns false for user role', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ userRole: 'user' }) + ); + + expect(context.isAdmin()).toBe(false); + }); + }); + + describe('isBoundToResource', () => { + it('returns true when no resource binding (unrestricted)', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ resourceType: undefined, resourceId: undefined }) + ); + + expect(context.isBoundToResource('page', 'any-page-id')).toBe(true); + expect(context.isBoundToResource('drive', 'any-drive-id')).toBe(true); + }); + + it('returns true when resource matches binding', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ resourceType: 'page', resourceId: 'page-123' }) + ); + + expect(context.isBoundToResource('page', 'page-123')).toBe(true); + }); + + it('returns false when resource type mismatches', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ resourceType: 'page', resourceId: 'page-123' }) + ); + + expect(context.isBoundToResource('drive', 'page-123')).toBe(false); + }); + + it('returns false when resource id mismatches', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ resourceType: 'page', resourceId: 'page-123' }) + ); + + expect(context.isBoundToResource('page', 'page-456')).toBe(false); + }); + }); + + describe('resource binding accessor', () => { + it('exposes resourceBinding when present', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ resourceType: 'page', resourceId: 'page-123' }) + ); + + expect(context.resourceBinding).toEqual({ type: 'page', id: 'page-123' }); + }); + + it('returns undefined when no binding', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ resourceType: undefined, resourceId: undefined }) + ); + + expect(context.resourceBinding).toBeUndefined(); + }); + }); + + describe('driveId accessor', () => { + it('exposes driveId when present', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ driveId: 'drive-123' }) + ); + + expect(context.driveId).toBe('drive-123'); + }); + + it('returns undefined when no driveId', () => { + const context = EnforcedAuthContext.fromSession( + createMockClaims({ driveId: undefined }) + ); + + expect(context.driveId).toBeUndefined(); + }); + }); +}); diff --git a/packages/lib/src/permissions/enforced-context.ts b/packages/lib/src/permissions/enforced-context.ts new file mode 100644 index 0000000000..c869628c9f --- /dev/null +++ b/packages/lib/src/permissions/enforced-context.ts @@ -0,0 +1,84 @@ +import type { SessionClaims } from '../auth/session-service'; + +export interface ResourceBinding { + type: string; + id: string; +} + +/** + * Enforced auth context - MUST be created from validated session. + * Cannot be constructed directly, only via fromSession(). + * Immutable after creation. + */ +export class EnforcedAuthContext { + public readonly userId: string; + public readonly userRole: 'user' | 'admin'; + public readonly resourceBinding?: ResourceBinding; + public readonly driveId?: string; + + private readonly _scopes: ReadonlySet; + + private constructor( + userId: string, + userRole: 'user' | 'admin', + scopes: string[], + resourceBinding?: ResourceBinding, + driveId?: string + ) { + this.userId = userId; + this.userRole = userRole; + this._scopes = new Set(scopes); + this.resourceBinding = resourceBinding; + this.driveId = driveId; + + Object.freeze(this); + } + + static fromSession(claims: SessionClaims): EnforcedAuthContext { + const resourceBinding = + claims.resourceType && claims.resourceId + ? { type: claims.resourceType, id: claims.resourceId } + : undefined; + + return new EnforcedAuthContext( + claims.userId, + claims.userRole, + claims.scopes, + resourceBinding, + claims.driveId + ); + } + + hasScope(scope: string): boolean { + // Global wildcard + if (this._scopes.has('*')) { + return true; + } + + // Exact match + if (this._scopes.has(scope)) { + return true; + } + + // Namespace wildcard (e.g., 'files:*' matches 'files:read') + const [namespace] = scope.split(':'); + if (namespace && this._scopes.has(`${namespace}:*`)) { + return true; + } + + return false; + } + + isAdmin(): boolean { + return this.userRole === 'admin'; + } + + isBoundToResource(type: string, id: string): boolean { + // No binding means unrestricted + if (!this.resourceBinding) { + return true; + } + + return this.resourceBinding.type === type && this.resourceBinding.id === id; + } +} diff --git a/packages/lib/src/permissions/index.ts b/packages/lib/src/permissions/index.ts index 1a1e883dbf..566ec0c711 100644 --- a/packages/lib/src/permissions/index.ts +++ b/packages/lib/src/permissions/index.ts @@ -18,3 +18,6 @@ export { // Export rollback permissions export * from './rollback-permissions'; + +// Export enforced auth context +export * from './enforced-context'; diff --git a/packages/lib/src/services/__tests__/service-auth.test.ts b/packages/lib/src/services/__tests__/service-auth.test.ts deleted file mode 100644 index c5b31fdd07..0000000000 --- a/packages/lib/src/services/__tests__/service-auth.test.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; - -// Mock security-redis before importing module under test -vi.mock('../../security/security-redis', () => ({ - recordJTI: vi.fn(), - isJTIRevoked: vi.fn(), - tryGetSecurityRedisClient: vi.fn(), -})); - -// Mock loggers -vi.mock('../../logging/logger-config', () => ({ - loggers: { - api: { - warn: vi.fn(), - error: vi.fn(), - info: vi.fn(), - debug: vi.fn(), - }, - }, -})); - -// Mock cuid2 for predictable JTI values -const DEFAULT_JTI = 'test-jti-predictable-123'; -vi.mock('@paralleldrive/cuid2', () => ({ - createId: vi.fn(() => DEFAULT_JTI), -})); - -import { - createServiceToken, - verifyServiceToken, - type ServiceTokenOptions, -} from '../service-auth'; -import { recordJTI, isJTIRevoked, tryGetSecurityRedisClient } from '../../security/security-redis'; -import { createId } from '@paralleldrive/cuid2'; -import { loggers } from '../../logging/logger-config'; - -/** - * Service Auth JTI Integration Tests (P1-T1) - * - * Tests the integration of JTI (JWT ID) tracking into the service token lifecycle: - * - JTI recording on token creation - * - JTI validation on token verification - * - Graceful degradation when Redis unavailable - */ -describe('service-auth JTI integration', () => { - const originalEnv = { ...process.env }; - - beforeEach(() => { - // Set required environment variables - process.env.SERVICE_JWT_SECRET = 'test-secret-that-is-at-least-32-characters-long'; - process.env.JWT_ISSUER = 'test-issuer'; - process.env.NODE_ENV = 'test'; - vi.clearAllMocks(); - // Reset createId mock to default behavior - vi.mocked(createId).mockReturnValue(DEFAULT_JTI); - }); - - afterEach(() => { - process.env = { ...originalEnv }; - }); - - const validOptions: ServiceTokenOptions = { - service: 'test-service', - subject: 'user-123', - scopes: ['files:read'], - }; - - describe('createServiceToken JTI recording', () => { - it('records JTI when Redis available', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - - await createServiceToken(validOptions); - - expect(recordJTI).toHaveBeenCalledWith( - 'test-jti-predictable-123', - 'user-123', - 300 // 5 minutes default expiry - ); - }); - - it('JTI expiration matches token expiration (5m default = 300s)', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - - await createServiceToken(validOptions); - - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - 300 - ); - }); - - it('JTI expiration matches custom token expiration (1h = 3600s)', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - - await createServiceToken({ ...validOptions, expiresIn: '1h' }); - - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - 3600 - ); - }); - - it('JTI expiration matches custom token expiration (30s)', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - - await createServiceToken({ ...validOptions, expiresIn: '30s' }); - - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - 30 - ); - }); - - it('JTI expiration matches custom token expiration (7d)', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - - await createServiceToken({ ...validOptions, expiresIn: '7d' }); - - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - 7 * 24 * 60 * 60 // 7 days in seconds - ); - }); - - it('token creation succeeds when Redis unavailable in dev/test (graceful degradation)', async () => { - process.env.NODE_ENV = 'test'; - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue(null); - - const token = await createServiceToken(validOptions); - - expect(token).toBeDefined(); - expect(typeof token).toBe('string'); - expect(recordJTI).not.toHaveBeenCalled(); - }); - - it('token creation succeeds when recordJTI throws in dev/test (graceful degradation)', async () => { - process.env.NODE_ENV = 'test'; - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(recordJTI).mockRejectedValue(new Error('Redis error')); - - const token = await createServiceToken(validOptions); - - expect(token).toBeDefined(); - expect(typeof token).toBe('string'); - expect(loggers.api.warn).toHaveBeenCalledWith( - 'Failed to record JTI in Redis, token created without tracking', - expect.objectContaining({ - error: 'Redis error', - subject: 'user-123', - }) - ); - }); - - it('token creation FAILS when Redis unavailable in production (fail-closed)', async () => { - process.env.NODE_ENV = 'production'; - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue(null); - - await expect(createServiceToken(validOptions)).rejects.toThrow( - 'Security infrastructure unavailable - cannot issue service token' - ); - expect(recordJTI).not.toHaveBeenCalled(); - }); - - it('token creation FAILS when recordJTI throws in production (fail-closed)', async () => { - process.env.NODE_ENV = 'production'; - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(recordJTI).mockRejectedValue(new Error('Redis connection lost')); - - await expect(createServiceToken(validOptions)).rejects.toThrow('Redis connection lost'); - }); - - it('each token gets a unique JTI', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - let callCount = 0; - vi.mocked(createId).mockImplementation(() => `jti-${++callCount}`); - - await createServiceToken(validOptions); - await createServiceToken(validOptions); - - expect(recordJTI).toHaveBeenNthCalledWith(1, 'jti-1', 'user-123', 300); - expect(recordJTI).toHaveBeenNthCalledWith(2, 'jti-2', 'user-123', 300); - }); - }); - - describe('verifyServiceToken JTI validation', () => { - let validToken: string; - - beforeEach(async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(isJTIRevoked).mockResolvedValue(false); - validToken = await createServiceToken(validOptions); - vi.clearAllMocks(); - }); - - it('checks JTI revocation status when Redis available', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(isJTIRevoked).mockResolvedValue(false); - - await verifyServiceToken(validToken); - - expect(isJTIRevoked).toHaveBeenCalledWith('test-jti-predictable-123'); - }); - - it('verification succeeds when JTI is valid (not revoked)', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(isJTIRevoked).mockResolvedValue(false); - - const claims = await verifyServiceToken(validToken); - - expect(claims).toBeDefined(); - expect(claims.sub).toBe('user-123'); - expect(claims.service).toBe('test-service'); - }); - - it('verification fails when JTI is revoked', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(isJTIRevoked).mockResolvedValue(true); - - await expect(verifyServiceToken(validToken)).rejects.toThrow( - /Token revoked or invalid/ - ); - }); - - it('verification fails when JTI not in Redis (fail-closed)', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - // isJTIRevoked returns true when JTI is not found (fail-closed behavior) - vi.mocked(isJTIRevoked).mockResolvedValue(true); - - await expect(verifyServiceToken(validToken)).rejects.toThrow( - /Token revoked or invalid/ - ); - }); - - it('verification succeeds when Redis unavailable in development', async () => { - process.env.NODE_ENV = 'development'; - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue(null); - - const claims = await verifyServiceToken(validToken); - - expect(claims).toBeDefined(); - expect(isJTIRevoked).not.toHaveBeenCalled(); - }); - - it('verification succeeds when Redis unavailable in test environment', async () => { - process.env.NODE_ENV = 'test'; - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue(null); - - const claims = await verifyServiceToken(validToken); - - expect(claims).toBeDefined(); - expect(isJTIRevoked).not.toHaveBeenCalled(); - }); - - it('verification FAILS when Redis unavailable in production', async () => { - process.env.NODE_ENV = 'production'; - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue(null); - - await expect(verifyServiceToken(validToken)).rejects.toThrow( - /Security infrastructure unavailable/ - ); - }); - - it('verification fails when isJTIRevoked throws (fail-closed)', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(isJTIRevoked).mockRejectedValue(new Error('Redis error')); - - await expect(verifyServiceToken(validToken)).rejects.toThrow(); - }); - }); - - describe('end-to-end token lifecycle', () => { - it('create then verify - JTI flows through correctly', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(isJTIRevoked).mockResolvedValue(false); - - const token = await createServiceToken(validOptions); - - expect(recordJTI).toHaveBeenCalledTimes(1); - - const claims = await verifyServiceToken(token); - - expect(isJTIRevoked).toHaveBeenCalledTimes(1); - expect(claims.jti).toBe('test-jti-predictable-123'); - }); - - it('revoked token cannot be verified', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - - // Token creation - JTI recorded - vi.mocked(isJTIRevoked).mockResolvedValue(false); - const token = await createServiceToken(validOptions); - - // Token revoked in Redis - vi.mocked(isJTIRevoked).mockResolvedValue(true); - - // Verification should fail - await expect(verifyServiceToken(token)).rejects.toThrow( - /Token revoked or invalid/ - ); - }); - }); - - describe('backward compatibility', () => { - it('token format unchanged (still valid JWT)', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue(null); // No Redis - - const token = await createServiceToken(validOptions); - - // Should be a valid JWT format: header.payload.signature - const parts = token.split('.'); - expect(parts).toHaveLength(3); - - // Decode payload (base64url) - const payload = JSON.parse( - Buffer.from(parts[1], 'base64url').toString() - ); - expect(payload.sub).toBe('user-123'); - expect(payload.service).toBe('test-service'); - expect(payload.jti).toBeDefined(); - expect(payload.tokenType).toBe('service'); - }); - - it('existing token validation still works without JTI check when Redis unavailable', async () => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue(null); - - const token = await createServiceToken(validOptions); - const claims = await verifyServiceToken(token); - - expect(claims.sub).toBe('user-123'); - expect(claims.service).toBe('test-service'); - }); - }); - - describe('duration validation', () => { - beforeEach(() => { - vi.mocked(tryGetSecurityRedisClient).mockResolvedValue({} as never); - vi.mocked(isJTIRevoked).mockResolvedValue(false); - }); - - it('zero duration returns default (5m = 300s)', async () => { - await createServiceToken({ ...validOptions, expiresIn: '0m' }); - - expect(loggers.api.warn).toHaveBeenCalledWith( - 'Invalid duration value (must be positive), using default', - expect.objectContaining({ duration: '0m', value: 0 }) - ); - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - 300 // default - ); - }); - - it('negative duration returns default (5m = 300s)', async () => { - await createServiceToken({ ...validOptions, expiresIn: '-5m' }); - - expect(loggers.api.warn).toHaveBeenCalledWith( - 'Invalid duration value (must be positive), using default', - expect.objectContaining({ duration: '-5m', value: -5 }) - ); - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - 300 // default - ); - }); - - it('invalid format returns default (5m = 300s)', async () => { - await createServiceToken({ ...validOptions, expiresIn: '5x' }); - - expect(loggers.api.warn).toHaveBeenCalledWith( - 'Invalid duration format, using default', - expect.objectContaining({ duration: '5x' }) - ); - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - 300 // default - ); - }); - - it('duration exceeding 30 days is capped at 30 days', async () => { - const thirtyDaysInSeconds = 30 * 24 * 60 * 60; // 2592000 - - await createServiceToken({ ...validOptions, expiresIn: '100d' }); - - expect(loggers.api.warn).toHaveBeenCalledWith( - 'Duration exceeds maximum, capping at 30 days', - expect.objectContaining({ - duration: '100d', - requestedSeconds: 100 * 24 * 60 * 60, - cappedTo: thirtyDaysInSeconds, - }) - ); - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - thirtyDaysInSeconds // capped at 30 days - ); - }); - - it('duration at exactly 30 days is accepted without warning', async () => { - const thirtyDaysInSeconds = 30 * 24 * 60 * 60; - - await createServiceToken({ ...validOptions, expiresIn: '30d' }); - - expect(loggers.api.warn).not.toHaveBeenCalledWith( - 'Duration exceeds maximum, capping at 30 days', - expect.anything() - ); - expect(recordJTI).toHaveBeenCalledWith( - expect.any(String), - 'user-123', - thirtyDaysInSeconds - ); - }); - }); -}); diff --git a/packages/lib/src/services/__tests__/validated-service-token.test.ts b/packages/lib/src/services/__tests__/validated-service-token.test.ts index 8b5ec054ba..d2000e1ae7 100644 --- a/packages/lib/src/services/__tests__/validated-service-token.test.ts +++ b/packages/lib/src/services/__tests__/validated-service-token.test.ts @@ -29,9 +29,12 @@ vi.mock('../../permissions/permissions-cached', () => ({ getUserDrivePermissions: vi.fn(), })); -// Mock the service-auth module -vi.mock('../service-auth', () => ({ - createServiceToken: vi.fn().mockResolvedValue('mock-service-token'), +// Mock the session service +const mockCreateSession = vi.fn().mockResolvedValue('ps_svc_mock-session-token'); +vi.mock('../../auth/session-service', () => ({ + sessionService: { + createSession: (...args: unknown[]) => mockCreateSession(...args), + }, })); // Mock the logger @@ -47,7 +50,7 @@ vi.mock('../../logging/logger-config', () => ({ })); import { getUserAccessLevel, getUserDrivePermissions } from '../../permissions/permissions-cached'; -import { createServiceToken } from '../service-auth'; +import { sessionService } from '../../auth/session-service'; import { loggers } from '../../logging/logger-config'; describe('createValidatedServiceToken', () => { @@ -75,7 +78,7 @@ describe('createValidatedServiceToken', () => { // Assert - only read scope granted expect(result.grantedScopes).toEqual(['files:read']); - expect(createServiceToken).toHaveBeenCalledWith( + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ scopes: ['files:read'], }) @@ -346,7 +349,7 @@ describe('createValidatedServiceToken', () => { }); // Assert - expect(createServiceToken).toHaveBeenCalledWith( + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ driveId: 'drive-1', }) @@ -396,9 +399,10 @@ describe('createValidatedServiceToken', () => { // Assert expect(result.grantedScopes).toEqual(['files:read']); - expect(createServiceToken).toHaveBeenCalledWith( + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - resource: 'page-1', + resourceId: 'page-1', + resourceType: 'page', }) ); }); @@ -418,9 +422,10 @@ describe('createValidatedServiceToken', () => { // Assert expect(result.grantedScopes).toEqual(['files:write']); - expect(createServiceToken).toHaveBeenCalledWith( + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - resource: 'drive-1', + resourceId: 'drive-1', + resourceType: 'drive', driveId: 'drive-1', }) ); @@ -432,17 +437,18 @@ describe('createValidatedServiceToken', () => { // Assert expect(result.grantedScopes).toEqual(['avatars:write']); - expect(createServiceToken).toHaveBeenCalledWith( + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - resource: 'user-1', - subject: 'user-1', + resourceId: 'user-1', + resourceType: 'user', + userId: 'user-1', }) ); }); }); describe('token options', () => { - it('passes expiresIn to createServiceToken', async () => { + it('converts expiresIn to expiresInMs', async () => { // Arrange (getUserAccessLevel as ReturnType).mockResolvedValue({ canView: true, @@ -460,15 +466,15 @@ describe('createValidatedServiceToken', () => { expiresIn: '10m', }); - // Assert - expect(createServiceToken).toHaveBeenCalledWith( + // Assert - 10m = 600000ms + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - expiresIn: '10m', + expiresInMs: 600000, }) ); }); - it('passes additionalClaims to createServiceToken', async () => { + it('passes driveId when specified in options', async () => { // Arrange (getUserAccessLevel as ReturnType).mockResolvedValue({ canView: true, @@ -483,18 +489,133 @@ describe('createValidatedServiceToken', () => { resourceType: 'page', resourceId: 'page-1', requestedScopes: ['files:read'], - additionalClaims: { driveId: 'drive-1' }, + driveId: 'drive-1', }); // Assert - expect(createServiceToken).toHaveBeenCalledWith( + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - additionalClaims: { driveId: 'drive-1' }, + driveId: 'drive-1', }) ); }); }); + describe('durationToMs bounds validation', () => { + beforeEach(() => { + vi.clearAllMocks(); + (getUserAccessLevel as ReturnType).mockResolvedValue({ + canView: true, + canEdit: false, + canShare: false, + canDelete: false, + }); + }); + + it('uses default for zero duration', async () => { + await createValidatedServiceToken({ + userId: 'user-1', + resourceType: 'page', + resourceId: 'page-1', + requestedScopes: ['files:read'], + expiresIn: '0m', + }); + + // 0m is invalid (zero value), should use default 5m = 300000ms + expect(mockCreateSession).toHaveBeenCalledWith( + expect.objectContaining({ + expiresInMs: 300000, + }) + ); + expect(loggers.api.warn).toHaveBeenCalledWith( + 'Invalid duration value (must be positive), using default', + expect.any(Object) + ); + }); + + it('uses default for invalid format', async () => { + await createValidatedServiceToken({ + userId: 'user-1', + resourceType: 'page', + resourceId: 'page-1', + requestedScopes: ['files:read'], + expiresIn: 'invalid', + }); + + // Invalid format should use default 5m = 300000ms + expect(mockCreateSession).toHaveBeenCalledWith( + expect.objectContaining({ + expiresInMs: 300000, + }) + ); + expect(loggers.api.warn).toHaveBeenCalledWith( + 'Invalid duration format, using default', + expect.any(Object) + ); + }); + + it('caps excessive duration at 30 days', async () => { + await createValidatedServiceToken({ + userId: 'user-1', + resourceType: 'page', + resourceId: 'page-1', + requestedScopes: ['files:read'], + expiresIn: '365d', + }); + + // 365d exceeds max, should cap at 30d = 2592000000ms + expect(mockCreateSession).toHaveBeenCalledWith( + expect.objectContaining({ + expiresInMs: 2592000000, + }) + ); + expect(loggers.api.warn).toHaveBeenCalledWith( + 'Duration exceeds maximum, capping', + expect.any(Object) + ); + }); + + it('enforces minimum of 10 seconds', async () => { + await createValidatedServiceToken({ + userId: 'user-1', + resourceType: 'page', + resourceId: 'page-1', + requestedScopes: ['files:read'], + expiresIn: '1s', + }); + + // 1s is below minimum, should floor to 10s = 10000ms + expect(mockCreateSession).toHaveBeenCalledWith( + expect.objectContaining({ + expiresInMs: 10000, + }) + ); + expect(loggers.api.warn).toHaveBeenCalledWith( + 'Duration below minimum, using minimum', + expect.any(Object) + ); + }); + + it('accepts valid duration within bounds', async () => { + await createValidatedServiceToken({ + userId: 'user-1', + resourceType: 'page', + resourceId: 'page-1', + requestedScopes: ['files:read'], + expiresIn: '5m', + }); + + // 5m is valid, should use exactly 300000ms + expect(mockCreateSession).toHaveBeenCalledWith( + expect.objectContaining({ + expiresInMs: 300000, + }) + ); + // No warnings for valid duration + expect(loggers.api.warn).not.toHaveBeenCalled(); + }); + }); + describe('createUploadServiceToken', () => { beforeEach(() => { vi.clearAllMocks(); @@ -522,13 +643,14 @@ describe('createValidatedServiceToken', () => { // Assert expect(result.grantedScopes).toEqual(['files:write']); - expect(result.token).toBe('mock-service-token'); + expect(result.token).toBe('ps_svc_mock-session-token'); expect(mockFindFirst).toHaveBeenCalled(); expect(getUserAccessLevel).toHaveBeenCalledWith('user-1', 'parent-page-1'); expect(getUserDrivePermissions).not.toHaveBeenCalled(); - expect(createServiceToken).toHaveBeenCalledWith( + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - resource: 'new-page-1', + resourceId: 'new-page-1', + resourceType: 'page', driveId: 'drive-1', scopes: ['files:write'], }) @@ -554,12 +676,13 @@ describe('createValidatedServiceToken', () => { // Assert expect(result.grantedScopes).toEqual(['files:write']); - expect(result.token).toBe('mock-service-token'); + expect(result.token).toBe('ps_svc_mock-session-token'); expect(getUserDrivePermissions).toHaveBeenCalledWith('user-1', 'drive-1'); expect(getUserAccessLevel).not.toHaveBeenCalled(); - expect(createServiceToken).toHaveBeenCalledWith( + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - resource: 'new-page-1', + resourceId: 'new-page-1', + resourceType: 'page', driveId: 'drive-1', scopes: ['files:write'], }) @@ -739,10 +862,10 @@ describe('createValidatedServiceToken', () => { expiresIn: '15m', }); - // Assert - expect(createServiceToken).toHaveBeenCalledWith( + // Assert - 15m = 900000ms + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - expiresIn: '15m', + expiresInMs: 900000, }) ); }); @@ -764,10 +887,10 @@ describe('createValidatedServiceToken', () => { pageId: 'new-page-1', }); - // Assert - expect(createServiceToken).toHaveBeenCalledWith( + // Assert - 10m = 600000ms + expect(mockCreateSession).toHaveBeenCalledWith( expect.objectContaining({ - expiresIn: '10m', + expiresInMs: 600000, }) ); }); @@ -783,7 +906,7 @@ describe('createValidatedServiceToken', () => { }); // But token signing fails const signingError = new Error('Token signing failed'); - (createServiceToken as ReturnType).mockRejectedValue( + (mockCreateSession as ReturnType).mockRejectedValue( signingError ); diff --git a/packages/lib/src/services/service-auth.ts b/packages/lib/src/services/service-auth.ts deleted file mode 100644 index d21b748ea7..0000000000 --- a/packages/lib/src/services/service-auth.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { SignJWT, decodeProtectedHeader, jwtVerify, type JWTPayload } from 'jose'; -import { createId } from '@paralleldrive/cuid2'; -import { recordJTI, isJTIRevoked, tryGetSecurityRedisClient } from '../security/security-redis'; -import { loggers } from '../logging/logger-config'; - -export type ServiceScope = - | '*' - | 'files:read' - | 'files:write' - | 'files:link' - | 'files:delete' - | 'files:optimize' - | 'files:ingest' - | 'files:write:any' - | 'avatars:write' - | 'avatars:write:any' - | 'queue:read'; - -export interface ServiceTokenClaims extends JWTPayload { - sub: string; // user id or system id - service: string; // calling service name (e.g., "web", "worker") - resource?: string; // pageId or driveId depending on scope - driveId?: string; - tenantId?: string; - userId?: string; - driveIds?: string[]; - scopes: ServiceScope[]; - tokenType: 'service'; - jti: string; -} - -export interface ServiceTokenOptions { - service: string; - subject: string; - resource?: string; - driveId?: string; - scopes: ServiceScope[]; - expiresIn?: string; // jose duration string - additionalClaims?: Record; -} - -interface ServiceJWTConfig { - secret: Uint8Array; - issuer: string; - audience: string; -} - -const SERVICE_JWT_ALG = 'HS256'; -const DEFAULT_SERVICE_TOKEN_EXPIRY = '5m'; -const DEFAULT_SERVICE_TOKEN_EXPIRY_SECONDS = 300; - -// Maximum allowed duration for service tokens (30 days) -const MAX_DURATION_SECONDS = 30 * 24 * 60 * 60; - -/** - * Convert jose duration string to seconds. - * Supports: 's' (seconds), 'm' (minutes), 'h' (hours), 'd' (days) - * - * Validation rules: - * - Invalid format returns default (5m) - * - Zero or negative values return default - * - Values exceeding 30 days are capped at 30 days - */ -function durationToSeconds(duration: string): number { - const match = duration.match(/^(-?\d+)([smhd])$/); - if (!match) { - loggers.api.warn('Invalid duration format, using default', { duration, default: DEFAULT_SERVICE_TOKEN_EXPIRY_SECONDS }); - return DEFAULT_SERVICE_TOKEN_EXPIRY_SECONDS; - } - - const value = parseInt(match[1], 10); - const unit = match[2]; - - // Validate value is positive - if (value <= 0) { - loggers.api.warn('Invalid duration value (must be positive), using default', { duration, value, default: DEFAULT_SERVICE_TOKEN_EXPIRY_SECONDS }); - return DEFAULT_SERVICE_TOKEN_EXPIRY_SECONDS; - } - - let seconds: number; - switch (unit) { - case 's': - seconds = value; - break; - case 'm': - seconds = value * 60; - break; - case 'h': - seconds = value * 60 * 60; - break; - case 'd': - seconds = value * 24 * 60 * 60; - break; - default: - return DEFAULT_SERVICE_TOKEN_EXPIRY_SECONDS; - } - - // Cap at maximum duration - if (seconds > MAX_DURATION_SECONDS) { - loggers.api.warn('Duration exceeds maximum, capping at 30 days', { duration, requestedSeconds: seconds, cappedTo: MAX_DURATION_SECONDS }); - return MAX_DURATION_SECONDS; - } - - return seconds; -} - -function getServiceConfig(): ServiceJWTConfig { - const rawSecret = process.env.SERVICE_JWT_SECRET; - if (!rawSecret) { - throw new Error('SERVICE_JWT_SECRET environment variable is required'); - } - if (rawSecret.length < 32) { - throw new Error('SERVICE_JWT_SECRET must be at least 32 characters long'); - } - - return { - secret: new TextEncoder().encode(rawSecret), - issuer: process.env.JWT_ISSUER || 'pagespace', - audience: 'pagespace-processor', - }; -} - -const RESERVED_SUPPLEMENTAL_CLAIMS = new Set([ - 'sub', - 'service', - 'scopes', - 'tokenType', - 'jti', - 'resource', - 'driveId', - 'driveIds', - 'tenantId', - 'userId', -]); - -export async function createServiceToken(options: ServiceTokenOptions): Promise { - const config = getServiceConfig(); - - if (!options.scopes || options.scopes.length === 0) { - throw new Error('Service token requires at least one scope'); - } - - if (!options.subject || typeof options.subject !== 'string') { - throw new Error('Service token requires a string subject'); - } - - const additionalClaims = options.additionalClaims ?? {}; - for (const key of Object.keys(additionalClaims)) { - if (RESERVED_SUPPLEMENTAL_CLAIMS.has(key as keyof ServiceTokenClaims)) { - throw new Error(`additionalClaims cannot override reserved service token claim: ${key}`); - } - } - - const jti = createId(); - const expiresIn = options.expiresIn ?? DEFAULT_SERVICE_TOKEN_EXPIRY; - const expiresInSeconds = durationToSeconds(expiresIn); - - const payload: ServiceTokenClaims = { - sub: options.subject, - service: options.service, - resource: options.resource, - driveId: options.driveId, - scopes: Array.from(new Set(options.scopes)), - tokenType: 'service', - jti, - ...additionalClaims, - }; - - const token = await new SignJWT(payload) - .setProtectedHeader({ alg: SERVICE_JWT_ALG }) - .setIssuer(config.issuer) - .setAudience(config.audience) - .setIssuedAt() - .setExpirationTime(`${expiresInSeconds}s`) // Use validated seconds to ensure valid duration - .sign(config.secret); - - // Record JTI in Redis for tracking/revocation - // In production: fail-closed - Redis required for token issuance - // In dev/test: graceful degradation - token issued without JTI tracking - try { - const redis = await tryGetSecurityRedisClient(); - if (redis) { - await recordJTI(jti, options.subject, expiresInSeconds); - } else if (process.env.NODE_ENV === 'production') { - throw new Error('Security infrastructure unavailable - cannot issue service token'); - } - // In dev/test: token issued without JTI tracking (acceptable) - } catch (error) { - // In production, propagate the error - fail-closed - if (process.env.NODE_ENV === 'production') { - throw error; - } - // In dev/test, log but don't fail - graceful degradation - loggers.api.warn('Failed to record JTI in Redis, token created without tracking', { - error: error instanceof Error ? error.message : String(error), - subject: options.subject, - }); - } - - return token; -} - -export async function verifyServiceToken(token: string): Promise { - const config = getServiceConfig(); - - try { - const { payload, protectedHeader } = await jwtVerify(token, config.secret, { - algorithms: [SERVICE_JWT_ALG], - issuer: config.issuer, - audience: config.audience, - }); - - if (protectedHeader.alg !== SERVICE_JWT_ALG) { - throw new Error('Unexpected service token algorithm'); - } - - const claims = payload as ServiceTokenClaims; - - if (claims.tokenType !== 'service') { - throw new Error('Invalid service token type'); - } - - if (!claims.sub || typeof claims.sub !== 'string') { - throw new Error('Service token missing subject'); - } - - if (!claims.service || typeof claims.service !== 'string') { - throw new Error('Service token missing service identifier'); - } - - if (!Array.isArray(claims.scopes) || claims.scopes.length === 0) { - throw new Error('Service token missing scopes'); - } - - if (claims.resource && typeof claims.resource !== 'string') { - throw new Error('Service token resource must be a string'); - } - - // JTI validation (fail-closed in production) - const redis = await tryGetSecurityRedisClient(); - if (redis) { - // Validate JTI exists before checking revocation - // Skip for tokens without JTI (backward compatibility with pre-tracking tokens) - if (typeof claims.jti === 'string' && claims.jti.length > 0) { - const revoked = await isJTIRevoked(claims.jti); - if (revoked) { - throw new Error('Token revoked or invalid'); - } - } else { - // Token lacks JTI - log for observability but don't reject - // (maintains backward compatibility with tokens created before JTI tracking) - loggers.api.debug('Service token verified without JTI tracking', { - subject: claims.sub, - service: claims.service, - }); - } - } else if (process.env.NODE_ENV === 'production') { - throw new Error('Security infrastructure unavailable'); - } - - return claims; - } catch (error) { - if (error instanceof Error) { - error.message = `Service token verification failed: ${error.message}`; - } - throw error; - } -} - -export function decodeServiceTokenHeader(token: string) { - return decodeProtectedHeader(token); -} - -export function hasScope(claims: ServiceTokenClaims, scope: ServiceScope): boolean { - if (claims.scopes.includes('*' as ServiceScope)) { - return true; - } - if (claims.scopes.includes(scope)) { - return true; - } - const [namespace] = scope.split(':'); - const wildcard = `${namespace}:*` as ServiceScope; - return claims.scopes.includes(wildcard); -} - -export function assertScope(claims: ServiceTokenClaims, scope: ServiceScope): void { - if (!hasScope(claims, scope)) { - const available = claims.scopes.join(', '); - throw new Error(`Insufficient service scopes. Required: ${scope}. Token scopes: ${available}`); - } -} - -export interface ServiceAuthResult { - claims: ServiceTokenClaims; -} - -export async function authenticateServiceToken(token: string): Promise { - const claims = await verifyServiceToken(token); - return { claims }; -} - -export function requireResource(claims: ServiceTokenClaims, resource: string | undefined, kind: 'page' | 'drive'): string { - const resolved = resource ?? claims.resource; - if (!resolved) { - throw new Error(`Service token missing ${kind} resource`); - } - return resolved; -} diff --git a/packages/lib/src/services/validated-service-token.ts b/packages/lib/src/services/validated-service-token.ts index bac0ecac44..ca366bad31 100644 --- a/packages/lib/src/services/validated-service-token.ts +++ b/packages/lib/src/services/validated-service-token.ts @@ -12,9 +12,79 @@ import { getUserAccessLevel, getUserDrivePermissions, } from '../permissions/permissions-cached'; -import { createServiceToken, ServiceScope } from './service-auth'; +import { sessionService } from '../auth/session-service'; import { loggers } from '../logging/logger-config'; +/** + * Service scope types - defines allowed permission scopes + */ +export type ServiceScope = + | '*' + | 'files:read' + | 'files:write' + | 'files:link' + | 'files:delete' + | 'files:optimize' + | 'files:ingest' + | 'files:write:any' + | 'avatars:write' + | 'avatars:write:any' + | 'queue:read'; + +// Duration bounds for service tokens +const DEFAULT_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes default +const MIN_EXPIRY_MS = 10 * 1000; // 10 seconds minimum +const MAX_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000; // 30 days maximum + +/** + * Convert duration string to milliseconds with bounds validation. + * + * Validation rules: + * - Invalid format returns default (5m) + * - Zero or negative values return default + * - Values below 10s are floored to 10s + * - Values exceeding 30 days are capped at 30 days + */ +function durationToMs(duration: string): number { + const match = duration.match(/^(\d+)([smhd])$/); + if (!match) { + loggers.api.warn('Invalid duration format, using default', { duration, default: DEFAULT_EXPIRY_MS }); + return DEFAULT_EXPIRY_MS; + } + + const value = parseInt(match[1], 10); + const unit = match[2]; + + // Reject zero/negative values + if (value <= 0) { + loggers.api.warn('Invalid duration value (must be positive), using default', { duration, value, default: DEFAULT_EXPIRY_MS }); + return DEFAULT_EXPIRY_MS; + } + + let ms: number; + switch (unit) { + case 's': ms = value * 1000; break; + case 'm': ms = value * 60 * 1000; break; + case 'h': ms = value * 60 * 60 * 1000; break; + case 'd': ms = value * 24 * 60 * 60 * 1000; break; + default: return DEFAULT_EXPIRY_MS; + } + + // Enforce minimum (prevent near-instant expiry) + if (ms < MIN_EXPIRY_MS) { + loggers.api.warn('Duration below minimum, using minimum', { duration, ms, min: MIN_EXPIRY_MS }); + return MIN_EXPIRY_MS; + } + + // Cap at maximum (prevent near-permanent tokens) + if (ms > MAX_EXPIRY_MS) { + loggers.api.warn('Duration exceeds maximum, capping', { duration, ms, max: MAX_EXPIRY_MS }); + return MAX_EXPIRY_MS; + } + + return ms; +} + /** * Error thrown when permission is denied for token creation. * Callers should return 403 for this error type only. @@ -73,8 +143,6 @@ export interface ValidatedTokenOptions { driveId?: string; /** Token expiration (jose duration string, default '5m') */ expiresIn?: string; - /** Additional context for the token */ - additionalClaims?: Record; } /** @@ -108,7 +176,6 @@ export async function createValidatedServiceToken( resourceId, requestedScopes, expiresIn = '5m', - additionalClaims, } = options; // Get user's actual permissions for this resource @@ -148,14 +215,15 @@ export async function createValidatedServiceToken( filtered: requestedScopes.length !== grantedScopes.length, }); - const token = await createServiceToken({ - service: 'web', - subject: userId, - resource: resourceId, + const token = await sessionService.createSession({ + userId, + type: 'service', + scopes: grantedScopes as string[], + resourceType, + resourceId, driveId: options.driveId, - scopes: grantedScopes, - expiresIn, - additionalClaims, + expiresInMs: durationToMs(expiresIn), + createdByService: 'web', }); return { @@ -409,14 +477,15 @@ export async function createUploadServiceToken( scopes: UPLOAD_SCOPES, }); - // Note: createServiceToken errors (signing failures, etc.) bubble up as-is - const token = await createServiceToken({ - service: 'web', - subject: userId, - resource: pageId, - driveId: driveId, - scopes: UPLOAD_SCOPES, - expiresIn, + const token = await sessionService.createSession({ + userId, + type: 'service', + scopes: UPLOAD_SCOPES as string[], + resourceType: 'page', + resourceId: pageId, + driveId, + expiresInMs: durationToMs(expiresIn), + createdByService: 'web', }); return {