@happyvertical/smrt-users 0.42.5 → 0.42.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{TerminalAuthService-CAomvhMa.js → TerminalAuthService-Cnr2pnQq.js} +173 -173
- package/dist/chunks/TerminalAuthService-Cnr2pnQq.js.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/dist/sveltekit.js +1 -1
- package/package.json +9 -9
- package/dist/chunks/TerminalAuthService-CAomvhMa.js.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["membership","tenant","entry","tenant"],"sources":["../src/models/MagicLinkToken.ts","../src/collections/MagicLinkTokenCollection.ts","../src/retention.ts","../src/models/AccessRequest.ts","../src/collections/AccessRequestCollection.ts","../src/models/TenantIntegration.ts","../src/collections/TenantIntegrationCollection.ts","../src/services/AccessRequestService.ts","../src/services/MagicLinkService.ts","../src/services/PostgresPermissionPolicies.ts","../src/services/TenantService.ts","../src/index.ts"],"sourcesContent":["/**\n * MagicLinkToken model - replay protection for magic link authentication\n *\n * Stores nonces embedded in magic link JWTs to prevent replay attacks.\n * Each nonce can only be used once within its expiry window.\n *\n * @packageDocumentation\n */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\n\n/**\n * Default magic link token expiry: 10 minutes in seconds\n */\nexport const DEFAULT_TOKEN_EXPIRY_SECONDS = 10 * 60;\n\n/**\n * Constructor options for {@link UsersMagicLinkToken}.\n */\nexport interface MagicLinkTokenOptions extends SmrtObjectOptions {\n nonce?: string;\n email?: string;\n used?: boolean;\n /** Accepts a Date or any value the Date constructor can coerce. */\n expiresAt?: Date | string | number;\n}\n\n@smrt({\n tableName: 'users_magic_link_tokens',\n // Magic link tokens are security-sensitive — no public API\n api: { include: [] },\n mcp: { include: [] },\n cli: true,\n})\nexport class UsersMagicLinkToken extends SmrtObject {\n /** Unique nonce embedded in the signed JWT */\n @field({ required: true, unique: true })\n nonce: string = '';\n\n /** Email address this token was generated for */\n email: string = '';\n\n /** Whether this token has been used */\n used: boolean = false;\n\n /**\n * When this token expires.\n *\n * Indexed: `deleteExpired()` — and the retention sweep that now schedules\n * it (#2375) — scans this column on every pass.\n */\n @field({ type: 'datetime', indexed: true })\n expiresAt: Date = new Date(Date.now() + DEFAULT_TOKEN_EXPIRY_SECONDS * 1000);\n\n constructor(options: MagicLinkTokenOptions = {}) {\n super(options);\n if (options.nonce !== undefined) this.nonce = options.nonce;\n if (options.email !== undefined) this.email = options.email;\n if (options.used !== undefined) this.used = options.used;\n if (options.expiresAt !== undefined) {\n this.expiresAt =\n options.expiresAt instanceof Date\n ? options.expiresAt\n : new Date(options.expiresAt);\n }\n }\n\n /** Check if the token has expired */\n isExpired(): boolean {\n return new Date() > this.expiresAt;\n }\n\n /** Check if the token is still valid (unused and not expired) */\n isValid(): boolean {\n return !this.used && !this.isExpired();\n }\n}\n\nexport { UsersMagicLinkToken as MagicLinkToken };\n","/**\n * MagicLinkTokenCollection - Collection manager for MagicLinkToken objects\n *\n * Provides nonce lookup and single-use enforcement for magic link authentication.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { UsersMagicLinkToken } from '../models/MagicLinkToken.js';\n\n/**\n * Collection for managing smrt-users magic link token records\n */\nexport class UsersMagicLinkTokenCollection extends SmrtCollection<UsersMagicLinkToken> {\n static readonly _itemClass = UsersMagicLinkToken;\n\n /**\n * Find a token by its nonce\n */\n async findByNonce(nonce: string): Promise<UsersMagicLinkToken | null> {\n return this.findOne({\n where: { nonce },\n }) as Promise<UsersMagicLinkToken | null>;\n }\n\n /**\n * Atomically mark a token as used (single-use enforcement).\n *\n * Returns true if the nonce was successfully claimed (transitioned from\n * unused to used). Returns false if the nonce was already used, expired,\n * or doesn't exist — preventing race conditions in concurrent verify() calls.\n */\n async markUsed(nonce: string): Promise<boolean> {\n const now = new Date().toISOString();\n const { rowCount } = await this.db.query(\n `UPDATE ${this.tableName}\n SET used = ?, updated_at = ?\n WHERE nonce = ? AND used = ? AND expires_at > ?`,\n true,\n now,\n nonce,\n false,\n now,\n );\n\n return rowCount > 0;\n }\n\n /**\n * Delete expired tokens (cleanup job)\n *\n * Scheduled by the framework retention sweep (#2375); `expiresAt` carries an\n * index for this predicate.\n *\n * @param options.dryRun - Count the tokens the predicate selects without\n * deleting them.\n * @returns Number of tokens deleted (or, under `dryRun`, matched)\n */\n async deleteExpired(options: { dryRun?: boolean } = {}): Promise<number> {\n // One statement, for the same reason `SessionCollection.deleteExpired()`\n // is one (#1400): this runs unattended on the retention sweep's timer, and\n // a per-row delete that throws part-way leaves the rest of the expired\n // tokens un-reaped. It also avoids hydrating every row just to delete it.\n const now = new Date().toISOString();\n const predicate = 'expires_at < ?';\n\n const counted = await this.db.query(\n `SELECT COUNT(*) AS total FROM ${this.tableName} WHERE ${predicate}`,\n now,\n );\n const total = Number(counted.rows?.[0]?.total ?? 0);\n if (!Number.isFinite(total) || total <= 0) return 0;\n\n if (!options.dryRun) {\n await this.db.query(\n `DELETE FROM ${this.tableName} WHERE ${predicate}`,\n now,\n );\n }\n\n return total;\n }\n}\n\nexport { UsersMagicLinkTokenCollection as MagicLinkTokenCollection };\n","/**\n * Credential retention — expired sessions and tokens (issue #2375, finding F2).\n *\n * `SessionCollection.deleteExpired()`, `MagicLinkTokenCollection.deleteExpired()`\n * and `CliAuthRequestCollection.deleteExpired()` all existed, and every one of\n * them waited for an application to remember to call it. Expired credential\n * rows are the worst kind of unbounded growth: they are worthless the moment\n * they expire and they are exactly the rows an attacker would like to still\n * find in the table.\n *\n * This module contributes them to the framework retention sweep, so\n * `smrt db:prune` and the jobs `TaskRunner`'s periodic sweep reap them like\n * every other framework-owned table.\n *\n * The package entry point registers all three on import (see `index.ts`), so\n * any process that loaded `@happyvertical/smrt-users` contributes them.\n * Registration is not scheduling: nothing is deleted until something runs a\n * sweep, and a policy can turn any task off by name. Task names are prefixed\n * with the owning package's short name, because the registry is one\n * process-global namespace shared with every other package.\n *\n * @see https://github.com/happyvertical/smrt/issues/2375\n * @packageDocumentation\n */\n\nimport {\n registerRetentionTask,\n unregisterRetentionTask,\n} from '@happyvertical/smrt-core';\nimport { UsersCliAuthRequestCollection } from './collections/CliAuthRequestCollection.js';\nimport { UsersMagicLinkTokenCollection } from './collections/MagicLinkTokenCollection.js';\nimport { SessionCollection } from './collections/SessionCollection.js';\n\n/** Retention task name for expired and revoked sessions. */\nexport const SESSIONS_RETENTION_TASK = 'users-sessions';\n\n/** Retention task name for expired magic-link tokens. */\nexport const MAGIC_LINK_RETENTION_TASK = 'users-magic-link-tokens';\n\n/** Retention task name for expired CLI device-code requests. */\nexport const CLI_AUTH_RETENTION_TASK = 'users-cli-auth-requests';\n\n/** Every task name {@link registerUserRetentionTasks} installs. */\nexport const USER_RETENTION_TASKS = [\n SESSIONS_RETENTION_TASK,\n MAGIC_LINK_RETENTION_TASK,\n CLI_AUTH_RETENTION_TASK,\n] as const;\n\n/**\n * Register session and token expiry with the framework retention sweep.\n *\n * Idempotent: re-registering replaces the previous tasks.\n *\n * Each task deletes only rows that have already expired — there is no\n * retention window to configure, because an expired credential has no value\n * to retain. Applications that keep expired sessions for audit should opt the\n * task out (`tasks: { 'users-sessions': false }`) and archive them\n * themselves.\n */\nexport function registerUserRetentionTasks(): void {\n registerRetentionTask({\n name: SESSIONS_RETENTION_TASK,\n description: 'Delete expired and revoked sessions',\n run: async (db, context) => {\n const sessions = await SessionCollection.create({ db });\n return sessions.deleteExpired({ dryRun: context.dryRun });\n },\n });\n\n registerRetentionTask({\n name: MAGIC_LINK_RETENTION_TASK,\n description: 'Delete expired magic-link tokens',\n run: async (db, context) => {\n const tokens = await UsersMagicLinkTokenCollection.create({ db });\n return tokens.deleteExpired({ dryRun: context.dryRun });\n },\n });\n\n registerRetentionTask({\n name: CLI_AUTH_RETENTION_TASK,\n description:\n 'Delete expired pending, lazily-expired, and consumed CLI auth requests',\n run: async (db, context) => {\n const requests = await UsersCliAuthRequestCollection.create({ db });\n return requests.deleteExpired({ dryRun: context.dryRun });\n },\n });\n}\n\n/** Remove the credential retention tasks from the framework sweep. */\nexport function unregisterUserRetentionTasks(): void {\n for (const name of USER_RETENTION_TASKS) {\n unregisterRetentionTask(name);\n }\n}\n","/**\n * AccessRequest model — the \"request access / join the waitlist\" identity\n * primitive.\n *\n * Captures a prospective user from a public form *before* they are a real\n * {@link User}: an operator triages the record, then either declines it or\n * approves and **graduates** it into a `User` (optionally attached to a\n * tenant). This keeps the `User` table clean — unverified/spam signups live as\n * requests, not users — while still giving operators a triage queue.\n *\n * The generated REST/MCP/CLI surface is intentionally CLOSED (`include: []`):\n * every read and mutation must go through {@link AccessRequestService} so the\n * public-safe creation path (email normalization + dedup), the lifecycle state\n * machine, capability gating, and event emission can never be bypassed by a\n * generated CRUD route. Creation is meant to be exposed *unauthenticated* by\n * consuming apps via their own rate-limited endpoint calling\n * `createAccessRequest`. The model is still registered (and its table created\n * via the consumer's normal migrate flow) regardless of the empty generation\n * config.\n *\n * @packageDocumentation\n */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { AccessRequestStatus } from '../types/index.js';\nimport { normalizeEmail } from './User.js';\n\n/**\n * Options accepted by the {@link AccessRequest} constructor / collection\n * `create`. `requestContext` and `tenantHint` are stored as JSON *strings* —\n * use {@link AccessRequest.setRequestContext} / {@link AccessRequest.setTenantHint}\n * (or pass pre-serialized strings) rather than raw objects.\n */\nexport interface AccessRequestOptions {\n email?: string;\n name?: string | null;\n status?: AccessRequestStatus;\n source?: string;\n requestContext?: string;\n note?: string | null;\n requestedAt?: Date;\n decidedAt?: Date | null;\n decidedBy?: string | null;\n resultingUserId?: string | null;\n tenantHint?: string | null;\n [key: string]: unknown;\n}\n\n/**\n * A prospective user's request for access, captured before they become a\n * {@link User}.\n *\n * @example\n * ```typescript\n * // Public, unauthenticated path (app adds rate-limiting):\n * const request = await service.createAccessRequest({\n * email: 'Jane@Example.com',\n * name: 'Jane Doe',\n * source: 'www',\n * context: { company: 'Acme', intendedUse: 'evaluation' },\n * });\n * // request.email === 'jane@example.com', request.status === 'requested'\n * ```\n */\n@smrt({\n tableName: 'access_requests',\n // Append-style: the natural key is the surrogate id, so a new request never\n // upserts over an existing row. Without this, SMRT defaults to upserting on\n // `slug`/`context`, and `slug` is derived from `name` — two public submissions\n // sharing a display name (e.g. two \"Jane Doe\"s with different emails) would\n // collide and overwrite each other. Open-request dedup is handled explicitly\n // by AccessRequestService.createAccessRequest, not by the storage conflict key.\n conflictColumns: ['id'],\n // CLOSED generated surface — all access flows through AccessRequestService.\n api: { include: [] },\n mcp: { include: [] },\n cli: { include: [] },\n})\nexport class AccessRequest extends SmrtObject {\n /**\n * Requester's email address. Required, normalized to lowercase, and indexed\n * for lookup + open-request dedup. NOT unique: the same email may accumulate\n * multiple requests over time (e.g. an old GRADUATED record plus a new\n * REQUESTED one); dedup is enforced on *open* requests by the service.\n */\n @field({ required: true, indexed: true })\n email: string = '';\n\n /**\n * Requester's display name, if supplied on the form.\n */\n @field({ nullable: true })\n name: string | null = null;\n\n /**\n * Lifecycle status. Indexed so the operator triage queue can filter by status\n * efficiently.\n */\n @field({ type: 'text', indexed: true })\n status: AccessRequestStatus = AccessRequestStatus.REQUESTED;\n\n /**\n * Where the request came from, e.g. `www`, `sdk`. Indexed for per-source\n * filtering in the operator queue.\n */\n @field({ indexed: true })\n source: string = '';\n\n /**\n * Free-form JSON metadata captured with the request (intended use, company,\n * message, referrer, etc.), stored as a JSON string.\n *\n * NOT named `context`: {@link SmrtObject} reserves the `context` field for\n * slug scoping, so this mirrors smrt-chat's `sessionContext` convention. Use\n * {@link getRequestContext} / {@link setRequestContext} for typed access.\n */\n @field()\n requestContext: string = '{}';\n\n /**\n * Operator note / decision reason (set on approve / decline / cancel).\n */\n @field({ nullable: true })\n note: string | null = null;\n\n /**\n * When the request was submitted. Defaults to creation time.\n */\n @field()\n requestedAt: Date = new Date();\n\n /**\n * When an operator decided the request (approve / decline / cancel /\n * graduate). Null while still `REQUESTED`.\n */\n @field({ nullable: true })\n decidedAt: Date | null = null;\n\n /**\n * User id of the operator who decided the request. Null while still pending.\n */\n @field({ nullable: true })\n decidedBy: string | null = null;\n\n /**\n * Id of the `User` produced (or linked) when the request graduated. Null\n * until graduation.\n */\n @field({ nullable: true })\n resultingUserId: string | null = null;\n\n /**\n * Optional JSON hint about the org/tenant the requester wants, stored as a\n * JSON string (null when absent). Advisory only — the operator decides the\n * actual tenant at graduation time. Use {@link getTenantHint} /\n * {@link setTenantHint} for typed access.\n */\n @field({ nullable: true })\n tenantHint: string | null = null;\n\n constructor(options: AccessRequestOptions = {}) {\n super(options);\n if (options.email !== undefined) this.email = normalizeEmail(options.email);\n if (options.name !== undefined) this.name = options.name;\n if (options.status !== undefined) this.status = options.status;\n if (options.source !== undefined) this.source = options.source;\n if (options.requestContext !== undefined)\n this.requestContext = options.requestContext;\n if (options.note !== undefined) this.note = options.note;\n if (options.requestedAt !== undefined)\n this.requestedAt = options.requestedAt;\n if (options.decidedAt !== undefined) this.decidedAt = options.decidedAt;\n if (options.decidedBy !== undefined) this.decidedBy = options.decidedBy;\n if (options.resultingUserId !== undefined)\n this.resultingUserId = options.resultingUserId;\n if (options.tenantHint !== undefined) this.tenantHint = options.tenantHint;\n }\n\n /**\n * Parse {@link requestContext} into an object. Returns `{}` on missing or\n * malformed JSON (graceful — never throws).\n */\n getRequestContext(): Record<string, unknown> {\n try {\n const parsed = JSON.parse(this.requestContext);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /**\n * Serialize and store {@link requestContext} from an object.\n */\n setRequestContext(value: Record<string, unknown>): void {\n this.requestContext = JSON.stringify(value ?? {});\n }\n\n /**\n * Parse {@link tenantHint} into an object, or `null` when unset / malformed.\n */\n getTenantHint(): Record<string, unknown> | null {\n if (!this.tenantHint) return null;\n try {\n const parsed = JSON.parse(this.tenantHint);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Serialize and store {@link tenantHint} from an object (or clear with null).\n */\n setTenantHint(value: Record<string, unknown> | null): void {\n this.tenantHint = value ? JSON.stringify(value) : null;\n }\n\n /**\n * Whether the request is still open (awaiting a decision).\n */\n isOpen(): boolean {\n return this.status === AccessRequestStatus.REQUESTED;\n }\n\n /**\n * Whether the request has been approved (and not yet graduated).\n */\n isApproved(): boolean {\n return this.status === AccessRequestStatus.APPROVED;\n }\n\n /**\n * Whether the request reached a terminal state (declined, graduated, or\n * canceled) and can no longer transition.\n */\n isTerminal(): boolean {\n return (\n this.status === AccessRequestStatus.DECLINED ||\n this.status === AccessRequestStatus.GRADUATED ||\n this.status === AccessRequestStatus.CANCELED\n );\n }\n}\n","/**\n * AccessRequestCollection — query helpers for {@link AccessRequest} records.\n *\n * This is the low-level data accessor. Lifecycle orchestration (the public-safe\n * create path, the state machine, capability gating, events, and graduation\n * into a `User`) lives in {@link AccessRequestService} — prefer the service for\n * application code.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { AccessRequest } from '../models/AccessRequest.js';\nimport { normalizeEmail } from '../models/User.js';\nimport { AccessRequestStatus } from '../types/index.js';\n\n/**\n * Collection for managing {@link AccessRequest} objects.\n */\nexport class AccessRequestCollection extends SmrtCollection<AccessRequest> {\n static readonly _itemClass = AccessRequest;\n\n /**\n * Find all access requests for an email (any status), newest first. The email\n * is normalized before querying so callers can pass any case.\n */\n async findByEmail(email: string): Promise<AccessRequest[]> {\n return await this.list({\n where: { email: normalizeEmail(email) },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * Find the single open (`REQUESTED`) request for an email, if any. This is\n * the dedup key used by {@link AccessRequestService.createAccessRequest}.\n */\n async findOpenByEmail(email: string): Promise<AccessRequest | null> {\n const results = await this.list({\n where: {\n email: normalizeEmail(email),\n status: AccessRequestStatus.REQUESTED,\n },\n limit: 1,\n orderBy: 'created_at DESC',\n });\n return results.length > 0 ? results[0] : null;\n }\n\n /**\n * Find access requests by status, newest first.\n */\n async findByStatus(status: AccessRequestStatus): Promise<AccessRequest[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * Find all open (`REQUESTED`) access requests — the operator triage queue.\n */\n async findOpen(): Promise<AccessRequest[]> {\n return await this.findByStatus(AccessRequestStatus.REQUESTED);\n }\n}\n","/**\n * TenantIntegration model - per-tenant external integration provisioning state.\n *\n * One row per (tenantId, provider) tuple. Stores provider identifiers and\n * health/provisioning metadata only; callers must keep secrets in a dedicated\n * secret store such as @happyvertical/smrt-secrets.\n *\n * @packageDocumentation\n */\n\nimport {\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n\n/**\n * Built-in provider names. The string type is intentionally open so apps can\n * add providers without a model migration.\n */\nexport type TenantIntegrationProvider =\n | 'aws'\n | 'bifrost'\n | 'glitchtip'\n | 'imago'\n | 'matomo'\n | (string & {});\n\n/**\n * Provisioning and health states for a tenant integration.\n *\n * - `unprovisioned`: no external resources have been created yet.\n * - `provisioning`: provisioning is currently queued or running.\n * - `active`: external resources exist and the latest health check passed.\n * - `drifted`: local bookkeeping disagrees with provider state.\n * - `failed`: the latest provisioning attempt or health check failed.\n */\nexport type TenantIntegrationStatus =\n | 'unprovisioned'\n | 'provisioning'\n | 'active'\n | 'drifted'\n | 'failed';\n\nexport interface TenantIntegrationCheckSummary {\n name: string;\n ok: boolean;\n message?: string;\n}\n\nexport interface TenantIntegrationOptions extends SmrtObjectOptions {\n tenantId?: string;\n provider?: TenantIntegrationProvider;\n externalIds?: Record<string, string> | string;\n status?: TenantIntegrationStatus;\n lastCheckedAt?: Date | null;\n lastCheckSummary?: TenantIntegrationCheckSummary[] | string;\n lastError?: string | null;\n}\n\nfunction parseExternalIds(raw: unknown): Record<string, string> {\n if (!raw) return {};\n\n if (typeof raw === 'object' && !Array.isArray(raw)) {\n return Object.fromEntries(\n Object.entries(raw).filter(([, value]) => typeof value === 'string'),\n ) as Record<string, string>;\n }\n\n if (typeof raw !== 'string') {\n return {};\n }\n\n try {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return {};\n }\n\n return Object.fromEntries(\n Object.entries(parsed).filter(([, value]) => typeof value === 'string'),\n ) as Record<string, string>;\n } catch {\n return {};\n }\n}\n\nfunction stringifyExternalIds(externalIds: Record<string, string>): string {\n for (const [key, value] of Object.entries(\n externalIds as Record<string, unknown>,\n )) {\n if (typeof value !== 'string') {\n throw new TypeError(\n `TenantIntegration external id '${key}' must be a string`,\n );\n }\n }\n\n return JSON.stringify(externalIds);\n}\n\nfunction parseCheckSummary(raw: unknown): TenantIntegrationCheckSummary[] {\n if (!raw) return [];\n\n if (Array.isArray(raw)) {\n return normalizeCheckSummary(raw);\n }\n\n if (typeof raw !== 'string') {\n return [];\n }\n\n try {\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? normalizeCheckSummary(parsed) : [];\n } catch {\n return [];\n }\n}\n\nfunction normalizeCheckSummary(\n raw: unknown[],\n): TenantIntegrationCheckSummary[] {\n return raw\n .filter(\n (entry): entry is Record<string, unknown> =>\n !!entry && typeof entry === 'object' && !Array.isArray(entry),\n )\n .map((entry) => ({\n name: typeof entry.name === 'string' ? entry.name : '',\n ok: entry.ok === true,\n ...(typeof entry.message === 'string' ? { message: entry.message } : {}),\n }))\n .filter((entry) => entry.name.length > 0);\n}\n\nfunction stringifyCheckSummary(\n summary: TenantIntegrationCheckSummary[],\n): string {\n return JSON.stringify(summary);\n}\n\n// TenantIntegration rows are always tenant-owned provider state; system-wide\n// provider defaults belong in app config or a secret store, not this table.\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'tenant_integrations',\n conflictColumns: ['tenant_id', 'provider'],\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: true,\n})\nexport class TenantIntegration extends SmrtObject {\n /**\n * Tenant this integration belongs to. Combined with `provider`, this is the\n * natural key for the row.\n */\n @tenantId()\n tenantId: string = '';\n\n /**\n * Provider identifier, such as `aws`, `matomo`, `glitchtip`, or an\n * app-defined provider string.\n */\n provider: TenantIntegrationProvider = 'aws';\n\n /**\n * Provider-side identifiers. Shape depends on the provider. This must not\n * contain provider credentials or secret material.\n */\n externalIds = '{}';\n\n /**\n * Current provisioning/health status.\n */\n status: TenantIntegrationStatus = 'unprovisioned';\n\n /**\n * UTC timestamp of the most recent doctor/provisioning check.\n */\n lastCheckedAt: Date | null = null;\n\n /**\n * Per-check pass/fail summary from the most recent doctor run.\n */\n lastCheckSummary = '[]';\n\n /**\n * Most recent error message when `status === 'failed'`, or null otherwise.\n */\n lastError: string | null = null;\n\n constructor(options: TenantIntegrationOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.provider !== undefined) this.provider = options.provider;\n if (options.externalIds !== undefined) {\n this.setExternalIds(options.externalIds);\n }\n if (options.status !== undefined) this.status = options.status;\n if (options.lastCheckedAt !== undefined)\n this.lastCheckedAt = options.lastCheckedAt;\n if (options.lastCheckSummary !== undefined) {\n this.setLastCheckSummary(options.lastCheckSummary);\n }\n if (options.lastError !== undefined) this.lastError = options.lastError;\n }\n\n public override async initialize(): Promise<this> {\n await super.initialize();\n this.normalizeJsonFields();\n return this;\n }\n\n private normalizeJsonFields(): void {\n const fields = this as unknown as {\n externalIds: unknown;\n lastCheckSummary: unknown;\n };\n\n if (fields.externalIds == null) {\n this.externalIds = '{}';\n } else if (typeof fields.externalIds !== 'string') {\n this.setExternalIds(fields.externalIds as Record<string, string>);\n }\n\n if (fields.lastCheckSummary == null) {\n this.lastCheckSummary = '[]';\n } else if (typeof fields.lastCheckSummary !== 'string') {\n this.setLastCheckSummary(\n fields.lastCheckSummary as TenantIntegrationCheckSummary[],\n );\n }\n }\n\n getExternalIds(): Record<string, string> {\n return parseExternalIds(this.externalIds);\n }\n\n setExternalIds(externalIds: Record<string, string> | string): void {\n this.externalIds =\n typeof externalIds === 'string'\n ? externalIds\n : stringifyExternalIds(externalIds);\n }\n\n getExternalId(key: string): string | undefined {\n const value = this.getExternalIds()[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n }\n\n setExternalId(key: string, value: string | undefined): void {\n const externalIds = this.getExternalIds();\n if (value === undefined) {\n delete externalIds[key];\n } else {\n externalIds[key] = value;\n }\n\n this.setExternalIds(externalIds);\n }\n\n getLastCheckSummary(): TenantIntegrationCheckSummary[] {\n return parseCheckSummary(this.lastCheckSummary);\n }\n\n setLastCheckSummary(summary: TenantIntegrationCheckSummary[] | string): void {\n this.lastCheckSummary =\n typeof summary === 'string' ? summary : stringifyCheckSummary(summary);\n }\n\n recordDoctorRun(outcome: {\n status: TenantIntegrationStatus;\n summary: TenantIntegrationCheckSummary[];\n error?: string | null;\n }): void {\n this.status = outcome.status;\n this.setLastCheckSummary(outcome.summary);\n this.lastCheckedAt = new Date();\n this.lastError = outcome.error ?? null;\n }\n}\n","/**\n * TenantIntegrationCollection - accessors for tenant integration rows.\n *\n * @packageDocumentation\n */\n\nimport {\n isUniqueViolationError,\n SmrtCollection,\n} from '@happyvertical/smrt-core';\nimport {\n TenantIntegration,\n type TenantIntegrationProvider,\n} from '../models/TenantIntegration.js';\n\nexport class TenantIntegrationCollection extends SmrtCollection<TenantIntegration> {\n static readonly _itemClass = TenantIntegration;\n\n async listForTenant(tenantId: string): Promise<TenantIntegration[]> {\n return this.list({\n where: { tenantId },\n orderBy: 'provider ASC',\n });\n }\n\n async findFor(\n tenantId: string,\n provider: TenantIntegrationProvider,\n ): Promise<TenantIntegration | null> {\n return this.findOne({\n where: { tenantId, provider },\n }) as Promise<TenantIntegration | null>;\n }\n\n async findOrInit(\n tenantId: string,\n provider: TenantIntegrationProvider,\n ): Promise<TenantIntegration> {\n const existing = await this.findFor(tenantId, provider);\n if (existing) {\n return existing;\n }\n\n try {\n return await this.create({\n tenantId,\n provider,\n status: 'unprovisioned',\n _insertOnly: true,\n });\n } catch (err) {\n if (!isDuplicateKeyError(err)) {\n throw err;\n }\n\n const winner = await this.findFor(tenantId, provider);\n if (winner) {\n return winner;\n }\n\n throw err;\n }\n }\n}\n\n/**\n * Detect the losing side of a concurrent `findOrInit` race.\n *\n * Reduced to the framework contract in #2366: the hand-rolled cause walk and\n * its dialect regexes were a workaround for `save()` surfacing constraint\n * violations as a generic `DatabaseError`. Core now classifies through the\n * driver-error chain, so the typed check is sufficient.\n */\nfunction isDuplicateKeyError(err: unknown): boolean {\n return isUniqueViolationError(err);\n}\n","/**\n * AccessRequestService — the mechanism behind the \"request access / waitlist\"\n * identity primitive, plus graduation of an approved request into a `User`.\n *\n * ## Responsibilities\n *\n * - **Public-safe creation** ({@link AccessRequestService.createAccessRequest}):\n * validates + normalizes the email and de-duplicates against any open request\n * for the same email. Intended to be called *unauthenticated* by consuming\n * apps from their own rate-limited endpoint (the app owns rate-limiting).\n * - **Operator triage** (`list` / `get` / `approve` / `decline` / `cancel`):\n * gated behind capabilities (see {@link AccessRequestAuthorizer}).\n * - **Graduation** ({@link AccessRequestService.graduateAccessRequest}): creates\n * or links a `User` from the request — reusing the existing user / membership\n * / tenant collections rather than duplicating them — and (optionally)\n * attaches the user to a brand-new tenant (requester as owner), an existing\n * tenant, or no tenant at all. Idempotent.\n * - **Events** ({@link AccessRequestEventHandler}): emits\n * `access-request.created | approved | declined | canceled | graduated` so\n * apps can react (send a magic-link/invite on `graduated`, notify ops on\n * `created`, …). This service deliberately does **not** own email/notification\n * delivery — bridge the event hook to your delivery channel (a function, an\n * `EventEmitter`, the core `DispatchBus`, …).\n *\n * @remarks\n * **Security.** Operator methods are gated only when an {@link AccessRequestAuthorizer}\n * is supplied via {@link AccessRequestServiceOptions.authorize}; without one\n * they are ungated, exactly like {@link TerminalAuthService.approveRequest}\n * trusts its caller to authenticate first. Production apps that expose these\n * methods MUST either provide an `authorize` hook or perform their own\n * capability checks at the route layer. `createAccessRequest` is *always*\n * ungated by design (public-safe). The model's generated REST/MCP/CLI surface\n * is closed, so there is no unauthenticated network route by default.\n *\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { AccessRequestCollection } from '../collections/AccessRequestCollection.js';\nimport { MembershipCollection } from '../collections/MembershipCollection.js';\nimport { RoleCollection } from '../collections/RoleCollection.js';\nimport { TenantCollection } from '../collections/TenantCollection.js';\nimport { UserCollection } from '../collections/UserCollection.js';\nimport type { AccessRequest } from '../models/AccessRequest.js';\nimport type { Membership } from '../models/Membership.js';\nimport type { Tenant } from '../models/Tenant.js';\nimport type { User } from '../models/User.js';\nimport { isValidEmail, normalizeEmail } from '../models/User.js';\nimport {\n AccessRequestStatus,\n DEFAULT_ROLE_SLUGS,\n MembershipStatus,\n type TenantStatus,\n UserStatus,\n} from '../types/index.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Capabilities that gate the operator-facing methods of\n * {@link AccessRequestService}.\n */\nexport const ACCESS_REQUEST_CAPABILITIES = {\n /** Read the access-request queue (`list` / `get`). */\n READ: 'access-requests:read',\n /** Decide requests (`approve` / `decline` / `cancel` / `graduate`). */\n MANAGE: 'access-requests:manage',\n} as const;\n\n/**\n * One of the capability slugs in {@link ACCESS_REQUEST_CAPABILITIES}.\n */\nexport type AccessRequestCapability =\n (typeof ACCESS_REQUEST_CAPABILITIES)[keyof typeof ACCESS_REQUEST_CAPABILITIES];\n\n/**\n * Context passed to an {@link AccessRequestAuthorizer} before an operator method\n * runs.\n */\nexport interface AccessRequestAuthorizationContext {\n /** The capability the operation requires. */\n capability: AccessRequestCapability;\n /** The operator user id supplied to the method (`by`), if any. */\n by?: string | null;\n /** The target access-request id, when the operation targets a specific row. */\n accessRequestId?: string;\n}\n\n/**\n * Hook that authorizes an operator action. Throw (or reject) to deny — the\n * thrown error propagates to the caller unchanged. Resolve/return to allow.\n *\n * Wire this to your permission system, e.g. resolve the operator's permissions\n * and assert the required capability:\n *\n * ```typescript\n * const service = await AccessRequestService.create({\n * db,\n * authorize: async ({ capability, by }) => {\n * if (!by || !(await isPlatformOperator(by, capability))) {\n * throw new Error(`Missing capability: ${capability}`);\n * }\n * },\n * });\n * ```\n */\nexport type AccessRequestAuthorizer = (\n context: AccessRequestAuthorizationContext,\n) => void | Promise<void>;\n\n/**\n * Lifecycle event types emitted by {@link AccessRequestService}.\n */\nexport type AccessRequestEventType =\n | 'access-request.created'\n | 'access-request.approved'\n | 'access-request.declined'\n | 'access-request.canceled'\n | 'access-request.graduated';\n\n/**\n * Payload delivered to an {@link AccessRequestEventHandler}.\n */\nexport interface AccessRequestEvent {\n /** Which lifecycle transition fired. */\n type: AccessRequestEventType;\n /** The access request after the transition. */\n accessRequest: AccessRequest;\n /** When the event was emitted. */\n at: Date;\n /** Operator user id responsible, for operator-driven transitions. */\n by?: string | null;\n /** Graduated user (only on `access-request.graduated`). */\n user?: User;\n /** Membership created/linked on graduation, when a tenant was attached. */\n membership?: Membership;\n /** Tenant created/linked on graduation, when a tenant was attached. */\n tenant?: Tenant;\n}\n\n/**\n * Event hook apps provide to react to access-request lifecycle changes.\n * Delivery is best-effort: a throwing handler is logged and swallowed so it\n * never rolls back an already-persisted transition. Do not rely on it for\n * critical-path work that must share the request's transaction.\n */\nexport type AccessRequestEventHandler = (\n event: AccessRequestEvent,\n) => void | Promise<void>;\n\n/**\n * Options for {@link AccessRequestService}.\n */\nexport interface AccessRequestServiceOptions extends SmrtClassOptions {\n /** Optional capability gate for operator methods (see {@link AccessRequestAuthorizer}). */\n authorize?: AccessRequestAuthorizer;\n /** Optional lifecycle event hook (see {@link AccessRequestEventHandler}). */\n onEvent?: AccessRequestEventHandler;\n}\n\n/**\n * Input for {@link AccessRequestService.createAccessRequest}. Only `email` is\n * required.\n */\nexport interface CreateAccessRequestInput {\n /** Requester email (validated + normalized to lowercase). */\n email: string;\n /** Requester display name. */\n name?: string | null;\n /** Where the request came from, e.g. `www`, `sdk`. */\n source?: string;\n /** Free-form metadata (intended use, company, message, referrer, …). */\n context?: Record<string, unknown>;\n /** Optional requested org/tenant hint (advisory). */\n tenantHint?: Record<string, unknown> | null;\n /** Optional initial note. */\n note?: string | null;\n}\n\n/**\n * Filter for {@link AccessRequestService.listAccessRequests}.\n */\nexport interface ListAccessRequestsFilter {\n /** Restrict to one status or any of several. */\n status?: AccessRequestStatus | AccessRequestStatus[];\n /** Restrict to a single (normalized) email. */\n email?: string;\n /** Restrict to a single source. */\n source?: string;\n /** Operator user id, forwarded to the authorizer as `by`. */\n by?: string | null;\n /** Max rows to return. */\n limit?: number;\n /** Rows to skip. */\n offset?: number;\n /** Order-by clause (defaults to `created_at DESC`). */\n orderBy?: string;\n}\n\n/**\n * Options shared by the operator decision methods.\n */\nexport interface DecideAccessRequestOptions {\n /** Operator user id recorded as `decidedBy` and forwarded to the authorizer. */\n by?: string | null;\n}\n\n/**\n * Options for {@link AccessRequestService.approveAccessRequest}.\n */\nexport interface ApproveAccessRequestOptions\n extends DecideAccessRequestOptions {\n /** Operator note stored on the request. */\n note?: string | null;\n}\n\n/**\n * Options for {@link AccessRequestService.declineAccessRequest}.\n */\nexport interface DeclineAccessRequestOptions\n extends DecideAccessRequestOptions {\n /** Decision reason stored as the request's note. */\n reason?: string | null;\n}\n\n/**\n * Options for {@link AccessRequestService.cancelAccessRequest}.\n */\nexport interface CancelAccessRequestOptions extends DecideAccessRequestOptions {\n /** Cancellation reason stored as the request's note. */\n reason?: string | null;\n}\n\n/**\n * Graduate into a **new** tenant, enrolling the requester (owner by default).\n */\nexport interface GraduateNewTenantOption {\n /** New tenant attributes — `name` is required. */\n create: {\n name: string;\n slug?: string;\n description?: string;\n status?: TenantStatus;\n };\n /** Role slug for the requester's membership (default `owner`). */\n role?: string;\n}\n\n/**\n * Graduate into an **existing** tenant, enrolling the requester.\n */\nexport interface GraduateExistingTenantOption {\n /** Target tenant id. */\n tenantId: string;\n /** Role slug for the requester's membership (default `member`). */\n role?: string;\n}\n\n/**\n * Tenant handling at graduation: create a new tenant, attach to an existing\n * one, or `'none'` (user only, no membership).\n */\nexport type GraduateTenantOption =\n | GraduateNewTenantOption\n | GraduateExistingTenantOption\n | 'none';\n\n/**\n * Options for {@link AccessRequestService.graduateAccessRequest}.\n */\nexport interface GraduateAccessRequestOptions\n extends DecideAccessRequestOptions {\n /** Tenant handling (default `'none'`). */\n tenant?: GraduateTenantOption;\n /** Status applied to the user produced/linked by graduation (default `ACTIVE`). */\n activate?: UserStatus;\n /**\n * Convenience: allow graduating directly from `REQUESTED` (skipping the\n * `APPROVED` step). Defaults to `false`.\n */\n allowFromRequested?: boolean;\n /** Operator note stored on the request. */\n note?: string | null;\n}\n\n/**\n * Result of {@link AccessRequestService.graduateAccessRequest}.\n */\nexport interface GraduateAccessRequestResult {\n /** The graduated (created or linked) user. */\n user: User;\n /** The membership, when a tenant was attached. */\n membership?: Membership;\n /** The tenant, when one was created or attached. */\n tenant?: Tenant;\n /** The access request, now `GRADUATED`. */\n accessRequest: AccessRequest;\n /** Whether a brand-new user was created (`false` when an existing one was linked). */\n created: boolean;\n}\n\n/**\n * Error codes raised by {@link AccessRequestError}.\n */\nexport type AccessRequestErrorCode =\n | 'INVALID_EMAIL'\n | 'NOT_FOUND'\n | 'INVALID_TRANSITION'\n | 'TENANT_NOT_FOUND'\n | 'ROLE_NOT_FOUND';\n\n/**\n * Error thrown for access-request domain failures the caller is expected to\n * surface (invalid email, unknown id, illegal state transition, …). Authorizer\n * denials are not wrapped — those propagate from the supplied authorizer\n * unchanged.\n */\nexport class AccessRequestError extends Error {\n readonly code: AccessRequestErrorCode;\n\n constructor(message: string, code: AccessRequestErrorCode) {\n super(message);\n this.name = 'AccessRequestError';\n this.code = code;\n }\n}\n\n/**\n * Source statuses each transition may legally start from. Idempotent no-ops\n * (e.g. approving an already-`APPROVED` request) are handled separately, before\n * these guards run.\n */\nconst APPROVE_FROM: AccessRequestStatus[] = [AccessRequestStatus.REQUESTED];\nconst DECLINE_FROM: AccessRequestStatus[] = [\n AccessRequestStatus.REQUESTED,\n AccessRequestStatus.APPROVED,\n];\nconst CANCEL_FROM: AccessRequestStatus[] = [\n AccessRequestStatus.REQUESTED,\n AccessRequestStatus.APPROVED,\n];\n\n/**\n * High-level orchestration for the access-request lifecycle and graduation.\n */\nexport class AccessRequestService {\n readonly #options: AccessRequestServiceOptions;\n readonly #authorize?: AccessRequestAuthorizer;\n readonly #onEvent?: AccessRequestEventHandler;\n\n #requests!: AccessRequestCollection;\n #users!: UserCollection;\n #tenants!: TenantCollection;\n #memberships!: MembershipCollection;\n #roles!: RoleCollection;\n #rolesSeeded = false;\n\n constructor(options: AccessRequestServiceOptions) {\n this.#options = options;\n this.#authorize = options.authorize;\n this.#onEvent = options.onEvent;\n }\n\n /**\n * Initialize the backing collections (creates/verifies their tables).\n */\n async initialize(): Promise<void> {\n // SmrtClassOptions carries `authorize`/`onEvent` too; the collection factory\n // only reads the db/ai keys and ignores the rest, so passing the whole\n // options bag is harmless and keeps the db config in one place.\n this.#requests = await AccessRequestCollection.create(this.#options);\n this.#users = await UserCollection.create(this.#options);\n this.#tenants = await TenantCollection.create(this.#options);\n this.#memberships = await MembershipCollection.create(this.#options);\n this.#roles = await RoleCollection.create(this.#options);\n }\n\n /**\n * Static factory — construct and initialize in one call.\n */\n static async create(\n options: AccessRequestServiceOptions,\n ): Promise<AccessRequestService> {\n const service = new AccessRequestService(options);\n await service.initialize();\n return service;\n }\n\n /**\n * The underlying collection, for advanced read scenarios. Prefer the service\n * methods, which apply normalization, the state machine, capability gating,\n * and events.\n */\n get collection(): AccessRequestCollection {\n return this.#requests;\n }\n\n // ============= Public-safe creation =============\n\n /**\n * Create an access request. **Public-safe**: no capability check — meant to be\n * callable unauthenticated by apps (which add their own rate-limiting).\n *\n * Validates and normalizes the email, then de-duplicates: if an open\n * (`REQUESTED`) request already exists for the email, this merges any newly\n * supplied context/name/source/hint into it and returns it instead of\n * creating a duplicate (no second `created` event).\n *\n * @remarks\n * De-duplication is **best-effort, not atomic**: it is a read-then-write\n * (`findOpenByEmail` → `create`) with no DB-level partial-unique constraint\n * (the table is append-style, keyed on `id`, because the same email may\n * accumulate many requests over its lifetime). Two requests for the same\n * email racing concurrently can therefore both create an open row. This is by\n * design — the spec makes dedup configurable and pushes abuse control to the\n * app (rate-limiting on the public endpoint). Operators triaging two open rows\n * for one email is benign; apps needing a hard single-open-request guarantee\n * should add a partial unique index (`UNIQUE(email) WHERE status='requested'`)\n * in their migration.\n *\n * @throws {@link AccessRequestError} (`INVALID_EMAIL`) when the email is invalid.\n */\n async createAccessRequest(\n input: CreateAccessRequestInput,\n ): Promise<AccessRequest> {\n const email = normalizeEmail(input.email);\n if (!isValidEmail(email)) {\n throw new AccessRequestError(\n 'A valid email address is required to request access.',\n 'INVALID_EMAIL',\n );\n }\n\n // Dedup against an existing open request for this email (idempotent).\n const existing = await this.#requests.findOpenByEmail(email);\n if (existing) {\n let changed = false;\n if (input.context && Object.keys(input.context).length > 0) {\n existing.setRequestContext({\n ...existing.getRequestContext(),\n ...input.context,\n });\n changed = true;\n }\n if (input.tenantHint) {\n existing.setTenantHint({\n ...(existing.getTenantHint() ?? {}),\n ...input.tenantHint,\n });\n changed = true;\n }\n if (input.name && !existing.name) {\n existing.name = input.name;\n changed = true;\n }\n if (input.source && !existing.source) {\n existing.source = input.source;\n changed = true;\n }\n if (changed) await existing.save();\n return existing;\n }\n\n const request = await this.#requests.create({\n email,\n name: input.name ?? null,\n source: input.source ?? '',\n status: AccessRequestStatus.REQUESTED,\n requestedAt: new Date(),\n requestContext: JSON.stringify(input.context ?? {}),\n tenantHint: input.tenantHint ? JSON.stringify(input.tenantHint) : null,\n note: input.note ?? null,\n });\n\n await this.#emit({\n type: 'access-request.created',\n accessRequest: request,\n at: new Date(),\n });\n\n return request;\n }\n\n // ============= Operator reads =============\n\n /**\n * List access requests (operator-facing). Requires the `access-requests:read`\n * capability when an authorizer is configured.\n */\n async listAccessRequests(\n filter: ListAccessRequestsFilter = {},\n ): Promise<AccessRequest[]> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.READ, {\n by: filter.by,\n });\n\n const where: Record<string, unknown> = {};\n if (filter.status !== undefined) where.status = filter.status;\n if (filter.email !== undefined) where.email = normalizeEmail(filter.email);\n if (filter.source !== undefined) where.source = filter.source;\n\n return await this.#requests.list({\n where,\n limit: filter.limit,\n offset: filter.offset,\n orderBy: filter.orderBy ?? 'created_at DESC',\n });\n }\n\n /**\n * Get a single access request by id (operator-facing). Requires the\n * `access-requests:read` capability when an authorizer is configured.\n */\n async getAccessRequest(\n id: string,\n options: { by?: string | null } = {},\n ): Promise<AccessRequest | null> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.READ, {\n by: options.by,\n accessRequestId: id,\n });\n return await this.#requests.get(id);\n }\n\n // ============= Operator decisions =============\n\n /**\n * Approve a request: `REQUESTED → APPROVED`. Idempotent (re-approving an\n * already-`APPROVED` request is a no-op returning it). Requires\n * `access-requests:manage`.\n *\n * @throws {@link AccessRequestError} (`INVALID_TRANSITION`) from a terminal state.\n */\n async approveAccessRequest(\n id: string,\n options: ApproveAccessRequestOptions = {},\n ): Promise<AccessRequest> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.MANAGE, {\n by: options.by,\n accessRequestId: id,\n });\n\n const request = await this.#load(id);\n if (request.status === AccessRequestStatus.APPROVED) return request;\n this.#assertTransition(request, APPROVE_FROM, 'approve');\n\n request.status = AccessRequestStatus.APPROVED;\n request.decidedAt = new Date();\n if (options.by) request.decidedBy = options.by;\n if (options.note != null) request.note = options.note;\n await request.save();\n\n await this.#emit({\n type: 'access-request.approved',\n accessRequest: request,\n at: new Date(),\n by: options.by,\n });\n return request;\n }\n\n /**\n * Decline a request: `REQUESTED | APPROVED → DECLINED`. Idempotent. Requires\n * `access-requests:manage`.\n *\n * @throws {@link AccessRequestError} (`INVALID_TRANSITION`) from a terminal state.\n */\n async declineAccessRequest(\n id: string,\n options: DeclineAccessRequestOptions = {},\n ): Promise<AccessRequest> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.MANAGE, {\n by: options.by,\n accessRequestId: id,\n });\n\n const request = await this.#load(id);\n if (request.status === AccessRequestStatus.DECLINED) return request;\n this.#assertTransition(request, DECLINE_FROM, 'decline');\n\n request.status = AccessRequestStatus.DECLINED;\n request.decidedAt = new Date();\n if (options.by) request.decidedBy = options.by;\n if (options.reason != null) request.note = options.reason;\n await request.save();\n\n await this.#emit({\n type: 'access-request.declined',\n accessRequest: request,\n at: new Date(),\n by: options.by,\n });\n return request;\n }\n\n /**\n * Cancel a request: `REQUESTED | APPROVED → CANCELED`. Idempotent. Requires\n * `access-requests:manage`.\n *\n * @throws {@link AccessRequestError} (`INVALID_TRANSITION`) from a terminal state.\n */\n async cancelAccessRequest(\n id: string,\n options: CancelAccessRequestOptions = {},\n ): Promise<AccessRequest> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.MANAGE, {\n by: options.by,\n accessRequestId: id,\n });\n\n const request = await this.#load(id);\n if (request.status === AccessRequestStatus.CANCELED) return request;\n this.#assertTransition(request, CANCEL_FROM, 'cancel');\n\n request.status = AccessRequestStatus.CANCELED;\n request.decidedAt = new Date();\n if (options.by) request.decidedBy = options.by;\n if (options.reason != null) request.note = options.reason;\n await request.save();\n\n await this.#emit({\n type: 'access-request.canceled',\n accessRequest: request,\n at: new Date(),\n by: options.by,\n });\n return request;\n }\n\n // ============= Graduation =============\n\n /**\n * Graduate an approved request into a `User`, optionally attaching a tenant.\n *\n * Valid from `APPROVED` (or from `REQUESTED` when\n * {@link GraduateAccessRequestOptions.allowFromRequested} is set). Creates a\n * user when none exists for the email, or **links** the existing one\n * otherwise (reusing {@link UserCollection}). Idempotent: a second call on an\n * already-`GRADUATED` request returns the same user (and an existing\n * membership for the requested tenant, if any) without re-creating anything.\n *\n * Requires `access-requests:manage`.\n *\n * @throws {@link AccessRequestError} — `NOT_FOUND` (unknown id),\n * `INVALID_TRANSITION` (terminal/declined/canceled or `REQUESTED` without\n * `allowFromRequested`), `TENANT_NOT_FOUND`, or `ROLE_NOT_FOUND`.\n */\n async graduateAccessRequest(\n id: string,\n options: GraduateAccessRequestOptions = {},\n ): Promise<GraduateAccessRequestResult> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.MANAGE, {\n by: options.by,\n accessRequestId: id,\n });\n\n const request = await this.#load(id);\n const tenantOption = options.tenant ?? 'none';\n\n // Idempotent re-graduation: return the same user (and existing membership\n // for the requested tenant, if resolvable) without mutating anything.\n if (\n request.status === AccessRequestStatus.GRADUATED &&\n request.resultingUserId\n ) {\n const existingUser = await this.#users.get(request.resultingUserId);\n if (existingUser) {\n const membership = await this.#resolveExistingMembership(\n existingUser.id as string,\n tenantOption,\n );\n return {\n user: existingUser,\n membership: membership ?? undefined,\n accessRequest: request,\n created: false,\n };\n }\n // resultingUserId is stale/missing — fall through and re-link by email.\n }\n\n // State-machine guard for a fresh graduation.\n if (request.status !== AccessRequestStatus.GRADUATED) {\n const allowFromRequested = options.allowFromRequested ?? false;\n const canGraduate =\n request.status === AccessRequestStatus.APPROVED ||\n (request.status === AccessRequestStatus.REQUESTED &&\n allowFromRequested);\n if (!canGraduate) {\n const hint =\n request.status === AccessRequestStatus.REQUESTED\n ? ' (approve it first, or pass allowFromRequested)'\n : '';\n throw new AccessRequestError(\n `Cannot graduate an access request in status \"${request.status}\"${hint}.`,\n 'INVALID_TRANSITION',\n );\n }\n }\n\n // Validate the tenant option BEFORE any write (user / tenant / membership):\n // an invalid role slug or missing tenant must fail fast so graduation never\n // leaves an orphan user or tenant behind (codex review #1713).\n if (tenantOption !== 'none') {\n await this.#ensureRolesSeeded();\n await this.#validateTenantOption(tenantOption);\n }\n\n // Create-or-link the user by normalized email. A brand-new user defaults to\n // ACTIVE (or `options.activate`). An existing linked user keeps its current\n // status unless the operator *explicitly* passes `activate` — so graduation\n // never silently un-suspends an already-managed account.\n const email = normalizeEmail(request.email);\n let user = await this.#users.findByEmail(email);\n let created = false;\n if (!user) {\n user = await this.#users.create({\n email,\n status: options.activate ?? UserStatus.ACTIVE,\n });\n created = true;\n } else if (\n options.activate !== undefined &&\n user.status !== options.activate\n ) {\n user.status = options.activate;\n await user.save();\n }\n\n // Attach a tenant if requested.\n let membership: Membership | undefined;\n let tenant: Tenant | undefined;\n if (tenantOption !== 'none') {\n const attached = await this.#attachTenant(\n user.id as string,\n tenantOption,\n );\n membership = attached.membership;\n tenant = attached.tenant;\n }\n\n // Mark graduated.\n request.status = AccessRequestStatus.GRADUATED;\n request.resultingUserId = user.id as string;\n request.decidedAt = new Date();\n if (options.by) request.decidedBy = options.by;\n if (options.note != null) request.note = options.note;\n await request.save();\n\n await this.#emit({\n type: 'access-request.graduated',\n accessRequest: request,\n at: new Date(),\n by: options.by,\n user,\n membership,\n tenant,\n });\n\n return { user, membership, tenant, accessRequest: request, created };\n }\n\n // ============= Internals =============\n\n /**\n * Load a request or throw `NOT_FOUND`.\n */\n async #load(id: string): Promise<AccessRequest> {\n const request = await this.#requests.get(id);\n if (!request) {\n throw new AccessRequestError('Access request not found.', 'NOT_FOUND');\n }\n return request;\n }\n\n /**\n * Guard a state transition; throw `INVALID_TRANSITION` if the current status\n * is not an allowed source.\n */\n #assertTransition(\n request: AccessRequest,\n allowedFrom: AccessRequestStatus[],\n action: string,\n ): void {\n if (!allowedFrom.includes(request.status)) {\n throw new AccessRequestError(\n `Cannot ${action} an access request in status \"${request.status}\".`,\n 'INVALID_TRANSITION',\n );\n }\n }\n\n /**\n * Run the configured authorizer (if any). Absent an authorizer, operator\n * methods are ungated — see the class-level security note.\n */\n async #requireCapability(\n capability: AccessRequestCapability,\n context: { by?: string | null; accessRequestId?: string },\n ): Promise<void> {\n if (!this.#authorize) return;\n await this.#authorize({\n capability,\n by: context.by ?? null,\n accessRequestId: context.accessRequestId,\n });\n }\n\n /**\n * Best-effort event delivery — a throwing handler is logged and swallowed so\n * it cannot roll back an already-persisted transition.\n */\n async #emit(event: AccessRequestEvent): Promise<void> {\n if (!this.#onEvent) return;\n try {\n await this.#onEvent(event);\n } catch (error) {\n logger.error(\n `AccessRequest event handler threw for \"${event.type}\" (request ${event.accessRequest.id})`,\n { error },\n );\n }\n }\n\n /**\n * Throw `ROLE_NOT_FOUND` if no role with `roleSlug` is resolvable (tenant-\n * specific first, then system). Roles must already be seeded.\n */\n async #assertRoleExists(roleSlug: string, tenantId?: string): Promise<void> {\n if (!(await this.#roles.findBySlug(roleSlug, tenantId))) {\n throw new AccessRequestError(\n `Role \"${roleSlug}\" not found — seed system roles or pass a valid role slug.`,\n 'ROLE_NOT_FOUND',\n );\n }\n }\n\n /**\n * Validate a graduation tenant option with NO side effects: the target tenant\n * must exist (existing-tenant variant) and the role slug must resolve. Run\n * before any persistence so a bad option can't leave orphan rows.\n */\n async #validateTenantOption(\n option: GraduateNewTenantOption | GraduateExistingTenantOption,\n ): Promise<void> {\n if ('tenantId' in option) {\n const tenant = await this.#tenants.get(option.tenantId);\n if (!tenant) {\n throw new AccessRequestError(\n `Target tenant \"${option.tenantId}\" not found.`,\n 'TENANT_NOT_FOUND',\n );\n }\n await this.#assertRoleExists(\n option.role ?? DEFAULT_ROLE_SLUGS.MEMBER,\n option.tenantId,\n );\n } else {\n // A brand-new tenant has no tenant-specific roles yet, so the role must\n // resolve as a system role.\n await this.#assertRoleExists(option.role ?? DEFAULT_ROLE_SLUGS.OWNER);\n }\n }\n\n /**\n * Create-or-attach the requester to a tenant, reusing the tenant / role /\n * membership collections (no duplication of user/membership logic).\n */\n async #attachTenant(\n userId: string,\n option: GraduateNewTenantOption | GraduateExistingTenantOption,\n ): Promise<{ tenant: Tenant; membership: Membership }> {\n await this.#ensureRolesSeeded();\n\n if ('tenantId' in option) {\n const tenant = await this.#tenants.get(option.tenantId);\n if (!tenant) {\n throw new AccessRequestError(\n `Target tenant \"${option.tenantId}\" not found.`,\n 'TENANT_NOT_FOUND',\n );\n }\n const membership = await this.#getOrCreateMembership(\n userId,\n tenant.id as string,\n option.role ?? DEFAULT_ROLE_SLUGS.MEMBER,\n );\n return { tenant, membership };\n }\n\n const tenant = await this.#tenants.create({\n name: option.create.name,\n slug: option.create.slug,\n description: option.create.description,\n status: option.create.status,\n });\n const membership = await this.#getOrCreateMembership(\n userId,\n tenant.id as string,\n option.role ?? DEFAULT_ROLE_SLUGS.OWNER,\n );\n return { tenant, membership };\n }\n\n /**\n * Resolve (creating if needed) the user's membership in a tenant with the\n * given role slug. Idempotent — returns any existing membership for the pair.\n */\n async #getOrCreateMembership(\n userId: string,\n tenantId: string,\n roleSlug: string,\n ): Promise<Membership> {\n const existing = await this.#memberships.findByUserAndTenant(\n userId,\n tenantId,\n );\n if (existing) return existing;\n\n const role = await this.#roles.findBySlug(roleSlug, tenantId);\n if (!role) {\n throw new AccessRequestError(\n `Role \"${roleSlug}\" not found — seed system roles or pass a valid role slug.`,\n 'ROLE_NOT_FOUND',\n );\n }\n\n return await this.#memberships.create({\n userId,\n tenantId,\n roleId: role.id as string,\n status: MembershipStatus.ACTIVE,\n });\n }\n\n /**\n * For idempotent re-graduation: find an existing membership for the requested\n * tenant. Only the existing-tenant variant is resolvable (a `{ create }`\n * variant has no known tenant id on a re-call).\n */\n async #resolveExistingMembership(\n userId: string,\n tenantOption: GraduateTenantOption,\n ): Promise<Membership | null> {\n if (tenantOption === 'none' || !('tenantId' in tenantOption)) return null;\n return await this.#memberships.findByUserAndTenant(\n userId,\n tenantOption.tenantId,\n );\n }\n\n /**\n * Lazily seed the default system roles (owner/admin/member/viewer) the first\n * time graduation attaches a tenant — so the public create path never incurs\n * the write.\n */\n async #ensureRolesSeeded(): Promise<void> {\n if (this.#rolesSeeded) return;\n await this.#roles.seedSystemRoles();\n this.#rolesSeeded = true;\n }\n}\n","/**\n * MagicLinkService - Passwordless email authentication via signed magic link tokens\n *\n * This service handles token generation and verification for magic link\n * authentication. It is framework-agnostic: it returns tokens and verification\n * results, leaving email delivery and cookie management to the caller.\n *\n * Flow:\n * 1. User enters email → generate(email) → token + expiresAt\n * 2. Caller sends email with link containing the token\n * 3. User clicks link → verify(token) → { email, nonce }\n * 4. Caller creates session (via SessionService or createSessionCookie)\n *\n * @packageDocumentation\n *\n * @example\n * ```typescript\n * const magicLink = await MagicLinkService.create({\n * db: { type: 'postgres', url: process.env.DATABASE_URL },\n * secret: process.env.AUTH_SECRET,\n * });\n *\n * // Generate a magic link token\n * const { token, expiresAt } = await magicLink.generate('user@example.com');\n *\n * // Verify a magic link token (single-use)\n * const { email, nonce } = await magicLink.verify(token);\n * ```\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { UsersMagicLinkTokenCollection } from '../collections/MagicLinkTokenCollection.js';\nimport { DEFAULT_TOKEN_EXPIRY_SECONDS } from '../models/MagicLinkToken.js';\nimport { isValidEmail, normalizeEmail } from '../models/User.js';\n\n/**\n * Options for MagicLinkService\n */\nexport interface MagicLinkServiceOptions extends SmrtClassOptions {\n /** Secret used to derive the HMAC signing key (required) */\n secret: string;\n /** Token expiry in seconds (default: 10 minutes) */\n tokenExpiry?: number;\n /** JWT issuer claim (default: 'smrt:magiclink') */\n issuer?: string;\n}\n\n/**\n * Result of generating a magic link token\n */\nexport interface MagicLinkResult {\n /** The signed JWT token */\n token: string;\n /** When the token expires */\n expiresAt: Date;\n}\n\n/**\n * Result of verifying a magic link token\n */\nexport interface MagicLinkVerifyResult {\n /** The email address from the token */\n email: string;\n /** The nonce (for correlation/logging) */\n nonce: string;\n}\n\n/**\n * Error class for magic link authentication failures\n */\nexport class MagicLinkError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'MagicLinkError';\n }\n}\n\n/**\n * MagicLinkService provides passwordless authentication via email magic links.\n *\n * Tokens are HMAC-signed JWTs with embedded nonces for replay protection.\n * The service is framework-agnostic — it does not send emails or set cookies.\n */\nexport class MagicLinkService {\n private tokenCollection!: UsersMagicLinkTokenCollection;\n private signingKey: Uint8Array | null = null;\n private readonly secret: string;\n private readonly tokenExpiry: number;\n private readonly issuer: string;\n private readonly options: MagicLinkServiceOptions;\n\n constructor(options: MagicLinkServiceOptions) {\n if (!options.secret) {\n throw new Error('MagicLinkService requires a secret for token signing');\n }\n this.secret = options.secret;\n this.tokenExpiry = options.tokenExpiry ?? DEFAULT_TOKEN_EXPIRY_SECONDS;\n this.issuer = options.issuer ?? 'smrt:magiclink';\n this.options = options;\n }\n\n /**\n * Initialize collections\n */\n async initialize(): Promise<void> {\n this.tokenCollection = await UsersMagicLinkTokenCollection.create(\n this.options,\n );\n }\n\n /**\n * Derive the HMAC signing key from the secret\n */\n private async getSigningKey(): Promise<Uint8Array> {\n if (this.signingKey) return this.signingKey;\n\n const encoder = new TextEncoder();\n const data = encoder.encode(`magiclink:${this.secret}`);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n this.signingKey = new Uint8Array(hashBuffer);\n\n return this.signingKey;\n }\n\n /**\n * Generate a magic link token for the given email.\n *\n * Stores a nonce in the database for replay protection.\n * The caller is responsible for emailing the token to the user.\n */\n async generate(email: string): Promise<MagicLinkResult> {\n // Dynamic import to avoid eagerly loading jose at module init\n const { SignJWT } = await import('jose');\n\n const key = await this.getSigningKey();\n const nonce = crypto.randomUUID();\n const normalizedEmail = normalizeEmail(email);\n\n if (!isValidEmail(normalizedEmail)) {\n throw new MagicLinkError('Invalid email address');\n }\n\n const expiresAt = new Date(Date.now() + this.tokenExpiry * 1000);\n\n // Store nonce for replay protection\n await this.tokenCollection.create({\n nonce,\n email: normalizedEmail,\n used: false,\n expiresAt,\n });\n\n // Sign the token\n const token = await new SignJWT({\n email: normalizedEmail,\n nonce,\n })\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(`${this.tokenExpiry}s`)\n .setIssuer(this.issuer)\n .sign(key);\n\n return { token, expiresAt };\n }\n\n /**\n * Verify a magic link token.\n *\n * Checks JWT signature, expiry, and that the nonce hasn't been used.\n * Marks the nonce as used on success (single-use enforcement).\n *\n * @throws {MagicLinkError} If the token is invalid, expired, or already used\n */\n async verify(token: string): Promise<MagicLinkVerifyResult> {\n const { jwtVerify, errors } = await import('jose');\n\n const key = await this.getSigningKey();\n\n // Verify JWT signature and expiry\n let payload: Record<string, unknown>;\n try {\n const result = await jwtVerify(token, key, {\n issuer: this.issuer,\n });\n payload = result.payload as Record<string, unknown>;\n } catch (err) {\n if (err instanceof errors.JWTExpired) {\n throw new MagicLinkError('Token has expired');\n }\n throw new MagicLinkError('Invalid token');\n }\n\n const email = payload.email;\n const nonce = payload.nonce;\n\n // Explicit type validation — JWT payload values are unknown\n if (typeof email !== 'string' || typeof nonce !== 'string') {\n throw new MagicLinkError('Invalid token payload');\n }\n\n // Atomically claim the nonce (single-use enforcement) via a single\n // conditional UPDATE. Only one concurrent verify() call can flip the\n // row from used=false to used=true before the DB expires the nonce.\n const claimed = await this.tokenCollection.markUsed(nonce);\n\n if (!claimed) {\n throw new MagicLinkError('Token has already been used or has expired');\n }\n\n return { email: normalizeEmail(email), nonce };\n }\n\n /**\n * Clean up expired tokens (run periodically)\n */\n async cleanupExpiredTokens(): Promise<number> {\n return this.tokenCollection.deleteExpired();\n }\n\n /**\n * Static factory method\n */\n static async create(\n options: MagicLinkServiceOptions,\n ): Promise<MagicLinkService> {\n const service = new MagicLinkService(options);\n await service.initialize();\n return service;\n }\n}\n","/**\n * Postgres policy generation and application for SMRT permissions\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport {\n findManifestEntryByQualifiedName,\n ObjectRegistry,\n type SmrtClassOptions,\n} from '@happyvertical/smrt-core';\nimport { PermissionCollection } from '../collections/PermissionCollection.js';\nimport {\n PermissionCatalogService,\n type PostgresPermissionAction,\n type PostgresPermissionBinding,\n} from './PermissionCatalogService.js';\n\ninterface QueryableDatabase {\n query: (sql: string, ...params: unknown[]) => Promise<unknown>;\n url?: string;\n}\n\ntype DatabaseConfig = SmrtClassOptions['db'] | SmrtClassOptions['persistence'];\n\ninterface TablePolicyTarget {\n actions: Map<PostgresPermissionAction, Set<string>>;\n className?: string;\n collection?: string;\n qualifiedName?: string;\n schemaName: string;\n tableName: string;\n tenantField: string;\n}\n\nexport interface PostgresPermissionPolicyReportItem {\n className?: string;\n collection?: string;\n qualifiedName?: string;\n reason: string;\n schemaName?: string;\n tableName?: string;\n}\n\nexport interface PostgresPermissionPolicyTarget {\n actions: Partial<Record<PostgresPermissionAction, string[]>>;\n className?: string;\n collection?: string;\n qualifiedName?: string;\n schemaName: string;\n tableName: string;\n tenantField: string;\n}\n\nexport interface GeneratePostgresPermissionSqlResult {\n bindings: PostgresPermissionBinding[];\n skipped: PostgresPermissionPolicyReportItem[];\n sql: string;\n statements: string[];\n targets: PostgresPermissionPolicyTarget[];\n}\n\nfunction toSnakeCase(value: string): string {\n return value\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[-\\s]+/g, '_')\n .toLowerCase();\n}\n\nfunction quoteIdent(identifier: string): string {\n return `\"${identifier.replaceAll('\"', '\"\"')}\"`;\n}\n\nfunction quoteLiteral(value: string): string {\n return `'${value.replaceAll(\"'\", \"''\")}'`;\n}\n\nfunction isProbablyPostgres(\n configDb: DatabaseConfig,\n database: QueryableDatabase,\n): boolean {\n if (\n configDb &&\n typeof configDb === 'object' &&\n !('query' in configDb) &&\n 'type' in configDb &&\n configDb.type === 'postgres'\n ) {\n return true;\n }\n\n if (typeof database.url === 'string' && database.url.startsWith('postgres')) {\n return true;\n }\n\n return (database.constructor?.name || '').toLowerCase().includes('postgres');\n}\n\nfunction normalizePostgresPermissionAction(\n action: PostgresPermissionBinding['action'],\n): PostgresPermissionAction {\n const normalized = action.toUpperCase();\n if (\n normalized === 'SELECT' ||\n normalized === 'INSERT' ||\n normalized === 'UPDATE' ||\n normalized === 'DELETE'\n ) {\n return normalized;\n }\n\n throw new Error(\n `Invalid Postgres permission binding action \"${action}\". Expected one of SELECT, INSERT, UPDATE, DELETE.`,\n );\n}\n\nfunction normalizePostgresPermissionBinding(\n binding: PostgresPermissionBinding,\n fallbackPermission?: string,\n): PostgresPermissionBinding {\n const tableName = binding.tableName?.trim();\n if (!tableName) {\n throw new Error(\n 'Postgres permission binding is missing a tableName value.',\n );\n }\n\n const permission = binding.permission ?? fallbackPermission;\n if (!permission) {\n throw new Error(\n 'Postgres permission binding is missing a permission value.',\n );\n }\n\n return {\n action: normalizePostgresPermissionAction(binding.action),\n permission,\n schemaName: binding.schemaName,\n tableName,\n tenantField: binding.tenantField,\n };\n}\n\nfunction parseTableReference(\n binding: Pick<PostgresPermissionBinding, 'schemaName' | 'tableName'>,\n): { schemaName: string; tableName: string } {\n if (binding.tableName.includes('.')) {\n const [schemaName, tableName] = binding.tableName.split('.', 2);\n return {\n schemaName,\n tableName,\n };\n }\n\n return {\n schemaName: binding.schemaName ?? 'public',\n tableName: binding.tableName,\n };\n}\n\nfunction buildPolicyName(\n tableName: string,\n action: PostgresPermissionAction,\n): string {\n const actionSegment = action.toLowerCase();\n const hash = createHash('sha1')\n .update(`${tableName}:${action}`)\n .digest('hex')\n .slice(0, 8);\n const sanitizedTable = tableName\n .replace(/[^a-zA-Z0-9_]+/g, '_')\n .replace(/^_+|_+$/g, '');\n const prefix = 'smrt_';\n const separatorLength = 2;\n const maxTableSegmentLength =\n 63 - prefix.length - actionSegment.length - hash.length - separatorLength;\n const tableSegment = (sanitizedTable || 'table').slice(\n 0,\n Math.max(maxTableSegmentLength, 1),\n );\n\n return `${prefix}${tableSegment}_${actionSegment}_${hash}`;\n}\n\nfunction buildPermissionExpression(permissionSlugs: string[]): string {\n if (permissionSlugs.length === 0) {\n return 'FALSE';\n }\n\n return permissionSlugs\n .map((permission) => `smrt_has_permission(${quoteLiteral(permission)})`)\n .join(' OR ');\n}\n\nfunction buildTenantMatchExpression(tenantField: string): string {\n return `${quoteIdent(tenantField)}::text = smrt_current_tenant_id()`;\n}\n\nfunction buildSelectPolicySql(\n target: TablePolicyTarget,\n permissions: string[],\n): string[] {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n const policyName = buildPolicyName(target.tableName, 'SELECT');\n const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;\n\n return [\n `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,\n `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR SELECT USING (${condition})`,\n ];\n}\n\nfunction buildInsertPolicySql(\n target: TablePolicyTarget,\n permissions: string[],\n): string[] {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n const policyName = buildPolicyName(target.tableName, 'INSERT');\n const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;\n\n return [\n `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,\n `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR INSERT WITH CHECK (${condition})`,\n ];\n}\n\nfunction buildUpdatePolicySql(\n target: TablePolicyTarget,\n permissions: string[],\n): string[] {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n const policyName = buildPolicyName(target.tableName, 'UPDATE');\n const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;\n\n return [\n `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,\n `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR UPDATE USING (${condition}) WITH CHECK (${condition})`,\n ];\n}\n\nfunction buildDeletePolicySql(\n target: TablePolicyTarget,\n permissions: string[],\n): string[] {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n const policyName = buildPolicyName(target.tableName, 'DELETE');\n const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;\n\n return [\n `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,\n `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR DELETE USING (${condition})`,\n ];\n}\n\nfunction buildHelperStatements(): string[] {\n return [\n [\n 'CREATE OR REPLACE FUNCTION smrt_rls_bypass()',\n 'RETURNS boolean',\n 'LANGUAGE sql',\n 'STABLE',\n 'AS $$',\n \" SELECT COALESCE(NULLIF(current_setting('smrt.system_context', true), ''), 'false')::boolean\",\n \" OR COALESCE(NULLIF(current_setting('smrt.super_admin_bypass', true), ''), 'false')::boolean\",\n '$$',\n ].join('\\n'),\n [\n 'CREATE OR REPLACE FUNCTION smrt_current_tenant_id()',\n 'RETURNS text',\n 'LANGUAGE sql',\n 'STABLE',\n 'AS $$',\n \" SELECT NULLIF(current_setting('smrt.tenant_id', true), '')\",\n '$$',\n ].join('\\n'),\n [\n 'CREATE OR REPLACE FUNCTION smrt_has_permission(required_permission text)',\n 'RETURNS boolean',\n 'LANGUAGE sql',\n 'STABLE',\n 'AS $$',\n ' SELECT smrt_rls_bypass()',\n \" OR jsonb_exists(COALESCE(NULLIF(current_setting('smrt.permissions', true), ''), '[]')::jsonb, required_permission)\",\n '$$',\n ].join('\\n'),\n ];\n}\n\nfunction addBindingToTarget(\n targets: Map<string, TablePolicyTarget>,\n binding: PostgresPermissionBinding,\n source: Pick<\n TablePolicyTarget,\n 'className' | 'collection' | 'qualifiedName'\n > = {},\n): void {\n const { schemaName, tableName } = parseTableReference(binding);\n const targetKey = `${schemaName}.${tableName}`;\n const existing = targets.get(targetKey);\n const tenantField = binding.tenantField ?? 'tenant_id';\n\n if (existing && existing.tenantField !== tenantField) {\n throw new Error(\n `Conflicting tenant fields for table '${targetKey}': '${existing.tenantField}' !== '${tenantField}'`,\n );\n }\n\n const target = existing ?? {\n actions: new Map<PostgresPermissionAction, Set<string>>(),\n ...source,\n schemaName,\n tableName,\n tenantField,\n };\n const action = normalizePostgresPermissionAction(binding.action);\n const permissions = target.actions.get(action) ?? new Set<string>();\n if (binding.permission) {\n permissions.add(binding.permission);\n }\n target.actions.set(action, permissions);\n targets.set(targetKey, target);\n}\n\nexport function generatePostgresPermissionSql(\n options: SmrtClassOptions = {},\n): GeneratePostgresPermissionSqlResult {\n const catalogService = PermissionCatalogService.create(options);\n const catalog = catalogService.getCatalog();\n const config = catalogService.getUsersConfig();\n const candidateTargets = new Map<string, TablePolicyTarget>();\n const skipped: PostgresPermissionPolicyReportItem[] = [];\n\n const autoCandidates = new Map<\n string,\n Array<{\n className?: string;\n collection: string;\n qualifiedName?: string;\n schemaName: string;\n tableName: string;\n tenantField: string;\n }>\n >();\n\n for (const metadata of ObjectRegistry.getAllObjectMetadata()) {\n const registered =\n ObjectRegistry.getClassByConstructor(metadata.constructor) ??\n ObjectRegistry.getClass(metadata.name);\n const tenantScoped = registered?.tenantScopedConfig;\n const manifestEntry = registered?.qualifiedName\n ? findManifestEntryByQualifiedName(registered.qualifiedName)\n : undefined;\n\n if (!tenantScoped) {\n skipped.push({\n className: metadata.name,\n qualifiedName: registered?.qualifiedName,\n reason: 'not tenant-scoped',\n });\n continue;\n }\n\n if (tenantScoped.mode !== 'required') {\n skipped.push({\n className: metadata.name,\n qualifiedName: registered?.qualifiedName,\n reason: `tenant mode '${tenantScoped.mode}' is not supported for automatic Postgres RLS generation`,\n });\n continue;\n }\n\n const rawTableName =\n registered?.schema?.tableName ?? manifestEntry?.schema?.tableName;\n if (!rawTableName) {\n skipped.push({\n className: metadata.name,\n qualifiedName: registered?.qualifiedName,\n reason: 'no schema table name available',\n });\n continue;\n }\n\n const parsedTable = parseTableReference({\n tableName: rawTableName,\n });\n const tableKey = `${parsedTable.schemaName}.${parsedTable.tableName}`;\n const objectConfig = manifestEntry?.decoratorConfig ?? metadata.config;\n const rawCollection = (objectConfig as { collection?: unknown } | undefined)\n ?.collection;\n const configuredCollection =\n typeof rawCollection === 'string' && rawCollection.length > 0\n ? rawCollection\n : undefined;\n const collection =\n configuredCollection ??\n manifestEntry?.collection ??\n `${toSnakeCase(metadata.name)}s`;\n\n const entries = autoCandidates.get(tableKey) ?? [];\n entries.push({\n className: metadata.name,\n collection,\n qualifiedName: registered?.qualifiedName,\n schemaName: parsedTable.schemaName,\n tableName: parsedTable.tableName,\n tenantField: toSnakeCase(tenantScoped.field),\n });\n autoCandidates.set(tableKey, entries);\n }\n\n for (const [tableKey, entries] of autoCandidates) {\n if (entries.length > 1) {\n for (const entry of entries) {\n skipped.push({\n className: entry.className,\n collection: entry.collection,\n qualifiedName: entry.qualifiedName,\n reason: `table '${tableKey}' is shared by multiple objects, so automatic policy generation was skipped`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n });\n }\n continue;\n }\n\n const entry = entries[0];\n addBindingToTarget(\n candidateTargets,\n {\n action: 'SELECT',\n permission: `${entry.collection}.read`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n tenantField: entry.tenantField,\n },\n entry,\n );\n addBindingToTarget(\n candidateTargets,\n {\n action: 'INSERT',\n permission: `${entry.collection}.create`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n tenantField: entry.tenantField,\n },\n entry,\n );\n addBindingToTarget(\n candidateTargets,\n {\n action: 'UPDATE',\n permission: `${entry.collection}.update`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n tenantField: entry.tenantField,\n },\n entry,\n );\n addBindingToTarget(\n candidateTargets,\n {\n action: 'DELETE',\n permission: `${entry.collection}.delete`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n tenantField: entry.tenantField,\n },\n entry,\n );\n }\n\n const explicitBindings: PostgresPermissionBinding[] = [];\n for (const definition of catalog.permissions) {\n for (const binding of definition.postgres?.bindings ?? []) {\n explicitBindings.push(\n normalizePostgresPermissionBinding(binding, definition.slug),\n );\n }\n }\n for (const binding of config.permissions?.postgres?.bindings ?? []) {\n explicitBindings.push(normalizePostgresPermissionBinding(binding));\n }\n\n for (const binding of explicitBindings) {\n addBindingToTarget(candidateTargets, binding);\n }\n\n const statements = [...buildHelperStatements()];\n const targets = Array.from(candidateTargets.values())\n .sort((left, right) =>\n `${left.schemaName}.${left.tableName}`.localeCompare(\n `${right.schemaName}.${right.tableName}`,\n ),\n )\n .map((target) => ({\n actions: Object.fromEntries(\n Array.from(target.actions.entries()).map(([action, permissions]) => [\n action,\n Array.from(permissions).sort(),\n ]),\n ) as Partial<Record<PostgresPermissionAction, string[]>>,\n className: target.className,\n collection: target.collection,\n qualifiedName: target.qualifiedName,\n schemaName: target.schemaName,\n tableName: target.tableName,\n tenantField: target.tenantField,\n }));\n\n for (const target of Array.from(candidateTargets.values())) {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY`);\n statements.push(`ALTER TABLE ${qualifiedTable} FORCE ROW LEVEL SECURITY`);\n\n const selectPermissions = Array.from(\n target.actions.get('SELECT') ?? [],\n ).sort();\n const insertPermissions = Array.from(\n target.actions.get('INSERT') ?? [],\n ).sort();\n const updatePermissions = Array.from(\n target.actions.get('UPDATE') ?? [],\n ).sort();\n const deletePermissions = Array.from(\n target.actions.get('DELETE') ?? [],\n ).sort();\n\n if (selectPermissions.length > 0) {\n statements.push(...buildSelectPolicySql(target, selectPermissions));\n }\n if (insertPermissions.length > 0) {\n statements.push(...buildInsertPolicySql(target, insertPermissions));\n }\n if (updatePermissions.length > 0) {\n statements.push(...buildUpdatePolicySql(target, updatePermissions));\n }\n if (deletePermissions.length > 0) {\n statements.push(...buildDeletePolicySql(target, deletePermissions));\n }\n }\n\n return {\n bindings: explicitBindings,\n skipped,\n sql: `${statements.join(';\\n')};\\n`,\n statements,\n targets,\n };\n}\n\nexport async function applyPostgresPermissionPolicies(\n options: SmrtClassOptions = {},\n): Promise<GeneratePostgresPermissionSqlResult> {\n const permissions = await PermissionCollection.create(options);\n const databaseOptions = options.db ?? options.persistence;\n if (\n !isProbablyPostgres(databaseOptions, permissions.db as QueryableDatabase)\n ) {\n throw new Error(\n 'applyPostgresPermissionPolicies() requires a Postgres database connection.',\n );\n }\n\n const result = generatePostgresPermissionSql(options);\n for (const statement of result.statements) {\n try {\n await permissions.db.query(statement);\n } catch (error) {\n throw new Error(\n `Failed to apply Postgres permission policy statement:\\n${statement}`,\n {\n cause: error,\n },\n );\n }\n }\n\n return result;\n}\n","/**\n * TenantService - Manages tenant creation with configurable policies\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { MembershipCollection } from '../collections/MembershipCollection.js';\nimport { RoleCollection } from '../collections/RoleCollection.js';\nimport { TenantCollection } from '../collections/TenantCollection.js';\nimport type { Membership } from '../models/Membership.js';\nimport type { Tenant } from '../models/Tenant.js';\nimport {\n DEFAULT_ROLE_SLUGS,\n DEFAULT_TENANT_POLICY,\n MembershipStatus,\n type TenantPolicy,\n} from '../types/index.js';\n\n/**\n * Result of tenant creation with ownership\n */\nexport interface TenantWithOwnershipResult {\n tenant: Tenant;\n membership: Membership;\n}\n\n/**\n * Result of ensureTenantForUser\n */\nexport interface EnsureTenantResult {\n /** The tenant (null if flexible mode and no existing tenant) */\n tenant: Tenant | null;\n /** The membership (null if no tenant) */\n membership: Membership | null;\n /** Whether a new tenant was created */\n created: boolean;\n}\n\n/**\n * TenantService manages tenant creation with configurable policies.\n *\n * Policies:\n * - `flexible`: No tenant created on signup, user can have zero tenants\n * - `personal`: Auto-create personal tenant on first login, can delete all\n * - `required`: Auto-create personal tenant, must keep at least one\n *\n * @example\n * ```typescript\n * const tenantService = new TenantService(options, {\n * mode: 'personal',\n * maxTenants: 5,\n * defaultName: 'My Workspace',\n * });\n * await tenantService.initialize();\n *\n * // During OIDC login\n * const { tenant, membership, created } = await tenantService.ensureTenantForUser(\n * user.id,\n * { email: user.email, name: user.name }\n * );\n *\n * // Creating additional tenants\n * if (await tenantService.canCreateTenant(user.id)) {\n * const { tenant } = await tenantService.createTenantWithOwnership(\n * user.id,\n * 'New Organization'\n * );\n * }\n * ```\n */\nexport class TenantService {\n private options: SmrtClassOptions;\n private policy: TenantPolicy;\n private tenantCollection!: TenantCollection;\n private membershipCollection!: MembershipCollection;\n private roleCollection!: RoleCollection;\n\n constructor(options: SmrtClassOptions, policy?: TenantPolicy) {\n this.options = options;\n this.policy = policy ?? DEFAULT_TENANT_POLICY;\n }\n\n /**\n * Initialize collections\n */\n async initialize(): Promise<void> {\n this.tenantCollection = await TenantCollection.create(this.options);\n this.membershipCollection = await MembershipCollection.create(this.options);\n this.roleCollection = await RoleCollection.create(this.options);\n\n // Seed system roles if needed\n await this.roleCollection.seedSystemRoles();\n }\n\n /**\n * Get the current policy\n */\n getPolicy(): TenantPolicy {\n return { ...this.policy };\n }\n\n /**\n * Create a tenant and make the user the owner\n *\n * @param userId - The user to make owner\n * @param name - Tenant name\n * @param options - Optional slug override\n * @returns The created tenant and membership\n */\n async createTenantWithOwnership(\n userId: string,\n name: string,\n options?: { slug?: string },\n ): Promise<TenantWithOwnershipResult> {\n // Check if user can create\n if (!(await this.canCreateTenant(userId))) {\n throw new Error(\n `User has reached maximum tenant limit (${this.policy.maxTenants})`,\n );\n }\n\n // Create tenant\n const tenant = await this.tenantCollection.create({\n name,\n slug: options?.slug,\n });\n await tenant.save();\n\n // Get owner role\n const ownerRole = await this.roleCollection.findBySlug(\n DEFAULT_ROLE_SLUGS.OWNER,\n );\n if (!ownerRole) {\n throw new Error('Owner role not found - run seedSystemRoles first');\n }\n\n // Create ownership membership\n const membership = await this.membershipCollection.create({\n userId,\n tenantId: tenant.id as string,\n roleId: ownerRole.id as string,\n status: MembershipStatus.ACTIVE,\n });\n await membership.save();\n\n return { tenant, membership };\n }\n\n /**\n * Check if a user can create a new tenant\n *\n * Returns false if maxTenants limit is reached (0 = unlimited)\n */\n async canCreateTenant(userId: string): Promise<boolean> {\n if (this.policy.maxTenants === 0) {\n return true; // Unlimited\n }\n\n const ownerRole = await this.roleCollection.findBySlug(\n DEFAULT_ROLE_SLUGS.OWNER,\n );\n if (!ownerRole) {\n // Fail closed if owner role is not configured\n return false;\n }\n\n const memberships =\n await this.membershipCollection.findActiveByUser(userId);\n const ownedCount = memberships.filter(\n (m) => m.roleId === ownerRole.id,\n ).length;\n\n return ownedCount < this.policy.maxTenants;\n }\n\n /**\n * Get a specific error message explaining why a tenant cannot be deleted.\n *\n * @returns Error message if deletion is not allowed, or null if allowed.\n */\n private async getDeleteTenantError(\n userId: string,\n tenantId: string,\n ): Promise<string | null> {\n // Check user has an active membership for this tenant\n const membership = await this.membershipCollection.findByUserAndTenant(\n userId,\n tenantId,\n );\n if (!membership || membership.status !== MembershipStatus.ACTIVE) {\n return 'You are not a member of this tenant or it does not exist.';\n }\n\n const ownerRole = await this.roleCollection.findBySlug(\n DEFAULT_ROLE_SLUGS.OWNER,\n );\n if (!ownerRole) {\n return 'Owner role is not configured. Cannot determine deletion permissions.';\n }\n\n if (membership.roleId !== ownerRole.id) {\n return 'Only the tenant owner can delete this tenant.';\n }\n\n // For 'required' policy, check if this is the last tenant\n if (this.policy.mode === 'required') {\n const allMemberships =\n await this.membershipCollection.findActiveByUser(userId);\n const ownerMemberships = allMemberships.filter(\n (m) => m.roleId === ownerRole.id,\n );\n\n if (ownerMemberships.length <= 1) {\n return 'Cannot delete your last tenant. Policy requires at least one tenant.';\n }\n }\n\n return null;\n }\n\n /**\n * Check if a user can delete a specific tenant\n *\n * Returns false if:\n * - User is not an active member\n * - User is not the owner\n * - Policy is 'required' and this is the last tenant\n */\n async canDeleteTenant(userId: string, tenantId: string): Promise<boolean> {\n const error = await this.getDeleteTenantError(userId, tenantId);\n return error === null;\n }\n\n /**\n * Delete a tenant\n *\n * Note: This does not cascade delete related records (memberships, etc.).\n * The caller should handle cleanup of related data if needed.\n *\n * @throws Error if user cannot delete the tenant (with specific reason)\n */\n async deleteTenant(userId: string, tenantId: string): Promise<void> {\n const error = await this.getDeleteTenantError(userId, tenantId);\n if (error) {\n throw new Error(error);\n }\n\n const tenant = await this.tenantCollection.get(tenantId);\n if (tenant) {\n await tenant.delete();\n }\n }\n\n /**\n * Ensure a tenant exists for the user based on policy\n *\n * Called during OIDC login to apply tenant policy:\n * - `flexible`: Returns first existing tenant or null (no auto-create)\n * - `personal`/`required`: Creates default tenant if none exists\n *\n * @param userId - The user ID\n * @param userInfo - User info for naming the auto-created tenant\n * @returns Tenant and membership (may be null in flexible mode)\n */\n async ensureTenantForUser(\n userId: string,\n userInfo: { email?: string; name?: string },\n ): Promise<EnsureTenantResult> {\n // Get existing memberships\n const memberships =\n await this.membershipCollection.findActiveByUser(userId);\n\n // If user has tenants, return the first one\n if (memberships.length > 0) {\n const firstMembership = memberships[0];\n const tenant = await this.tenantCollection.get(\n firstMembership.tenantId as string,\n );\n\n return {\n tenant: tenant ?? null,\n membership: firstMembership,\n created: false,\n };\n }\n\n // No existing tenants - apply policy\n if (this.policy.mode === 'flexible') {\n // Flexible mode: don't auto-create\n return {\n tenant: null,\n membership: null,\n created: false,\n };\n }\n\n // Personal or required mode: create default tenant\n // Use user's name for personalized tenant name, or fall back to policy default\n const tenantName = userInfo.name\n ? `${userInfo.name}'s Workspace`\n : this.policy.defaultName;\n\n const { tenant, membership } = await this.createTenantWithOwnership(\n userId,\n tenantName,\n );\n\n return {\n tenant,\n membership,\n created: true,\n };\n }\n\n /**\n * Get all tenants for a user (where they are owner)\n */\n async getOwnedTenants(userId: string): Promise<Tenant[]> {\n const ownerRole = await this.roleCollection.findBySlug(\n DEFAULT_ROLE_SLUGS.OWNER,\n );\n if (!ownerRole) {\n return [];\n }\n\n const memberships =\n await this.membershipCollection.findActiveByUser(userId);\n const ownerMemberships = memberships.filter(\n (m) => m.roleId === ownerRole.id,\n );\n\n // Fetch all tenants in parallel to avoid N+1 queries\n const tenantPromises = ownerMemberships.map((m) =>\n this.tenantCollection.get(m.tenantId as string),\n );\n const tenantsOrNull = await Promise.all(tenantPromises);\n\n return tenantsOrNull.filter((tenant): tenant is Tenant => tenant !== null);\n }\n\n /**\n * Static factory method\n */\n static async create(\n options: SmrtClassOptions,\n policy?: TenantPolicy,\n ): Promise<TenantService> {\n const service = new TenantService(options, policy);\n await service.initialize();\n return service;\n }\n}\n","/**\n * @happyvertical/smrt-users\n *\n * Multi-tenant user management for the SMRT framework.\n * Provides users, tenants, roles, permissions, and groups.\n *\n * @example\n * ```typescript\n * import {\n * User,\n * UserCollection,\n * Tenant,\n * TenantCollection,\n * Role,\n * RoleCollection,\n * Permission,\n * PermissionCollection,\n * Membership,\n * MembershipCollection,\n * PermissionResolver,\n * UserStatus,\n * MembershipStatus,\n * } from '@happyvertical/smrt-users';\n *\n * // Create collections\n * const users = await UserCollection.create({\n * persistence: { type: 'sql', url: 'app.db' }\n * });\n *\n * // Seed system roles\n * const roles = await RoleCollection.create({ persistence: { type: 'sql', url: 'app.db' } });\n * await roles.seedSystemRoles();\n *\n * // Resolve permissions\n * const resolver = await PermissionResolver.create({ persistence: { type: 'sql', url: 'app.db' } });\n * const hasAccess = await resolver.hasPermission(user.id, tenant.id, 'articles.create');\n *\n * // Sync manifest-derived permissions into the Permission table\n * const syncResult = await syncPermissionCatalog({\n * db: { type: 'postgres', url: process.env.DATABASE_URL! }\n * });\n * console.log(syncResult.created);\n *\n * // Generate or apply Postgres RLS policies\n * const sql = generatePostgresPermissionSql({\n * db: { type: 'postgres', url: process.env.DATABASE_URL! }\n * });\n * console.log(sql.targets);\n * await applyPostgresPermissionPolicies({\n * db: { type: 'postgres', url: process.env.DATABASE_URL! }\n * });\n * ```\n *\n * @packageDocumentation\n */\n\n// Self-register this package's manifest before any @smrt() decorator fires\n// downstream. Must come first so the side effect runs ahead of the class\n// module loads below. See __smrt-register__.ts for issue #1132 context.\nimport './__smrt-register__.js';\n// Contribute expired-credential retention to the framework sweep as soon as\n// this package is loaded (#2375), so `smrt db:prune` and any host process see\n// the tasks without an app-level wiring step. Registering is not scheduling:\n// nothing is deleted until something runs a sweep, and the policy can still\n// disable any task by name.\nimport { registerUserRetentionTasks } from './retention.js';\n\nregisterUserRetentionTasks();\n\n// Collections\nexport {\n AccessRequestCollection,\n CliAuthRequestCollection,\n type CreateChildTenantOptions,\n type CreateSessionOptions,\n DEFAULT_ROLE_PERMISSION_PATTERNS,\n type GetOrCreateFromOidcOptions,\n GroupCollection,\n GroupMemberCollection,\n GroupRoleCollection,\n MagicLinkTokenCollection,\n MembershipCollection,\n MembershipOverrideCollection,\n type NormalizedOidcClaims,\n type OidcClaims,\n type OidcIdentityResult,\n type OidcProfileOwnerAuthorization,\n type OidcProfileOwnerAuthorizer,\n type OidcProfileOwnerAuthorizerContext,\n type OidcProfileResolver,\n type OidcProfileResolverContext,\n OidcProvisioningError,\n type OidcProvisioningErrorCode,\n PermissionCollection,\n RoleCollection,\n RolePermissionCollection,\n type RolePermissionPatternMatrix,\n type SeedRolePermissionsOptions,\n type SeedRolePermissionsResult,\n type SeedSystemRolesOptions,\n SessionCollection,\n TenantCollection,\n TenantHierarchyError,\n TenantIntegrationCollection,\n TenantPermissionOverrideCollection,\n type TenantPermissionOverrideResult,\n UserCollection,\n UsersCliAuthRequestCollection,\n UsersMagicLinkTokenCollection,\n} from './collections/index.js';\n// Deploy-time data migrations\nexport {\n type BackfillUserEmailKeysResult,\n backfillUserEmailKeys,\n type DuplicateUserEmailKey,\n UserEmailKeyBackfillError,\n type UserEmailKeyBackfillErrorCode,\n} from './migrations/backfillUserEmailKeys.js';\n// Models\nexport {\n AccessRequest,\n type AccessRequestOptions,\n CliAuthRequest,\n type CliAuthRequestStatus,\n DEFAULT_SESSION_TTL,\n DEFAULT_TOKEN_EXPIRY_SECONDS,\n Group,\n GroupMember,\n GroupRole,\n generateSessionId,\n MAX_TENANT_HIERARCHY_DEPTH,\n MagicLinkToken,\n Membership,\n MembershipOverride,\n Permission,\n Role,\n RolePermission,\n Session,\n Tenant,\n TenantIntegration,\n type TenantIntegrationCheckSummary,\n type TenantIntegrationOptions,\n type TenantIntegrationProvider,\n type TenantIntegrationStatus,\n TenantPermissionOverride,\n User,\n UsersCliAuthRequest,\n UsersMagicLinkToken,\n} from './models/index.js';\n// Credential retention — expired sessions and tokens (#2375)\nexport {\n CLI_AUTH_RETENTION_TASK,\n MAGIC_LINK_RETENTION_TASK,\n registerUserRetentionTasks,\n SESSIONS_RETENTION_TASK,\n USER_RETENTION_TASKS,\n unregisterUserRetentionTasks,\n} from './retention.js';\n\n// Services\nexport {\n ACCESS_REQUEST_CAPABILITIES,\n type AccessRequestAuthorizationContext,\n type AccessRequestAuthorizer,\n type AccessRequestCapability,\n AccessRequestError,\n type AccessRequestErrorCode,\n type AccessRequestEvent,\n type AccessRequestEventHandler,\n type AccessRequestEventType,\n AccessRequestService,\n type AccessRequestServiceOptions,\n type ApproveAccessRequestOptions,\n type ApproveCliAuthRequestInput,\n applyPostgresPermissionPolicies,\n assertOperationPermission,\n type CancelAccessRequestOptions,\n type CliAuthStartResult,\n type CliAuthTokenResult,\n type CreateAccessRequestInput,\n type CreateAuthorizationUrlOptions,\n checkOperationPermission,\n DEFAULT_CLI_AUTH_POLL_INTERVAL_SECONDS,\n DEFAULT_CLI_AUTH_REQUEST_TTL_SECONDS,\n DEFAULT_CLI_SESSION_TTL_SECONDS,\n type DeclineAccessRequestOptions,\n decodeOidcTransaction,\n deriveOperationPermissionCollectionName,\n deriveOperationPermissionSlug,\n type EnsureTenantResult,\n encodeOidcTransaction,\n type GeneratePostgresPermissionSqlResult,\n type GraduateAccessRequestOptions,\n type GraduateAccessRequestResult,\n type GraduateExistingTenantOption,\n type GraduateNewTenantOption,\n type GraduateTenantOption,\n generatePostgresPermissionSql,\n getCurrentSessionPermissionContext,\n getRequestScopedDatabase,\n getUsersOidcConfig,\n hasOperationPermission,\n type ListAccessRequestsFilter,\n MagicLinkError,\n type MagicLinkResult,\n MagicLinkService,\n type MagicLinkServiceOptions,\n type MagicLinkVerifyResult,\n MobileAuthError,\n type MobileAuthErrorCode,\n MobileAuthService,\n type MobileAuthServiceOptions,\n type MobileBootstrapContext,\n type MobileLoginContext,\n type MobileLogoutResult,\n type MobileRequestMeta,\n type MobileResolvedUser,\n type MobileTenantContext,\n normalizeOperationPermissionAction,\n type OidcCallbackResult,\n OidcLoginError,\n type OidcLoginResult,\n OidcLoginService,\n type OidcLoginServiceOptions,\n type OidcProviderConfig,\n type OidcProviderKind,\n type OidcProviderMetadata,\n type OidcProviderResolution,\n type OidcProviderResolutionOptions,\n type OidcTokenEndpointAuthMethod,\n type OidcTokenSet,\n type OidcTransaction,\n type OperationPermissionAllowReason,\n type OperationPermissionCollectionInput,\n type OperationPermissionDecision,\n type OperationPermissionDenyReason,\n OperationPermissionError,\n type OperationPermissionOptions,\n type PermissionCatalog,\n PermissionCatalogService,\n type PermissionCatalogSource,\n type PermissionCatalogSyncResult,\n type PermissionDefinition,\n type PermissionResolutionOptions,\n type PermissionResolutionResult,\n PermissionResolver,\n type PostgresPermissionAction,\n type PostgresPermissionBinding,\n type PostgresPermissionPolicyReportItem,\n type PostgresPermissionPolicyTarget,\n type PrincipalPermissionRuntimeOptions,\n type ResolvedOidcProviderConfig,\n readMobileBearerToken,\n registerPermissionDefinitions,\n resolveOidcProviderConfig,\n type SessionContext,\n type SessionPermissionRuntimeContext,\n type SessionPermissionRuntimeOptions,\n SessionService,\n type SessionServiceOptions,\n type SwitchTenantResult,\n syncPermissionCatalog,\n type TenantPermissionInheritanceResult,\n TenantService,\n type TenantWithOwnershipResult,\n TerminalAuthError,\n TerminalAuthRateLimitError,\n TerminalAuthService,\n type TerminalAuthServiceOptions,\n type UsersConfig,\n type UsersOidcConfig,\n validateMobileRedirectUri,\n withPrincipalPermissionContext,\n withSessionPermissionContext,\n} from './services/index.js';\n\n// Types\nexport {\n AccessRequestStatus,\n DEFAULT_ROLE_SLUGS,\n DEFAULT_ROLES,\n DEFAULT_TENANT_POLICY,\n type DefaultRoleSlug,\n MembershipStatus,\n OverrideEffect,\n SessionStatus,\n TenantPermissionEffect,\n type TenantPolicy,\n type TenantPolicyMode,\n TenantStatus,\n UserStatus,\n} from './types/index.js';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["membership","tenant","entry","tenant"],"sources":["../src/models/MagicLinkToken.ts","../src/collections/MagicLinkTokenCollection.ts","../src/retention.ts","../src/models/AccessRequest.ts","../src/collections/AccessRequestCollection.ts","../src/models/TenantIntegration.ts","../src/collections/TenantIntegrationCollection.ts","../src/services/AccessRequestService.ts","../src/services/MagicLinkService.ts","../src/services/PostgresPermissionPolicies.ts","../src/services/TenantService.ts","../src/index.ts"],"sourcesContent":["/**\n * MagicLinkToken model - replay protection for magic link authentication\n *\n * Stores nonces embedded in magic link JWTs to prevent replay attacks.\n * Each nonce can only be used once within its expiry window.\n *\n * @packageDocumentation\n */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\n\n/**\n * Default magic link token expiry: 10 minutes in seconds\n */\nexport const DEFAULT_TOKEN_EXPIRY_SECONDS = 10 * 60;\n\n/**\n * Constructor options for {@link UsersMagicLinkToken}.\n */\nexport interface MagicLinkTokenOptions extends SmrtObjectOptions {\n nonce?: string;\n email?: string;\n used?: boolean;\n /** Accepts a Date or any value the Date constructor can coerce. */\n expiresAt?: Date | string | number;\n}\n\n@smrt({\n tableName: 'users_magic_link_tokens',\n // Magic link tokens are security-sensitive — no public API\n api: { include: [] },\n mcp: { include: [] },\n cli: true,\n})\nexport class UsersMagicLinkToken extends SmrtObject {\n /** Unique nonce embedded in the signed JWT */\n @field({ required: true, unique: true })\n nonce: string = '';\n\n /** Email address this token was generated for */\n email: string = '';\n\n /** Whether this token has been used */\n used: boolean = false;\n\n /**\n * When this token expires.\n *\n * Indexed: `deleteExpired()` — and the retention sweep that now schedules\n * it (#2375) — scans this column on every pass.\n */\n @field({ type: 'datetime', indexed: true })\n expiresAt: Date = new Date(Date.now() + DEFAULT_TOKEN_EXPIRY_SECONDS * 1000);\n\n constructor(options: MagicLinkTokenOptions = {}) {\n super(options);\n if (options.nonce !== undefined) this.nonce = options.nonce;\n if (options.email !== undefined) this.email = options.email;\n if (options.used !== undefined) this.used = options.used;\n if (options.expiresAt !== undefined) {\n this.expiresAt =\n options.expiresAt instanceof Date\n ? options.expiresAt\n : new Date(options.expiresAt);\n }\n }\n\n /** Check if the token has expired */\n isExpired(): boolean {\n return new Date() > this.expiresAt;\n }\n\n /** Check if the token is still valid (unused and not expired) */\n isValid(): boolean {\n return !this.used && !this.isExpired();\n }\n}\n\nexport { UsersMagicLinkToken as MagicLinkToken };\n","/**\n * MagicLinkTokenCollection - Collection manager for MagicLinkToken objects\n *\n * Provides nonce lookup and single-use enforcement for magic link authentication.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { UsersMagicLinkToken } from '../models/MagicLinkToken.js';\n\n/**\n * Collection for managing smrt-users magic link token records\n */\nexport class UsersMagicLinkTokenCollection extends SmrtCollection<UsersMagicLinkToken> {\n static readonly _itemClass = UsersMagicLinkToken;\n\n /**\n * Find a token by its nonce\n */\n async findByNonce(nonce: string): Promise<UsersMagicLinkToken | null> {\n return this.findOne({\n where: { nonce },\n }) as Promise<UsersMagicLinkToken | null>;\n }\n\n /**\n * Atomically mark a token as used (single-use enforcement).\n *\n * Returns true if the nonce was successfully claimed (transitioned from\n * unused to used). Returns false if the nonce was already used, expired,\n * or doesn't exist — preventing race conditions in concurrent verify() calls.\n */\n async markUsed(nonce: string): Promise<boolean> {\n const now = new Date().toISOString();\n const { rowCount } = await this.db.query(\n `UPDATE ${this.tableName}\n SET used = ?, updated_at = ?\n WHERE nonce = ? AND used = ? AND expires_at > ?`,\n true,\n now,\n nonce,\n false,\n now,\n );\n\n return rowCount > 0;\n }\n\n /**\n * Delete expired tokens (cleanup job)\n *\n * Scheduled by the framework retention sweep (#2375); `expiresAt` carries an\n * index for this predicate.\n *\n * @param options.dryRun - Count the tokens the predicate selects without\n * deleting them.\n * @returns Number of tokens deleted (or, under `dryRun`, matched)\n */\n async deleteExpired(options: { dryRun?: boolean } = {}): Promise<number> {\n // One statement, for the same reason `SessionCollection.deleteExpired()`\n // is one (#1400): this runs unattended on the retention sweep's timer, and\n // a per-row delete that throws part-way leaves the rest of the expired\n // tokens un-reaped. It also avoids hydrating every row just to delete it.\n const now = new Date().toISOString();\n const predicate = 'expires_at < ?';\n\n const counted = await this.db.query(\n `SELECT COUNT(*) AS total FROM ${this.tableName} WHERE ${predicate}`,\n now,\n );\n const total = Number(counted.rows?.[0]?.total ?? 0);\n if (!Number.isFinite(total) || total <= 0) return 0;\n\n if (!options.dryRun) {\n await this.db.query(\n `DELETE FROM ${this.tableName} WHERE ${predicate}`,\n now,\n );\n }\n\n return total;\n }\n}\n\nexport { UsersMagicLinkTokenCollection as MagicLinkTokenCollection };\n","/**\n * Credential retention — expired sessions and tokens (issue #2375, finding F2).\n *\n * `SessionCollection.deleteExpired()`, `MagicLinkTokenCollection.deleteExpired()`\n * and `CliAuthRequestCollection.deleteExpired()` all existed, and every one of\n * them waited for an application to remember to call it. Expired credential\n * rows are the worst kind of unbounded growth: they are worthless the moment\n * they expire and they are exactly the rows an attacker would like to still\n * find in the table.\n *\n * This module contributes them to the framework retention sweep, so\n * `smrt db:prune` and the jobs `TaskRunner`'s periodic sweep reap them like\n * every other framework-owned table.\n *\n * The package entry point registers all three on import (see `index.ts`), so\n * any process that loaded `@happyvertical/smrt-users` contributes them.\n * Registration is not scheduling: nothing is deleted until something runs a\n * sweep, and a policy can turn any task off by name. Task names are prefixed\n * with the owning package's short name, because the registry is one\n * process-global namespace shared with every other package.\n *\n * @see https://github.com/happyvertical/smrt/issues/2375\n * @packageDocumentation\n */\n\nimport {\n registerRetentionTask,\n unregisterRetentionTask,\n} from '@happyvertical/smrt-core';\nimport { UsersCliAuthRequestCollection } from './collections/CliAuthRequestCollection.js';\nimport { UsersMagicLinkTokenCollection } from './collections/MagicLinkTokenCollection.js';\nimport { SessionCollection } from './collections/SessionCollection.js';\n\n/** Retention task name for expired and revoked sessions. */\nexport const SESSIONS_RETENTION_TASK = 'users-sessions';\n\n/** Retention task name for expired magic-link tokens. */\nexport const MAGIC_LINK_RETENTION_TASK = 'users-magic-link-tokens';\n\n/** Retention task name for expired CLI device-code requests. */\nexport const CLI_AUTH_RETENTION_TASK = 'users-cli-auth-requests';\n\n/** Every task name {@link registerUserRetentionTasks} installs. */\nexport const USER_RETENTION_TASKS = [\n SESSIONS_RETENTION_TASK,\n MAGIC_LINK_RETENTION_TASK,\n CLI_AUTH_RETENTION_TASK,\n] as const;\n\n/**\n * Register session and token expiry with the framework retention sweep.\n *\n * Idempotent: re-registering replaces the previous tasks.\n *\n * Each task deletes only rows that have already expired — there is no\n * retention window to configure, because an expired credential has no value\n * to retain. Applications that keep expired sessions for audit should opt the\n * task out (`tasks: { 'users-sessions': false }`) and archive them\n * themselves.\n */\nexport function registerUserRetentionTasks(): void {\n registerRetentionTask({\n name: SESSIONS_RETENTION_TASK,\n description: 'Delete expired and revoked sessions',\n run: async (db, context) => {\n const sessions = await SessionCollection.create({ db });\n return sessions.deleteExpired({ dryRun: context.dryRun });\n },\n });\n\n registerRetentionTask({\n name: MAGIC_LINK_RETENTION_TASK,\n description: 'Delete expired magic-link tokens',\n run: async (db, context) => {\n const tokens = await UsersMagicLinkTokenCollection.create({ db });\n return tokens.deleteExpired({ dryRun: context.dryRun });\n },\n });\n\n registerRetentionTask({\n name: CLI_AUTH_RETENTION_TASK,\n description:\n 'Delete expired pending, lazily-expired, and consumed CLI auth requests',\n run: async (db, context) => {\n const requests = await UsersCliAuthRequestCollection.create({ db });\n return requests.deleteExpired({ dryRun: context.dryRun });\n },\n });\n}\n\n/** Remove the credential retention tasks from the framework sweep. */\nexport function unregisterUserRetentionTasks(): void {\n for (const name of USER_RETENTION_TASKS) {\n unregisterRetentionTask(name);\n }\n}\n","/**\n * AccessRequest model — the \"request access / join the waitlist\" identity\n * primitive.\n *\n * Captures a prospective user from a public form *before* they are a real\n * {@link User}: an operator triages the record, then either declines it or\n * approves and **graduates** it into a `User` (optionally attached to a\n * tenant). This keeps the `User` table clean — unverified/spam signups live as\n * requests, not users — while still giving operators a triage queue.\n *\n * The generated REST/MCP/CLI surface is intentionally CLOSED (`include: []`):\n * every read and mutation must go through {@link AccessRequestService} so the\n * public-safe creation path (email normalization + dedup), the lifecycle state\n * machine, capability gating, and event emission can never be bypassed by a\n * generated CRUD route. Creation is meant to be exposed *unauthenticated* by\n * consuming apps via their own rate-limited endpoint calling\n * `createAccessRequest`. The model is still registered (and its table created\n * via the consumer's normal migrate flow) regardless of the empty generation\n * config.\n *\n * @packageDocumentation\n */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { AccessRequestStatus } from '../types/index.js';\nimport { normalizeEmail } from './User.js';\n\n/**\n * Options accepted by the {@link AccessRequest} constructor / collection\n * `create`. `requestContext` and `tenantHint` are stored as JSON *strings* —\n * use {@link AccessRequest.setRequestContext} / {@link AccessRequest.setTenantHint}\n * (or pass pre-serialized strings) rather than raw objects.\n */\nexport interface AccessRequestOptions {\n email?: string;\n name?: string | null;\n status?: AccessRequestStatus;\n source?: string;\n requestContext?: string;\n note?: string | null;\n requestedAt?: Date;\n decidedAt?: Date | null;\n decidedBy?: string | null;\n resultingUserId?: string | null;\n tenantHint?: string | null;\n [key: string]: unknown;\n}\n\n/**\n * A prospective user's request for access, captured before they become a\n * {@link User}.\n *\n * @example\n * ```typescript\n * // Public, unauthenticated path (app adds rate-limiting):\n * const request = await service.createAccessRequest({\n * email: 'Jane@Example.com',\n * name: 'Jane Doe',\n * source: 'www',\n * context: { company: 'Acme', intendedUse: 'evaluation' },\n * });\n * // request.email === 'jane@example.com', request.status === 'requested'\n * ```\n */\n@smrt({\n tableName: 'access_requests',\n // Append-style: the natural key is the surrogate id, so a new request never\n // upserts over an existing row. Without this, SMRT defaults to upserting on\n // `slug`/`context`, and `slug` is derived from `name` — two public submissions\n // sharing a display name (e.g. two \"Jane Doe\"s with different emails) would\n // collide and overwrite each other. Open-request dedup is handled explicitly\n // by AccessRequestService.createAccessRequest, not by the storage conflict key.\n conflictColumns: ['id'],\n // CLOSED generated surface — all access flows through AccessRequestService.\n api: { include: [] },\n mcp: { include: [] },\n cli: { include: [] },\n})\nexport class AccessRequest extends SmrtObject {\n /**\n * Requester's email address. Required, normalized to lowercase, and indexed\n * for lookup + open-request dedup. NOT unique: the same email may accumulate\n * multiple requests over time (e.g. an old GRADUATED record plus a new\n * REQUESTED one); dedup is enforced on *open* requests by the service.\n */\n @field({ required: true, indexed: true })\n email: string = '';\n\n /**\n * Requester's display name, if supplied on the form.\n */\n @field({ nullable: true })\n name: string | null = null;\n\n /**\n * Lifecycle status. Indexed so the operator triage queue can filter by status\n * efficiently.\n */\n @field({ type: 'text', indexed: true })\n status: AccessRequestStatus = AccessRequestStatus.REQUESTED;\n\n /**\n * Where the request came from, e.g. `www`, `sdk`. Indexed for per-source\n * filtering in the operator queue.\n */\n @field({ indexed: true })\n source: string = '';\n\n /**\n * Free-form JSON metadata captured with the request (intended use, company,\n * message, referrer, etc.), stored as a JSON string.\n *\n * NOT named `context`: {@link SmrtObject} reserves the `context` field for\n * slug scoping, so this mirrors smrt-chat's `sessionContext` convention. Use\n * {@link getRequestContext} / {@link setRequestContext} for typed access.\n */\n @field()\n requestContext: string = '{}';\n\n /**\n * Operator note / decision reason (set on approve / decline / cancel).\n */\n @field({ nullable: true })\n note: string | null = null;\n\n /**\n * When the request was submitted. Defaults to creation time.\n */\n @field()\n requestedAt: Date = new Date();\n\n /**\n * When an operator decided the request (approve / decline / cancel /\n * graduate). Null while still `REQUESTED`.\n */\n @field({ nullable: true })\n decidedAt: Date | null = null;\n\n /**\n * User id of the operator who decided the request. Null while still pending.\n */\n @field({ nullable: true })\n decidedBy: string | null = null;\n\n /**\n * Id of the `User` produced (or linked) when the request graduated. Null\n * until graduation.\n */\n @field({ nullable: true })\n resultingUserId: string | null = null;\n\n /**\n * Optional JSON hint about the org/tenant the requester wants, stored as a\n * JSON string (null when absent). Advisory only — the operator decides the\n * actual tenant at graduation time. Use {@link getTenantHint} /\n * {@link setTenantHint} for typed access.\n */\n @field({ nullable: true })\n tenantHint: string | null = null;\n\n constructor(options: AccessRequestOptions = {}) {\n super(options);\n if (options.email !== undefined) this.email = normalizeEmail(options.email);\n if (options.name !== undefined) this.name = options.name;\n if (options.status !== undefined) this.status = options.status;\n if (options.source !== undefined) this.source = options.source;\n if (options.requestContext !== undefined)\n this.requestContext = options.requestContext;\n if (options.note !== undefined) this.note = options.note;\n if (options.requestedAt !== undefined)\n this.requestedAt = options.requestedAt;\n if (options.decidedAt !== undefined) this.decidedAt = options.decidedAt;\n if (options.decidedBy !== undefined) this.decidedBy = options.decidedBy;\n if (options.resultingUserId !== undefined)\n this.resultingUserId = options.resultingUserId;\n if (options.tenantHint !== undefined) this.tenantHint = options.tenantHint;\n }\n\n /**\n * Parse {@link requestContext} into an object. Returns `{}` on missing or\n * malformed JSON (graceful — never throws).\n */\n getRequestContext(): Record<string, unknown> {\n try {\n const parsed = JSON.parse(this.requestContext);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /**\n * Serialize and store {@link requestContext} from an object.\n */\n setRequestContext(value: Record<string, unknown>): void {\n this.requestContext = JSON.stringify(value ?? {});\n }\n\n /**\n * Parse {@link tenantHint} into an object, or `null` when unset / malformed.\n */\n getTenantHint(): Record<string, unknown> | null {\n if (!this.tenantHint) return null;\n try {\n const parsed = JSON.parse(this.tenantHint);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Serialize and store {@link tenantHint} from an object (or clear with null).\n */\n setTenantHint(value: Record<string, unknown> | null): void {\n this.tenantHint = value ? JSON.stringify(value) : null;\n }\n\n /**\n * Whether the request is still open (awaiting a decision).\n */\n isOpen(): boolean {\n return this.status === AccessRequestStatus.REQUESTED;\n }\n\n /**\n * Whether the request has been approved (and not yet graduated).\n */\n isApproved(): boolean {\n return this.status === AccessRequestStatus.APPROVED;\n }\n\n /**\n * Whether the request reached a terminal state (declined, graduated, or\n * canceled) and can no longer transition.\n */\n isTerminal(): boolean {\n return (\n this.status === AccessRequestStatus.DECLINED ||\n this.status === AccessRequestStatus.GRADUATED ||\n this.status === AccessRequestStatus.CANCELED\n );\n }\n}\n","/**\n * AccessRequestCollection — query helpers for {@link AccessRequest} records.\n *\n * This is the low-level data accessor. Lifecycle orchestration (the public-safe\n * create path, the state machine, capability gating, events, and graduation\n * into a `User`) lives in {@link AccessRequestService} — prefer the service for\n * application code.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { AccessRequest } from '../models/AccessRequest.js';\nimport { normalizeEmail } from '../models/User.js';\nimport { AccessRequestStatus } from '../types/index.js';\n\n/**\n * Collection for managing {@link AccessRequest} objects.\n */\nexport class AccessRequestCollection extends SmrtCollection<AccessRequest> {\n static readonly _itemClass = AccessRequest;\n\n /**\n * Find all access requests for an email (any status), newest first. The email\n * is normalized before querying so callers can pass any case.\n */\n async findByEmail(email: string): Promise<AccessRequest[]> {\n return await this.list({\n where: { email: normalizeEmail(email) },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * Find the single open (`REQUESTED`) request for an email, if any. This is\n * the dedup key used by {@link AccessRequestService.createAccessRequest}.\n */\n async findOpenByEmail(email: string): Promise<AccessRequest | null> {\n const results = await this.list({\n where: {\n email: normalizeEmail(email),\n status: AccessRequestStatus.REQUESTED,\n },\n limit: 1,\n orderBy: 'created_at DESC',\n });\n return results.length > 0 ? results[0] : null;\n }\n\n /**\n * Find access requests by status, newest first.\n */\n async findByStatus(status: AccessRequestStatus): Promise<AccessRequest[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * Find all open (`REQUESTED`) access requests — the operator triage queue.\n */\n async findOpen(): Promise<AccessRequest[]> {\n return await this.findByStatus(AccessRequestStatus.REQUESTED);\n }\n}\n","/**\n * TenantIntegration model - per-tenant external integration provisioning state.\n *\n * One row per (tenantId, provider) tuple. Stores provider identifiers and\n * health/provisioning metadata only; callers must keep secrets in a dedicated\n * secret store such as @happyvertical/smrt-secrets.\n *\n * @packageDocumentation\n */\n\nimport {\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n\n/**\n * Built-in provider names. The string type is intentionally open so apps can\n * add providers without a model migration.\n */\nexport type TenantIntegrationProvider =\n | 'aws'\n | 'bifrost'\n | 'glitchtip'\n | 'imago'\n | 'matomo'\n | (string & {});\n\n/**\n * Provisioning and health states for a tenant integration.\n *\n * - `unprovisioned`: no external resources have been created yet.\n * - `provisioning`: provisioning is currently queued or running.\n * - `active`: external resources exist and the latest health check passed.\n * - `drifted`: local bookkeeping disagrees with provider state.\n * - `failed`: the latest provisioning attempt or health check failed.\n */\nexport type TenantIntegrationStatus =\n | 'unprovisioned'\n | 'provisioning'\n | 'active'\n | 'drifted'\n | 'failed';\n\nexport interface TenantIntegrationCheckSummary {\n name: string;\n ok: boolean;\n message?: string;\n}\n\nexport interface TenantIntegrationOptions extends SmrtObjectOptions {\n tenantId?: string;\n provider?: TenantIntegrationProvider;\n externalIds?: Record<string, string> | string;\n status?: TenantIntegrationStatus;\n lastCheckedAt?: Date | null;\n lastCheckSummary?: TenantIntegrationCheckSummary[] | string;\n lastError?: string | null;\n}\n\nfunction parseExternalIds(raw: unknown): Record<string, string> {\n if (!raw) return {};\n\n if (typeof raw === 'object' && !Array.isArray(raw)) {\n return Object.fromEntries(\n Object.entries(raw).filter(([, value]) => typeof value === 'string'),\n ) as Record<string, string>;\n }\n\n if (typeof raw !== 'string') {\n return {};\n }\n\n try {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return {};\n }\n\n return Object.fromEntries(\n Object.entries(parsed).filter(([, value]) => typeof value === 'string'),\n ) as Record<string, string>;\n } catch {\n return {};\n }\n}\n\nfunction stringifyExternalIds(externalIds: Record<string, string>): string {\n for (const [key, value] of Object.entries(\n externalIds as Record<string, unknown>,\n )) {\n if (typeof value !== 'string') {\n throw new TypeError(\n `TenantIntegration external id '${key}' must be a string`,\n );\n }\n }\n\n return JSON.stringify(externalIds);\n}\n\nfunction parseCheckSummary(raw: unknown): TenantIntegrationCheckSummary[] {\n if (!raw) return [];\n\n if (Array.isArray(raw)) {\n return normalizeCheckSummary(raw);\n }\n\n if (typeof raw !== 'string') {\n return [];\n }\n\n try {\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? normalizeCheckSummary(parsed) : [];\n } catch {\n return [];\n }\n}\n\nfunction normalizeCheckSummary(\n raw: unknown[],\n): TenantIntegrationCheckSummary[] {\n return raw\n .filter(\n (entry): entry is Record<string, unknown> =>\n !!entry && typeof entry === 'object' && !Array.isArray(entry),\n )\n .map((entry) => ({\n name: typeof entry.name === 'string' ? entry.name : '',\n ok: entry.ok === true,\n ...(typeof entry.message === 'string' ? { message: entry.message } : {}),\n }))\n .filter((entry) => entry.name.length > 0);\n}\n\nfunction stringifyCheckSummary(\n summary: TenantIntegrationCheckSummary[],\n): string {\n return JSON.stringify(summary);\n}\n\n// TenantIntegration rows are always tenant-owned provider state; system-wide\n// provider defaults belong in app config or a secret store, not this table.\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'tenant_integrations',\n conflictColumns: ['tenant_id', 'provider'],\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: true,\n})\nexport class TenantIntegration extends SmrtObject {\n /**\n * Tenant this integration belongs to. Combined with `provider`, this is the\n * natural key for the row.\n */\n @tenantId()\n tenantId: string = '';\n\n /**\n * Provider identifier, such as `aws`, `matomo`, `glitchtip`, or an\n * app-defined provider string.\n */\n provider: TenantIntegrationProvider = 'aws';\n\n /**\n * Provider-side identifiers. Shape depends on the provider. This must not\n * contain provider credentials or secret material.\n */\n externalIds = '{}';\n\n /**\n * Current provisioning/health status.\n */\n status: TenantIntegrationStatus = 'unprovisioned';\n\n /**\n * UTC timestamp of the most recent doctor/provisioning check.\n */\n lastCheckedAt: Date | null = null;\n\n /**\n * Per-check pass/fail summary from the most recent doctor run.\n */\n lastCheckSummary = '[]';\n\n /**\n * Most recent error message when `status === 'failed'`, or null otherwise.\n */\n lastError: string | null = null;\n\n constructor(options: TenantIntegrationOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.provider !== undefined) this.provider = options.provider;\n if (options.externalIds !== undefined) {\n this.setExternalIds(options.externalIds);\n }\n if (options.status !== undefined) this.status = options.status;\n if (options.lastCheckedAt !== undefined)\n this.lastCheckedAt = options.lastCheckedAt;\n if (options.lastCheckSummary !== undefined) {\n this.setLastCheckSummary(options.lastCheckSummary);\n }\n if (options.lastError !== undefined) this.lastError = options.lastError;\n }\n\n public override async initialize(): Promise<this> {\n await super.initialize();\n this.normalizeJsonFields();\n return this;\n }\n\n private normalizeJsonFields(): void {\n const fields = this as unknown as {\n externalIds: unknown;\n lastCheckSummary: unknown;\n };\n\n if (fields.externalIds == null) {\n this.externalIds = '{}';\n } else if (typeof fields.externalIds !== 'string') {\n this.setExternalIds(fields.externalIds as Record<string, string>);\n }\n\n if (fields.lastCheckSummary == null) {\n this.lastCheckSummary = '[]';\n } else if (typeof fields.lastCheckSummary !== 'string') {\n this.setLastCheckSummary(\n fields.lastCheckSummary as TenantIntegrationCheckSummary[],\n );\n }\n }\n\n getExternalIds(): Record<string, string> {\n return parseExternalIds(this.externalIds);\n }\n\n setExternalIds(externalIds: Record<string, string> | string): void {\n this.externalIds =\n typeof externalIds === 'string'\n ? externalIds\n : stringifyExternalIds(externalIds);\n }\n\n getExternalId(key: string): string | undefined {\n const value = this.getExternalIds()[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n }\n\n setExternalId(key: string, value: string | undefined): void {\n const externalIds = this.getExternalIds();\n if (value === undefined) {\n delete externalIds[key];\n } else {\n externalIds[key] = value;\n }\n\n this.setExternalIds(externalIds);\n }\n\n getLastCheckSummary(): TenantIntegrationCheckSummary[] {\n return parseCheckSummary(this.lastCheckSummary);\n }\n\n setLastCheckSummary(summary: TenantIntegrationCheckSummary[] | string): void {\n this.lastCheckSummary =\n typeof summary === 'string' ? summary : stringifyCheckSummary(summary);\n }\n\n recordDoctorRun(outcome: {\n status: TenantIntegrationStatus;\n summary: TenantIntegrationCheckSummary[];\n error?: string | null;\n }): void {\n this.status = outcome.status;\n this.setLastCheckSummary(outcome.summary);\n this.lastCheckedAt = new Date();\n this.lastError = outcome.error ?? null;\n }\n}\n","/**\n * TenantIntegrationCollection - accessors for tenant integration rows.\n *\n * @packageDocumentation\n */\n\nimport {\n isUniqueViolationError,\n SmrtCollection,\n} from '@happyvertical/smrt-core';\nimport {\n TenantIntegration,\n type TenantIntegrationProvider,\n} from '../models/TenantIntegration.js';\n\nexport class TenantIntegrationCollection extends SmrtCollection<TenantIntegration> {\n static readonly _itemClass = TenantIntegration;\n\n async listForTenant(tenantId: string): Promise<TenantIntegration[]> {\n return this.list({\n where: { tenantId },\n orderBy: 'provider ASC',\n });\n }\n\n async findFor(\n tenantId: string,\n provider: TenantIntegrationProvider,\n ): Promise<TenantIntegration | null> {\n return this.findOne({\n where: { tenantId, provider },\n }) as Promise<TenantIntegration | null>;\n }\n\n async findOrInit(\n tenantId: string,\n provider: TenantIntegrationProvider,\n ): Promise<TenantIntegration> {\n const existing = await this.findFor(tenantId, provider);\n if (existing) {\n return existing;\n }\n\n try {\n return await this.create({\n tenantId,\n provider,\n status: 'unprovisioned',\n _insertOnly: true,\n });\n } catch (err) {\n if (!isDuplicateKeyError(err)) {\n throw err;\n }\n\n const winner = await this.findFor(tenantId, provider);\n if (winner) {\n return winner;\n }\n\n throw err;\n }\n }\n}\n\n/**\n * Detect the losing side of a concurrent `findOrInit` race.\n *\n * Reduced to the framework contract in #2366: the hand-rolled cause walk and\n * its dialect regexes were a workaround for `save()` surfacing constraint\n * violations as a generic `DatabaseError`. Core now classifies through the\n * driver-error chain, so the typed check is sufficient.\n */\nfunction isDuplicateKeyError(err: unknown): boolean {\n return isUniqueViolationError(err);\n}\n","/**\n * AccessRequestService — the mechanism behind the \"request access / waitlist\"\n * identity primitive, plus graduation of an approved request into a `User`.\n *\n * ## Responsibilities\n *\n * - **Public-safe creation** ({@link AccessRequestService.createAccessRequest}):\n * validates + normalizes the email and de-duplicates against any open request\n * for the same email. Intended to be called *unauthenticated* by consuming\n * apps from their own rate-limited endpoint (the app owns rate-limiting).\n * - **Operator triage** (`list` / `get` / `approve` / `decline` / `cancel`):\n * gated behind capabilities (see {@link AccessRequestAuthorizer}).\n * - **Graduation** ({@link AccessRequestService.graduateAccessRequest}): creates\n * or links a `User` from the request — reusing the existing user / membership\n * / tenant collections rather than duplicating them — and (optionally)\n * attaches the user to a brand-new tenant (requester as owner), an existing\n * tenant, or no tenant at all. Idempotent.\n * - **Events** ({@link AccessRequestEventHandler}): emits\n * `access-request.created | approved | declined | canceled | graduated` so\n * apps can react (send a magic-link/invite on `graduated`, notify ops on\n * `created`, …). This service deliberately does **not** own email/notification\n * delivery — bridge the event hook to your delivery channel (a function, an\n * `EventEmitter`, the core `DispatchBus`, …).\n *\n * @remarks\n * **Security.** Operator methods are gated only when an {@link AccessRequestAuthorizer}\n * is supplied via {@link AccessRequestServiceOptions.authorize}; without one\n * they are ungated, exactly like {@link TerminalAuthService.approveRequest}\n * trusts its caller to authenticate first. Production apps that expose these\n * methods MUST either provide an `authorize` hook or perform their own\n * capability checks at the route layer. `createAccessRequest` is *always*\n * ungated by design (public-safe). The model's generated REST/MCP/CLI surface\n * is closed, so there is no unauthenticated network route by default.\n *\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { AccessRequestCollection } from '../collections/AccessRequestCollection.js';\nimport { MembershipCollection } from '../collections/MembershipCollection.js';\nimport { RoleCollection } from '../collections/RoleCollection.js';\nimport { TenantCollection } from '../collections/TenantCollection.js';\nimport { UserCollection } from '../collections/UserCollection.js';\nimport type { AccessRequest } from '../models/AccessRequest.js';\nimport type { Membership } from '../models/Membership.js';\nimport type { Tenant } from '../models/Tenant.js';\nimport type { User } from '../models/User.js';\nimport { isValidEmail, normalizeEmail } from '../models/User.js';\nimport {\n AccessRequestStatus,\n DEFAULT_ROLE_SLUGS,\n MembershipStatus,\n type TenantStatus,\n UserStatus,\n} from '../types/index.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Capabilities that gate the operator-facing methods of\n * {@link AccessRequestService}.\n */\nexport const ACCESS_REQUEST_CAPABILITIES = {\n /** Read the access-request queue (`list` / `get`). */\n READ: 'access-requests:read',\n /** Decide requests (`approve` / `decline` / `cancel` / `graduate`). */\n MANAGE: 'access-requests:manage',\n} as const;\n\n/**\n * One of the capability slugs in {@link ACCESS_REQUEST_CAPABILITIES}.\n */\nexport type AccessRequestCapability =\n (typeof ACCESS_REQUEST_CAPABILITIES)[keyof typeof ACCESS_REQUEST_CAPABILITIES];\n\n/**\n * Context passed to an {@link AccessRequestAuthorizer} before an operator method\n * runs.\n */\nexport interface AccessRequestAuthorizationContext {\n /** The capability the operation requires. */\n capability: AccessRequestCapability;\n /** The operator user id supplied to the method (`by`), if any. */\n by?: string | null;\n /** The target access-request id, when the operation targets a specific row. */\n accessRequestId?: string;\n}\n\n/**\n * Hook that authorizes an operator action. Throw (or reject) to deny — the\n * thrown error propagates to the caller unchanged. Resolve/return to allow.\n *\n * Wire this to your permission system, e.g. resolve the operator's permissions\n * and assert the required capability:\n *\n * ```typescript\n * const service = await AccessRequestService.create({\n * db,\n * authorize: async ({ capability, by }) => {\n * if (!by || !(await isPlatformOperator(by, capability))) {\n * throw new Error(`Missing capability: ${capability}`);\n * }\n * },\n * });\n * ```\n */\nexport type AccessRequestAuthorizer = (\n context: AccessRequestAuthorizationContext,\n) => void | Promise<void>;\n\n/**\n * Lifecycle event types emitted by {@link AccessRequestService}.\n */\nexport type AccessRequestEventType =\n | 'access-request.created'\n | 'access-request.approved'\n | 'access-request.declined'\n | 'access-request.canceled'\n | 'access-request.graduated';\n\n/**\n * Payload delivered to an {@link AccessRequestEventHandler}.\n */\nexport interface AccessRequestEvent {\n /** Which lifecycle transition fired. */\n type: AccessRequestEventType;\n /** The access request after the transition. */\n accessRequest: AccessRequest;\n /** When the event was emitted. */\n at: Date;\n /** Operator user id responsible, for operator-driven transitions. */\n by?: string | null;\n /** Graduated user (only on `access-request.graduated`). */\n user?: User;\n /** Membership created/linked on graduation, when a tenant was attached. */\n membership?: Membership;\n /** Tenant created/linked on graduation, when a tenant was attached. */\n tenant?: Tenant;\n}\n\n/**\n * Event hook apps provide to react to access-request lifecycle changes.\n * Delivery is best-effort: a throwing handler is logged and swallowed so it\n * never rolls back an already-persisted transition. Do not rely on it for\n * critical-path work that must share the request's transaction.\n */\nexport type AccessRequestEventHandler = (\n event: AccessRequestEvent,\n) => void | Promise<void>;\n\n/**\n * Options for {@link AccessRequestService}.\n */\nexport interface AccessRequestServiceOptions extends SmrtClassOptions {\n /** Optional capability gate for operator methods (see {@link AccessRequestAuthorizer}). */\n authorize?: AccessRequestAuthorizer;\n /** Optional lifecycle event hook (see {@link AccessRequestEventHandler}). */\n onEvent?: AccessRequestEventHandler;\n}\n\n/**\n * Input for {@link AccessRequestService.createAccessRequest}. Only `email` is\n * required.\n */\nexport interface CreateAccessRequestInput {\n /** Requester email (validated + normalized to lowercase). */\n email: string;\n /** Requester display name. */\n name?: string | null;\n /** Where the request came from, e.g. `www`, `sdk`. */\n source?: string;\n /** Free-form metadata (intended use, company, message, referrer, …). */\n context?: Record<string, unknown>;\n /** Optional requested org/tenant hint (advisory). */\n tenantHint?: Record<string, unknown> | null;\n /** Optional initial note. */\n note?: string | null;\n}\n\n/**\n * Filter for {@link AccessRequestService.listAccessRequests}.\n */\nexport interface ListAccessRequestsFilter {\n /** Restrict to one status or any of several. */\n status?: AccessRequestStatus | AccessRequestStatus[];\n /** Restrict to a single (normalized) email. */\n email?: string;\n /** Restrict to a single source. */\n source?: string;\n /** Operator user id, forwarded to the authorizer as `by`. */\n by?: string | null;\n /** Max rows to return. */\n limit?: number;\n /** Rows to skip. */\n offset?: number;\n /** Order-by clause (defaults to `created_at DESC`). */\n orderBy?: string;\n}\n\n/**\n * Options shared by the operator decision methods.\n */\nexport interface DecideAccessRequestOptions {\n /** Operator user id recorded as `decidedBy` and forwarded to the authorizer. */\n by?: string | null;\n}\n\n/**\n * Options for {@link AccessRequestService.approveAccessRequest}.\n */\nexport interface ApproveAccessRequestOptions\n extends DecideAccessRequestOptions {\n /** Operator note stored on the request. */\n note?: string | null;\n}\n\n/**\n * Options for {@link AccessRequestService.declineAccessRequest}.\n */\nexport interface DeclineAccessRequestOptions\n extends DecideAccessRequestOptions {\n /** Decision reason stored as the request's note. */\n reason?: string | null;\n}\n\n/**\n * Options for {@link AccessRequestService.cancelAccessRequest}.\n */\nexport interface CancelAccessRequestOptions extends DecideAccessRequestOptions {\n /** Cancellation reason stored as the request's note. */\n reason?: string | null;\n}\n\n/**\n * Graduate into a **new** tenant, enrolling the requester (owner by default).\n */\nexport interface GraduateNewTenantOption {\n /** New tenant attributes — `name` is required. */\n create: {\n name: string;\n slug?: string;\n description?: string;\n status?: TenantStatus;\n };\n /** Role slug for the requester's membership (default `owner`). */\n role?: string;\n}\n\n/**\n * Graduate into an **existing** tenant, enrolling the requester.\n */\nexport interface GraduateExistingTenantOption {\n /** Target tenant id. */\n tenantId: string;\n /** Role slug for the requester's membership (default `member`). */\n role?: string;\n}\n\n/**\n * Tenant handling at graduation: create a new tenant, attach to an existing\n * one, or `'none'` (user only, no membership).\n */\nexport type GraduateTenantOption =\n | GraduateNewTenantOption\n | GraduateExistingTenantOption\n | 'none';\n\n/**\n * Options for {@link AccessRequestService.graduateAccessRequest}.\n */\nexport interface GraduateAccessRequestOptions\n extends DecideAccessRequestOptions {\n /** Tenant handling (default `'none'`). */\n tenant?: GraduateTenantOption;\n /** Status applied to the user produced/linked by graduation (default `ACTIVE`). */\n activate?: UserStatus;\n /**\n * Convenience: allow graduating directly from `REQUESTED` (skipping the\n * `APPROVED` step). Defaults to `false`.\n */\n allowFromRequested?: boolean;\n /** Operator note stored on the request. */\n note?: string | null;\n}\n\n/**\n * Result of {@link AccessRequestService.graduateAccessRequest}.\n */\nexport interface GraduateAccessRequestResult {\n /** The graduated (created or linked) user. */\n user: User;\n /** The membership, when a tenant was attached. */\n membership?: Membership;\n /** The tenant, when one was created or attached. */\n tenant?: Tenant;\n /** The access request, now `GRADUATED`. */\n accessRequest: AccessRequest;\n /** Whether a brand-new user was created (`false` when an existing one was linked). */\n created: boolean;\n}\n\n/**\n * Error codes raised by {@link AccessRequestError}.\n */\nexport type AccessRequestErrorCode =\n | 'INVALID_EMAIL'\n | 'NOT_FOUND'\n | 'INVALID_TRANSITION'\n | 'TENANT_NOT_FOUND'\n | 'ROLE_NOT_FOUND';\n\n/**\n * Error thrown for access-request domain failures the caller is expected to\n * surface (invalid email, unknown id, illegal state transition, …). Authorizer\n * denials are not wrapped — those propagate from the supplied authorizer\n * unchanged.\n */\nexport class AccessRequestError extends Error {\n readonly code: AccessRequestErrorCode;\n\n constructor(message: string, code: AccessRequestErrorCode) {\n super(message);\n this.name = 'AccessRequestError';\n this.code = code;\n }\n}\n\n/**\n * Source statuses each transition may legally start from. Idempotent no-ops\n * (e.g. approving an already-`APPROVED` request) are handled separately, before\n * these guards run.\n */\nconst APPROVE_FROM: AccessRequestStatus[] = [AccessRequestStatus.REQUESTED];\nconst DECLINE_FROM: AccessRequestStatus[] = [\n AccessRequestStatus.REQUESTED,\n AccessRequestStatus.APPROVED,\n];\nconst CANCEL_FROM: AccessRequestStatus[] = [\n AccessRequestStatus.REQUESTED,\n AccessRequestStatus.APPROVED,\n];\n\n/**\n * High-level orchestration for the access-request lifecycle and graduation.\n */\nexport class AccessRequestService {\n readonly #options: AccessRequestServiceOptions;\n readonly #authorize?: AccessRequestAuthorizer;\n readonly #onEvent?: AccessRequestEventHandler;\n\n #requests!: AccessRequestCollection;\n #users!: UserCollection;\n #tenants!: TenantCollection;\n #memberships!: MembershipCollection;\n #roles!: RoleCollection;\n #rolesSeeded = false;\n\n constructor(options: AccessRequestServiceOptions) {\n this.#options = options;\n this.#authorize = options.authorize;\n this.#onEvent = options.onEvent;\n }\n\n /**\n * Initialize the backing collections (creates/verifies their tables).\n */\n async initialize(): Promise<void> {\n // SmrtClassOptions carries `authorize`/`onEvent` too; the collection factory\n // only reads the db/ai keys and ignores the rest, so passing the whole\n // options bag is harmless and keeps the db config in one place.\n this.#requests = await AccessRequestCollection.create(this.#options);\n this.#users = await UserCollection.create(this.#options);\n this.#tenants = await TenantCollection.create(this.#options);\n this.#memberships = await MembershipCollection.create(this.#options);\n this.#roles = await RoleCollection.create(this.#options);\n }\n\n /**\n * Static factory — construct and initialize in one call.\n */\n static async create(\n options: AccessRequestServiceOptions,\n ): Promise<AccessRequestService> {\n const service = new AccessRequestService(options);\n await service.initialize();\n return service;\n }\n\n /**\n * The underlying collection, for advanced read scenarios. Prefer the service\n * methods, which apply normalization, the state machine, capability gating,\n * and events.\n */\n get collection(): AccessRequestCollection {\n return this.#requests;\n }\n\n // ============= Public-safe creation =============\n\n /**\n * Create an access request. **Public-safe**: no capability check — meant to be\n * callable unauthenticated by apps (which add their own rate-limiting).\n *\n * Validates and normalizes the email, then de-duplicates: if an open\n * (`REQUESTED`) request already exists for the email, this merges any newly\n * supplied context/name/source/hint into it and returns it instead of\n * creating a duplicate (no second `created` event).\n *\n * @remarks\n * De-duplication is **best-effort, not atomic**: it is a read-then-write\n * (`findOpenByEmail` → `create`) with no DB-level partial-unique constraint\n * (the table is append-style, keyed on `id`, because the same email may\n * accumulate many requests over its lifetime). Two requests for the same\n * email racing concurrently can therefore both create an open row. This is by\n * design — the spec makes dedup configurable and pushes abuse control to the\n * app (rate-limiting on the public endpoint). Operators triaging two open rows\n * for one email is benign; apps needing a hard single-open-request guarantee\n * should add a partial unique index (`UNIQUE(email) WHERE status='requested'`)\n * in their migration.\n *\n * @throws {@link AccessRequestError} (`INVALID_EMAIL`) when the email is invalid.\n */\n async createAccessRequest(\n input: CreateAccessRequestInput,\n ): Promise<AccessRequest> {\n const email = normalizeEmail(input.email);\n if (!isValidEmail(email)) {\n throw new AccessRequestError(\n 'A valid email address is required to request access.',\n 'INVALID_EMAIL',\n );\n }\n\n // Dedup against an existing open request for this email (idempotent).\n const existing = await this.#requests.findOpenByEmail(email);\n if (existing) {\n let changed = false;\n if (input.context && Object.keys(input.context).length > 0) {\n existing.setRequestContext({\n ...existing.getRequestContext(),\n ...input.context,\n });\n changed = true;\n }\n if (input.tenantHint) {\n existing.setTenantHint({\n ...(existing.getTenantHint() ?? {}),\n ...input.tenantHint,\n });\n changed = true;\n }\n if (input.name && !existing.name) {\n existing.name = input.name;\n changed = true;\n }\n if (input.source && !existing.source) {\n existing.source = input.source;\n changed = true;\n }\n if (changed) await existing.save();\n return existing;\n }\n\n const request = await this.#requests.create({\n email,\n name: input.name ?? null,\n source: input.source ?? '',\n status: AccessRequestStatus.REQUESTED,\n requestedAt: new Date(),\n requestContext: JSON.stringify(input.context ?? {}),\n tenantHint: input.tenantHint ? JSON.stringify(input.tenantHint) : null,\n note: input.note ?? null,\n });\n\n await this.#emit({\n type: 'access-request.created',\n accessRequest: request,\n at: new Date(),\n });\n\n return request;\n }\n\n // ============= Operator reads =============\n\n /**\n * List access requests (operator-facing). Requires the `access-requests:read`\n * capability when an authorizer is configured.\n */\n async listAccessRequests(\n filter: ListAccessRequestsFilter = {},\n ): Promise<AccessRequest[]> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.READ, {\n by: filter.by,\n });\n\n const where: Record<string, unknown> = {};\n if (filter.status !== undefined) where.status = filter.status;\n if (filter.email !== undefined) where.email = normalizeEmail(filter.email);\n if (filter.source !== undefined) where.source = filter.source;\n\n return await this.#requests.list({\n where,\n limit: filter.limit,\n offset: filter.offset,\n orderBy: filter.orderBy ?? 'created_at DESC',\n });\n }\n\n /**\n * Get a single access request by id (operator-facing). Requires the\n * `access-requests:read` capability when an authorizer is configured.\n */\n async getAccessRequest(\n id: string,\n options: { by?: string | null } = {},\n ): Promise<AccessRequest | null> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.READ, {\n by: options.by,\n accessRequestId: id,\n });\n return await this.#requests.get(id);\n }\n\n // ============= Operator decisions =============\n\n /**\n * Approve a request: `REQUESTED → APPROVED`. Idempotent (re-approving an\n * already-`APPROVED` request is a no-op returning it). Requires\n * `access-requests:manage`.\n *\n * @throws {@link AccessRequestError} (`INVALID_TRANSITION`) from a terminal state.\n */\n async approveAccessRequest(\n id: string,\n options: ApproveAccessRequestOptions = {},\n ): Promise<AccessRequest> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.MANAGE, {\n by: options.by,\n accessRequestId: id,\n });\n\n const request = await this.#load(id);\n if (request.status === AccessRequestStatus.APPROVED) return request;\n this.#assertTransition(request, APPROVE_FROM, 'approve');\n\n request.status = AccessRequestStatus.APPROVED;\n request.decidedAt = new Date();\n if (options.by) request.decidedBy = options.by;\n if (options.note != null) request.note = options.note;\n await request.save();\n\n await this.#emit({\n type: 'access-request.approved',\n accessRequest: request,\n at: new Date(),\n by: options.by,\n });\n return request;\n }\n\n /**\n * Decline a request: `REQUESTED | APPROVED → DECLINED`. Idempotent. Requires\n * `access-requests:manage`.\n *\n * @throws {@link AccessRequestError} (`INVALID_TRANSITION`) from a terminal state.\n */\n async declineAccessRequest(\n id: string,\n options: DeclineAccessRequestOptions = {},\n ): Promise<AccessRequest> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.MANAGE, {\n by: options.by,\n accessRequestId: id,\n });\n\n const request = await this.#load(id);\n if (request.status === AccessRequestStatus.DECLINED) return request;\n this.#assertTransition(request, DECLINE_FROM, 'decline');\n\n request.status = AccessRequestStatus.DECLINED;\n request.decidedAt = new Date();\n if (options.by) request.decidedBy = options.by;\n if (options.reason != null) request.note = options.reason;\n await request.save();\n\n await this.#emit({\n type: 'access-request.declined',\n accessRequest: request,\n at: new Date(),\n by: options.by,\n });\n return request;\n }\n\n /**\n * Cancel a request: `REQUESTED | APPROVED → CANCELED`. Idempotent. Requires\n * `access-requests:manage`.\n *\n * @throws {@link AccessRequestError} (`INVALID_TRANSITION`) from a terminal state.\n */\n async cancelAccessRequest(\n id: string,\n options: CancelAccessRequestOptions = {},\n ): Promise<AccessRequest> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.MANAGE, {\n by: options.by,\n accessRequestId: id,\n });\n\n const request = await this.#load(id);\n if (request.status === AccessRequestStatus.CANCELED) return request;\n this.#assertTransition(request, CANCEL_FROM, 'cancel');\n\n request.status = AccessRequestStatus.CANCELED;\n request.decidedAt = new Date();\n if (options.by) request.decidedBy = options.by;\n if (options.reason != null) request.note = options.reason;\n await request.save();\n\n await this.#emit({\n type: 'access-request.canceled',\n accessRequest: request,\n at: new Date(),\n by: options.by,\n });\n return request;\n }\n\n // ============= Graduation =============\n\n /**\n * Graduate an approved request into a `User`, optionally attaching a tenant.\n *\n * Valid from `APPROVED` (or from `REQUESTED` when\n * {@link GraduateAccessRequestOptions.allowFromRequested} is set). Creates a\n * user when none exists for the email, or **links** the existing one\n * otherwise (reusing {@link UserCollection}). Idempotent: a second call on an\n * already-`GRADUATED` request returns the same user (and an existing\n * membership for the requested tenant, if any) without re-creating anything.\n *\n * Requires `access-requests:manage`.\n *\n * @throws {@link AccessRequestError} — `NOT_FOUND` (unknown id),\n * `INVALID_TRANSITION` (terminal/declined/canceled or `REQUESTED` without\n * `allowFromRequested`), `TENANT_NOT_FOUND`, or `ROLE_NOT_FOUND`.\n */\n async graduateAccessRequest(\n id: string,\n options: GraduateAccessRequestOptions = {},\n ): Promise<GraduateAccessRequestResult> {\n await this.#requireCapability(ACCESS_REQUEST_CAPABILITIES.MANAGE, {\n by: options.by,\n accessRequestId: id,\n });\n\n const request = await this.#load(id);\n const tenantOption = options.tenant ?? 'none';\n\n // Idempotent re-graduation: return the same user (and existing membership\n // for the requested tenant, if resolvable) without mutating anything.\n if (\n request.status === AccessRequestStatus.GRADUATED &&\n request.resultingUserId\n ) {\n const existingUser = await this.#users.get(request.resultingUserId);\n if (existingUser) {\n const membership = await this.#resolveExistingMembership(\n existingUser.id as string,\n tenantOption,\n );\n return {\n user: existingUser,\n membership: membership ?? undefined,\n accessRequest: request,\n created: false,\n };\n }\n // resultingUserId is stale/missing — fall through and re-link by email.\n }\n\n // State-machine guard for a fresh graduation.\n if (request.status !== AccessRequestStatus.GRADUATED) {\n const allowFromRequested = options.allowFromRequested ?? false;\n const canGraduate =\n request.status === AccessRequestStatus.APPROVED ||\n (request.status === AccessRequestStatus.REQUESTED &&\n allowFromRequested);\n if (!canGraduate) {\n const hint =\n request.status === AccessRequestStatus.REQUESTED\n ? ' (approve it first, or pass allowFromRequested)'\n : '';\n throw new AccessRequestError(\n `Cannot graduate an access request in status \"${request.status}\"${hint}.`,\n 'INVALID_TRANSITION',\n );\n }\n }\n\n // Validate the tenant option BEFORE any write (user / tenant / membership):\n // an invalid role slug or missing tenant must fail fast so graduation never\n // leaves an orphan user or tenant behind (codex review #1713).\n if (tenantOption !== 'none') {\n await this.#ensureRolesSeeded();\n await this.#validateTenantOption(tenantOption);\n }\n\n // Create-or-link the user by normalized email. A brand-new user defaults to\n // ACTIVE (or `options.activate`). An existing linked user keeps its current\n // status unless the operator *explicitly* passes `activate` — so graduation\n // never silently un-suspends an already-managed account.\n const email = normalizeEmail(request.email);\n let user = await this.#users.findByEmail(email);\n let created = false;\n if (!user) {\n user = await this.#users.create({\n email,\n status: options.activate ?? UserStatus.ACTIVE,\n });\n created = true;\n } else if (\n options.activate !== undefined &&\n user.status !== options.activate\n ) {\n user.status = options.activate;\n await user.save();\n }\n\n // Attach a tenant if requested.\n let membership: Membership | undefined;\n let tenant: Tenant | undefined;\n if (tenantOption !== 'none') {\n const attached = await this.#attachTenant(\n user.id as string,\n tenantOption,\n );\n membership = attached.membership;\n tenant = attached.tenant;\n }\n\n // Mark graduated.\n request.status = AccessRequestStatus.GRADUATED;\n request.resultingUserId = user.id as string;\n request.decidedAt = new Date();\n if (options.by) request.decidedBy = options.by;\n if (options.note != null) request.note = options.note;\n await request.save();\n\n await this.#emit({\n type: 'access-request.graduated',\n accessRequest: request,\n at: new Date(),\n by: options.by,\n user,\n membership,\n tenant,\n });\n\n return { user, membership, tenant, accessRequest: request, created };\n }\n\n // ============= Internals =============\n\n /**\n * Load a request or throw `NOT_FOUND`.\n */\n async #load(id: string): Promise<AccessRequest> {\n const request = await this.#requests.get(id);\n if (!request) {\n throw new AccessRequestError('Access request not found.', 'NOT_FOUND');\n }\n return request;\n }\n\n /**\n * Guard a state transition; throw `INVALID_TRANSITION` if the current status\n * is not an allowed source.\n */\n #assertTransition(\n request: AccessRequest,\n allowedFrom: AccessRequestStatus[],\n action: string,\n ): void {\n if (!allowedFrom.includes(request.status)) {\n throw new AccessRequestError(\n `Cannot ${action} an access request in status \"${request.status}\".`,\n 'INVALID_TRANSITION',\n );\n }\n }\n\n /**\n * Run the configured authorizer (if any). Absent an authorizer, operator\n * methods are ungated — see the class-level security note.\n */\n async #requireCapability(\n capability: AccessRequestCapability,\n context: { by?: string | null; accessRequestId?: string },\n ): Promise<void> {\n if (!this.#authorize) return;\n await this.#authorize({\n capability,\n by: context.by ?? null,\n accessRequestId: context.accessRequestId,\n });\n }\n\n /**\n * Best-effort event delivery — a throwing handler is logged and swallowed so\n * it cannot roll back an already-persisted transition.\n */\n async #emit(event: AccessRequestEvent): Promise<void> {\n if (!this.#onEvent) return;\n try {\n await this.#onEvent(event);\n } catch (error) {\n logger.error(\n `AccessRequest event handler threw for \"${event.type}\" (request ${event.accessRequest.id})`,\n { error },\n );\n }\n }\n\n /**\n * Throw `ROLE_NOT_FOUND` if no role with `roleSlug` is resolvable (tenant-\n * specific first, then system). Roles must already be seeded.\n */\n async #assertRoleExists(roleSlug: string, tenantId?: string): Promise<void> {\n if (!(await this.#roles.findBySlug(roleSlug, tenantId))) {\n throw new AccessRequestError(\n `Role \"${roleSlug}\" not found — seed system roles or pass a valid role slug.`,\n 'ROLE_NOT_FOUND',\n );\n }\n }\n\n /**\n * Validate a graduation tenant option with NO side effects: the target tenant\n * must exist (existing-tenant variant) and the role slug must resolve. Run\n * before any persistence so a bad option can't leave orphan rows.\n */\n async #validateTenantOption(\n option: GraduateNewTenantOption | GraduateExistingTenantOption,\n ): Promise<void> {\n if ('tenantId' in option) {\n const tenant = await this.#tenants.get(option.tenantId);\n if (!tenant) {\n throw new AccessRequestError(\n `Target tenant \"${option.tenantId}\" not found.`,\n 'TENANT_NOT_FOUND',\n );\n }\n await this.#assertRoleExists(\n option.role ?? DEFAULT_ROLE_SLUGS.MEMBER,\n option.tenantId,\n );\n } else {\n // A brand-new tenant has no tenant-specific roles yet, so the role must\n // resolve as a system role.\n await this.#assertRoleExists(option.role ?? DEFAULT_ROLE_SLUGS.OWNER);\n }\n }\n\n /**\n * Create-or-attach the requester to a tenant, reusing the tenant / role /\n * membership collections (no duplication of user/membership logic).\n */\n async #attachTenant(\n userId: string,\n option: GraduateNewTenantOption | GraduateExistingTenantOption,\n ): Promise<{ tenant: Tenant; membership: Membership }> {\n await this.#ensureRolesSeeded();\n\n if ('tenantId' in option) {\n const tenant = await this.#tenants.get(option.tenantId);\n if (!tenant) {\n throw new AccessRequestError(\n `Target tenant \"${option.tenantId}\" not found.`,\n 'TENANT_NOT_FOUND',\n );\n }\n const membership = await this.#getOrCreateMembership(\n userId,\n tenant.id as string,\n option.role ?? DEFAULT_ROLE_SLUGS.MEMBER,\n );\n return { tenant, membership };\n }\n\n const tenant = await this.#tenants.create({\n name: option.create.name,\n slug: option.create.slug,\n description: option.create.description,\n status: option.create.status,\n });\n const membership = await this.#getOrCreateMembership(\n userId,\n tenant.id as string,\n option.role ?? DEFAULT_ROLE_SLUGS.OWNER,\n );\n return { tenant, membership };\n }\n\n /**\n * Resolve (creating if needed) the user's membership in a tenant with the\n * given role slug. Idempotent — returns any existing membership for the pair.\n */\n async #getOrCreateMembership(\n userId: string,\n tenantId: string,\n roleSlug: string,\n ): Promise<Membership> {\n const existing = await this.#memberships.findByUserAndTenant(\n userId,\n tenantId,\n );\n if (existing) return existing;\n\n const role = await this.#roles.findBySlug(roleSlug, tenantId);\n if (!role) {\n throw new AccessRequestError(\n `Role \"${roleSlug}\" not found — seed system roles or pass a valid role slug.`,\n 'ROLE_NOT_FOUND',\n );\n }\n\n return await this.#memberships.create({\n userId,\n tenantId,\n roleId: role.id as string,\n status: MembershipStatus.ACTIVE,\n });\n }\n\n /**\n * For idempotent re-graduation: find an existing membership for the requested\n * tenant. Only the existing-tenant variant is resolvable (a `{ create }`\n * variant has no known tenant id on a re-call).\n */\n async #resolveExistingMembership(\n userId: string,\n tenantOption: GraduateTenantOption,\n ): Promise<Membership | null> {\n if (tenantOption === 'none' || !('tenantId' in tenantOption)) return null;\n return await this.#memberships.findByUserAndTenant(\n userId,\n tenantOption.tenantId,\n );\n }\n\n /**\n * Lazily seed the default system roles (owner/admin/member/viewer) the first\n * time graduation attaches a tenant — so the public create path never incurs\n * the write.\n */\n async #ensureRolesSeeded(): Promise<void> {\n if (this.#rolesSeeded) return;\n await this.#roles.seedSystemRoles();\n this.#rolesSeeded = true;\n }\n}\n","/**\n * MagicLinkService - Passwordless email authentication via signed magic link tokens\n *\n * This service handles token generation and verification for magic link\n * authentication. It is framework-agnostic: it returns tokens and verification\n * results, leaving email delivery and cookie management to the caller.\n *\n * Flow:\n * 1. User enters email → generate(email) → token + expiresAt\n * 2. Caller sends email with link containing the token\n * 3. User clicks link → verify(token) → { email, nonce }\n * 4. Caller creates session (via SessionService or createSessionCookie)\n *\n * @packageDocumentation\n *\n * @example\n * ```typescript\n * const magicLink = await MagicLinkService.create({\n * db: { type: 'postgres', url: process.env.DATABASE_URL },\n * secret: process.env.AUTH_SECRET,\n * });\n *\n * // Generate a magic link token\n * const { token, expiresAt } = await magicLink.generate('user@example.com');\n *\n * // Verify a magic link token (single-use)\n * const { email, nonce } = await magicLink.verify(token);\n * ```\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { UsersMagicLinkTokenCollection } from '../collections/MagicLinkTokenCollection.js';\nimport { DEFAULT_TOKEN_EXPIRY_SECONDS } from '../models/MagicLinkToken.js';\nimport { isValidEmail, normalizeEmail } from '../models/User.js';\n\n/**\n * Options for MagicLinkService\n */\nexport interface MagicLinkServiceOptions extends SmrtClassOptions {\n /** Secret used to derive the HMAC signing key (required) */\n secret: string;\n /** Token expiry in seconds (default: 10 minutes) */\n tokenExpiry?: number;\n /** JWT issuer claim (default: 'smrt:magiclink') */\n issuer?: string;\n}\n\n/**\n * Result of generating a magic link token\n */\nexport interface MagicLinkResult {\n /** The signed JWT token */\n token: string;\n /** When the token expires */\n expiresAt: Date;\n}\n\n/**\n * Result of verifying a magic link token\n */\nexport interface MagicLinkVerifyResult {\n /** The email address from the token */\n email: string;\n /** The nonce (for correlation/logging) */\n nonce: string;\n}\n\n/**\n * Error class for magic link authentication failures\n */\nexport class MagicLinkError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'MagicLinkError';\n }\n}\n\n/**\n * MagicLinkService provides passwordless authentication via email magic links.\n *\n * Tokens are HMAC-signed JWTs with embedded nonces for replay protection.\n * The service is framework-agnostic — it does not send emails or set cookies.\n */\nexport class MagicLinkService {\n private tokenCollection!: UsersMagicLinkTokenCollection;\n private signingKey: Uint8Array | null = null;\n private readonly secret: string;\n private readonly tokenExpiry: number;\n private readonly issuer: string;\n private readonly options: MagicLinkServiceOptions;\n\n constructor(options: MagicLinkServiceOptions) {\n if (!options.secret) {\n throw new Error('MagicLinkService requires a secret for token signing');\n }\n this.secret = options.secret;\n this.tokenExpiry = options.tokenExpiry ?? DEFAULT_TOKEN_EXPIRY_SECONDS;\n this.issuer = options.issuer ?? 'smrt:magiclink';\n this.options = options;\n }\n\n /**\n * Initialize collections\n */\n async initialize(): Promise<void> {\n this.tokenCollection = await UsersMagicLinkTokenCollection.create(\n this.options,\n );\n }\n\n /**\n * Derive the HMAC signing key from the secret\n */\n private async getSigningKey(): Promise<Uint8Array> {\n if (this.signingKey) return this.signingKey;\n\n const encoder = new TextEncoder();\n const data = encoder.encode(`magiclink:${this.secret}`);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n this.signingKey = new Uint8Array(hashBuffer);\n\n return this.signingKey;\n }\n\n /**\n * Generate a magic link token for the given email.\n *\n * Stores a nonce in the database for replay protection.\n * The caller is responsible for emailing the token to the user.\n */\n async generate(email: string): Promise<MagicLinkResult> {\n // Dynamic import to avoid eagerly loading jose at module init\n const { SignJWT } = await import('jose');\n\n const key = await this.getSigningKey();\n const nonce = crypto.randomUUID();\n const normalizedEmail = normalizeEmail(email);\n\n if (!isValidEmail(normalizedEmail)) {\n throw new MagicLinkError('Invalid email address');\n }\n\n const expiresAt = new Date(Date.now() + this.tokenExpiry * 1000);\n\n // Store nonce for replay protection\n await this.tokenCollection.create({\n nonce,\n email: normalizedEmail,\n used: false,\n expiresAt,\n });\n\n // Sign the token\n const token = await new SignJWT({\n email: normalizedEmail,\n nonce,\n })\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(`${this.tokenExpiry}s`)\n .setIssuer(this.issuer)\n .sign(key);\n\n return { token, expiresAt };\n }\n\n /**\n * Verify a magic link token.\n *\n * Checks JWT signature, expiry, and that the nonce hasn't been used.\n * Marks the nonce as used on success (single-use enforcement).\n *\n * @throws {MagicLinkError} If the token is invalid, expired, or already used\n */\n async verify(token: string): Promise<MagicLinkVerifyResult> {\n const { jwtVerify, errors } = await import('jose');\n\n const key = await this.getSigningKey();\n\n // Verify JWT signature and expiry\n let payload: Record<string, unknown>;\n try {\n const result = await jwtVerify(token, key, {\n issuer: this.issuer,\n });\n payload = result.payload as Record<string, unknown>;\n } catch (err) {\n if (err instanceof errors.JWTExpired) {\n throw new MagicLinkError('Token has expired');\n }\n throw new MagicLinkError('Invalid token');\n }\n\n const email = payload.email;\n const nonce = payload.nonce;\n\n // Explicit type validation — JWT payload values are unknown\n if (typeof email !== 'string' || typeof nonce !== 'string') {\n throw new MagicLinkError('Invalid token payload');\n }\n\n // Atomically claim the nonce (single-use enforcement) via a single\n // conditional UPDATE. Only one concurrent verify() call can flip the\n // row from used=false to used=true before the DB expires the nonce.\n const claimed = await this.tokenCollection.markUsed(nonce);\n\n if (!claimed) {\n throw new MagicLinkError('Token has already been used or has expired');\n }\n\n return { email: normalizeEmail(email), nonce };\n }\n\n /**\n * Clean up expired tokens (run periodically)\n */\n async cleanupExpiredTokens(): Promise<number> {\n return this.tokenCollection.deleteExpired();\n }\n\n /**\n * Static factory method\n */\n static async create(\n options: MagicLinkServiceOptions,\n ): Promise<MagicLinkService> {\n const service = new MagicLinkService(options);\n await service.initialize();\n return service;\n }\n}\n","/**\n * Postgres policy generation and application for SMRT permissions\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport {\n findManifestEntryByQualifiedName,\n ObjectRegistry,\n type SmrtClassOptions,\n} from '@happyvertical/smrt-core';\nimport { PermissionCollection } from '../collections/PermissionCollection.js';\nimport {\n PermissionCatalogService,\n type PostgresPermissionAction,\n type PostgresPermissionBinding,\n} from './PermissionCatalogService.js';\n\ninterface QueryableDatabase {\n query: (sql: string, ...params: unknown[]) => Promise<unknown>;\n url?: string;\n}\n\ntype DatabaseConfig = SmrtClassOptions['db'] | SmrtClassOptions['persistence'];\n\ninterface TablePolicyTarget {\n actions: Map<PostgresPermissionAction, Set<string>>;\n className?: string;\n collection?: string;\n qualifiedName?: string;\n schemaName: string;\n tableName: string;\n tenantField: string;\n}\n\nexport interface PostgresPermissionPolicyReportItem {\n className?: string;\n collection?: string;\n qualifiedName?: string;\n reason: string;\n schemaName?: string;\n tableName?: string;\n}\n\nexport interface PostgresPermissionPolicyTarget {\n actions: Partial<Record<PostgresPermissionAction, string[]>>;\n className?: string;\n collection?: string;\n qualifiedName?: string;\n schemaName: string;\n tableName: string;\n tenantField: string;\n}\n\nexport interface GeneratePostgresPermissionSqlResult {\n bindings: PostgresPermissionBinding[];\n skipped: PostgresPermissionPolicyReportItem[];\n sql: string;\n statements: string[];\n targets: PostgresPermissionPolicyTarget[];\n}\n\nfunction toSnakeCase(value: string): string {\n return value\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[-\\s]+/g, '_')\n .toLowerCase();\n}\n\nfunction quoteIdent(identifier: string): string {\n return `\"${identifier.replaceAll('\"', '\"\"')}\"`;\n}\n\nfunction quoteLiteral(value: string): string {\n return `'${value.replaceAll(\"'\", \"''\")}'`;\n}\n\nfunction isProbablyPostgres(\n configDb: DatabaseConfig,\n database: QueryableDatabase,\n): boolean {\n if (\n configDb &&\n typeof configDb === 'object' &&\n !('query' in configDb) &&\n 'type' in configDb &&\n configDb.type === 'postgres'\n ) {\n return true;\n }\n\n if (typeof database.url === 'string' && database.url.startsWith('postgres')) {\n return true;\n }\n\n return (database.constructor?.name || '').toLowerCase().includes('postgres');\n}\n\nfunction normalizePostgresPermissionAction(\n action: PostgresPermissionBinding['action'],\n): PostgresPermissionAction {\n const normalized = action.toUpperCase();\n if (\n normalized === 'SELECT' ||\n normalized === 'INSERT' ||\n normalized === 'UPDATE' ||\n normalized === 'DELETE'\n ) {\n return normalized;\n }\n\n throw new Error(\n `Invalid Postgres permission binding action \"${action}\". Expected one of SELECT, INSERT, UPDATE, DELETE.`,\n );\n}\n\nfunction normalizePostgresPermissionBinding(\n binding: PostgresPermissionBinding,\n fallbackPermission?: string,\n): PostgresPermissionBinding {\n const tableName = binding.tableName?.trim();\n if (!tableName) {\n throw new Error(\n 'Postgres permission binding is missing a tableName value.',\n );\n }\n\n const permission = binding.permission ?? fallbackPermission;\n if (!permission) {\n throw new Error(\n 'Postgres permission binding is missing a permission value.',\n );\n }\n\n return {\n action: normalizePostgresPermissionAction(binding.action),\n permission,\n schemaName: binding.schemaName,\n tableName,\n tenantField: binding.tenantField,\n };\n}\n\nfunction parseTableReference(\n binding: Pick<PostgresPermissionBinding, 'schemaName' | 'tableName'>,\n): { schemaName: string; tableName: string } {\n if (binding.tableName.includes('.')) {\n const [schemaName, tableName] = binding.tableName.split('.', 2);\n return {\n schemaName,\n tableName,\n };\n }\n\n return {\n schemaName: binding.schemaName ?? 'public',\n tableName: binding.tableName,\n };\n}\n\nfunction buildPolicyName(\n tableName: string,\n action: PostgresPermissionAction,\n): string {\n const actionSegment = action.toLowerCase();\n const hash = createHash('sha1')\n .update(`${tableName}:${action}`)\n .digest('hex')\n .slice(0, 8);\n const sanitizedTable = tableName\n .replace(/[^a-zA-Z0-9_]+/g, '_')\n .replace(/^_+|_+$/g, '');\n const prefix = 'smrt_';\n const separatorLength = 2;\n const maxTableSegmentLength =\n 63 - prefix.length - actionSegment.length - hash.length - separatorLength;\n const tableSegment = (sanitizedTable || 'table').slice(\n 0,\n Math.max(maxTableSegmentLength, 1),\n );\n\n return `${prefix}${tableSegment}_${actionSegment}_${hash}`;\n}\n\nfunction buildPermissionExpression(permissionSlugs: string[]): string {\n if (permissionSlugs.length === 0) {\n return 'FALSE';\n }\n\n return permissionSlugs\n .map((permission) => `smrt_has_permission(${quoteLiteral(permission)})`)\n .join(' OR ');\n}\n\nfunction buildTenantMatchExpression(tenantField: string): string {\n return `${quoteIdent(tenantField)}::text = smrt_current_tenant_id()`;\n}\n\nfunction buildSelectPolicySql(\n target: TablePolicyTarget,\n permissions: string[],\n): string[] {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n const policyName = buildPolicyName(target.tableName, 'SELECT');\n const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;\n\n return [\n `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,\n `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR SELECT USING (${condition})`,\n ];\n}\n\nfunction buildInsertPolicySql(\n target: TablePolicyTarget,\n permissions: string[],\n): string[] {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n const policyName = buildPolicyName(target.tableName, 'INSERT');\n const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;\n\n return [\n `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,\n `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR INSERT WITH CHECK (${condition})`,\n ];\n}\n\nfunction buildUpdatePolicySql(\n target: TablePolicyTarget,\n permissions: string[],\n): string[] {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n const policyName = buildPolicyName(target.tableName, 'UPDATE');\n const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;\n\n return [\n `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,\n `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR UPDATE USING (${condition}) WITH CHECK (${condition})`,\n ];\n}\n\nfunction buildDeletePolicySql(\n target: TablePolicyTarget,\n permissions: string[],\n): string[] {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n const policyName = buildPolicyName(target.tableName, 'DELETE');\n const condition = `smrt_rls_bypass() OR ((${buildTenantMatchExpression(target.tenantField)}) AND (${buildPermissionExpression(permissions)}))`;\n\n return [\n `DROP POLICY IF EXISTS ${quoteIdent(policyName)} ON ${qualifiedTable}`,\n `CREATE POLICY ${quoteIdent(policyName)} ON ${qualifiedTable} FOR DELETE USING (${condition})`,\n ];\n}\n\nfunction buildHelperStatements(): string[] {\n return [\n [\n 'CREATE OR REPLACE FUNCTION smrt_rls_bypass()',\n 'RETURNS boolean',\n 'LANGUAGE sql',\n 'STABLE',\n 'AS $$',\n \" SELECT COALESCE(NULLIF(current_setting('smrt.system_context', true), ''), 'false')::boolean\",\n \" OR COALESCE(NULLIF(current_setting('smrt.super_admin_bypass', true), ''), 'false')::boolean\",\n '$$',\n ].join('\\n'),\n [\n 'CREATE OR REPLACE FUNCTION smrt_current_tenant_id()',\n 'RETURNS text',\n 'LANGUAGE sql',\n 'STABLE',\n 'AS $$',\n \" SELECT NULLIF(current_setting('smrt.tenant_id', true), '')\",\n '$$',\n ].join('\\n'),\n [\n 'CREATE OR REPLACE FUNCTION smrt_has_permission(required_permission text)',\n 'RETURNS boolean',\n 'LANGUAGE sql',\n 'STABLE',\n 'AS $$',\n ' SELECT smrt_rls_bypass()',\n \" OR jsonb_exists(COALESCE(NULLIF(current_setting('smrt.permissions', true), ''), '[]')::jsonb, required_permission)\",\n '$$',\n ].join('\\n'),\n ];\n}\n\nfunction addBindingToTarget(\n targets: Map<string, TablePolicyTarget>,\n binding: PostgresPermissionBinding,\n source: Pick<\n TablePolicyTarget,\n 'className' | 'collection' | 'qualifiedName'\n > = {},\n): void {\n const { schemaName, tableName } = parseTableReference(binding);\n const targetKey = `${schemaName}.${tableName}`;\n const existing = targets.get(targetKey);\n const tenantField = binding.tenantField ?? 'tenant_id';\n\n if (existing && existing.tenantField !== tenantField) {\n throw new Error(\n `Conflicting tenant fields for table '${targetKey}': '${existing.tenantField}' !== '${tenantField}'`,\n );\n }\n\n const target = existing ?? {\n actions: new Map<PostgresPermissionAction, Set<string>>(),\n ...source,\n schemaName,\n tableName,\n tenantField,\n };\n const action = normalizePostgresPermissionAction(binding.action);\n const permissions = target.actions.get(action) ?? new Set<string>();\n if (binding.permission) {\n permissions.add(binding.permission);\n }\n target.actions.set(action, permissions);\n targets.set(targetKey, target);\n}\n\nexport function generatePostgresPermissionSql(\n options: SmrtClassOptions = {},\n): GeneratePostgresPermissionSqlResult {\n const catalogService = PermissionCatalogService.create(options);\n const catalog = catalogService.getCatalog();\n const config = catalogService.getUsersConfig();\n const candidateTargets = new Map<string, TablePolicyTarget>();\n const skipped: PostgresPermissionPolicyReportItem[] = [];\n\n const autoCandidates = new Map<\n string,\n Array<{\n className?: string;\n collection: string;\n qualifiedName?: string;\n schemaName: string;\n tableName: string;\n tenantField: string;\n }>\n >();\n\n for (const metadata of ObjectRegistry.getAllObjectMetadata()) {\n const registered =\n ObjectRegistry.getClassByConstructor(metadata.constructor) ??\n ObjectRegistry.getClass(metadata.name);\n const tenantScoped = registered?.tenantScopedConfig;\n const manifestEntry = registered?.qualifiedName\n ? findManifestEntryByQualifiedName(registered.qualifiedName)\n : undefined;\n\n if (!tenantScoped) {\n skipped.push({\n className: metadata.name,\n qualifiedName: registered?.qualifiedName,\n reason: 'not tenant-scoped',\n });\n continue;\n }\n\n if (tenantScoped.mode !== 'required') {\n skipped.push({\n className: metadata.name,\n qualifiedName: registered?.qualifiedName,\n reason: `tenant mode '${tenantScoped.mode}' is not supported for automatic Postgres RLS generation`,\n });\n continue;\n }\n\n const rawTableName =\n registered?.schema?.tableName ?? manifestEntry?.schema?.tableName;\n if (!rawTableName) {\n skipped.push({\n className: metadata.name,\n qualifiedName: registered?.qualifiedName,\n reason: 'no schema table name available',\n });\n continue;\n }\n\n const parsedTable = parseTableReference({\n tableName: rawTableName,\n });\n const tableKey = `${parsedTable.schemaName}.${parsedTable.tableName}`;\n const objectConfig = manifestEntry?.decoratorConfig ?? metadata.config;\n const rawCollection = (objectConfig as { collection?: unknown } | undefined)\n ?.collection;\n const configuredCollection =\n typeof rawCollection === 'string' && rawCollection.length > 0\n ? rawCollection\n : undefined;\n const collection =\n configuredCollection ??\n manifestEntry?.collection ??\n `${toSnakeCase(metadata.name)}s`;\n\n const entries = autoCandidates.get(tableKey) ?? [];\n entries.push({\n className: metadata.name,\n collection,\n qualifiedName: registered?.qualifiedName,\n schemaName: parsedTable.schemaName,\n tableName: parsedTable.tableName,\n tenantField: toSnakeCase(tenantScoped.field),\n });\n autoCandidates.set(tableKey, entries);\n }\n\n for (const [tableKey, entries] of autoCandidates) {\n if (entries.length > 1) {\n for (const entry of entries) {\n skipped.push({\n className: entry.className,\n collection: entry.collection,\n qualifiedName: entry.qualifiedName,\n reason: `table '${tableKey}' is shared by multiple objects, so automatic policy generation was skipped`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n });\n }\n continue;\n }\n\n const entry = entries[0];\n addBindingToTarget(\n candidateTargets,\n {\n action: 'SELECT',\n permission: `${entry.collection}.read`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n tenantField: entry.tenantField,\n },\n entry,\n );\n addBindingToTarget(\n candidateTargets,\n {\n action: 'INSERT',\n permission: `${entry.collection}.create`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n tenantField: entry.tenantField,\n },\n entry,\n );\n addBindingToTarget(\n candidateTargets,\n {\n action: 'UPDATE',\n permission: `${entry.collection}.update`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n tenantField: entry.tenantField,\n },\n entry,\n );\n addBindingToTarget(\n candidateTargets,\n {\n action: 'DELETE',\n permission: `${entry.collection}.delete`,\n schemaName: entry.schemaName,\n tableName: entry.tableName,\n tenantField: entry.tenantField,\n },\n entry,\n );\n }\n\n const explicitBindings: PostgresPermissionBinding[] = [];\n for (const definition of catalog.permissions) {\n for (const binding of definition.postgres?.bindings ?? []) {\n explicitBindings.push(\n normalizePostgresPermissionBinding(binding, definition.slug),\n );\n }\n }\n for (const binding of config.permissions?.postgres?.bindings ?? []) {\n explicitBindings.push(normalizePostgresPermissionBinding(binding));\n }\n\n for (const binding of explicitBindings) {\n addBindingToTarget(candidateTargets, binding);\n }\n\n const statements = [...buildHelperStatements()];\n const targets = Array.from(candidateTargets.values())\n .sort((left, right) =>\n `${left.schemaName}.${left.tableName}`.localeCompare(\n `${right.schemaName}.${right.tableName}`,\n ),\n )\n .map((target) => ({\n actions: Object.fromEntries(\n Array.from(target.actions.entries()).map(([action, permissions]) => [\n action,\n Array.from(permissions).sort(),\n ]),\n ) as Partial<Record<PostgresPermissionAction, string[]>>,\n className: target.className,\n collection: target.collection,\n qualifiedName: target.qualifiedName,\n schemaName: target.schemaName,\n tableName: target.tableName,\n tenantField: target.tenantField,\n }));\n\n for (const target of Array.from(candidateTargets.values())) {\n const qualifiedTable = `${quoteIdent(target.schemaName)}.${quoteIdent(target.tableName)}`;\n statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY`);\n statements.push(`ALTER TABLE ${qualifiedTable} FORCE ROW LEVEL SECURITY`);\n\n const selectPermissions = Array.from(\n target.actions.get('SELECT') ?? [],\n ).sort();\n const insertPermissions = Array.from(\n target.actions.get('INSERT') ?? [],\n ).sort();\n const updatePermissions = Array.from(\n target.actions.get('UPDATE') ?? [],\n ).sort();\n const deletePermissions = Array.from(\n target.actions.get('DELETE') ?? [],\n ).sort();\n\n if (selectPermissions.length > 0) {\n statements.push(...buildSelectPolicySql(target, selectPermissions));\n }\n if (insertPermissions.length > 0) {\n statements.push(...buildInsertPolicySql(target, insertPermissions));\n }\n if (updatePermissions.length > 0) {\n statements.push(...buildUpdatePolicySql(target, updatePermissions));\n }\n if (deletePermissions.length > 0) {\n statements.push(...buildDeletePolicySql(target, deletePermissions));\n }\n }\n\n return {\n bindings: explicitBindings,\n skipped,\n sql: `${statements.join(';\\n')};\\n`,\n statements,\n targets,\n };\n}\n\nexport async function applyPostgresPermissionPolicies(\n options: SmrtClassOptions = {},\n): Promise<GeneratePostgresPermissionSqlResult> {\n const permissions = await PermissionCollection.create(options);\n const databaseOptions = options.db ?? options.persistence;\n if (\n !isProbablyPostgres(databaseOptions, permissions.db as QueryableDatabase)\n ) {\n throw new Error(\n 'applyPostgresPermissionPolicies() requires a Postgres database connection.',\n );\n }\n\n const result = generatePostgresPermissionSql(options);\n for (const statement of result.statements) {\n try {\n await permissions.db.query(statement);\n } catch (error) {\n throw new Error(\n `Failed to apply Postgres permission policy statement:\\n${statement}`,\n {\n cause: error,\n },\n );\n }\n }\n\n return result;\n}\n","/**\n * TenantService - Manages tenant creation with configurable policies\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { MembershipCollection } from '../collections/MembershipCollection.js';\nimport { RoleCollection } from '../collections/RoleCollection.js';\nimport { TenantCollection } from '../collections/TenantCollection.js';\nimport type { Membership } from '../models/Membership.js';\nimport type { Tenant } from '../models/Tenant.js';\nimport {\n DEFAULT_ROLE_SLUGS,\n DEFAULT_TENANT_POLICY,\n MembershipStatus,\n type TenantPolicy,\n} from '../types/index.js';\n\n/**\n * Result of tenant creation with ownership\n */\nexport interface TenantWithOwnershipResult {\n tenant: Tenant;\n membership: Membership;\n}\n\n/**\n * Result of ensureTenantForUser\n */\nexport interface EnsureTenantResult {\n /** The tenant (null if flexible mode and no existing tenant) */\n tenant: Tenant | null;\n /** The membership (null if no tenant) */\n membership: Membership | null;\n /** Whether a new tenant was created */\n created: boolean;\n}\n\n/**\n * TenantService manages tenant creation with configurable policies.\n *\n * Policies:\n * - `flexible`: No tenant created on signup, user can have zero tenants\n * - `personal`: Auto-create personal tenant on first login, can delete all\n * - `required`: Auto-create personal tenant, must keep at least one\n *\n * @example\n * ```typescript\n * const tenantService = new TenantService(options, {\n * mode: 'personal',\n * maxTenants: 5,\n * defaultName: 'My Workspace',\n * });\n * await tenantService.initialize();\n *\n * // During OIDC login\n * const { tenant, membership, created } = await tenantService.ensureTenantForUser(\n * user.id,\n * { email: user.email, name: user.name }\n * );\n *\n * // Creating additional tenants\n * if (await tenantService.canCreateTenant(user.id)) {\n * const { tenant } = await tenantService.createTenantWithOwnership(\n * user.id,\n * 'New Organization'\n * );\n * }\n * ```\n */\nexport class TenantService {\n private options: SmrtClassOptions;\n private policy: TenantPolicy;\n private tenantCollection!: TenantCollection;\n private membershipCollection!: MembershipCollection;\n private roleCollection!: RoleCollection;\n\n constructor(options: SmrtClassOptions, policy?: TenantPolicy) {\n this.options = options;\n this.policy = policy ?? DEFAULT_TENANT_POLICY;\n }\n\n /**\n * Initialize collections\n */\n async initialize(): Promise<void> {\n this.tenantCollection = await TenantCollection.create(this.options);\n this.membershipCollection = await MembershipCollection.create(this.options);\n this.roleCollection = await RoleCollection.create(this.options);\n\n // Seed system roles if needed\n await this.roleCollection.seedSystemRoles();\n }\n\n /**\n * Get the current policy\n */\n getPolicy(): TenantPolicy {\n return { ...this.policy };\n }\n\n /**\n * Create a tenant and make the user the owner\n *\n * @param userId - The user to make owner\n * @param name - Tenant name\n * @param options - Optional slug override\n * @returns The created tenant and membership\n */\n async createTenantWithOwnership(\n userId: string,\n name: string,\n options?: { slug?: string },\n ): Promise<TenantWithOwnershipResult> {\n // Check if user can create\n if (!(await this.canCreateTenant(userId))) {\n throw new Error(\n `User has reached maximum tenant limit (${this.policy.maxTenants})`,\n );\n }\n\n // Create tenant\n const tenant = await this.tenantCollection.create({\n name,\n slug: options?.slug,\n });\n await tenant.save();\n\n // Get owner role\n const ownerRole = await this.roleCollection.findBySlug(\n DEFAULT_ROLE_SLUGS.OWNER,\n );\n if (!ownerRole) {\n throw new Error('Owner role not found - run seedSystemRoles first');\n }\n\n // Create ownership membership\n const membership = await this.membershipCollection.create({\n userId,\n tenantId: tenant.id as string,\n roleId: ownerRole.id as string,\n status: MembershipStatus.ACTIVE,\n });\n await membership.save();\n\n return { tenant, membership };\n }\n\n /**\n * Check if a user can create a new tenant\n *\n * Returns false if maxTenants limit is reached (0 = unlimited)\n */\n async canCreateTenant(userId: string): Promise<boolean> {\n if (this.policy.maxTenants === 0) {\n return true; // Unlimited\n }\n\n const ownerRole = await this.roleCollection.findBySlug(\n DEFAULT_ROLE_SLUGS.OWNER,\n );\n if (!ownerRole) {\n // Fail closed if owner role is not configured\n return false;\n }\n\n const memberships =\n await this.membershipCollection.findActiveByUser(userId);\n const ownedCount = memberships.filter(\n (m) => m.roleId === ownerRole.id,\n ).length;\n\n return ownedCount < this.policy.maxTenants;\n }\n\n /**\n * Get a specific error message explaining why a tenant cannot be deleted.\n *\n * @returns Error message if deletion is not allowed, or null if allowed.\n */\n private async getDeleteTenantError(\n userId: string,\n tenantId: string,\n ): Promise<string | null> {\n // Check user has an active membership for this tenant\n const membership = await this.membershipCollection.findByUserAndTenant(\n userId,\n tenantId,\n );\n if (!membership || membership.status !== MembershipStatus.ACTIVE) {\n return 'You are not a member of this tenant or it does not exist.';\n }\n\n const ownerRole = await this.roleCollection.findBySlug(\n DEFAULT_ROLE_SLUGS.OWNER,\n );\n if (!ownerRole) {\n return 'Owner role is not configured. Cannot determine deletion permissions.';\n }\n\n if (membership.roleId !== ownerRole.id) {\n return 'Only the tenant owner can delete this tenant.';\n }\n\n // For 'required' policy, check if this is the last tenant\n if (this.policy.mode === 'required') {\n const allMemberships =\n await this.membershipCollection.findActiveByUser(userId);\n const ownerMemberships = allMemberships.filter(\n (m) => m.roleId === ownerRole.id,\n );\n\n if (ownerMemberships.length <= 1) {\n return 'Cannot delete your last tenant. Policy requires at least one tenant.';\n }\n }\n\n return null;\n }\n\n /**\n * Check if a user can delete a specific tenant\n *\n * Returns false if:\n * - User is not an active member\n * - User is not the owner\n * - Policy is 'required' and this is the last tenant\n */\n async canDeleteTenant(userId: string, tenantId: string): Promise<boolean> {\n const error = await this.getDeleteTenantError(userId, tenantId);\n return error === null;\n }\n\n /**\n * Delete a tenant\n *\n * Note: This does not cascade delete related records (memberships, etc.).\n * The caller should handle cleanup of related data if needed.\n *\n * @throws Error if user cannot delete the tenant (with specific reason)\n */\n async deleteTenant(userId: string, tenantId: string): Promise<void> {\n const error = await this.getDeleteTenantError(userId, tenantId);\n if (error) {\n throw new Error(error);\n }\n\n const tenant = await this.tenantCollection.get(tenantId);\n if (tenant) {\n await tenant.delete();\n }\n }\n\n /**\n * Ensure a tenant exists for the user based on policy\n *\n * Called during OIDC login to apply tenant policy:\n * - `flexible`: Returns first existing tenant or null (no auto-create)\n * - `personal`/`required`: Creates default tenant if none exists\n *\n * @param userId - The user ID\n * @param userInfo - User info for naming the auto-created tenant\n * @returns Tenant and membership (may be null in flexible mode)\n */\n async ensureTenantForUser(\n userId: string,\n userInfo: { email?: string; name?: string },\n ): Promise<EnsureTenantResult> {\n // Get existing memberships\n const memberships =\n await this.membershipCollection.findActiveByUser(userId);\n\n // If user has tenants, return the first one\n if (memberships.length > 0) {\n const firstMembership = memberships[0];\n const tenant = await this.tenantCollection.get(\n firstMembership.tenantId as string,\n );\n\n return {\n tenant: tenant ?? null,\n membership: firstMembership,\n created: false,\n };\n }\n\n // No existing tenants - apply policy\n if (this.policy.mode === 'flexible') {\n // Flexible mode: don't auto-create\n return {\n tenant: null,\n membership: null,\n created: false,\n };\n }\n\n // Personal or required mode: create default tenant\n // Use user's name for personalized tenant name, or fall back to policy default\n const tenantName = userInfo.name\n ? `${userInfo.name}'s Workspace`\n : this.policy.defaultName;\n\n const { tenant, membership } = await this.createTenantWithOwnership(\n userId,\n tenantName,\n );\n\n return {\n tenant,\n membership,\n created: true,\n };\n }\n\n /**\n * Get all tenants for a user (where they are owner)\n */\n async getOwnedTenants(userId: string): Promise<Tenant[]> {\n const ownerRole = await this.roleCollection.findBySlug(\n DEFAULT_ROLE_SLUGS.OWNER,\n );\n if (!ownerRole) {\n return [];\n }\n\n const memberships =\n await this.membershipCollection.findActiveByUser(userId);\n const ownerMemberships = memberships.filter(\n (m) => m.roleId === ownerRole.id,\n );\n\n // Fetch all tenants in parallel to avoid N+1 queries\n const tenantPromises = ownerMemberships.map((m) =>\n this.tenantCollection.get(m.tenantId as string),\n );\n const tenantsOrNull = await Promise.all(tenantPromises);\n\n return tenantsOrNull.filter((tenant): tenant is Tenant => tenant !== null);\n }\n\n /**\n * Static factory method\n */\n static async create(\n options: SmrtClassOptions,\n policy?: TenantPolicy,\n ): Promise<TenantService> {\n const service = new TenantService(options, policy);\n await service.initialize();\n return service;\n }\n}\n","/**\n * @happyvertical/smrt-users\n *\n * Multi-tenant user management for the SMRT framework.\n * Provides users, tenants, roles, permissions, and groups.\n *\n * @example\n * ```typescript\n * import {\n * User,\n * UserCollection,\n * Tenant,\n * TenantCollection,\n * Role,\n * RoleCollection,\n * Permission,\n * PermissionCollection,\n * Membership,\n * MembershipCollection,\n * PermissionResolver,\n * UserStatus,\n * MembershipStatus,\n * } from '@happyvertical/smrt-users';\n *\n * // Create collections\n * const users = await UserCollection.create({\n * persistence: { type: 'sql', url: 'app.db' }\n * });\n *\n * // Seed system roles\n * const roles = await RoleCollection.create({ persistence: { type: 'sql', url: 'app.db' } });\n * await roles.seedSystemRoles();\n *\n * // Resolve permissions\n * const resolver = await PermissionResolver.create({ persistence: { type: 'sql', url: 'app.db' } });\n * const hasAccess = await resolver.hasPermission(user.id, tenant.id, 'articles.create');\n *\n * // Sync manifest-derived permissions into the Permission table\n * const syncResult = await syncPermissionCatalog({\n * db: { type: 'postgres', url: process.env.DATABASE_URL! }\n * });\n * console.log(syncResult.created);\n *\n * // Generate or apply Postgres RLS policies\n * const sql = generatePostgresPermissionSql({\n * db: { type: 'postgres', url: process.env.DATABASE_URL! }\n * });\n * console.log(sql.targets);\n * await applyPostgresPermissionPolicies({\n * db: { type: 'postgres', url: process.env.DATABASE_URL! }\n * });\n * ```\n *\n * @packageDocumentation\n */\n\n// Self-register this package's manifest before any @smrt() decorator fires\n// downstream. Must come first so the side effect runs ahead of the class\n// module loads below. See __smrt-register__.ts for issue #1132 context.\nimport './__smrt-register__.js';\n// Contribute expired-credential retention to the framework sweep as soon as\n// this package is loaded (#2375), so `smrt db:prune` and any host process see\n// the tasks without an app-level wiring step. Registering is not scheduling:\n// nothing is deleted until something runs a sweep, and the policy can still\n// disable any task by name.\nimport { registerUserRetentionTasks } from './retention.js';\n\nregisterUserRetentionTasks();\n\n// Collections\nexport {\n AccessRequestCollection,\n type CliAuthApproveReservation,\n CliAuthRequestCollection,\n type CreateChildTenantOptions,\n type CreateSessionOptions,\n DEFAULT_ROLE_PERMISSION_PATTERNS,\n type GetOrCreateFromOidcOptions,\n GroupCollection,\n GroupMemberCollection,\n GroupRoleCollection,\n MagicLinkTokenCollection,\n MembershipCollection,\n MembershipOverrideCollection,\n type NormalizedOidcClaims,\n type OidcClaims,\n type OidcIdentityResult,\n type OidcProfileOwnerAuthorization,\n type OidcProfileOwnerAuthorizer,\n type OidcProfileOwnerAuthorizerContext,\n type OidcProfileResolver,\n type OidcProfileResolverContext,\n OidcProvisioningError,\n type OidcProvisioningErrorCode,\n PermissionCollection,\n RoleCollection,\n RolePermissionCollection,\n type RolePermissionPatternMatrix,\n type SeedRolePermissionsOptions,\n type SeedRolePermissionsResult,\n type SeedSystemRolesOptions,\n SessionCollection,\n TenantCollection,\n TenantHierarchyError,\n TenantIntegrationCollection,\n TenantPermissionOverrideCollection,\n type TenantPermissionOverrideResult,\n UserCollection,\n UsersCliAuthApproveLimitCollection,\n UsersCliAuthRequestCollection,\n UsersMagicLinkTokenCollection,\n} from './collections/index.js';\n// Deploy-time data migrations\nexport {\n type BackfillUserEmailKeysResult,\n backfillUserEmailKeys,\n type DuplicateUserEmailKey,\n UserEmailKeyBackfillError,\n type UserEmailKeyBackfillErrorCode,\n} from './migrations/backfillUserEmailKeys.js';\n// Models\nexport {\n AccessRequest,\n type AccessRequestOptions,\n CliAuthRequest,\n type CliAuthRequestStatus,\n DEFAULT_SESSION_TTL,\n DEFAULT_TOKEN_EXPIRY_SECONDS,\n Group,\n GroupMember,\n GroupRole,\n generateSessionId,\n MAX_TENANT_HIERARCHY_DEPTH,\n MagicLinkToken,\n Membership,\n MembershipOverride,\n Permission,\n Role,\n RolePermission,\n Session,\n Tenant,\n TenantIntegration,\n type TenantIntegrationCheckSummary,\n type TenantIntegrationOptions,\n type TenantIntegrationProvider,\n type TenantIntegrationStatus,\n TenantPermissionOverride,\n User,\n UsersCliAuthApproveLimit,\n UsersCliAuthRequest,\n UsersMagicLinkToken,\n} from './models/index.js';\n// Credential retention — expired sessions and tokens (#2375)\nexport {\n CLI_AUTH_RETENTION_TASK,\n MAGIC_LINK_RETENTION_TASK,\n registerUserRetentionTasks,\n SESSIONS_RETENTION_TASK,\n USER_RETENTION_TASKS,\n unregisterUserRetentionTasks,\n} from './retention.js';\n\n// Services\nexport {\n ACCESS_REQUEST_CAPABILITIES,\n type AccessRequestAuthorizationContext,\n type AccessRequestAuthorizer,\n type AccessRequestCapability,\n AccessRequestError,\n type AccessRequestErrorCode,\n type AccessRequestEvent,\n type AccessRequestEventHandler,\n type AccessRequestEventType,\n AccessRequestService,\n type AccessRequestServiceOptions,\n type ApproveAccessRequestOptions,\n type ApproveCliAuthRequestInput,\n applyPostgresPermissionPolicies,\n assertOperationPermission,\n type CancelAccessRequestOptions,\n type CliAuthStartResult,\n type CliAuthTokenResult,\n type CreateAccessRequestInput,\n type CreateAuthorizationUrlOptions,\n checkOperationPermission,\n DEFAULT_CLI_AUTH_POLL_INTERVAL_SECONDS,\n DEFAULT_CLI_AUTH_REQUEST_TTL_SECONDS,\n DEFAULT_CLI_SESSION_TTL_SECONDS,\n type DeclineAccessRequestOptions,\n decodeOidcTransaction,\n deriveOperationPermissionCollectionName,\n deriveOperationPermissionSlug,\n type EnsureTenantResult,\n encodeOidcTransaction,\n type GeneratePostgresPermissionSqlResult,\n type GraduateAccessRequestOptions,\n type GraduateAccessRequestResult,\n type GraduateExistingTenantOption,\n type GraduateNewTenantOption,\n type GraduateTenantOption,\n generatePostgresPermissionSql,\n getCurrentSessionPermissionContext,\n getRequestScopedDatabase,\n getUsersOidcConfig,\n hasOperationPermission,\n type ListAccessRequestsFilter,\n MagicLinkError,\n type MagicLinkResult,\n MagicLinkService,\n type MagicLinkServiceOptions,\n type MagicLinkVerifyResult,\n MobileAuthError,\n type MobileAuthErrorCode,\n MobileAuthService,\n type MobileAuthServiceOptions,\n type MobileBootstrapContext,\n type MobileLoginContext,\n type MobileLogoutResult,\n type MobileRequestMeta,\n type MobileResolvedUser,\n type MobileTenantContext,\n normalizeOperationPermissionAction,\n type OidcCallbackResult,\n OidcLoginError,\n type OidcLoginResult,\n OidcLoginService,\n type OidcLoginServiceOptions,\n type OidcProviderConfig,\n type OidcProviderKind,\n type OidcProviderMetadata,\n type OidcProviderResolution,\n type OidcProviderResolutionOptions,\n type OidcTokenEndpointAuthMethod,\n type OidcTokenSet,\n type OidcTransaction,\n type OperationPermissionAllowReason,\n type OperationPermissionCollectionInput,\n type OperationPermissionDecision,\n type OperationPermissionDenyReason,\n OperationPermissionError,\n type OperationPermissionOptions,\n type PermissionCatalog,\n PermissionCatalogService,\n type PermissionCatalogSource,\n type PermissionCatalogSyncResult,\n type PermissionDefinition,\n type PermissionResolutionOptions,\n type PermissionResolutionResult,\n PermissionResolver,\n type PostgresPermissionAction,\n type PostgresPermissionBinding,\n type PostgresPermissionPolicyReportItem,\n type PostgresPermissionPolicyTarget,\n type PrincipalPermissionRuntimeOptions,\n type ResolvedOidcProviderConfig,\n readMobileBearerToken,\n registerPermissionDefinitions,\n resolveOidcProviderConfig,\n type SessionContext,\n type SessionPermissionRuntimeContext,\n type SessionPermissionRuntimeOptions,\n SessionService,\n type SessionServiceOptions,\n type SwitchTenantResult,\n syncPermissionCatalog,\n type TenantPermissionInheritanceResult,\n TenantService,\n type TenantWithOwnershipResult,\n TerminalAuthError,\n TerminalAuthRateLimitError,\n TerminalAuthService,\n type TerminalAuthServiceOptions,\n type UsersConfig,\n type UsersOidcConfig,\n validateMobileRedirectUri,\n withPrincipalPermissionContext,\n withSessionPermissionContext,\n} from './services/index.js';\n\n// Types\nexport {\n AccessRequestStatus,\n DEFAULT_ROLE_SLUGS,\n DEFAULT_ROLES,\n DEFAULT_TENANT_POLICY,\n type DefaultRoleSlug,\n MembershipStatus,\n OverrideEffect,\n SessionStatus,\n TenantPermissionEffect,\n type TenantPolicy,\n type TenantPolicyMode,\n TenantStatus,\n UserStatus,\n} from './types/index.js';\n"],"mappings":"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"}
|