feat(db): federation schema — grants/peers/audit_log [FED-M2-01] (#486)
This commit was merged in pull request #486.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
import {
|
||||
pgTable,
|
||||
pgEnum,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
@@ -585,3 +586,194 @@ export const summarizationJobs = pgTable(
|
||||
},
|
||||
(t) => [index('summarization_jobs_status_idx').on(t.status)],
|
||||
);
|
||||
|
||||
// ─── Federation ──────────────────────────────────────────────────────────────
|
||||
// Enums declared before tables that reference them.
|
||||
// All federation definitions live in this file (avoids CJS/ESM cross-import
|
||||
// issues when drizzle-kit loads schema files via esbuild-register).
|
||||
// Application code imports from `federation.ts` which re-exports from here.
|
||||
|
||||
/**
|
||||
* Lifecycle state of a federation peer.
|
||||
* - pending: registered but not yet approved / TLS handshake not confirmed
|
||||
* - active: fully operational; mTLS verified
|
||||
* - suspended: temporarily blocked; cert still valid
|
||||
* - revoked: cert revoked; no traffic allowed
|
||||
*/
|
||||
export const peerStateEnum = pgEnum('peer_state', ['pending', 'active', 'suspended', 'revoked']);
|
||||
|
||||
/**
|
||||
* Lifecycle state of a federation grant.
|
||||
* - active: grant is in effect
|
||||
* - revoked: manually revoked before expiry
|
||||
* - expired: natural expiry (expires_at passed)
|
||||
*/
|
||||
export const grantStatusEnum = pgEnum('grant_status', ['active', 'revoked', 'expired']);
|
||||
|
||||
/**
|
||||
* A registered peer gateway identified by its Step-CA certificate CN.
|
||||
* Represents both inbound peers (other gateways querying us) and outbound
|
||||
* peers (gateways we query — identified by client_key_pem being set).
|
||||
*/
|
||||
export const federationPeers = pgTable(
|
||||
'federation_peers',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
/** Certificate CN, e.g. "gateway-uscllc-com". Unique — one row per peer identity. */
|
||||
commonName: text('common_name').notNull().unique(),
|
||||
|
||||
/** Human-friendly label shown in admin UI. */
|
||||
displayName: text('display_name').notNull(),
|
||||
|
||||
/** Pinned PEM certificate used for mTLS verification. */
|
||||
certPem: text('cert_pem').notNull(),
|
||||
|
||||
/** Certificate serial number — used for CRL / revocation lookup. */
|
||||
certSerial: text('cert_serial').notNull().unique(),
|
||||
|
||||
/** Certificate expiry — used by the renewal scheduler (FED-M6). */
|
||||
certNotAfter: timestamp('cert_not_after', { withTimezone: true }).notNull(),
|
||||
|
||||
/**
|
||||
* Sealed (encrypted) private key for outbound connections TO this peer.
|
||||
* NULL for inbound-only peer rows (we serve them; we don't call them).
|
||||
*/
|
||||
clientKeyPem: text('client_key_pem'),
|
||||
|
||||
/** Current peer lifecycle state. */
|
||||
state: peerStateEnum('state').notNull().default('pending'),
|
||||
|
||||
/** Base URL for outbound queries, e.g. "https://woltje.com:443". NULL for inbound-only peers. */
|
||||
endpointUrl: text('endpoint_url'),
|
||||
|
||||
/** Timestamp of the most recent successful inbound or outbound request. */
|
||||
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
/** Populated when the cert is revoked; NULL while the peer is active. */
|
||||
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [
|
||||
// CRL / revocation lookups by serial.
|
||||
index('federation_peers_cert_serial_idx').on(t.certSerial),
|
||||
// Filter peers by state (e.g. find all active peers for outbound routing).
|
||||
index('federation_peers_state_idx').on(t.state),
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* A grant lets a specific peer cert query a specific local user's data within
|
||||
* a defined scope. Scopes are validated by JSON Schema in M2-03; this table
|
||||
* stores them as raw jsonb.
|
||||
*/
|
||||
export const federationGrants = pgTable(
|
||||
'federation_grants',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
/**
|
||||
* The local user whose data this grant exposes.
|
||||
* Cascade delete: if the user account is deleted, revoke all their grants.
|
||||
*/
|
||||
subjectUserId: text('subject_user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
|
||||
/**
|
||||
* The peer gateway holding the grant.
|
||||
* Cascade delete: if the peer record is removed, the grant is moot.
|
||||
*/
|
||||
peerId: uuid('peer_id')
|
||||
.notNull()
|
||||
.references(() => federationPeers.id, { onDelete: 'cascade' }),
|
||||
|
||||
/**
|
||||
* Scope object — validated by JSON Schema (M2-03).
|
||||
* Example: { "resources": ["tasks", "notes"], "operations": ["list", "get"] }
|
||||
*/
|
||||
scope: jsonb('scope').notNull(),
|
||||
|
||||
/** Current grant lifecycle state. */
|
||||
status: grantStatusEnum('status').notNull().default('active'),
|
||||
|
||||
/** Optional hard expiry. NULL means the grant does not expire automatically. */
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
/** Populated when the grant is explicitly revoked. */
|
||||
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
||||
|
||||
/** Human-readable reason for revocation (audit trail). */
|
||||
revokedReason: text('revoked_reason'),
|
||||
},
|
||||
(t) => [
|
||||
// Hot path: look up active grants for a subject user (auth middleware).
|
||||
index('federation_grants_subject_status_idx').on(t.subjectUserId, t.status),
|
||||
// Hot path: look up active grants held by a peer (inbound request check).
|
||||
index('federation_grants_peer_status_idx').on(t.peerId, t.status),
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Append-only audit log of all federation requests.
|
||||
* M4 writes rows here. M2 only creates the table.
|
||||
*
|
||||
* All FKs use SET NULL so audit rows survive peer/user/grant deletion.
|
||||
*/
|
||||
export const federationAuditLog = pgTable(
|
||||
'federation_audit_log',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
/** UUIDv7 from the X-Request-ID header — correlates with OTEL traces. */
|
||||
requestId: text('request_id').notNull(),
|
||||
|
||||
/** Peer that made the request. SET NULL if the peer is later deleted. */
|
||||
peerId: uuid('peer_id').references(() => federationPeers.id, { onDelete: 'set null' }),
|
||||
|
||||
/** Subject user whose data was queried. SET NULL if the user is deleted. */
|
||||
subjectUserId: text('subject_user_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
|
||||
/** Grant under which the request was authorised. SET NULL if the grant is deleted. */
|
||||
grantId: uuid('grant_id').references(() => federationGrants.id, { onDelete: 'set null' }),
|
||||
|
||||
/** Request verb: "list" | "get" | "search". */
|
||||
verb: text('verb').notNull(),
|
||||
|
||||
/** Resource type: "tasks" | "notes" | "memory" | etc. */
|
||||
resource: text('resource').notNull(),
|
||||
|
||||
/** HTTP status code returned to the peer. */
|
||||
statusCode: integer('status_code').notNull(),
|
||||
|
||||
/** Number of items returned (NULL for non-list requests or errors). */
|
||||
resultCount: integer('result_count'),
|
||||
|
||||
/** Why the request was denied (NULL when allowed). */
|
||||
deniedReason: text('denied_reason'),
|
||||
|
||||
/** End-to-end latency in milliseconds. */
|
||||
latencyMs: integer('latency_ms'),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
// Reserved for M4 — see PRD 7.3
|
||||
/** SHA-256 of the normalised GraphQL/REST query string; written by M4 search. */
|
||||
queryHash: text('query_hash'),
|
||||
/** Request outcome: "allowed" | "denied" | "partial"; written by M4. */
|
||||
outcome: text('outcome'),
|
||||
/** Response payload size in bytes; written by M4. */
|
||||
bytesOut: integer('bytes_out'),
|
||||
},
|
||||
(t) => [
|
||||
// Per-peer request history in reverse chronological order.
|
||||
index('federation_audit_log_peer_created_at_idx').on(t.peerId, t.createdAt.desc()),
|
||||
// Per-user access log in reverse chronological order.
|
||||
index('federation_audit_log_subject_created_at_idx').on(t.subjectUserId, t.createdAt.desc()),
|
||||
// Global time-range scans (dashboards, rate-limit windows).
|
||||
index('federation_audit_log_created_at_idx').on(t.createdAt.desc()),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user