diff --git a/apps/gateway/src/federation/__tests__/enrollment.service.spec.ts b/apps/gateway/src/federation/__tests__/enrollment.service.spec.ts new file mode 100644 index 0000000..8bf474e --- /dev/null +++ b/apps/gateway/src/federation/__tests__/enrollment.service.spec.ts @@ -0,0 +1,373 @@ +/** + * Unit tests for EnrollmentService — federation enrollment token flow (FED-M2-07). + * + * Coverage: + * createToken: + * - inserts token row with correct grantId, peerId, and future expiresAt + * - returns { token, expiresAt } with a 64-char hex token + * - clamps ttlSeconds to 900 + * + * redeem — error paths: + * - NotFoundException when token row not found + * - GoneException when token already used (usedAt set) + * - GoneException when token expired (expiresAt < now) + * - GoneException when grant status is not pending + * + * redeem — success path: + * - atomically claims token BEFORE cert issuance (claim → issueCert → tx) + * - calls CaService.issueCert with correct args + * - activates grant + updates peer + writes audit log inside a transaction + * - returns { certPem, certChainPem } + * + * redeem — replay protection: + * - GoneException when claim UPDATE returns empty array (concurrent request won) + */ + +import 'reflect-metadata'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { GoneException, NotFoundException } from '@nestjs/common'; +import type { Db } from '@mosaicstack/db'; +import { EnrollmentService } from '../enrollment.service.js'; + +// --------------------------------------------------------------------------- +// Test constants +// --------------------------------------------------------------------------- + +const GRANT_ID = 'g1111111-1111-1111-1111-111111111111'; +const PEER_ID = 'p2222222-2222-2222-2222-222222222222'; +const USER_ID = 'u3333333-3333-3333-3333-333333333333'; +const TOKEN = 'a'.repeat(64); // 64-char hex + +const MOCK_CERT_PEM = '-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----\n'; +const MOCK_CHAIN_PEM = MOCK_CERT_PEM + MOCK_CERT_PEM; +const MOCK_SERIAL = 'ABCD1234'; + +// --------------------------------------------------------------------------- +// Factory helpers +// --------------------------------------------------------------------------- + +function makeTokenRow(overrides: Partial> = {}) { + return { + token: TOKEN, + grantId: GRANT_ID, + peerId: PEER_ID, + expiresAt: new Date(Date.now() + 60_000), // 1 min from now + usedAt: null, + createdAt: new Date(), + ...overrides, + }; +} + +function makeGrant(overrides: Partial> = {}) { + return { + id: GRANT_ID, + peerId: PEER_ID, + subjectUserId: USER_ID, + scope: { resources: ['tasks'], excluded_resources: [], max_rows_per_query: 100 }, + status: 'pending', + expiresAt: null, + createdAt: new Date(), + revokedAt: null, + revokedReason: null, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Mock DB builder +// --------------------------------------------------------------------------- + +function makeDb({ + tokenRows = [makeTokenRow()], + // claimedRows is returned by the .returning() on the token-claim UPDATE. + // Empty array = concurrent request won the race (GoneException). + claimedRows = [{ token: TOKEN }], +}: { + tokenRows?: unknown[]; + claimedRows?: unknown[]; +} = {}) { + // insert().values() — for createToken (outer db, not tx) + const insertValues = vi.fn().mockResolvedValue(undefined); + const insertMock = vi.fn().mockReturnValue({ values: insertValues }); + + // select().from().where().limit() — for fetching the token row + const limitSelect = vi.fn().mockResolvedValue(tokenRows); + const whereSelect = vi.fn().mockReturnValue({ limit: limitSelect }); + const fromSelect = vi.fn().mockReturnValue({ where: whereSelect }); + const selectMock = vi.fn().mockReturnValue({ from: fromSelect }); + + // update().set().where().returning() — for the atomic token claim (outer db) + const returningMock = vi.fn().mockResolvedValue(claimedRows); + const whereClaimUpdate = vi.fn().mockReturnValue({ returning: returningMock }); + const setClaimMock = vi.fn().mockReturnValue({ where: whereClaimUpdate }); + const claimUpdateMock = vi.fn().mockReturnValue({ set: setClaimMock }); + + // transaction(cb) — cb receives txMock; txMock has update + insert + const txInsertValues = vi.fn().mockResolvedValue(undefined); + const txInsertMock = vi.fn().mockReturnValue({ values: txInsertValues }); + const txWhereUpdate = vi.fn().mockResolvedValue(undefined); + const txSetMock = vi.fn().mockReturnValue({ where: txWhereUpdate }); + const txUpdateMock = vi.fn().mockReturnValue({ set: txSetMock }); + const txMock = { update: txUpdateMock, insert: txInsertMock }; + const transactionMock = vi + .fn() + .mockImplementation(async (cb: (tx: typeof txMock) => Promise) => cb(txMock)); + + return { + insert: insertMock, + select: selectMock, + update: claimUpdateMock, + transaction: transactionMock, + _mocks: { + insertValues, + insertMock, + limitSelect, + whereSelect, + fromSelect, + selectMock, + returningMock, + whereClaimUpdate, + setClaimMock, + claimUpdateMock, + txInsertValues, + txInsertMock, + txWhereUpdate, + txSetMock, + txUpdateMock, + txMock, + transactionMock, + }, + }; +} + +// --------------------------------------------------------------------------- +// Mock CaService +// --------------------------------------------------------------------------- + +function makeCaService() { + return { + issueCert: vi.fn().mockResolvedValue({ + certPem: MOCK_CERT_PEM, + certChainPem: MOCK_CHAIN_PEM, + serialNumber: MOCK_SERIAL, + }), + }; +} + +// --------------------------------------------------------------------------- +// Mock GrantsService +// --------------------------------------------------------------------------- + +function makeGrantsService(grantOverrides: Partial> = {}) { + return { + getGrant: vi.fn().mockResolvedValue(makeGrant(grantOverrides)), + activateGrant: vi.fn().mockResolvedValue(makeGrant({ status: 'active' })), + }; +} + +// --------------------------------------------------------------------------- +// Helper: build service under test +// --------------------------------------------------------------------------- + +function buildService({ + db = makeDb(), + caService = makeCaService(), + grantsService = makeGrantsService(), +}: { + db?: ReturnType; + caService?: ReturnType; + grantsService?: ReturnType; +} = {}) { + return new EnrollmentService(db as unknown as Db, caService as never, grantsService as never); +} + +// --------------------------------------------------------------------------- +// Tests: createToken +// --------------------------------------------------------------------------- + +describe('EnrollmentService.createToken', () => { + it('inserts a token row and returns { token, expiresAt }', async () => { + const db = makeDb(); + const service = buildService({ db }); + + const result = await service.createToken({ + grantId: GRANT_ID, + peerId: PEER_ID, + ttlSeconds: 900, + }); + + expect(result.token).toHaveLength(64); // 32 bytes hex + expect(result.expiresAt).toBeDefined(); + expect(new Date(result.expiresAt).getTime()).toBeGreaterThan(Date.now()); + expect(db._mocks.insertValues).toHaveBeenCalledWith( + expect.objectContaining({ grantId: GRANT_ID, peerId: PEER_ID }), + ); + }); + + it('clamps ttlSeconds to 900', async () => { + const db = makeDb(); + const service = buildService({ db }); + + const before = Date.now(); + const result = await service.createToken({ + grantId: GRANT_ID, + peerId: PEER_ID, + ttlSeconds: 9999, + }); + const after = Date.now(); + + const expiresMs = new Date(result.expiresAt).getTime(); + // Should be at most 900s from now + expect(expiresMs - before).toBeLessThanOrEqual(900_000 + 100); + expect(expiresMs - after).toBeGreaterThanOrEqual(0); + }); +}); + +// --------------------------------------------------------------------------- +// Tests: redeem — error paths +// --------------------------------------------------------------------------- + +describe('EnrollmentService.redeem — error paths', () => { + it('throws NotFoundException when token row not found', async () => { + const db = makeDb({ tokenRows: [] }); + const service = buildService({ db }); + + await expect(service.redeem(TOKEN, '---CSR---')).rejects.toBeInstanceOf(NotFoundException); + }); + + it('throws GoneException when usedAt is set (already redeemed)', async () => { + const db = makeDb({ tokenRows: [makeTokenRow({ usedAt: new Date(Date.now() - 1000) })] }); + const service = buildService({ db }); + + await expect(service.redeem(TOKEN, '---CSR---')).rejects.toBeInstanceOf(GoneException); + }); + + it('throws GoneException when token has expired', async () => { + const db = makeDb({ tokenRows: [makeTokenRow({ expiresAt: new Date(Date.now() - 1000) })] }); + const service = buildService({ db }); + + await expect(service.redeem(TOKEN, '---CSR---')).rejects.toBeInstanceOf(GoneException); + }); + + it('throws GoneException when grant status is not pending', async () => { + const db = makeDb(); + const grantsService = makeGrantsService({ status: 'active' }); + const service = buildService({ db, grantsService }); + + await expect(service.redeem(TOKEN, '---CSR---')).rejects.toBeInstanceOf(GoneException); + }); + + it('throws GoneException when token claim UPDATE returns empty array (concurrent replay)', async () => { + const db = makeDb({ claimedRows: [] }); + const caService = makeCaService(); + const grantsService = makeGrantsService(); + const service = buildService({ db, caService, grantsService }); + + await expect(service.redeem(TOKEN, '---CSR---')).rejects.toBeInstanceOf(GoneException); + }); + + it('does NOT call issueCert when token claim fails (no double minting)', async () => { + const db = makeDb({ claimedRows: [] }); + const caService = makeCaService(); + const service = buildService({ db, caService }); + + await expect(service.redeem(TOKEN, '---CSR---')).rejects.toBeInstanceOf(GoneException); + expect(caService.issueCert).not.toHaveBeenCalled(); + }); +}); + +// --------------------------------------------------------------------------- +// Tests: redeem — success path +// --------------------------------------------------------------------------- + +describe('EnrollmentService.redeem — success path', () => { + let db: ReturnType; + let caService: ReturnType; + let grantsService: ReturnType; + let service: EnrollmentService; + + beforeEach(() => { + db = makeDb(); + caService = makeCaService(); + grantsService = makeGrantsService(); + service = buildService({ db, caService, grantsService }); + }); + + it('claims token BEFORE calling issueCert (prevents double minting)', async () => { + const callOrder: string[] = []; + db._mocks.returningMock.mockImplementation(async () => { + callOrder.push('claim'); + return [{ token: TOKEN }]; + }); + caService.issueCert.mockImplementation(async () => { + callOrder.push('issueCert'); + return { certPem: MOCK_CERT_PEM, certChainPem: MOCK_CHAIN_PEM, serialNumber: MOCK_SERIAL }; + }); + + await service.redeem(TOKEN, MOCK_CERT_PEM); + + expect(callOrder).toEqual(['claim', 'issueCert']); + }); + + it('calls CaService.issueCert with grantId, subjectUserId, csrPem, ttlSeconds=300', async () => { + await service.redeem(TOKEN, MOCK_CERT_PEM); + + expect(caService.issueCert).toHaveBeenCalledWith( + expect.objectContaining({ + grantId: GRANT_ID, + subjectUserId: USER_ID, + csrPem: MOCK_CERT_PEM, + ttlSeconds: 300, + }), + ); + }); + + it('runs activate grant + peer update + audit inside a transaction', async () => { + await service.redeem(TOKEN, MOCK_CERT_PEM); + + expect(db._mocks.transactionMock).toHaveBeenCalledOnce(); + // tx.update called twice: activate grant + update peer + expect(db._mocks.txUpdateMock).toHaveBeenCalledTimes(2); + // tx.insert called once: audit log + expect(db._mocks.txInsertMock).toHaveBeenCalledOnce(); + }); + + it('activates grant (sets status=active) inside the transaction', async () => { + await service.redeem(TOKEN, MOCK_CERT_PEM); + + expect(db._mocks.txSetMock).toHaveBeenCalledWith(expect.objectContaining({ status: 'active' })); + }); + + it('updates the federationPeers row with certPem, certSerial, state=active inside the transaction', async () => { + await service.redeem(TOKEN, MOCK_CERT_PEM); + + expect(db._mocks.txSetMock).toHaveBeenCalledWith( + expect.objectContaining({ + certPem: MOCK_CERT_PEM, + certSerial: MOCK_SERIAL, + state: 'active', + }), + ); + }); + + it('inserts an audit log row inside the transaction', async () => { + await service.redeem(TOKEN, MOCK_CERT_PEM); + + expect(db._mocks.txInsertValues).toHaveBeenCalledWith( + expect.objectContaining({ + peerId: PEER_ID, + grantId: GRANT_ID, + verb: 'enrollment', + }), + ); + }); + + it('returns { certPem, certChainPem } from CaService', async () => { + const result = await service.redeem(TOKEN, MOCK_CERT_PEM); + + expect(result).toEqual({ + certPem: MOCK_CERT_PEM, + certChainPem: MOCK_CHAIN_PEM, + }); + }); +}); diff --git a/apps/gateway/src/federation/enrollment.controller.ts b/apps/gateway/src/federation/enrollment.controller.ts new file mode 100644 index 0000000..9bc5b96 --- /dev/null +++ b/apps/gateway/src/federation/enrollment.controller.ts @@ -0,0 +1,54 @@ +/** + * EnrollmentController — federation enrollment HTTP layer (FED-M2-07). + * + * Routes: + * POST /api/federation/enrollment/tokens — admin creates a single-use token + * POST /api/federation/enrollment/:token — unauthenticated; token IS the auth + */ + +import { + Body, + Controller, + HttpCode, + HttpStatus, + Inject, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { AdminGuard } from '../admin/admin.guard.js'; +import { EnrollmentService } from './enrollment.service.js'; +import { CreateEnrollmentTokenDto, RedeemEnrollmentTokenDto } from './enrollment.dto.js'; + +@Controller('api/federation/enrollment') +export class EnrollmentController { + constructor(@Inject(EnrollmentService) private readonly enrollmentService: EnrollmentService) {} + + /** + * Admin-only: generate a single-use enrollment token for a pending grant. + * The token should be distributed out-of-band to the remote peer operator. + * + * POST /api/federation/enrollment/tokens + */ + @Post('tokens') + @UseGuards(AdminGuard) + @HttpCode(HttpStatus.CREATED) + async createToken(@Body() dto: CreateEnrollmentTokenDto) { + return this.enrollmentService.createToken(dto); + } + + /** + * Unauthenticated: remote peer redeems a token by submitting its CSR. + * The token itself is the credential — no session or bearer token required. + * + * POST /api/federation/enrollment/:token + * + * Returns the signed leaf cert and full chain PEM on success. + * Returns 410 Gone if the token was already used or has expired. + */ + @Post(':token') + @HttpCode(HttpStatus.OK) + async redeem(@Param('token') token: string, @Body() dto: RedeemEnrollmentTokenDto) { + return this.enrollmentService.redeem(token, dto.csrPem); + } +} diff --git a/apps/gateway/src/federation/enrollment.dto.ts b/apps/gateway/src/federation/enrollment.dto.ts new file mode 100644 index 0000000..d6356e5 --- /dev/null +++ b/apps/gateway/src/federation/enrollment.dto.ts @@ -0,0 +1,35 @@ +/** + * DTOs for the federation enrollment flow (FED-M2-07). + * + * CreateEnrollmentTokenDto — admin generates a single-use enrollment token + * RedeemEnrollmentTokenDto — remote peer submits CSR to redeem the token + */ + +import { IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator'; + +export class CreateEnrollmentTokenDto { + /** UUID of the federation grant this token will activate on redemption. */ + @IsUUID() + grantId!: string; + + /** UUID of the peer record that will receive the issued cert on redemption. */ + @IsUUID() + peerId!: string; + + /** + * Token lifetime in seconds. Default 900 (15 min). Min 60. Max 900. + * After this time the token is rejected even if unused. + */ + @IsOptional() + @IsInt() + @Min(60) + @Max(900) + ttlSeconds: number = 900; +} + +export class RedeemEnrollmentTokenDto { + /** PEM-encoded PKCS#10 Certificate Signing Request from the remote peer. */ + @IsString() + @IsNotEmpty() + csrPem!: string; +} diff --git a/apps/gateway/src/federation/enrollment.service.ts b/apps/gateway/src/federation/enrollment.service.ts new file mode 100644 index 0000000..3559a16 --- /dev/null +++ b/apps/gateway/src/federation/enrollment.service.ts @@ -0,0 +1,230 @@ +/** + * EnrollmentService — single-use enrollment token lifecycle (FED-M2-07). + * + * Responsibilities: + * 1. Generate time-limited single-use enrollment tokens (admin action). + * 2. Redeem a token: validate → atomically claim token → issue cert via + * CaService → transactionally activate grant + update peer + write audit. + * + * Replay protection: the token is claimed (UPDATE WHERE used_at IS NULL) BEFORE + * cert issuance. This prevents double cert minting on concurrent requests. + * If cert issuance fails after claim, the token is consumed and the grant + * stays pending — admin must create a new grant. + */ + +import { + BadRequestException, + GoneException, + Inject, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import * as crypto from 'node:crypto'; +// X509Certificate is available as a named export in Node.js ≥ 15.6 +const { X509Certificate } = crypto; +import { + type Db, + and, + eq, + isNull, + sql, + federationEnrollmentTokens, + federationGrants, + federationPeers, + federationAuditLog, +} from '@mosaicstack/db'; +import { DB } from '../database/database.module.js'; +import { CaService } from './ca.service.js'; +import { GrantsService } from './grants.service.js'; +import { FederationScopeError } from './scope-schema.js'; +import type { CreateEnrollmentTokenDto } from './enrollment.dto.js'; + +export interface EnrollmentTokenResult { + token: string; + expiresAt: string; +} + +export interface RedeemResult { + certPem: string; + certChainPem: string; +} + +@Injectable() +export class EnrollmentService { + private readonly logger = new Logger(EnrollmentService.name); + + constructor( + @Inject(DB) private readonly db: Db, + private readonly caService: CaService, + private readonly grantsService: GrantsService, + ) {} + + /** + * Generate a single-use enrollment token for an admin to distribute + * out-of-band to the remote peer operator. + */ + async createToken(dto: CreateEnrollmentTokenDto): Promise { + const ttl = Math.min(dto.ttlSeconds, 900); + const token = crypto.randomBytes(32).toString('hex'); + const expiresAt = new Date(Date.now() + ttl * 1000); + + await this.db.insert(federationEnrollmentTokens).values({ + token, + grantId: dto.grantId, + peerId: dto.peerId, + expiresAt, + }); + + this.logger.log( + `Enrollment token created — grantId=${dto.grantId} peerId=${dto.peerId} expiresAt=${expiresAt.toISOString()}`, + ); + + return { token, expiresAt: expiresAt.toISOString() }; + } + + /** + * Redeem an enrollment token. + * + * Full flow: + * 1. Fetch token row — NotFoundException if not found + * 2. usedAt set → GoneException (already used) + * 3. expiresAt < now → GoneException (expired) + * 4. Load grant — verify status is 'pending' + * 5. Atomically claim token (UPDATE WHERE used_at IS NULL RETURNING token) + * — if no rows returned, concurrent request won → GoneException + * 6. Issue cert via CaService (network call, outside transaction) + * — if this fails, token is consumed; grant stays pending; admin must recreate + * 7. Transaction: activate grant + update peer record + write audit log + * 8. Return { certPem, certChainPem } + */ + async redeem(token: string, csrPem: string): Promise { + // 1. Fetch token row + const [row] = await this.db + .select() + .from(federationEnrollmentTokens) + .where(eq(federationEnrollmentTokens.token, token)) + .limit(1); + + if (!row) { + throw new NotFoundException('Enrollment token not found'); + } + + // 2. Already used? + if (row.usedAt !== null) { + throw new GoneException('Enrollment token has already been used'); + } + + // 3. Expired? + if (row.expiresAt < new Date()) { + throw new GoneException('Enrollment token has expired'); + } + + // 4. Load grant and verify it is still pending + let grant; + try { + grant = await this.grantsService.getGrant(row.grantId); + } catch (err) { + if (err instanceof FederationScopeError) { + throw new BadRequestException(err.message); + } + throw err; + } + + if (grant.status !== 'pending') { + throw new GoneException( + `Grant ${row.grantId} is no longer pending (status: ${grant.status})`, + ); + } + + // 5. Atomically claim the token BEFORE cert issuance to prevent double-minting. + // WHERE used_at IS NULL ensures only one concurrent request wins. + // Using .returning() works on both node-postgres and PGlite without rowCount inspection. + const claimed = await this.db + .update(federationEnrollmentTokens) + .set({ usedAt: sql`NOW()` }) + .where( + and(eq(federationEnrollmentTokens.token, token), isNull(federationEnrollmentTokens.usedAt)), + ) + .returning({ token: federationEnrollmentTokens.token }); + + if (claimed.length === 0) { + throw new GoneException('Enrollment token has already been used (concurrent request)'); + } + + // 6. Issue certificate via CaService (network call — outside any transaction). + // If this throws, the token is already consumed. The grant stays pending. + // Admin must revoke the grant and create a new one. + let issued; + try { + issued = await this.caService.issueCert({ + csrPem, + grantId: row.grantId, + subjectUserId: grant.subjectUserId, + ttlSeconds: 300, + }); + } catch (err) { + this.logger.error( + `issueCert failed after token ${token} was claimed — grant ${row.grantId} is stranded pending`, + err instanceof Error ? err.stack : String(err), + ); + if (err instanceof FederationScopeError) { + throw new BadRequestException((err as Error).message); + } + throw err; + } + + // 7. Atomically activate grant, update peer record, and write audit log. + const certNotAfter = this.extractCertNotAfter(issued.certPem); + await this.db.transaction(async (tx) => { + await tx + .update(federationGrants) + .set({ status: 'active' }) + .where(eq(federationGrants.id, row.grantId)); + + await tx + .update(federationPeers) + .set({ + certPem: issued.certPem, + certSerial: issued.serialNumber, + certNotAfter, + state: 'active', + }) + .where(eq(federationPeers.id, row.peerId)); + + await tx.insert(federationAuditLog).values({ + requestId: crypto.randomUUID(), + peerId: row.peerId, + grantId: row.grantId, + verb: 'enrollment', + resource: 'federation_grant', + statusCode: 200, + outcome: 'allowed', + }); + }); + + this.logger.log( + `Enrollment complete — peerId=${row.peerId} grantId=${row.grantId} serial=${issued.serialNumber}`, + ); + + // 8. Return cert material + return { + certPem: issued.certPem, + certChainPem: issued.certChainPem, + }; + } + + /** + * Extract the notAfter date from a PEM certificate. + * Falls back to 90 days from now if parsing fails. + */ + private extractCertNotAfter(certPem: string): Date { + try { + const cert = new X509Certificate(certPem); + return new Date(cert.validTo); + } catch { + // Fallback: 90 days from now + return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); + } + } +} diff --git a/apps/gateway/src/federation/federation.module.ts b/apps/gateway/src/federation/federation.module.ts index 7108713..82b3708 100644 --- a/apps/gateway/src/federation/federation.module.ts +++ b/apps/gateway/src/federation/federation.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; +import { AdminGuard } from '../admin/admin.guard.js'; import { CaService } from './ca.service.js'; +import { EnrollmentController } from './enrollment.controller.js'; +import { EnrollmentService } from './enrollment.service.js'; import { GrantsService } from './grants.service.js'; @Module({ - providers: [CaService, GrantsService], - exports: [CaService, GrantsService], + controllers: [EnrollmentController], + providers: [AdminGuard, CaService, EnrollmentService, GrantsService], + exports: [CaService, EnrollmentService, GrantsService], }) export class FederationModule {} diff --git a/packages/db/drizzle/0010_federation_enrollment_tokens.sql b/packages/db/drizzle/0010_federation_enrollment_tokens.sql new file mode 100644 index 0000000..725d66f --- /dev/null +++ b/packages/db/drizzle/0010_federation_enrollment_tokens.sql @@ -0,0 +1,11 @@ +CREATE TABLE "federation_enrollment_tokens" ( + "token" text PRIMARY KEY NOT NULL, + "grant_id" uuid NOT NULL, + "peer_id" uuid NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "used_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "federation_enrollment_tokens" ADD CONSTRAINT "federation_enrollment_tokens_grant_id_federation_grants_id_fk" FOREIGN KEY ("grant_id") REFERENCES "public"."federation_grants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "federation_enrollment_tokens" ADD CONSTRAINT "federation_enrollment_tokens_peer_id_federation_peers_id_fk" FOREIGN KEY ("peer_id") REFERENCES "public"."federation_peers"("id") ON DELETE cascade ON UPDATE no action; diff --git a/packages/db/drizzle/meta/0010_snapshot.json b/packages/db/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..db01fc2 --- /dev/null +++ b/packages/db/drizzle/meta/0010_snapshot.json @@ -0,0 +1,3453 @@ +{ + "id": "e8e804d3-3556-469f-bb48-e079cdf2fd84", + "prevId": "f2a3b4c5-d6e7-8901-abcd-ef2345678901", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_tokens": { + "name": "admin_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_tokens_user_id_idx": { + "name": "admin_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_tokens_hash_idx": { + "name": "admin_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_tokens_user_id_users_id_fk": { + "name": "admin_tokens_user_id_users_id_fk", + "tableFrom": "admin_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_logs": { + "name": "agent_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'hot'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summarized_at": { + "name": "summarized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_logs_session_tier_idx": { + "name": "agent_logs_session_tier_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_user_id_idx": { + "name": "agent_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_tier_created_at_idx": { + "name": "agent_logs_tier_created_at_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_logs_user_id_users_id_fk": { + "name": "agent_logs_user_id_users_id_fk", + "tableFrom": "agent_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_project_id_idx": { + "name": "agents_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_owner_id_idx": { + "name": "agents_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_is_system_idx": { + "name": "agents_is_system_idx", + "columns": [ + { + "expression": "is_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_project_id_projects_id_fk": { + "name": "agents_project_id_projects_id_fk", + "tableFrom": "agents", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_owner_id_users_id_fk": { + "name": "agents_owner_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appreciations": { + "name": "appreciations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conversations_user_archived_idx": { + "name": "conversations_user_archived_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_project_id_idx": { + "name": "conversations_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_agent_id_idx": { + "name": "conversations_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_user_id_users_id_fk": { + "name": "conversations_user_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_project_id_projects_id_fk": { + "name": "conversations_project_id_projects_id_fk", + "tableFrom": "conversations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_agent_id_agents_id_fk": { + "name": "conversations_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_type_idx": { + "name": "events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_date_idx": { + "name": "events_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_audit_log": { + "name": "federation_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "verb": { + "name": "verb", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "denied_reason": { + "name": "denied_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "query_hash": { + "name": "query_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bytes_out": { + "name": "bytes_out", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_audit_log_peer_created_at_idx": { + "name": "federation_audit_log_peer_created_at_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_subject_created_at_idx": { + "name": "federation_audit_log_subject_created_at_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_created_at_idx": { + "name": "federation_audit_log_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_audit_log_peer_id_federation_peers_id_fk": { + "name": "federation_audit_log_peer_id_federation_peers_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_subject_user_id_users_id_fk": { + "name": "federation_audit_log_subject_user_id_users_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_grant_id_federation_grants_id_fk": { + "name": "federation_audit_log_grant_id_federation_grants_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_enrollment_tokens": { + "name": "federation_enrollment_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_enrollment_tokens_grant_id_federation_grants_id_fk": { + "name": "federation_enrollment_tokens_grant_id_federation_grants_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_enrollment_tokens_peer_id_federation_peers_id_fk": { + "name": "federation_enrollment_tokens_peer_id_federation_peers_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_grants": { + "name": "federation_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "grant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_grants_subject_status_idx": { + "name": "federation_grants_subject_status_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_grants_peer_status_idx": { + "name": "federation_grants_peer_status_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_grants_subject_user_id_users_id_fk": { + "name": "federation_grants_subject_user_id_users_id_fk", + "tableFrom": "federation_grants", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_grants_peer_id_federation_peers_id_fk": { + "name": "federation_grants_peer_id_federation_peers_id_fk", + "tableFrom": "federation_grants", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_peers": { + "name": "federation_peers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "common_name": { + "name": "common_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_serial": { + "name": "cert_serial", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_not_after": { + "name": "cert_not_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "client_key_pem": { + "name": "client_key_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "peer_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_peers_cert_serial_idx": { + "name": "federation_peers_cert_serial_idx", + "columns": [ + { + "expression": "cert_serial", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_peers_state_idx": { + "name": "federation_peers_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_peers_common_name_unique": { + "name": "federation_peers_common_name_unique", + "nullsNotDistinct": false, + "columns": [ + "common_name" + ] + }, + "federation_peers_cert_serial_unique": { + "name": "federation_peers_cert_serial_unique", + "nullsNotDistinct": false, + "columns": [ + "cert_serial" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insights": { + "name": "insights", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "relevance_score": { + "name": "relevance_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decayed_at": { + "name": "decayed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "insights_user_id_idx": { + "name": "insights_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_category_idx": { + "name": "insights_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_relevance_idx": { + "name": "insights_relevance_idx", + "columns": [ + { + "expression": "relevance_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insights_user_id_users_id_fk": { + "name": "insights_user_id_users_id_fk", + "tableFrom": "insights", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_tasks": { + "name": "mission_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr": { + "name": "pr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mission_tasks_mission_id_idx": { + "name": "mission_tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_task_id_idx": { + "name": "mission_tasks_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_user_id_idx": { + "name": "mission_tasks_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_status_idx": { + "name": "mission_tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_tasks_mission_id_missions_id_fk": { + "name": "mission_tasks_mission_id_missions_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mission_tasks_task_id_tasks_id_fk": { + "name": "mission_tasks_task_id_tasks_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mission_tasks_user_id_users_id_fk": { + "name": "mission_tasks_user_id_users_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions": { + "name": "missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "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": "'planning'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "milestones": { + "name": "milestones", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "missions_project_id_idx": { + "name": "missions_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_user_id_idx": { + "name": "missions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_project_id_projects_id_fk": { + "name": "missions_project_id_projects_id_fk", + "tableFrom": "missions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_user_id_users_id_fk": { + "name": "missions_user_id_users_id_fk", + "tableFrom": "missions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preferences": { + "name": "preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mutable": { + "name": "mutable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "preferences_user_id_idx": { + "name": "preferences_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "preferences_user_key_idx": { + "name": "preferences_user_key_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "preferences_user_id_users_id_fk": { + "name": "preferences_user_id_users_id_fk", + "tableFrom": "preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "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": "'active'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_credentials": { + "name": "provider_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_credentials_user_provider_idx": { + "name": "provider_credentials_user_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_credentials_user_id_idx": { + "name": "provider_credentials_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_credentials_user_id_users_id_fk": { + "name": "provider_credentials_user_id_users_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routing_rules": { + "name": "routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routing_rules_scope_priority_idx": { + "name": "routing_rules_scope_priority_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_user_id_idx": { + "name": "routing_rules_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_enabled_idx": { + "name": "routing_rules_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routing_rules_user_id_users_id_fk": { + "name": "routing_rules_user_id_users_id_fk", + "tableFrom": "routing_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "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": {} + } + }, + "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_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_by": { + "name": "installed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_enabled_idx": { + "name": "skills_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_installed_by_users_id_fk": { + "name": "skills_installed_by_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "installed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_name_unique": { + "name": "skills_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summarization_jobs": { + "name": "summarization_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "logs_processed": { + "name": "logs_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "insights_created": { + "name": "insights_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "summarization_jobs_status_idx": { + "name": "summarization_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "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": "'not-started'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee": { + "name": "assignee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_project_id_idx": { + "name": "tasks_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_mission_id_idx": { + "name": "tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_mission_id_missions_id_fk": { + "name": "tasks_mission_id_missions_id_fk", + "tableFrom": "tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_team_user_idx": { + "name": "team_members_team_user_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_invited_by_users_id_fk": { + "name": "team_members_invited_by_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "teams_manager_id_users_id_fk": { + "name": "teams_manager_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "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": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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 + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.grant_status": { + "name": "grant_status", + "schema": "public", + "values": [ + "pending", + "active", + "revoked", + "expired" + ] + }, + "public.peer_state": { + "name": "peer_state", + "schema": "public", + "values": [ + "pending", + "active", + "suspended", + "revoked" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_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 a5e1840..f7764f6 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1745280000000, "tag": "0009_federation_grant_pending", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1745366400000, + "tag": "0010_federation_enrollment_tokens", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/db/src/federation.ts b/packages/db/src/federation.ts index 0f56966..5046abf 100644 --- a/packages/db/src/federation.ts +++ b/packages/db/src/federation.ts @@ -17,4 +17,5 @@ export { federationPeers, federationGrants, federationAuditLog, + federationEnrollmentTokens, } from './schema.js'; diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 3496d1e..6ce5ec3 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -778,3 +778,34 @@ export const federationAuditLog = pgTable( index('federation_audit_log_created_at_idx').on(t.createdAt.desc()), ], ); + +/** + * Single-use enrollment tokens — M2-07. + * + * An admin creates a token (with a TTL) and hands it out-of-band to the + * remote peer operator. The peer redeems it exactly once by posting its + * CSR to POST /api/federation/enrollment/:token. The token is atomically + * marked as used to prevent replay attacks. + */ +export const federationEnrollmentTokens = pgTable('federation_enrollment_tokens', { + /** 32-byte hex token — crypto.randomBytes(32).toString('hex') */ + token: text('token').primaryKey(), + + /** The federation grant this enrollment activates. */ + grantId: uuid('grant_id') + .notNull() + .references(() => federationGrants.id, { onDelete: 'cascade' }), + + /** The peer record that will be updated on successful enrollment. */ + peerId: uuid('peer_id') + .notNull() + .references(() => federationPeers.id, { onDelete: 'cascade' }), + + /** Hard expiry — token rejected after this time even if not used. */ + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + + /** NULL until the token is redeemed. Set atomically to prevent replay. */ + usedAt: timestamp('used_at', { withTimezone: true }), + + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +});