@pattern-stack/codegen 0.6.5 → 0.6.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/CHANGELOG.md +26 -1
- package/dist/runtime/subsystems/auth/auth.module.js +1 -1
- package/dist/runtime/subsystems/auth/auth.module.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth.tokens.d.ts +1 -1
- package/dist/runtime/subsystems/auth/auth.tokens.js.map +1 -1
- package/dist/runtime/subsystems/auth/backends/encryption-key/env.d.ts +1 -1
- package/dist/runtime/subsystems/auth/backends/encryption-key/env.js +1 -1
- package/dist/runtime/subsystems/auth/backends/encryption-key/env.js.map +1 -1
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js.map +1 -1
- package/dist/runtime/subsystems/auth/index.d.ts +1 -1
- package/dist/runtime/subsystems/auth/index.js +1 -1
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/protocols/auth-strategy.d.ts +1 -1
- package/dist/runtime/subsystems/auth/protocols/integration-store.d.ts +2 -2
- package/dist/runtime/subsystems/auth/protocols/provider-strategy.d.ts +13 -6
- package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.d.ts +2 -2
- package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.js.map +1 -1
- package/dist/runtime/subsystems/auth/runtime/session-expired.error.d.ts +2 -2
- package/dist/runtime/subsystems/auth/runtime/session-expired.error.js.map +1 -1
- package/dist/runtime/subsystems/auth/runtime/with-auth-retry.d.ts +1 -1
- package/dist/runtime/subsystems/auth/runtime/with-auth-retry.js.map +1 -1
- package/dist/runtime/subsystems/index.d.ts +1 -1
- package/dist/runtime/subsystems/index.js +1 -1
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/sync/deep-equal.differ.js.map +1 -1
- package/dist/runtime/subsystems/sync/execute-sync.use-case.js.map +1 -1
- package/dist/runtime/subsystems/sync/index.js.map +1 -1
- package/dist/runtime/subsystems/sync/sync-change-source.protocol.d.ts +1 -1
- package/dist/runtime/subsystems/sync/sync-cursor-store.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/sync/sync-loopback.protocol.d.ts +3 -4
- package/dist/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/sync/sync.module.js.map +1 -1
- package/dist/src/cli/index.js +55 -19
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/examples/auth-integrations/README.md +125 -0
- package/examples/auth-integrations/definitions/entities/integration.yaml +98 -0
- package/examples/auth-integrations/runtime/integrations/adapters/integration-grant-sink.adapter.ts +29 -0
- package/examples/auth-integrations/runtime/integrations/adapters/integration-reader.adapter.ts +52 -0
- package/examples/auth-integrations/runtime/integrations/adapters/integration-token-writer.adapter.ts +43 -0
- package/examples/auth-integrations/runtime/integrations/facade/integrations.service.ts +150 -0
- package/examples/auth-integrations/runtime/integrations/integrations-auth.module.ts +81 -0
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/create-or-update-from-oauth-grant.use-case.ts +75 -0
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/disconnect-integration.use-case.ts +29 -0
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/list-user-integrations.use-case.ts +21 -0
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/mark-integration-requires-reauth.use-case.ts +21 -0
- package/package.json +2 -1
- package/runtime/subsystems/auth/auth.tokens.ts +1 -1
- package/runtime/subsystems/auth/backends/encryption-key/env.ts +3 -3
- package/runtime/subsystems/auth/controllers/auth.controller.ts +3 -3
- package/runtime/subsystems/auth/index.ts +2 -2
- package/runtime/subsystems/auth/protocols/auth-strategy.ts +1 -1
- package/runtime/subsystems/auth/protocols/integration-store.ts +2 -2
- package/runtime/subsystems/auth/protocols/provider-strategy.ts +12 -5
- package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +2 -2
- package/runtime/subsystems/auth/runtime/session-expired.error.ts +2 -2
- package/runtime/subsystems/auth/runtime/with-auth-retry.ts +1 -1
- package/runtime/subsystems/index.ts +1 -1
- package/runtime/subsystems/sync/deep-equal.differ.ts +1 -1
- package/runtime/subsystems/sync/execute-sync.use-case.ts +1 -1
- package/runtime/subsystems/sync/sync-change-source.protocol.ts +1 -1
- package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +1 -1
- package/runtime/subsystems/sync/sync-loopback.protocol.ts +3 -4
- package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +1 -1
- package/templates/subsystem/auth/app-module-hook.ejs.t +1 -1
- package/templates/subsystem/auth/env-config.ejs.t +5 -5
- package/templates/subsystem/auth/prompt.js +3 -3
- package/templates/subsystem/auth-config/codegen-config-auth-block.ejs.t +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,32 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
## [0.6.
|
|
7
|
+
## [0.6.7] — 2026-04-28
|
|
8
|
+
|
|
9
|
+
Hotfix bundle for `cdp subsystem install auth-integrations`. 0.6.5 / 0.6.6 shipped the auth-integrations starter and install template, but every downstream consumer ran into four blockers on a fresh install. None of them surfaced from the source-checkout smoke (the install code resolves examples/ via the package root, which exists in dev) — they only exposed themselves through `npm install + bunx cdp subsystem install auth-integrations` against the published tarball. Bundles a fifth fix that unifies the integrations folder layout. Surfaced by integration-patterns Wave 0b.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **BREAKING — `fix(cli)` #303 (fix #5)** — `cdp subsystem install auth-integrations` now vendors the starter under `<paths.backend_src>/modules/integrations/` (override via `paths.modules_dir`), next to the codegen-emitted `integration` entity module. Previously: `<paths.backend_src>/shared/integrations/`. The starter's runtime tree is now organized under `adapters/`, `facade/`, and `oauth/use-cases/` subfolders to avoid collision with codegen output, and `IntegrationsAuthModule` lives at the integrations folder root. Relative imports inside the vendored files (and the bare-package auth import rewriter, fix #3) target the new layout. Detection (`detectInstalledSubsystems`) checks the new vendor target first and falls back to the legacy shared/integrations location for any pre-0.6.7 install. Pre-1.0; the only downstream consumer of 0.6.5/0.6.6 is integration-patterns Wave 0b (unmerged).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **P0 — `fix(packaging)` #303** — added `examples/auth-integrations/**` to `package.json:files`. The 0.6.6 tarball did not include the starter source; `cdp subsystem install auth-integrations` therefore failed to find `node_modules/@pattern-stack/codegen/examples/auth-integrations/` and aborted the vendor copy. Narrowed to the auth-integrations subtree only — internal `examples/` (eav etc.) stay out of the published artifact. Prevention: `src/__tests__/templates/auth-integrations-files-coverage.test.ts` walks every file under `examples/auth-integrations/` and asserts it matches a `files` pattern.
|
|
18
|
+
- **`fix(cli)` #303** — `cdp subsystem install auth-integrations` no longer drops `integration.yaml` at the wrong path. The scaffold-locals resolver was reading `paths.definitions` (a key that doesn't exist in the schema); switched to `paths.entities` with a fallback to legacy `paths.entities_dir`, matching the resolution order in `Context.entitiesDir` and `cdp project init`. Default location (`<cwd>/definitions/entities/integration.yaml`) is unchanged.
|
|
19
|
+
- **`fix(cli)` #303** — vendored adapters under `<sharedRoot>/integrations/` no longer carry bare-package imports `from '@pattern-stack/codegen/runtime/subsystems/auth'`. Those imports both fail `tsc --noEmit` (the package's `exports` map points at compiled `dist/runtime/*` files, not deep subpaths) AND would inject against the publisher's compiled token Symbols rather than the consumer's vendored auth subsystem (duplicate-DI hazard). The install logic now rewrites every such specifier to a relative path resolving against `<subsystemsRoot>/auth` at copy time.
|
|
20
|
+
- **`fix(examples)` #303** — `IntegrationsAuthModule` is now `@Global()`. `AuthController` lives inside `AuthModule`'s injector and resolves the `AUTH_INTEGRATION_*` providers exposed by `IntegrationsAuthModule`; without `@Global()`, Nest fails to boot. Same root cause as the auth-bindings module pattern in integration-patterns PR #93.
|
|
21
|
+
|
|
22
|
+
## [0.6.6] — 2026-04-27
|
|
23
|
+
|
|
24
|
+
Bundled cleanup PR for the auth subsystem surfaced during integration-patterns review. Pre-1.0, so two breaking renames are taken without compatibility shims.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **BREAKING — env var rename.** `TOKEN_ENCRYPTION_KEY` → `INTEGRATION_TOKEN_ENCRYPTION_KEY`. The auth subsystem only encrypts integration tokens; the scoped name is clearer about what the key protects and avoids colliding with other token-encryption keys a consumer might own. Read site (`runtime/subsystems/auth/backends/encryption-key/env.ts`), install template (`templates/subsystem/auth/env-config.ejs.t` + idempotency `skip_if`), CLI scaffold helpers, tests, and docs all moved together. Consumers running 0.6.5 must rename the env var and the `skip_if` line in their generated `.env.config`.
|
|
29
|
+
- **BREAKING — interface rename.** `ProviderStrategy` → `IProviderStrategy` to match the rest of the auth port naming (`IIntegrationReader`, `IUserContext`, `IOAuthStateStore`, `IEncryptionKey`). Convention documented at the top of `runtime/subsystems/auth/protocols/provider-strategy.ts`: `I*` for behavioral ports, no prefix for data DTOs / template-method abstract classes. `ProviderStrategyRegistry` (a `ReadonlyMap` value-shape) keeps its name.
|
|
30
|
+
- **OSS-hygiene scrub.** Removed or generalized references to the upstream extraction-source consumer across `runtime/`, `examples/`, and user-facing docs. ADRs and RFCs that document decision history retain their references intentionally.
|
|
31
|
+
|
|
32
|
+
|
|
8
33
|
|
|
9
34
|
Auth subsystem reaches consumers. `runtime/subsystems/auth/`, the `auth-integrations` starter, and the `cdp subsystem install auth` + `auth-integrations` templates were all merged on `main` (PRs #289, #290, #292, #293, #294, #295) but the published artifact was still pinned at `0.6.4`. This release ships them.
|
|
10
35
|
|
|
@@ -31,7 +31,7 @@ var EnvEncryptionKey = class {
|
|
|
31
31
|
key;
|
|
32
32
|
constructor(opts = {}) {
|
|
33
33
|
const env = opts.env ?? process.env;
|
|
34
|
-
const envVar = opts.envVar ?? "
|
|
34
|
+
const envVar = opts.envVar ?? "INTEGRATION_TOKEN_ENCRYPTION_KEY";
|
|
35
35
|
const raw = env[envVar];
|
|
36
36
|
if (!raw) {
|
|
37
37
|
throw new Error(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth.module.ts","../../../../runtime/subsystems/auth/auth.tokens.ts","../../../../runtime/subsystems/auth/backends/encryption-key/env.ts","../../../../runtime/subsystems/auth/backends/state-store.memory-backend.ts","../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts","../../../../runtime/subsystems/auth/backends/state-store.drizzle-backend.ts","../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts","../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../runtime/constants/tokens.ts"],"sourcesContent":["/**\n * AuthModule — DynamicModule factory for the auth subsystem.\n *\n * Wires the pluggable backends the subsystem ships with:\n * - `ENCRYPTION_KEY` → `EnvEncryptionKey` (AES-256-GCM from env)\n * - `OAUTH_STATE_STORE` → `MemoryOAuthStateStore` (dev/tests) or\n * `DrizzleOAuthStateStore` (prod, requires\n * DRIZZLE provider).\n * - `AUTH_OPTIONS` → resolved options bag (used by AuthController\n * for `redirectUriBase`).\n *\n * The integration-store ports (`AUTH_INTEGRATION_READER`,\n * `AUTH_INTEGRATION_TOKEN_WRITER`, `AUTH_INTEGRATION_GRANT_SINK`),\n * `AUTH_USER_CONTEXT`, and `STRATEGY_REGISTRY` are deliberately **not**\n * wired here — they are always consumer-specific:\n * - integration-store ports adapt the consumer's `integrations` storage;\n * - `IUserContext` adapts the app's session/JWT scheme;\n * - `STRATEGY_REGISTRY` is populated from the per-provider strategy\n * classes the consumer maintains.\n *\n * Consumers provide them in their app module (or by importing the\n * `auth-integrations` starter, which binds the three integration-store\n * ports off a single canonical entity).\n *\n * Usage in AppModule:\n * ```typescript\n * AuthModule.forRoot({\n * encryptionKey: 'env',\n * oauthStateStore: 'memory', // or 'drizzle'\n * enableController: true,\n * redirectUriBase: 'http://localhost:3000',\n * });\n * ```\n *\n * `global: true` means other modules don't need to re-import AuthModule to\n * inject the auth tokens.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n AUTH_OPTIONS,\n ENCRYPTION_KEY,\n OAUTH_STATE_STORE,\n} from './auth.tokens';\nimport { EnvEncryptionKey } from './backends/encryption-key/env';\nimport { MemoryOAuthStateStore } from './backends/state-store.memory-backend';\nimport { DrizzleOAuthStateStore } from './backends/state-store.drizzle-backend';\nimport { AuthController } from './controllers/auth.controller';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type { DrizzleClient } from '../../types/drizzle';\n\ntype EncryptionKeyChoice =\n | 'env'\n | Omit<Provider, 'provide'>;\n\ntype OAuthStateStoreChoice =\n | 'memory'\n | 'drizzle'\n | Omit<Provider, 'provide'>;\n\nexport interface AuthModuleOptions {\n /** `'env'` (default) or a full provider definition (e.g. `{ useClass: MyKmsEncryptionKey }`). */\n encryptionKey?: EncryptionKeyChoice;\n /**\n * `'memory'` (default — tests/dev) or `'drizzle'` (prod, requires DRIZZLE\n * provider) or a full provider definition for a custom impl.\n */\n oauthStateStore?: OAuthStateStoreChoice;\n /**\n * Mount `AuthController` (`/auth/:provider/connect` + `/callback`).\n * Default `false` — apps that hand-roll connect/callback (rare) or that\n * use the subsystem only for the refresh path can opt out.\n */\n enableController?: boolean;\n /**\n * Public base URL of the API server. Used to construct per-provider\n * callback URIs as `${redirectUriBase}/auth/:provider/callback`.\n * Required when `enableController: true`.\n */\n redirectUriBase?: string;\n}\n\nfunction resolveEncryptionKeyProvider(choice: EncryptionKeyChoice): Provider {\n if (choice === 'env') {\n return { provide: ENCRYPTION_KEY, useClass: EnvEncryptionKey };\n }\n return { provide: ENCRYPTION_KEY, ...choice } as Provider;\n}\n\nfunction resolveOAuthStateStoreProvider(\n choice: OAuthStateStoreChoice,\n): Provider {\n if (choice === 'memory') {\n return { provide: OAUTH_STATE_STORE, useClass: MemoryOAuthStateStore };\n }\n if (choice === 'drizzle') {\n return {\n provide: OAUTH_STATE_STORE,\n useFactory: (db: DrizzleClient | null) => {\n if (!db) {\n throw new Error(\n \"AuthModule.forRoot: oauthStateStore: 'drizzle' selected but DRIZZLE provider is not available. \" +\n 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before AuthModule.forRoot.',\n );\n }\n return new DrizzleOAuthStateStore(db);\n },\n inject: [{ token: DRIZZLE, optional: true }],\n };\n }\n return { provide: OAUTH_STATE_STORE, ...choice } as Provider;\n}\n\n@Module({})\nexport class AuthModule {\n static forRoot(options: AuthModuleOptions = {}): DynamicModule {\n const resolved: AuthModuleOptions = {\n encryptionKey: options.encryptionKey ?? 'env',\n oauthStateStore: options.oauthStateStore ?? 'memory',\n enableController: options.enableController ?? false,\n redirectUriBase: options.redirectUriBase,\n };\n\n if (resolved.enableController && !resolved.redirectUriBase) {\n throw new Error(\n 'AuthModule.forRoot: redirectUriBase is required when enableController: true',\n );\n }\n\n const encryptionKeyProvider = resolveEncryptionKeyProvider(\n resolved.encryptionKey!,\n );\n const oauthStateStoreProvider = resolveOAuthStateStoreProvider(\n resolved.oauthStateStore!,\n );\n const optionsProvider: Provider = {\n provide: AUTH_OPTIONS,\n useValue: resolved,\n };\n\n return {\n module: AuthModule,\n global: true,\n providers: [encryptionKeyProvider, oauthStateStoreProvider, optionsProvider],\n controllers: resolved.enableController ? [AuthController] : [],\n exports: [ENCRYPTION_KEY, OAUTH_STATE_STORE, AUTH_OPTIONS],\n };\n }\n}\n","/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,\n * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,\n * @Inject(AUTH_INTEGRATION_GRANT_SINK) private readonly grants: IIntegrationGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, ProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_INTEGRATION_READER = Symbol('AUTH_INTEGRATION_READER');\nexport const AUTH_INTEGRATION_TOKEN_WRITER = Symbol('AUTH_INTEGRATION_TOKEN_WRITER');\nexport const AUTH_INTEGRATION_GRANT_SINK = Symbol('AUTH_INTEGRATION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n","/**\n * Env-backed AES-256-GCM encryption.\n *\n * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce\n * per call means two encryptions of the same plaintext produce different\n * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;\n * any tampering throws on decrypt.\n *\n * Key source: `TOKEN_ENCRYPTION_KEY` env var, 32 bytes base64-encoded.\n * Generate via `openssl rand -base64 32`.\n *\n * Future backend: `kms.ts` (AWS/GCP KMS) for production deployments that\n * need key rotation + audit trails.\n */\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport type { IEncryptionKey } from '../../protocols/encryption-key';\n\nexport interface EnvEncryptionKeyOptions {\n /** Defaults to `process.env`. Tests inject a fixture. */\n env?: NodeJS.ProcessEnv;\n /** Defaults to `'TOKEN_ENCRYPTION_KEY'`. */\n envVar?: string;\n}\n\nconst ALGO = 'aes-256-gcm';\nconst NONCE_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport class EnvEncryptionKey implements IEncryptionKey {\n private readonly key: Buffer;\n\n constructor(opts: EnvEncryptionKeyOptions = {}) {\n const env = opts.env ?? process.env;\n const envVar = opts.envVar ?? 'TOKEN_ENCRYPTION_KEY';\n const raw = env[envVar];\n if (!raw) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} is not set. Generate with: openssl rand -base64 32`,\n );\n }\n const decoded = Buffer.from(raw, 'base64');\n if (decoded.length !== KEY_BYTES) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} must decode to ${KEY_BYTES} bytes (got ${decoded.length}). Use: openssl rand -base64 32`,\n );\n }\n this.key = decoded;\n }\n\n async encrypt(plaintext: string): Promise<string> {\n const nonce = randomBytes(NONCE_BYTES);\n const cipher = createCipheriv(ALGO, this.key, nonce);\n const ciphertext = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([nonce, ciphertext, authTag]).toString('base64');\n }\n\n async decrypt(ciphertext: string): Promise<string> {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < NONCE_BYTES + TAG_BYTES) {\n throw new Error('EnvEncryptionKey: ciphertext too short');\n }\n const nonce = buf.subarray(0, NONCE_BYTES);\n const authTag = buf.subarray(buf.length - TAG_BYTES);\n const body = buf.subarray(NONCE_BYTES, buf.length - TAG_BYTES);\n\n const decipher = createDecipheriv(ALGO, this.key, nonce);\n decipher.setAuthTag(authTag);\n const plain = Buffer.concat([decipher.update(body), decipher.final()]);\n return plain.toString('utf8');\n }\n}\n","/**\n * In-memory `IOAuthStateStore` backend.\n *\n * Single-process store — Map<state, { record, expiresAt }>. Suitable for\n * tests and single-worker dev. Production deployments select the drizzle\n * backend so state survives restarts and is shared across workers.\n *\n * Single-use semantics:\n * - `generate(record)` mints a 256-bit random token (base64url, opaque).\n * - `consume(state)` deletes the entry on read. A second call with the\n * same state throws `OAuthStateError('replay')`.\n * - Expired entries also throw (`'expired'`); the entry is deleted as a\n * side effect so a later replay still surfaces correctly.\n *\n * TTL defaults to 10 minutes — long enough for a user to complete the\n * provider's consent screen, short enough that abandoned states age out.\n */\nimport { randomBytes } from 'node:crypto';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface MemoryOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\ninterface Slot {\n record: OAuthStateRecord;\n expiresAt: number;\n}\n\nexport class MemoryOAuthStateStore implements IOAuthStateStore {\n private readonly store = new Map<string, Slot>();\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(opts: MemoryOAuthStateStoreOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n this.store.set(state, {\n record: { ...record },\n expiresAt: this.now() + this.ttlMs,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const slot = this.store.get(state);\n if (!slot) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n // Delete first so a concurrent consume can't replay.\n this.store.delete(state);\n if (slot.expiresAt <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return slot.record;\n }\n}\n","/**\n * Auth subsystem — `IOAuthStateStore` port.\n *\n * CSRF protection for the OAuth2 authorize-code callback. Generic across\n * providers. The store mints opaque state tokens at /connect time and\n * single-use consumes them at /callback time, returning the original\n * record (userId + optional post-callback redirect path).\n *\n * Concrete backends live under `../backends/`:\n * - `state-store.memory-backend.ts` — in-process Map (tests/dev).\n * - `state-store.drizzle-backend.ts` — Postgres (prod).\n *\n * Semantics:\n * - `generate(record)` → returns an opaque state token; record is stored\n * under that token until consumed or until TTL expires.\n * - `consume(state)` → atomically deletes the entry and returns the\n * record. Throws on missing, expired, or replayed state. Never returns\n * null — a missing/expired state is a CSRF signal.\n */\nexport interface OAuthStateRecord {\n userId: string;\n /** Optional post-callback redirect path (relative URL). */\n redirect?: string;\n}\n\nexport interface IOAuthStateStore {\n /** Mint an opaque state token bound to `record`. Single-use. */\n generate(record: OAuthStateRecord): Promise<string>;\n /**\n * Atomically consume `state`, returning the bound record. Throws on\n * missing / expired / replayed state.\n */\n consume(state: string): Promise<OAuthStateRecord>;\n}\n\n/**\n * Thrown by `IOAuthStateStore.consume` when the state token is unknown,\n * expired, or has already been consumed (replay attempt).\n */\nexport class OAuthStateError extends Error {\n constructor(\n message: string,\n public readonly reason: 'missing' | 'expired',\n ) {\n super(message);\n this.name = 'OAuthStateError';\n }\n}\n","/**\n * Drizzle-backed `IOAuthStateStore`.\n *\n * Uses the `auth_oauth_state` table (see `auth-oauth-state.schema.ts`).\n * Single-use semantics enforced via `DELETE ... RETURNING`: the consume\n * path atomically deletes and returns the row, so a concurrent /callback\n * with the same state cannot replay.\n *\n * Behaviour:\n * - `generate(record)` mints a 256-bit base64url token, INSERTs the row\n * with `expires_at = now() + ttlMs`.\n * - `consume(state)` runs `DELETE ... WHERE state = $1 RETURNING ...`\n * once. Throws `OAuthStateError('missing')` if no row was deleted\n * (unknown or already consumed) and `OAuthStateError('expired')` if\n * the deleted row was past its `expires_at`.\n */\nimport { randomBytes } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { authOAuthState } from '../auth-oauth-state.schema';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface DrizzleOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\nexport class DrizzleOAuthStateStore implements IOAuthStateStore {\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(\n private readonly db: DrizzleClient,\n opts: DrizzleOAuthStateStoreOptions = {},\n ) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n const expiresAt = new Date(this.now() + this.ttlMs);\n await this.db.insert(authOAuthState).values({\n state,\n userId: record.userId,\n redirect: record.redirect ?? null,\n expiresAt,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const rows = await this.db\n .delete(authOAuthState)\n .where(eq(authOAuthState.state, state))\n .returning();\n const row = rows[0];\n if (!row) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n if (row.expiresAt.getTime() <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return {\n userId: row.userId,\n redirect: row.redirect ?? undefined,\n };\n }\n}\n","/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `sync-audit.schema.ts`, `domain-events.schema.ts`).\n */\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const authOAuthState = pgTable('auth_oauth_state', {\n state: text('state').primaryKey(),\n userId: text('user_id').notNull(),\n redirect: text('redirect'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n});\n\nexport type AuthOAuthState = InferSelectModel<typeof authOAuthState>;\n","/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, ProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `AUTH_INTEGRATION_GRANT_SINK` (IIntegrationGrantSink) — persists the\n * freshly-minted grant. Adapter lives consumer-side (e.g. the\n * auth-integrations starter from #285).\n *\n * The controller never imports `IntegrationsService` or any other concrete\n * consumer type — it goes through ports only.\n */\nimport {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n HttpException,\n HttpStatus,\n} from '@nestjs/common';\nimport {\n AUTH_INTEGRATION_GRANT_SINK,\n AUTH_OPTIONS,\n AUTH_USER_CONTEXT,\n OAUTH_STATE_STORE,\n STRATEGY_REGISTRY,\n} from '../auth.tokens';\nimport type { AuthModuleOptions } from '../auth.module';\nimport type { IOAuthStateStore } from '../protocols/oauth-state-store';\nimport type { IUserContext } from '../protocols/user-context';\nimport type {\n ProviderStrategy,\n ProviderStrategyRegistry,\n} from '../protocols/provider-strategy';\nimport type { IIntegrationGrantSink } from '../protocols/integration-store';\n\n/**\n * Minimal response surface used by the controller — typed loosely so we\n * don't pull a hard dep on `express` or `fastify`. Both popular HTTP\n * adapters expose `redirect(status, url)`.\n */\ninterface RedirectingResponse {\n redirect(statusCode: number, url: string): unknown;\n}\n\n@Controller('auth')\nexport class AuthController {\n constructor(\n @Inject(STRATEGY_REGISTRY)\n private readonly registry: ProviderStrategyRegistry,\n @Inject(AUTH_USER_CONTEXT)\n private readonly userContext: IUserContext,\n @Inject(OAUTH_STATE_STORE)\n private readonly stateStore: IOAuthStateStore,\n @Inject(AUTH_INTEGRATION_GRANT_SINK)\n private readonly grantSink: IIntegrationGrantSink,\n @Inject(AUTH_OPTIONS)\n private readonly options: AuthModuleOptions,\n ) {}\n\n @Get(':provider/connect')\n async connect(\n @Param('provider') slug: string,\n @Query('redirect') redirect: string | undefined,\n @Req() req: unknown,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n const userId = await this.userContext.getCurrentUserId(req);\n const state = await this.stateStore.generate({ userId, redirect });\n const url = strategy.buildAuthorizeUrl({\n state,\n redirectUri: this.redirectUriFor(slug),\n });\n return res.redirect(HttpStatus.FOUND, url);\n }\n\n @Get(':provider/callback')\n async callback(\n @Param('provider') slug: string,\n @Query('code') code: string | undefined,\n @Query('state') state: string | undefined,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n if (!code) {\n throw new HttpException(\n `Missing 'code' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n if (!state) {\n throw new HttpException(\n `Missing 'state' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n const { userId, redirect } = await this.stateStore.consume(state);\n const tokens = await strategy.exchangeCodeForTokens({\n code,\n redirectUri: this.redirectUriFor(slug),\n });\n await this.grantSink.createOrUpdateFromOAuthGrant({\n userId,\n provider: slug,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n externalAccountId: tokens.externalAccountId,\n providerMetadata: tokens.providerMetadata,\n });\n return res.redirect(\n HttpStatus.FOUND,\n redirect ?? `/settings/integrations?connected=${encodeURIComponent(slug)}`,\n );\n }\n\n private requireStrategy(slug: string): ProviderStrategy {\n const strategy = this.registry.get(slug);\n if (!strategy) {\n throw new HttpException(\n `Unknown provider '${slug}'`,\n HttpStatus.NOT_FOUND,\n );\n }\n return strategy;\n }\n\n private redirectUriFor(slug: string): string {\n const base = this.options.redirectUriBase;\n if (!base) {\n throw new Error(\n `AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`,\n );\n }\n const trimmed = base.replace(/\\/+$/, '');\n return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n"],"mappings":"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","names":["randomBytes","randomBytes","randomBytes","randomBytes"]}
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth.module.ts","../../../../runtime/subsystems/auth/auth.tokens.ts","../../../../runtime/subsystems/auth/backends/encryption-key/env.ts","../../../../runtime/subsystems/auth/backends/state-store.memory-backend.ts","../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts","../../../../runtime/subsystems/auth/backends/state-store.drizzle-backend.ts","../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts","../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../runtime/constants/tokens.ts"],"sourcesContent":["/**\n * AuthModule — DynamicModule factory for the auth subsystem.\n *\n * Wires the pluggable backends the subsystem ships with:\n * - `ENCRYPTION_KEY` → `EnvEncryptionKey` (AES-256-GCM from env)\n * - `OAUTH_STATE_STORE` → `MemoryOAuthStateStore` (dev/tests) or\n * `DrizzleOAuthStateStore` (prod, requires\n * DRIZZLE provider).\n * - `AUTH_OPTIONS` → resolved options bag (used by AuthController\n * for `redirectUriBase`).\n *\n * The integration-store ports (`AUTH_INTEGRATION_READER`,\n * `AUTH_INTEGRATION_TOKEN_WRITER`, `AUTH_INTEGRATION_GRANT_SINK`),\n * `AUTH_USER_CONTEXT`, and `STRATEGY_REGISTRY` are deliberately **not**\n * wired here — they are always consumer-specific:\n * - integration-store ports adapt the consumer's `integrations` storage;\n * - `IUserContext` adapts the app's session/JWT scheme;\n * - `STRATEGY_REGISTRY` is populated from the per-provider strategy\n * classes the consumer maintains.\n *\n * Consumers provide them in their app module (or by importing the\n * `auth-integrations` starter, which binds the three integration-store\n * ports off a single canonical entity).\n *\n * Usage in AppModule:\n * ```typescript\n * AuthModule.forRoot({\n * encryptionKey: 'env',\n * oauthStateStore: 'memory', // or 'drizzle'\n * enableController: true,\n * redirectUriBase: 'http://localhost:3000',\n * });\n * ```\n *\n * `global: true` means other modules don't need to re-import AuthModule to\n * inject the auth tokens.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n AUTH_OPTIONS,\n ENCRYPTION_KEY,\n OAUTH_STATE_STORE,\n} from './auth.tokens';\nimport { EnvEncryptionKey } from './backends/encryption-key/env';\nimport { MemoryOAuthStateStore } from './backends/state-store.memory-backend';\nimport { DrizzleOAuthStateStore } from './backends/state-store.drizzle-backend';\nimport { AuthController } from './controllers/auth.controller';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type { DrizzleClient } from '../../types/drizzle';\n\ntype EncryptionKeyChoice =\n | 'env'\n | Omit<Provider, 'provide'>;\n\ntype OAuthStateStoreChoice =\n | 'memory'\n | 'drizzle'\n | Omit<Provider, 'provide'>;\n\nexport interface AuthModuleOptions {\n /** `'env'` (default) or a full provider definition (e.g. `{ useClass: MyKmsEncryptionKey }`). */\n encryptionKey?: EncryptionKeyChoice;\n /**\n * `'memory'` (default — tests/dev) or `'drizzle'` (prod, requires DRIZZLE\n * provider) or a full provider definition for a custom impl.\n */\n oauthStateStore?: OAuthStateStoreChoice;\n /**\n * Mount `AuthController` (`/auth/:provider/connect` + `/callback`).\n * Default `false` — apps that hand-roll connect/callback (rare) or that\n * use the subsystem only for the refresh path can opt out.\n */\n enableController?: boolean;\n /**\n * Public base URL of the API server. Used to construct per-provider\n * callback URIs as `${redirectUriBase}/auth/:provider/callback`.\n * Required when `enableController: true`.\n */\n redirectUriBase?: string;\n}\n\nfunction resolveEncryptionKeyProvider(choice: EncryptionKeyChoice): Provider {\n if (choice === 'env') {\n return { provide: ENCRYPTION_KEY, useClass: EnvEncryptionKey };\n }\n return { provide: ENCRYPTION_KEY, ...choice } as Provider;\n}\n\nfunction resolveOAuthStateStoreProvider(\n choice: OAuthStateStoreChoice,\n): Provider {\n if (choice === 'memory') {\n return { provide: OAUTH_STATE_STORE, useClass: MemoryOAuthStateStore };\n }\n if (choice === 'drizzle') {\n return {\n provide: OAUTH_STATE_STORE,\n useFactory: (db: DrizzleClient | null) => {\n if (!db) {\n throw new Error(\n \"AuthModule.forRoot: oauthStateStore: 'drizzle' selected but DRIZZLE provider is not available. \" +\n 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before AuthModule.forRoot.',\n );\n }\n return new DrizzleOAuthStateStore(db);\n },\n inject: [{ token: DRIZZLE, optional: true }],\n };\n }\n return { provide: OAUTH_STATE_STORE, ...choice } as Provider;\n}\n\n@Module({})\nexport class AuthModule {\n static forRoot(options: AuthModuleOptions = {}): DynamicModule {\n const resolved: AuthModuleOptions = {\n encryptionKey: options.encryptionKey ?? 'env',\n oauthStateStore: options.oauthStateStore ?? 'memory',\n enableController: options.enableController ?? false,\n redirectUriBase: options.redirectUriBase,\n };\n\n if (resolved.enableController && !resolved.redirectUriBase) {\n throw new Error(\n 'AuthModule.forRoot: redirectUriBase is required when enableController: true',\n );\n }\n\n const encryptionKeyProvider = resolveEncryptionKeyProvider(\n resolved.encryptionKey!,\n );\n const oauthStateStoreProvider = resolveOAuthStateStoreProvider(\n resolved.oauthStateStore!,\n );\n const optionsProvider: Provider = {\n provide: AUTH_OPTIONS,\n useValue: resolved,\n };\n\n return {\n module: AuthModule,\n global: true,\n providers: [encryptionKeyProvider, oauthStateStoreProvider, optionsProvider],\n controllers: resolved.enableController ? [AuthController] : [],\n exports: [ENCRYPTION_KEY, OAUTH_STATE_STORE, AUTH_OPTIONS],\n };\n }\n}\n","/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,\n * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,\n * @Inject(AUTH_INTEGRATION_GRANT_SINK) private readonly grants: IIntegrationGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_INTEGRATION_READER = Symbol('AUTH_INTEGRATION_READER');\nexport const AUTH_INTEGRATION_TOKEN_WRITER = Symbol('AUTH_INTEGRATION_TOKEN_WRITER');\nexport const AUTH_INTEGRATION_GRANT_SINK = Symbol('AUTH_INTEGRATION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n","/**\n * Env-backed AES-256-GCM encryption.\n *\n * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce\n * per call means two encryptions of the same plaintext produce different\n * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;\n * any tampering throws on decrypt.\n *\n * Key source: `INTEGRATION_TOKEN_ENCRYPTION_KEY` env var, 32 bytes base64-encoded.\n * Generate via `openssl rand -base64 32`.\n *\n * Future backend: `kms.ts` (AWS/GCP KMS) for production deployments that\n * need key rotation + audit trails.\n */\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport type { IEncryptionKey } from '../../protocols/encryption-key';\n\nexport interface EnvEncryptionKeyOptions {\n /** Defaults to `process.env`. Tests inject a fixture. */\n env?: NodeJS.ProcessEnv;\n /** Defaults to `'INTEGRATION_TOKEN_ENCRYPTION_KEY'`. */\n envVar?: string;\n}\n\nconst ALGO = 'aes-256-gcm';\nconst NONCE_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport class EnvEncryptionKey implements IEncryptionKey {\n private readonly key: Buffer;\n\n constructor(opts: EnvEncryptionKeyOptions = {}) {\n const env = opts.env ?? process.env;\n const envVar = opts.envVar ?? 'INTEGRATION_TOKEN_ENCRYPTION_KEY';\n const raw = env[envVar];\n if (!raw) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} is not set. Generate with: openssl rand -base64 32`,\n );\n }\n const decoded = Buffer.from(raw, 'base64');\n if (decoded.length !== KEY_BYTES) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} must decode to ${KEY_BYTES} bytes (got ${decoded.length}). Use: openssl rand -base64 32`,\n );\n }\n this.key = decoded;\n }\n\n async encrypt(plaintext: string): Promise<string> {\n const nonce = randomBytes(NONCE_BYTES);\n const cipher = createCipheriv(ALGO, this.key, nonce);\n const ciphertext = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([nonce, ciphertext, authTag]).toString('base64');\n }\n\n async decrypt(ciphertext: string): Promise<string> {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < NONCE_BYTES + TAG_BYTES) {\n throw new Error('EnvEncryptionKey: ciphertext too short');\n }\n const nonce = buf.subarray(0, NONCE_BYTES);\n const authTag = buf.subarray(buf.length - TAG_BYTES);\n const body = buf.subarray(NONCE_BYTES, buf.length - TAG_BYTES);\n\n const decipher = createDecipheriv(ALGO, this.key, nonce);\n decipher.setAuthTag(authTag);\n const plain = Buffer.concat([decipher.update(body), decipher.final()]);\n return plain.toString('utf8');\n }\n}\n","/**\n * In-memory `IOAuthStateStore` backend.\n *\n * Single-process store — Map<state, { record, expiresAt }>. Suitable for\n * tests and single-worker dev. Production deployments select the drizzle\n * backend so state survives restarts and is shared across workers.\n *\n * Single-use semantics:\n * - `generate(record)` mints a 256-bit random token (base64url, opaque).\n * - `consume(state)` deletes the entry on read. A second call with the\n * same state throws `OAuthStateError('replay')`.\n * - Expired entries also throw (`'expired'`); the entry is deleted as a\n * side effect so a later replay still surfaces correctly.\n *\n * TTL defaults to 10 minutes — long enough for a user to complete the\n * provider's consent screen, short enough that abandoned states age out.\n */\nimport { randomBytes } from 'node:crypto';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface MemoryOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\ninterface Slot {\n record: OAuthStateRecord;\n expiresAt: number;\n}\n\nexport class MemoryOAuthStateStore implements IOAuthStateStore {\n private readonly store = new Map<string, Slot>();\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(opts: MemoryOAuthStateStoreOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n this.store.set(state, {\n record: { ...record },\n expiresAt: this.now() + this.ttlMs,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const slot = this.store.get(state);\n if (!slot) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n // Delete first so a concurrent consume can't replay.\n this.store.delete(state);\n if (slot.expiresAt <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return slot.record;\n }\n}\n","/**\n * Auth subsystem — `IOAuthStateStore` port.\n *\n * CSRF protection for the OAuth2 authorize-code callback. Generic across\n * providers. The store mints opaque state tokens at /connect time and\n * single-use consumes them at /callback time, returning the original\n * record (userId + optional post-callback redirect path).\n *\n * Concrete backends live under `../backends/`:\n * - `state-store.memory-backend.ts` — in-process Map (tests/dev).\n * - `state-store.drizzle-backend.ts` — Postgres (prod).\n *\n * Semantics:\n * - `generate(record)` → returns an opaque state token; record is stored\n * under that token until consumed or until TTL expires.\n * - `consume(state)` → atomically deletes the entry and returns the\n * record. Throws on missing, expired, or replayed state. Never returns\n * null — a missing/expired state is a CSRF signal.\n */\nexport interface OAuthStateRecord {\n userId: string;\n /** Optional post-callback redirect path (relative URL). */\n redirect?: string;\n}\n\nexport interface IOAuthStateStore {\n /** Mint an opaque state token bound to `record`. Single-use. */\n generate(record: OAuthStateRecord): Promise<string>;\n /**\n * Atomically consume `state`, returning the bound record. Throws on\n * missing / expired / replayed state.\n */\n consume(state: string): Promise<OAuthStateRecord>;\n}\n\n/**\n * Thrown by `IOAuthStateStore.consume` when the state token is unknown,\n * expired, or has already been consumed (replay attempt).\n */\nexport class OAuthStateError extends Error {\n constructor(\n message: string,\n public readonly reason: 'missing' | 'expired',\n ) {\n super(message);\n this.name = 'OAuthStateError';\n }\n}\n","/**\n * Drizzle-backed `IOAuthStateStore`.\n *\n * Uses the `auth_oauth_state` table (see `auth-oauth-state.schema.ts`).\n * Single-use semantics enforced via `DELETE ... RETURNING`: the consume\n * path atomically deletes and returns the row, so a concurrent /callback\n * with the same state cannot replay.\n *\n * Behaviour:\n * - `generate(record)` mints a 256-bit base64url token, INSERTs the row\n * with `expires_at = now() + ttlMs`.\n * - `consume(state)` runs `DELETE ... WHERE state = $1 RETURNING ...`\n * once. Throws `OAuthStateError('missing')` if no row was deleted\n * (unknown or already consumed) and `OAuthStateError('expired')` if\n * the deleted row was past its `expires_at`.\n */\nimport { randomBytes } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { authOAuthState } from '../auth-oauth-state.schema';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface DrizzleOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\nexport class DrizzleOAuthStateStore implements IOAuthStateStore {\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(\n private readonly db: DrizzleClient,\n opts: DrizzleOAuthStateStoreOptions = {},\n ) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n const expiresAt = new Date(this.now() + this.ttlMs);\n await this.db.insert(authOAuthState).values({\n state,\n userId: record.userId,\n redirect: record.redirect ?? null,\n expiresAt,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const rows = await this.db\n .delete(authOAuthState)\n .where(eq(authOAuthState.state, state))\n .returning();\n const row = rows[0];\n if (!row) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n if (row.expiresAt.getTime() <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return {\n userId: row.userId,\n redirect: row.redirect ?? undefined,\n };\n }\n}\n","/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `sync-audit.schema.ts`, `domain-events.schema.ts`).\n */\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const authOAuthState = pgTable('auth_oauth_state', {\n state: text('state').primaryKey(),\n userId: text('user_id').notNull(),\n redirect: text('redirect'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n});\n\nexport type AuthOAuthState = InferSelectModel<typeof authOAuthState>;\n","/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, IProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `AUTH_INTEGRATION_GRANT_SINK` (IIntegrationGrantSink) — persists the\n * freshly-minted grant. Adapter lives consumer-side (e.g. the\n * auth-integrations starter from #285).\n *\n * The controller never imports `IntegrationsService` or any other concrete\n * consumer type — it goes through ports only.\n */\nimport {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n HttpException,\n HttpStatus,\n} from '@nestjs/common';\nimport {\n AUTH_INTEGRATION_GRANT_SINK,\n AUTH_OPTIONS,\n AUTH_USER_CONTEXT,\n OAUTH_STATE_STORE,\n STRATEGY_REGISTRY,\n} from '../auth.tokens';\nimport type { AuthModuleOptions } from '../auth.module';\nimport type { IOAuthStateStore } from '../protocols/oauth-state-store';\nimport type { IUserContext } from '../protocols/user-context';\nimport type {\n IProviderStrategy,\n ProviderStrategyRegistry,\n} from '../protocols/provider-strategy';\nimport type { IIntegrationGrantSink } from '../protocols/integration-store';\n\n/**\n * Minimal response surface used by the controller — typed loosely so we\n * don't pull a hard dep on `express` or `fastify`. Both popular HTTP\n * adapters expose `redirect(status, url)`.\n */\ninterface RedirectingResponse {\n redirect(statusCode: number, url: string): unknown;\n}\n\n@Controller('auth')\nexport class AuthController {\n constructor(\n @Inject(STRATEGY_REGISTRY)\n private readonly registry: ProviderStrategyRegistry,\n @Inject(AUTH_USER_CONTEXT)\n private readonly userContext: IUserContext,\n @Inject(OAUTH_STATE_STORE)\n private readonly stateStore: IOAuthStateStore,\n @Inject(AUTH_INTEGRATION_GRANT_SINK)\n private readonly grantSink: IIntegrationGrantSink,\n @Inject(AUTH_OPTIONS)\n private readonly options: AuthModuleOptions,\n ) {}\n\n @Get(':provider/connect')\n async connect(\n @Param('provider') slug: string,\n @Query('redirect') redirect: string | undefined,\n @Req() req: unknown,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n const userId = await this.userContext.getCurrentUserId(req);\n const state = await this.stateStore.generate({ userId, redirect });\n const url = strategy.buildAuthorizeUrl({\n state,\n redirectUri: this.redirectUriFor(slug),\n });\n return res.redirect(HttpStatus.FOUND, url);\n }\n\n @Get(':provider/callback')\n async callback(\n @Param('provider') slug: string,\n @Query('code') code: string | undefined,\n @Query('state') state: string | undefined,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n if (!code) {\n throw new HttpException(\n `Missing 'code' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n if (!state) {\n throw new HttpException(\n `Missing 'state' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n const { userId, redirect } = await this.stateStore.consume(state);\n const tokens = await strategy.exchangeCodeForTokens({\n code,\n redirectUri: this.redirectUriFor(slug),\n });\n await this.grantSink.createOrUpdateFromOAuthGrant({\n userId,\n provider: slug,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n externalAccountId: tokens.externalAccountId,\n providerMetadata: tokens.providerMetadata,\n });\n return res.redirect(\n HttpStatus.FOUND,\n redirect ?? `/settings/integrations?connected=${encodeURIComponent(slug)}`,\n );\n }\n\n private requireStrategy(slug: string): IProviderStrategy {\n const strategy = this.registry.get(slug);\n if (!strategy) {\n throw new HttpException(\n `Unknown provider '${slug}'`,\n HttpStatus.NOT_FOUND,\n );\n }\n return strategy;\n }\n\n private redirectUriFor(slug: string): string {\n const base = this.options.redirectUriBase;\n if (!base) {\n throw new Error(\n `AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`,\n );\n }\n const trimmed = base.replace(/\\/+$/, '');\n return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n"],"mappings":"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","names":["randomBytes","randomBytes","randomBytes","randomBytes"]}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does
|
|
24
24
|
* not mandate a single `AUTH_STRATEGY` token because an app typically has
|
|
25
25
|
* many concurrent strategies, one per provider. They are dispatched through
|
|
26
|
-
* `STRATEGY_REGISTRY` (a `ReadonlyMap<slug,
|
|
26
|
+
* `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated
|
|
27
27
|
* by per-provider modules via a `useFactory` provider.
|
|
28
28
|
*/
|
|
29
29
|
declare const ENCRYPTION_KEY: unique symbol;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth.tokens.ts"],"sourcesContent":["/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,\n * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,\n * @Inject(AUTH_INTEGRATION_GRANT_SINK) private readonly grants: IIntegrationGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug,
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/auth/auth.tokens.ts"],"sourcesContent":["/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,\n * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,\n * @Inject(AUTH_INTEGRATION_GRANT_SINK) private readonly grants: IIntegrationGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_INTEGRATION_READER = Symbol('AUTH_INTEGRATION_READER');\nexport const AUTH_INTEGRATION_TOKEN_WRITER = Symbol('AUTH_INTEGRATION_TOKEN_WRITER');\nexport const AUTH_INTEGRATION_GRANT_SINK = Symbol('AUTH_INTEGRATION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n"],"mappings":";AA4BO,IAAM,iBAAiB,uBAAO,gBAAgB;AAC9C,IAAM,oBAAoB,uBAAO,mBAAmB;AACpD,IAAM,0BAA0B,uBAAO,yBAAyB;AAChE,IAAM,gCAAgC,uBAAO,+BAA+B;AAC5E,IAAM,8BAA8B,uBAAO,6BAA6B;AACxE,IAAM,oBAAoB,uBAAO,mBAAmB;AACpD,IAAM,oBAAoB,uBAAO,mBAAmB;AAKpD,IAAM,eAAe,uBAAO,cAAc;","names":[]}
|
|
@@ -3,7 +3,7 @@ import { IEncryptionKey } from '../../protocols/encryption-key.js';
|
|
|
3
3
|
interface EnvEncryptionKeyOptions {
|
|
4
4
|
/** Defaults to `process.env`. Tests inject a fixture. */
|
|
5
5
|
env?: NodeJS.ProcessEnv;
|
|
6
|
-
/** Defaults to `'
|
|
6
|
+
/** Defaults to `'INTEGRATION_TOKEN_ENCRYPTION_KEY'`. */
|
|
7
7
|
envVar?: string;
|
|
8
8
|
}
|
|
9
9
|
declare class EnvEncryptionKey implements IEncryptionKey {
|
|
@@ -8,7 +8,7 @@ var EnvEncryptionKey = class {
|
|
|
8
8
|
key;
|
|
9
9
|
constructor(opts = {}) {
|
|
10
10
|
const env = opts.env ?? process.env;
|
|
11
|
-
const envVar = opts.envVar ?? "
|
|
11
|
+
const envVar = opts.envVar ?? "INTEGRATION_TOKEN_ENCRYPTION_KEY";
|
|
12
12
|
const raw = env[envVar];
|
|
13
13
|
if (!raw) {
|
|
14
14
|
throw new Error(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../runtime/subsystems/auth/backends/encryption-key/env.ts"],"sourcesContent":["/**\n * Env-backed AES-256-GCM encryption.\n *\n * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce\n * per call means two encryptions of the same plaintext produce different\n * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;\n * any tampering throws on decrypt.\n *\n * Key source: `
|
|
1
|
+
{"version":3,"sources":["../../../../../../runtime/subsystems/auth/backends/encryption-key/env.ts"],"sourcesContent":["/**\n * Env-backed AES-256-GCM encryption.\n *\n * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce\n * per call means two encryptions of the same plaintext produce different\n * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;\n * any tampering throws on decrypt.\n *\n * Key source: `INTEGRATION_TOKEN_ENCRYPTION_KEY` env var, 32 bytes base64-encoded.\n * Generate via `openssl rand -base64 32`.\n *\n * Future backend: `kms.ts` (AWS/GCP KMS) for production deployments that\n * need key rotation + audit trails.\n */\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport type { IEncryptionKey } from '../../protocols/encryption-key';\n\nexport interface EnvEncryptionKeyOptions {\n /** Defaults to `process.env`. Tests inject a fixture. */\n env?: NodeJS.ProcessEnv;\n /** Defaults to `'INTEGRATION_TOKEN_ENCRYPTION_KEY'`. */\n envVar?: string;\n}\n\nconst ALGO = 'aes-256-gcm';\nconst NONCE_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport class EnvEncryptionKey implements IEncryptionKey {\n private readonly key: Buffer;\n\n constructor(opts: EnvEncryptionKeyOptions = {}) {\n const env = opts.env ?? process.env;\n const envVar = opts.envVar ?? 'INTEGRATION_TOKEN_ENCRYPTION_KEY';\n const raw = env[envVar];\n if (!raw) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} is not set. Generate with: openssl rand -base64 32`,\n );\n }\n const decoded = Buffer.from(raw, 'base64');\n if (decoded.length !== KEY_BYTES) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} must decode to ${KEY_BYTES} bytes (got ${decoded.length}). Use: openssl rand -base64 32`,\n );\n }\n this.key = decoded;\n }\n\n async encrypt(plaintext: string): Promise<string> {\n const nonce = randomBytes(NONCE_BYTES);\n const cipher = createCipheriv(ALGO, this.key, nonce);\n const ciphertext = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([nonce, ciphertext, authTag]).toString('base64');\n }\n\n async decrypt(ciphertext: string): Promise<string> {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < NONCE_BYTES + TAG_BYTES) {\n throw new Error('EnvEncryptionKey: ciphertext too short');\n }\n const nonce = buf.subarray(0, NONCE_BYTES);\n const authTag = buf.subarray(buf.length - TAG_BYTES);\n const body = buf.subarray(NONCE_BYTES, buf.length - TAG_BYTES);\n\n const decipher = createDecipheriv(ALGO, this.key, nonce);\n decipher.setAuthTag(authTag);\n const plain = Buffer.concat([decipher.update(body), decipher.final()]);\n return plain.toString('utf8');\n }\n}\n"],"mappings":";AAcA,SAAS,gBAAgB,kBAAkB,mBAAmB;AAU9D,IAAM,OAAO;AACb,IAAM,cAAc;AACpB,IAAM,YAAY;AAClB,IAAM,YAAY;AAEX,IAAM,mBAAN,MAAiD;AAAA,EACrC;AAAA,EAEjB,YAAY,OAAgC,CAAC,GAAG;AAC9C,UAAM,MAAM,KAAK,OAAO,QAAQ;AAChC,UAAM,SAAS,KAAK,UAAU;AAC9B,UAAM,MAAM,IAAI,MAAM;AACtB,QAAI,CAAC,KAAK;AACR,YAAM,IAAI;AAAA,QACR,qBAAqB,MAAM;AAAA,MAC7B;AAAA,IACF;AACA,UAAM,UAAU,OAAO,KAAK,KAAK,QAAQ;AACzC,QAAI,QAAQ,WAAW,WAAW;AAChC,YAAM,IAAI;AAAA,QACR,qBAAqB,MAAM,mBAAmB,SAAS,eAAe,QAAQ,MAAM;AAAA,MACtF;AAAA,IACF;AACA,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,MAAM,QAAQ,WAAoC;AAChD,UAAM,QAAQ,YAAY,WAAW;AACrC,UAAM,SAAS,eAAe,MAAM,KAAK,KAAK,KAAK;AACnD,UAAM,aAAa,OAAO,OAAO;AAAA,MAC/B,OAAO,OAAO,WAAW,MAAM;AAAA,MAC/B,OAAO,MAAM;AAAA,IACf,CAAC;AACD,UAAM,UAAU,OAAO,WAAW;AAClC,WAAO,OAAO,OAAO,CAAC,OAAO,YAAY,OAAO,CAAC,EAAE,SAAS,QAAQ;AAAA,EACtE;AAAA,EAEA,MAAM,QAAQ,YAAqC;AACjD,UAAM,MAAM,OAAO,KAAK,YAAY,QAAQ;AAC5C,QAAI,IAAI,SAAS,cAAc,WAAW;AACxC,YAAM,IAAI,MAAM,wCAAwC;AAAA,IAC1D;AACA,UAAM,QAAQ,IAAI,SAAS,GAAG,WAAW;AACzC,UAAM,UAAU,IAAI,SAAS,IAAI,SAAS,SAAS;AACnD,UAAM,OAAO,IAAI,SAAS,aAAa,IAAI,SAAS,SAAS;AAE7D,UAAM,WAAW,iBAAiB,MAAM,KAAK,KAAK,KAAK;AACvD,aAAS,WAAW,OAAO;AAC3B,UAAM,QAAQ,OAAO,OAAO,CAAC,SAAS,OAAO,IAAI,GAAG,SAAS,MAAM,CAAC,CAAC;AACrE,WAAO,MAAM,SAAS,MAAM;AAAA,EAC9B;AACF;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../../runtime/subsystems/auth/auth.tokens.ts"],"sourcesContent":["/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug,
|
|
1
|
+
{"version":3,"sources":["../../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../../runtime/subsystems/auth/auth.tokens.ts"],"sourcesContent":["/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, IProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `AUTH_INTEGRATION_GRANT_SINK` (IIntegrationGrantSink) — persists the\n * freshly-minted grant. Adapter lives consumer-side (e.g. the\n * auth-integrations starter from #285).\n *\n * The controller never imports `IntegrationsService` or any other concrete\n * consumer type — it goes through ports only.\n */\nimport {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n HttpException,\n HttpStatus,\n} from '@nestjs/common';\nimport {\n AUTH_INTEGRATION_GRANT_SINK,\n AUTH_OPTIONS,\n AUTH_USER_CONTEXT,\n OAUTH_STATE_STORE,\n STRATEGY_REGISTRY,\n} from '../auth.tokens';\nimport type { AuthModuleOptions } from '../auth.module';\nimport type { IOAuthStateStore } from '../protocols/oauth-state-store';\nimport type { IUserContext } from '../protocols/user-context';\nimport type {\n IProviderStrategy,\n ProviderStrategyRegistry,\n} from '../protocols/provider-strategy';\nimport type { IIntegrationGrantSink } from '../protocols/integration-store';\n\n/**\n * Minimal response surface used by the controller — typed loosely so we\n * don't pull a hard dep on `express` or `fastify`. Both popular HTTP\n * adapters expose `redirect(status, url)`.\n */\ninterface RedirectingResponse {\n redirect(statusCode: number, url: string): unknown;\n}\n\n@Controller('auth')\nexport class AuthController {\n constructor(\n @Inject(STRATEGY_REGISTRY)\n private readonly registry: ProviderStrategyRegistry,\n @Inject(AUTH_USER_CONTEXT)\n private readonly userContext: IUserContext,\n @Inject(OAUTH_STATE_STORE)\n private readonly stateStore: IOAuthStateStore,\n @Inject(AUTH_INTEGRATION_GRANT_SINK)\n private readonly grantSink: IIntegrationGrantSink,\n @Inject(AUTH_OPTIONS)\n private readonly options: AuthModuleOptions,\n ) {}\n\n @Get(':provider/connect')\n async connect(\n @Param('provider') slug: string,\n @Query('redirect') redirect: string | undefined,\n @Req() req: unknown,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n const userId = await this.userContext.getCurrentUserId(req);\n const state = await this.stateStore.generate({ userId, redirect });\n const url = strategy.buildAuthorizeUrl({\n state,\n redirectUri: this.redirectUriFor(slug),\n });\n return res.redirect(HttpStatus.FOUND, url);\n }\n\n @Get(':provider/callback')\n async callback(\n @Param('provider') slug: string,\n @Query('code') code: string | undefined,\n @Query('state') state: string | undefined,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n if (!code) {\n throw new HttpException(\n `Missing 'code' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n if (!state) {\n throw new HttpException(\n `Missing 'state' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n const { userId, redirect } = await this.stateStore.consume(state);\n const tokens = await strategy.exchangeCodeForTokens({\n code,\n redirectUri: this.redirectUriFor(slug),\n });\n await this.grantSink.createOrUpdateFromOAuthGrant({\n userId,\n provider: slug,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n externalAccountId: tokens.externalAccountId,\n providerMetadata: tokens.providerMetadata,\n });\n return res.redirect(\n HttpStatus.FOUND,\n redirect ?? `/settings/integrations?connected=${encodeURIComponent(slug)}`,\n );\n }\n\n private requireStrategy(slug: string): IProviderStrategy {\n const strategy = this.registry.get(slug);\n if (!strategy) {\n throw new HttpException(\n `Unknown provider '${slug}'`,\n HttpStatus.NOT_FOUND,\n );\n }\n return strategy;\n }\n\n private redirectUriFor(slug: string): string {\n const base = this.options.redirectUriBase;\n if (!base) {\n throw new Error(\n `AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`,\n );\n }\n const trimmed = base.replace(/\\/+$/, '');\n return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;\n }\n}\n","/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,\n * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,\n * @Inject(AUTH_INTEGRATION_GRANT_SINK) private readonly grants: IIntegrationGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_INTEGRATION_READER = Symbol('AUTH_INTEGRATION_READER');\nexport const AUTH_INTEGRATION_TOKEN_WRITER = Symbol('AUTH_INTEGRATION_TOKEN_WRITER');\nexport const AUTH_INTEGRATION_GRANT_SINK = Symbol('AUTH_INTEGRATION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n"],"mappings":"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","names":[]}
|
|
@@ -3,7 +3,7 @@ export { IEncryptionKey } from './protocols/encryption-key.js';
|
|
|
3
3
|
export { IOAuthStateStore, OAuthStateError, OAuthStateRecord } from './protocols/oauth-state-store.js';
|
|
4
4
|
export { DecryptedIntegration, IIntegrationGrantSink, IIntegrationReader, IIntegrationTokenWriter, IntegrationGrantInput, IntegrationTokenUpdate } from './protocols/integration-store.js';
|
|
5
5
|
export { IUserContext } from './protocols/user-context.js';
|
|
6
|
-
export { ExchangedTokens,
|
|
6
|
+
export { ExchangedTokens, IProviderStrategy, ProviderStrategyRegistry } from './protocols/provider-strategy.js';
|
|
7
7
|
export { AUTH_INTEGRATION_GRANT_SINK, AUTH_INTEGRATION_READER, AUTH_INTEGRATION_TOKEN_WRITER, AUTH_OPTIONS, AUTH_USER_CONTEXT, ENCRYPTION_KEY, OAUTH_STATE_STORE, STRATEGY_REGISTRY } from './auth.tokens.js';
|
|
8
8
|
export { FetchLike, OAuth2RefreshStrategy, OAuth2RefreshStrategyOptions, ParsedRefreshResponse } from './runtime/oauth2-refresh.strategy.js';
|
|
9
9
|
export { WithAuthRetryOptions, withAuthRetry } from './runtime/with-auth-retry.js';
|
|
@@ -184,7 +184,7 @@ var EnvEncryptionKey = class {
|
|
|
184
184
|
key;
|
|
185
185
|
constructor(opts = {}) {
|
|
186
186
|
const env = opts.env ?? process.env;
|
|
187
|
-
const envVar = opts.envVar ?? "
|
|
187
|
+
const envVar = opts.envVar ?? "INTEGRATION_TOKEN_ENCRYPTION_KEY";
|
|
188
188
|
const raw = env[envVar];
|
|
189
189
|
if (!raw) {
|
|
190
190
|
throw new Error(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts","../../../../runtime/subsystems/auth/auth.tokens.ts","../../../../runtime/subsystems/auth/runtime/integration-broken.error.ts","../../../../runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts","../../../../runtime/subsystems/auth/runtime/session-expired.error.ts","../../../../runtime/subsystems/auth/runtime/with-auth-retry.ts","../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts","../../../../runtime/subsystems/auth/backends/encryption-key/env.ts","../../../../runtime/subsystems/auth/backends/state-store.memory-backend.ts","../../../../runtime/subsystems/auth/backends/state-store.drizzle-backend.ts","../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../runtime/subsystems/auth/auth.module.ts","../../../../runtime/constants/tokens.ts"],"sourcesContent":["/**\n * Auth subsystem — `IOAuthStateStore` port.\n *\n * CSRF protection for the OAuth2 authorize-code callback. Generic across\n * providers. The store mints opaque state tokens at /connect time and\n * single-use consumes them at /callback time, returning the original\n * record (userId + optional post-callback redirect path).\n *\n * Concrete backends live under `../backends/`:\n * - `state-store.memory-backend.ts` — in-process Map (tests/dev).\n * - `state-store.drizzle-backend.ts` — Postgres (prod).\n *\n * Semantics:\n * - `generate(record)` → returns an opaque state token; record is stored\n * under that token until consumed or until TTL expires.\n * - `consume(state)` → atomically deletes the entry and returns the\n * record. Throws on missing, expired, or replayed state. Never returns\n * null — a missing/expired state is a CSRF signal.\n */\nexport interface OAuthStateRecord {\n userId: string;\n /** Optional post-callback redirect path (relative URL). */\n redirect?: string;\n}\n\nexport interface IOAuthStateStore {\n /** Mint an opaque state token bound to `record`. Single-use. */\n generate(record: OAuthStateRecord): Promise<string>;\n /**\n * Atomically consume `state`, returning the bound record. Throws on\n * missing / expired / replayed state.\n */\n consume(state: string): Promise<OAuthStateRecord>;\n}\n\n/**\n * Thrown by `IOAuthStateStore.consume` when the state token is unknown,\n * expired, or has already been consumed (replay attempt).\n */\nexport class OAuthStateError extends Error {\n constructor(\n message: string,\n public readonly reason: 'missing' | 'expired',\n ) {\n super(message);\n this.name = 'OAuthStateError';\n }\n}\n","/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,\n * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,\n * @Inject(AUTH_INTEGRATION_GRANT_SINK) private readonly grants: IIntegrationGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, ProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_INTEGRATION_READER = Symbol('AUTH_INTEGRATION_READER');\nexport const AUTH_INTEGRATION_TOKEN_WRITER = Symbol('AUTH_INTEGRATION_TOKEN_WRITER');\nexport const AUTH_INTEGRATION_GRANT_SINK = Symbol('AUTH_INTEGRATION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n","/**\n * Thrown when an OAuth2 provider returns `400 invalid_grant`/`invalid_token`\n * on refresh — the refresh token itself is dead (user revoked, org\n * deactivated, token expired beyond the provider's rotation window). The\n * integration should be marked broken so background sync stops picking it\n * up; the user re-initiates OAuth.\n *\n * Shared across every OAuth2 strategy.\n */\nexport class IntegrationBrokenError extends Error {\n constructor(\n readonly integrationId: string,\n readonly errorCode: string,\n readonly errorDescription: string,\n ) {\n super(\n `Integration ${integrationId} broken: ${errorCode} - ${errorDescription}`,\n );\n this.name = 'IntegrationBrokenError';\n }\n}\n","/**\n * Abstract base class for OAuth2 refresh-token strategies.\n *\n * Template-method pattern: `resolve()` is concrete; four small hooks inject\n * provider specifics. Validated across two providers in dealbrain-v2\n * (SalesforceAuthStrategy, HubSpotAuthStrategy) before extraction here — see\n * `docs/gate-1-auth-extraction-findings.md` for the \"build first, extract\n * later\" evidence.\n *\n * Subclass contract:\n * - `provider` — slug matched against `integrations.provider`\n * - `defaultExpiresInSec` — fallback when refresh response omits `expires_in`\n * - `tokenEndpoint()` — URL to POST the refresh grant\n * - `refreshBodyExtras()` — provider-specific body params\n * - `parseRefreshResponse()` — raw JSON → ParsedRefreshResponse\n * - `buildCredentials()` — stored or freshly-refreshed access token +\n * integration + optional raw refresh response\n * → provider credentials\n *\n * Base handles: expiry check w/ 5-min safety window, `forceRefresh` escape\n * hatch, POST form-urlencoded body, OAuth2 error mapping to\n * `IntegrationBrokenError`, refresh-token rotation persistence, fetch +\n * clock injection for tests.\n */\nimport type {\n AuthCredentials,\n AuthResolveOptions,\n IAuthStrategy,\n} from '../protocols/auth-strategy';\nimport type {\n DecryptedIntegration,\n IIntegrationReader,\n IIntegrationTokenWriter,\n} from '../protocols/integration-store';\nimport { IntegrationBrokenError } from './integration-broken.error';\n\nexport type FetchLike = (\n input: string | URL | Request,\n init?: RequestInit,\n) => Promise<Response>;\n\n/** Safety window before expiry that triggers a refresh. */\nconst REFRESH_SAFETY_MS = 5 * 60 * 1000;\n\nexport interface OAuth2RefreshStrategyOptions {\n integrationReader: IIntegrationReader;\n tokenWriter: IIntegrationTokenWriter;\n /** Injectable fetch for tests. Defaults to the global `fetch`. */\n fetch?: FetchLike;\n /** Injectable clock for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\nexport interface ParsedRefreshResponse {\n accessToken: string;\n /**\n * New refresh token if the provider rotated it (HubSpot: always, Salesforce:\n * sometimes). Omit when the provider reused the old refresh token.\n */\n refreshToken?: string;\n /** Seconds from now. If omitted, subclass `defaultExpiresInSec` applies. */\n expiresInSec?: number;\n}\n\nexport abstract class OAuth2RefreshStrategy implements IAuthStrategy {\n protected abstract readonly provider: string;\n protected abstract readonly defaultExpiresInSec: number;\n\n protected readonly integrationReader: IIntegrationReader;\n protected readonly tokenWriter: IIntegrationTokenWriter;\n protected readonly fetchImpl: FetchLike;\n protected readonly now: () => number;\n\n constructor(opts: OAuth2RefreshStrategyOptions) {\n this.integrationReader = opts.integrationReader;\n this.tokenWriter = opts.tokenWriter;\n this.fetchImpl = opts.fetch ?? fetch;\n this.now = opts.now ?? Date.now;\n }\n\n async resolve(\n integrationId: string,\n opts: AuthResolveOptions = {},\n ): Promise<AuthCredentials> {\n const integration =\n await this.integrationReader.findByIdDecrypted(integrationId);\n if (!integration) {\n throw new Error(`Integration ${integrationId} not found`);\n }\n if (integration.provider !== this.provider) {\n throw new Error(\n `${this.constructor.name} called for non-${this.provider} integration ${integrationId} (provider=${integration.provider})`,\n );\n }\n\n const needsRefresh =\n opts.forceRefresh ||\n this.isExpiring(integration.expiresAt) ||\n !integration.accessToken;\n\n if (!needsRefresh) {\n return this.buildCredentials(integration.accessToken, integration);\n }\n\n if (!integration.refreshToken) {\n throw new IntegrationBrokenError(\n integrationId,\n 'no_refresh_token',\n 'Integration has no refresh token; user must reconnect',\n );\n }\n\n const { parsed, raw } = await this.executeRefresh(\n integrationId,\n integration.refreshToken,\n );\n const newExpiresAt = new Date(\n this.now() + (parsed.expiresInSec ?? this.defaultExpiresInSec) * 1000,\n );\n await this.tokenWriter.persistRefresh({\n integrationId,\n accessToken: parsed.accessToken,\n refreshToken: parsed.refreshToken ?? undefined,\n expiresAt: newExpiresAt,\n });\n\n return this.buildCredentials(parsed.accessToken, integration, raw);\n }\n\n protected abstract tokenEndpoint(): string;\n protected abstract refreshBodyExtras(): Record<string, string>;\n protected abstract parseRefreshResponse(raw: unknown): ParsedRefreshResponse;\n protected abstract buildCredentials(\n accessToken: string,\n integration: DecryptedIntegration,\n refreshRaw?: unknown,\n ): AuthCredentials;\n\n private async executeRefresh(\n integrationId: string,\n refreshToken: string,\n ): Promise<{ parsed: ParsedRefreshResponse; raw: unknown }> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n ...this.refreshBodyExtras(),\n });\n const response = await this.fetchImpl(this.tokenEndpoint(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!response.ok) {\n const err = (await safeJson(response)) as Partial<{\n error: string;\n error_description: string;\n message: string;\n }>;\n if (\n response.status === 400 &&\n (err.error === 'invalid_grant' || err.error === 'invalid_token')\n ) {\n throw new IntegrationBrokenError(\n integrationId,\n err.error ?? 'invalid_grant',\n err.error_description ?? err.message ?? 'refresh token rejected',\n );\n }\n throw new Error(\n `${this.provider} token refresh failed: ${response.status} ${err.error ?? ''} ${err.error_description ?? err.message ?? ''}`.trim(),\n );\n }\n const raw = await response.json();\n return { parsed: this.parseRefreshResponse(raw), raw };\n }\n\n private isExpiring(expiresAt: Date | null): boolean {\n if (!expiresAt) return true;\n return expiresAt.getTime() - this.now() < REFRESH_SAFETY_MS;\n }\n}\n\nasync function safeJson(response: Response): Promise<unknown> {\n try {\n return await response.clone().json();\n } catch {\n return {};\n }\n}\n","/**\n * Provider-agnostic marker for \"the access token was rejected; a forced\n * refresh may recover.\"\n *\n * Concrete provider error classes (e.g. SalesforceSessionExpiredError,\n * HubSpotUnauthorizedError) either extend `SessionExpiredError` directly or\n * set `isSessionExpired === true` on their instances. `withAuthRetry` uses\n * the `isSessionExpiredError` predicate to decide whether to force-refresh\n * and retry once.\n *\n * This discriminator replaces the SFDC-only `instanceof` check from\n * dealbrain-v2's original `withAuthRetry`. See\n * `docs/gate-1-auth-extraction-findings.md` (recommendation 4).\n */\nexport class SessionExpiredError extends Error {\n /** Duck-type marker — works across package boundaries where `instanceof` fails. */\n readonly isSessionExpired = true as const;\n\n constructor(message = 'Access token rejected by provider') {\n super(message);\n this.name = 'SessionExpiredError';\n }\n}\n\n/**\n * Predicate used by `withAuthRetry` by default.\n *\n * Matches any error that either `instanceof SessionExpiredError` or carries\n * the `isSessionExpired === true` marker property. Provider adapters that\n * want their existing error classes to participate can simply add the\n * marker property without touching the class hierarchy.\n */\nexport function isSessionExpiredError(err: unknown): boolean {\n if (err instanceof SessionExpiredError) return true;\n if (err !== null && typeof err === 'object' && 'isSessionExpired' in err) {\n return (err as { isSessionExpired?: unknown }).isSessionExpired === true;\n }\n return false;\n}\n","/**\n * Run `op` with auth-aware retry-once on session-expired errors.\n *\n * Pattern: resolve creds → run op → if `isSessionExpired(e)` → resolve with\n * `forceRefresh: true` → retry → propagate. A second session-expired error\n * on the refreshed token propagates rather than looping, so transient\n * adapter bugs can't hang the caller.\n *\n * Generalisation over dealbrain's original SFDC-specific version: the\n * session-expired classifier is injected. Providers mark their session-\n * expired errors (via `instanceof` of a marker class, or by setting a known\n * property) and pass a classifier matching that shape.\n *\n * Default classifier recognises the marker interface `SessionExpiredError`\n * shipped in `session-expired.error.ts` — concrete provider errors that\n * extend it (or set `isSessionExpired === true`) get retried without any\n * further wiring.\n */\nimport type {\n AuthCredentials,\n IAuthStrategy,\n} from '../protocols/auth-strategy';\nimport { isSessionExpiredError } from './session-expired.error';\n\nexport interface WithAuthRetryOptions {\n /**\n * Classifier that decides whether a thrown error is a session-expired\n * signal worth retrying once with a fresh token. Defaults to the marker-\n * interface check in `session-expired.error.ts`.\n */\n isSessionExpired?: (err: unknown) => boolean;\n}\n\nexport async function withAuthRetry<T>(\n authStrategy: IAuthStrategy,\n integrationId: string,\n op: (credentials: AuthCredentials) => Promise<T>,\n options: WithAuthRetryOptions = {},\n): Promise<T> {\n const classify = options.isSessionExpired ?? isSessionExpiredError;\n\n let creds = await authStrategy.resolve(integrationId);\n try {\n return await op(creds);\n } catch (e) {\n if (!classify(e)) throw e;\n creds = await authStrategy.resolve(integrationId, { forceRefresh: true });\n return op(creds);\n }\n}\n","/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `sync-audit.schema.ts`, `domain-events.schema.ts`).\n */\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const authOAuthState = pgTable('auth_oauth_state', {\n state: text('state').primaryKey(),\n userId: text('user_id').notNull(),\n redirect: text('redirect'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n});\n\nexport type AuthOAuthState = InferSelectModel<typeof authOAuthState>;\n","/**\n * Env-backed AES-256-GCM encryption.\n *\n * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce\n * per call means two encryptions of the same plaintext produce different\n * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;\n * any tampering throws on decrypt.\n *\n * Key source: `TOKEN_ENCRYPTION_KEY` env var, 32 bytes base64-encoded.\n * Generate via `openssl rand -base64 32`.\n *\n * Future backend: `kms.ts` (AWS/GCP KMS) for production deployments that\n * need key rotation + audit trails.\n */\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport type { IEncryptionKey } from '../../protocols/encryption-key';\n\nexport interface EnvEncryptionKeyOptions {\n /** Defaults to `process.env`. Tests inject a fixture. */\n env?: NodeJS.ProcessEnv;\n /** Defaults to `'TOKEN_ENCRYPTION_KEY'`. */\n envVar?: string;\n}\n\nconst ALGO = 'aes-256-gcm';\nconst NONCE_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport class EnvEncryptionKey implements IEncryptionKey {\n private readonly key: Buffer;\n\n constructor(opts: EnvEncryptionKeyOptions = {}) {\n const env = opts.env ?? process.env;\n const envVar = opts.envVar ?? 'TOKEN_ENCRYPTION_KEY';\n const raw = env[envVar];\n if (!raw) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} is not set. Generate with: openssl rand -base64 32`,\n );\n }\n const decoded = Buffer.from(raw, 'base64');\n if (decoded.length !== KEY_BYTES) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} must decode to ${KEY_BYTES} bytes (got ${decoded.length}). Use: openssl rand -base64 32`,\n );\n }\n this.key = decoded;\n }\n\n async encrypt(plaintext: string): Promise<string> {\n const nonce = randomBytes(NONCE_BYTES);\n const cipher = createCipheriv(ALGO, this.key, nonce);\n const ciphertext = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([nonce, ciphertext, authTag]).toString('base64');\n }\n\n async decrypt(ciphertext: string): Promise<string> {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < NONCE_BYTES + TAG_BYTES) {\n throw new Error('EnvEncryptionKey: ciphertext too short');\n }\n const nonce = buf.subarray(0, NONCE_BYTES);\n const authTag = buf.subarray(buf.length - TAG_BYTES);\n const body = buf.subarray(NONCE_BYTES, buf.length - TAG_BYTES);\n\n const decipher = createDecipheriv(ALGO, this.key, nonce);\n decipher.setAuthTag(authTag);\n const plain = Buffer.concat([decipher.update(body), decipher.final()]);\n return plain.toString('utf8');\n }\n}\n","/**\n * In-memory `IOAuthStateStore` backend.\n *\n * Single-process store — Map<state, { record, expiresAt }>. Suitable for\n * tests and single-worker dev. Production deployments select the drizzle\n * backend so state survives restarts and is shared across workers.\n *\n * Single-use semantics:\n * - `generate(record)` mints a 256-bit random token (base64url, opaque).\n * - `consume(state)` deletes the entry on read. A second call with the\n * same state throws `OAuthStateError('replay')`.\n * - Expired entries also throw (`'expired'`); the entry is deleted as a\n * side effect so a later replay still surfaces correctly.\n *\n * TTL defaults to 10 minutes — long enough for a user to complete the\n * provider's consent screen, short enough that abandoned states age out.\n */\nimport { randomBytes } from 'node:crypto';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface MemoryOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\ninterface Slot {\n record: OAuthStateRecord;\n expiresAt: number;\n}\n\nexport class MemoryOAuthStateStore implements IOAuthStateStore {\n private readonly store = new Map<string, Slot>();\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(opts: MemoryOAuthStateStoreOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n this.store.set(state, {\n record: { ...record },\n expiresAt: this.now() + this.ttlMs,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const slot = this.store.get(state);\n if (!slot) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n // Delete first so a concurrent consume can't replay.\n this.store.delete(state);\n if (slot.expiresAt <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return slot.record;\n }\n}\n","/**\n * Drizzle-backed `IOAuthStateStore`.\n *\n * Uses the `auth_oauth_state` table (see `auth-oauth-state.schema.ts`).\n * Single-use semantics enforced via `DELETE ... RETURNING`: the consume\n * path atomically deletes and returns the row, so a concurrent /callback\n * with the same state cannot replay.\n *\n * Behaviour:\n * - `generate(record)` mints a 256-bit base64url token, INSERTs the row\n * with `expires_at = now() + ttlMs`.\n * - `consume(state)` runs `DELETE ... WHERE state = $1 RETURNING ...`\n * once. Throws `OAuthStateError('missing')` if no row was deleted\n * (unknown or already consumed) and `OAuthStateError('expired')` if\n * the deleted row was past its `expires_at`.\n */\nimport { randomBytes } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { authOAuthState } from '../auth-oauth-state.schema';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface DrizzleOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\nexport class DrizzleOAuthStateStore implements IOAuthStateStore {\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(\n private readonly db: DrizzleClient,\n opts: DrizzleOAuthStateStoreOptions = {},\n ) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n const expiresAt = new Date(this.now() + this.ttlMs);\n await this.db.insert(authOAuthState).values({\n state,\n userId: record.userId,\n redirect: record.redirect ?? null,\n expiresAt,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const rows = await this.db\n .delete(authOAuthState)\n .where(eq(authOAuthState.state, state))\n .returning();\n const row = rows[0];\n if (!row) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n if (row.expiresAt.getTime() <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return {\n userId: row.userId,\n redirect: row.redirect ?? undefined,\n };\n }\n}\n","/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, ProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `AUTH_INTEGRATION_GRANT_SINK` (IIntegrationGrantSink) — persists the\n * freshly-minted grant. Adapter lives consumer-side (e.g. the\n * auth-integrations starter from #285).\n *\n * The controller never imports `IntegrationsService` or any other concrete\n * consumer type — it goes through ports only.\n */\nimport {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n HttpException,\n HttpStatus,\n} from '@nestjs/common';\nimport {\n AUTH_INTEGRATION_GRANT_SINK,\n AUTH_OPTIONS,\n AUTH_USER_CONTEXT,\n OAUTH_STATE_STORE,\n STRATEGY_REGISTRY,\n} from '../auth.tokens';\nimport type { AuthModuleOptions } from '../auth.module';\nimport type { IOAuthStateStore } from '../protocols/oauth-state-store';\nimport type { IUserContext } from '../protocols/user-context';\nimport type {\n ProviderStrategy,\n ProviderStrategyRegistry,\n} from '../protocols/provider-strategy';\nimport type { IIntegrationGrantSink } from '../protocols/integration-store';\n\n/**\n * Minimal response surface used by the controller — typed loosely so we\n * don't pull a hard dep on `express` or `fastify`. Both popular HTTP\n * adapters expose `redirect(status, url)`.\n */\ninterface RedirectingResponse {\n redirect(statusCode: number, url: string): unknown;\n}\n\n@Controller('auth')\nexport class AuthController {\n constructor(\n @Inject(STRATEGY_REGISTRY)\n private readonly registry: ProviderStrategyRegistry,\n @Inject(AUTH_USER_CONTEXT)\n private readonly userContext: IUserContext,\n @Inject(OAUTH_STATE_STORE)\n private readonly stateStore: IOAuthStateStore,\n @Inject(AUTH_INTEGRATION_GRANT_SINK)\n private readonly grantSink: IIntegrationGrantSink,\n @Inject(AUTH_OPTIONS)\n private readonly options: AuthModuleOptions,\n ) {}\n\n @Get(':provider/connect')\n async connect(\n @Param('provider') slug: string,\n @Query('redirect') redirect: string | undefined,\n @Req() req: unknown,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n const userId = await this.userContext.getCurrentUserId(req);\n const state = await this.stateStore.generate({ userId, redirect });\n const url = strategy.buildAuthorizeUrl({\n state,\n redirectUri: this.redirectUriFor(slug),\n });\n return res.redirect(HttpStatus.FOUND, url);\n }\n\n @Get(':provider/callback')\n async callback(\n @Param('provider') slug: string,\n @Query('code') code: string | undefined,\n @Query('state') state: string | undefined,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n if (!code) {\n throw new HttpException(\n `Missing 'code' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n if (!state) {\n throw new HttpException(\n `Missing 'state' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n const { userId, redirect } = await this.stateStore.consume(state);\n const tokens = await strategy.exchangeCodeForTokens({\n code,\n redirectUri: this.redirectUriFor(slug),\n });\n await this.grantSink.createOrUpdateFromOAuthGrant({\n userId,\n provider: slug,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n externalAccountId: tokens.externalAccountId,\n providerMetadata: tokens.providerMetadata,\n });\n return res.redirect(\n HttpStatus.FOUND,\n redirect ?? `/settings/integrations?connected=${encodeURIComponent(slug)}`,\n );\n }\n\n private requireStrategy(slug: string): ProviderStrategy {\n const strategy = this.registry.get(slug);\n if (!strategy) {\n throw new HttpException(\n `Unknown provider '${slug}'`,\n HttpStatus.NOT_FOUND,\n );\n }\n return strategy;\n }\n\n private redirectUriFor(slug: string): string {\n const base = this.options.redirectUriBase;\n if (!base) {\n throw new Error(\n `AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`,\n );\n }\n const trimmed = base.replace(/\\/+$/, '');\n return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;\n }\n}\n","/**\n * AuthModule — DynamicModule factory for the auth subsystem.\n *\n * Wires the pluggable backends the subsystem ships with:\n * - `ENCRYPTION_KEY` → `EnvEncryptionKey` (AES-256-GCM from env)\n * - `OAUTH_STATE_STORE` → `MemoryOAuthStateStore` (dev/tests) or\n * `DrizzleOAuthStateStore` (prod, requires\n * DRIZZLE provider).\n * - `AUTH_OPTIONS` → resolved options bag (used by AuthController\n * for `redirectUriBase`).\n *\n * The integration-store ports (`AUTH_INTEGRATION_READER`,\n * `AUTH_INTEGRATION_TOKEN_WRITER`, `AUTH_INTEGRATION_GRANT_SINK`),\n * `AUTH_USER_CONTEXT`, and `STRATEGY_REGISTRY` are deliberately **not**\n * wired here — they are always consumer-specific:\n * - integration-store ports adapt the consumer's `integrations` storage;\n * - `IUserContext` adapts the app's session/JWT scheme;\n * - `STRATEGY_REGISTRY` is populated from the per-provider strategy\n * classes the consumer maintains.\n *\n * Consumers provide them in their app module (or by importing the\n * `auth-integrations` starter, which binds the three integration-store\n * ports off a single canonical entity).\n *\n * Usage in AppModule:\n * ```typescript\n * AuthModule.forRoot({\n * encryptionKey: 'env',\n * oauthStateStore: 'memory', // or 'drizzle'\n * enableController: true,\n * redirectUriBase: 'http://localhost:3000',\n * });\n * ```\n *\n * `global: true` means other modules don't need to re-import AuthModule to\n * inject the auth tokens.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n AUTH_OPTIONS,\n ENCRYPTION_KEY,\n OAUTH_STATE_STORE,\n} from './auth.tokens';\nimport { EnvEncryptionKey } from './backends/encryption-key/env';\nimport { MemoryOAuthStateStore } from './backends/state-store.memory-backend';\nimport { DrizzleOAuthStateStore } from './backends/state-store.drizzle-backend';\nimport { AuthController } from './controllers/auth.controller';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type { DrizzleClient } from '../../types/drizzle';\n\ntype EncryptionKeyChoice =\n | 'env'\n | Omit<Provider, 'provide'>;\n\ntype OAuthStateStoreChoice =\n | 'memory'\n | 'drizzle'\n | Omit<Provider, 'provide'>;\n\nexport interface AuthModuleOptions {\n /** `'env'` (default) or a full provider definition (e.g. `{ useClass: MyKmsEncryptionKey }`). */\n encryptionKey?: EncryptionKeyChoice;\n /**\n * `'memory'` (default — tests/dev) or `'drizzle'` (prod, requires DRIZZLE\n * provider) or a full provider definition for a custom impl.\n */\n oauthStateStore?: OAuthStateStoreChoice;\n /**\n * Mount `AuthController` (`/auth/:provider/connect` + `/callback`).\n * Default `false` — apps that hand-roll connect/callback (rare) or that\n * use the subsystem only for the refresh path can opt out.\n */\n enableController?: boolean;\n /**\n * Public base URL of the API server. Used to construct per-provider\n * callback URIs as `${redirectUriBase}/auth/:provider/callback`.\n * Required when `enableController: true`.\n */\n redirectUriBase?: string;\n}\n\nfunction resolveEncryptionKeyProvider(choice: EncryptionKeyChoice): Provider {\n if (choice === 'env') {\n return { provide: ENCRYPTION_KEY, useClass: EnvEncryptionKey };\n }\n return { provide: ENCRYPTION_KEY, ...choice } as Provider;\n}\n\nfunction resolveOAuthStateStoreProvider(\n choice: OAuthStateStoreChoice,\n): Provider {\n if (choice === 'memory') {\n return { provide: OAUTH_STATE_STORE, useClass: MemoryOAuthStateStore };\n }\n if (choice === 'drizzle') {\n return {\n provide: OAUTH_STATE_STORE,\n useFactory: (db: DrizzleClient | null) => {\n if (!db) {\n throw new Error(\n \"AuthModule.forRoot: oauthStateStore: 'drizzle' selected but DRIZZLE provider is not available. \" +\n 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before AuthModule.forRoot.',\n );\n }\n return new DrizzleOAuthStateStore(db);\n },\n inject: [{ token: DRIZZLE, optional: true }],\n };\n }\n return { provide: OAUTH_STATE_STORE, ...choice } as Provider;\n}\n\n@Module({})\nexport class AuthModule {\n static forRoot(options: AuthModuleOptions = {}): DynamicModule {\n const resolved: AuthModuleOptions = {\n encryptionKey: options.encryptionKey ?? 'env',\n oauthStateStore: options.oauthStateStore ?? 'memory',\n enableController: options.enableController ?? false,\n redirectUriBase: options.redirectUriBase,\n };\n\n if (resolved.enableController && !resolved.redirectUriBase) {\n throw new Error(\n 'AuthModule.forRoot: redirectUriBase is required when enableController: true',\n );\n }\n\n const encryptionKeyProvider = resolveEncryptionKeyProvider(\n resolved.encryptionKey!,\n );\n const oauthStateStoreProvider = resolveOAuthStateStoreProvider(\n resolved.oauthStateStore!,\n );\n const optionsProvider: Provider = {\n provide: AUTH_OPTIONS,\n useValue: resolved,\n };\n\n return {\n module: AuthModule,\n global: true,\n providers: [encryptionKeyProvider, oauthStateStoreProvider, optionsProvider],\n controllers: resolved.enableController ? [AuthController] : [],\n exports: [ENCRYPTION_KEY, OAUTH_STATE_STORE, AUTH_OPTIONS],\n };\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n"],"mappings":"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","names":["randomBytes","randomBytes","randomBytes","randomBytes"]}
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts","../../../../runtime/subsystems/auth/auth.tokens.ts","../../../../runtime/subsystems/auth/runtime/integration-broken.error.ts","../../../../runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts","../../../../runtime/subsystems/auth/runtime/session-expired.error.ts","../../../../runtime/subsystems/auth/runtime/with-auth-retry.ts","../../../../runtime/subsystems/auth/auth-oauth-state.schema.ts","../../../../runtime/subsystems/auth/backends/encryption-key/env.ts","../../../../runtime/subsystems/auth/backends/state-store.memory-backend.ts","../../../../runtime/subsystems/auth/backends/state-store.drizzle-backend.ts","../../../../runtime/subsystems/auth/controllers/auth.controller.ts","../../../../runtime/subsystems/auth/auth.module.ts","../../../../runtime/constants/tokens.ts"],"sourcesContent":["/**\n * Auth subsystem — `IOAuthStateStore` port.\n *\n * CSRF protection for the OAuth2 authorize-code callback. Generic across\n * providers. The store mints opaque state tokens at /connect time and\n * single-use consumes them at /callback time, returning the original\n * record (userId + optional post-callback redirect path).\n *\n * Concrete backends live under `../backends/`:\n * - `state-store.memory-backend.ts` — in-process Map (tests/dev).\n * - `state-store.drizzle-backend.ts` — Postgres (prod).\n *\n * Semantics:\n * - `generate(record)` → returns an opaque state token; record is stored\n * under that token until consumed or until TTL expires.\n * - `consume(state)` → atomically deletes the entry and returns the\n * record. Throws on missing, expired, or replayed state. Never returns\n * null — a missing/expired state is a CSRF signal.\n */\nexport interface OAuthStateRecord {\n userId: string;\n /** Optional post-callback redirect path (relative URL). */\n redirect?: string;\n}\n\nexport interface IOAuthStateStore {\n /** Mint an opaque state token bound to `record`. Single-use. */\n generate(record: OAuthStateRecord): Promise<string>;\n /**\n * Atomically consume `state`, returning the bound record. Throws on\n * missing / expired / replayed state.\n */\n consume(state: string): Promise<OAuthStateRecord>;\n}\n\n/**\n * Thrown by `IOAuthStateStore.consume` when the state token is unknown,\n * expired, or has already been consumed (replay attempt).\n */\nexport class OAuthStateError extends Error {\n constructor(\n message: string,\n public readonly reason: 'missing' | 'expired',\n ) {\n super(message);\n this.name = 'OAuthStateError';\n }\n}\n","/**\n * Auth subsystem — injection tokens.\n *\n * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision\n * avoidance. Consumers inject these via `@Inject(...)` against the matching\n * protocol interface.\n *\n * Usage:\n * ```typescript\n * constructor(\n * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,\n * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,\n * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,\n * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,\n * @Inject(AUTH_INTEGRATION_GRANT_SINK) private readonly grants: IIntegrationGrantSink,\n * @Inject(AUTH_USER_CONTEXT) private readonly userCtx: IUserContext,\n * @Inject(STRATEGY_REGISTRY) private readonly registry: ProviderStrategyRegistry,\n * ) {}\n * ```\n *\n * `IAuthStrategy` implementations are provider-specific and registered under\n * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,\n * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does\n * not mandate a single `AUTH_STRATEGY` token because an app typically has\n * many concurrent strategies, one per provider. They are dispatched through\n * `STRATEGY_REGISTRY` (a `ReadonlyMap<slug, IProviderStrategy>`), populated\n * by per-provider modules via a `useFactory` provider.\n */\nexport const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');\nexport const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');\nexport const AUTH_INTEGRATION_READER = Symbol('AUTH_INTEGRATION_READER');\nexport const AUTH_INTEGRATION_TOKEN_WRITER = Symbol('AUTH_INTEGRATION_TOKEN_WRITER');\nexport const AUTH_INTEGRATION_GRANT_SINK = Symbol('AUTH_INTEGRATION_GRANT_SINK');\nexport const AUTH_USER_CONTEXT = Symbol('AUTH_USER_CONTEXT');\nexport const STRATEGY_REGISTRY = Symbol('STRATEGY_REGISTRY');\n/**\n * Holds the resolved `AuthModuleOptions` (used by `AuthController` to read\n * `redirectUriBase` for building per-provider callback URIs).\n */\nexport const AUTH_OPTIONS = Symbol('AUTH_OPTIONS');\n","/**\n * Thrown when an OAuth2 provider returns `400 invalid_grant`/`invalid_token`\n * on refresh — the refresh token itself is dead (user revoked, org\n * deactivated, token expired beyond the provider's rotation window). The\n * integration should be marked broken so background sync stops picking it\n * up; the user re-initiates OAuth.\n *\n * Shared across every OAuth2 strategy.\n */\nexport class IntegrationBrokenError extends Error {\n constructor(\n readonly integrationId: string,\n readonly errorCode: string,\n readonly errorDescription: string,\n ) {\n super(\n `Integration ${integrationId} broken: ${errorCode} - ${errorDescription}`,\n );\n this.name = 'IntegrationBrokenError';\n }\n}\n","/**\n * Abstract base class for OAuth2 refresh-token strategies.\n *\n * Template-method pattern: `resolve()` is concrete; four small hooks inject\n * provider specifics. Validated across two providers (Salesforce, HubSpot)\n * in the extraction-source app before being extracted here — see\n * `docs/gate-1-auth-extraction-findings.md` for the \"build first, extract\n * later\" evidence.\n *\n * Subclass contract:\n * - `provider` — slug matched against `integrations.provider`\n * - `defaultExpiresInSec` — fallback when refresh response omits `expires_in`\n * - `tokenEndpoint()` — URL to POST the refresh grant\n * - `refreshBodyExtras()` — provider-specific body params\n * - `parseRefreshResponse()` — raw JSON → ParsedRefreshResponse\n * - `buildCredentials()` — stored or freshly-refreshed access token +\n * integration + optional raw refresh response\n * → provider credentials\n *\n * Base handles: expiry check w/ 5-min safety window, `forceRefresh` escape\n * hatch, POST form-urlencoded body, OAuth2 error mapping to\n * `IntegrationBrokenError`, refresh-token rotation persistence, fetch +\n * clock injection for tests.\n */\nimport type {\n AuthCredentials,\n AuthResolveOptions,\n IAuthStrategy,\n} from '../protocols/auth-strategy';\nimport type {\n DecryptedIntegration,\n IIntegrationReader,\n IIntegrationTokenWriter,\n} from '../protocols/integration-store';\nimport { IntegrationBrokenError } from './integration-broken.error';\n\nexport type FetchLike = (\n input: string | URL | Request,\n init?: RequestInit,\n) => Promise<Response>;\n\n/** Safety window before expiry that triggers a refresh. */\nconst REFRESH_SAFETY_MS = 5 * 60 * 1000;\n\nexport interface OAuth2RefreshStrategyOptions {\n integrationReader: IIntegrationReader;\n tokenWriter: IIntegrationTokenWriter;\n /** Injectable fetch for tests. Defaults to the global `fetch`. */\n fetch?: FetchLike;\n /** Injectable clock for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\nexport interface ParsedRefreshResponse {\n accessToken: string;\n /**\n * New refresh token if the provider rotated it (HubSpot: always, Salesforce:\n * sometimes). Omit when the provider reused the old refresh token.\n */\n refreshToken?: string;\n /** Seconds from now. If omitted, subclass `defaultExpiresInSec` applies. */\n expiresInSec?: number;\n}\n\nexport abstract class OAuth2RefreshStrategy implements IAuthStrategy {\n protected abstract readonly provider: string;\n protected abstract readonly defaultExpiresInSec: number;\n\n protected readonly integrationReader: IIntegrationReader;\n protected readonly tokenWriter: IIntegrationTokenWriter;\n protected readonly fetchImpl: FetchLike;\n protected readonly now: () => number;\n\n constructor(opts: OAuth2RefreshStrategyOptions) {\n this.integrationReader = opts.integrationReader;\n this.tokenWriter = opts.tokenWriter;\n this.fetchImpl = opts.fetch ?? fetch;\n this.now = opts.now ?? Date.now;\n }\n\n async resolve(\n integrationId: string,\n opts: AuthResolveOptions = {},\n ): Promise<AuthCredentials> {\n const integration =\n await this.integrationReader.findByIdDecrypted(integrationId);\n if (!integration) {\n throw new Error(`Integration ${integrationId} not found`);\n }\n if (integration.provider !== this.provider) {\n throw new Error(\n `${this.constructor.name} called for non-${this.provider} integration ${integrationId} (provider=${integration.provider})`,\n );\n }\n\n const needsRefresh =\n opts.forceRefresh ||\n this.isExpiring(integration.expiresAt) ||\n !integration.accessToken;\n\n if (!needsRefresh) {\n return this.buildCredentials(integration.accessToken, integration);\n }\n\n if (!integration.refreshToken) {\n throw new IntegrationBrokenError(\n integrationId,\n 'no_refresh_token',\n 'Integration has no refresh token; user must reconnect',\n );\n }\n\n const { parsed, raw } = await this.executeRefresh(\n integrationId,\n integration.refreshToken,\n );\n const newExpiresAt = new Date(\n this.now() + (parsed.expiresInSec ?? this.defaultExpiresInSec) * 1000,\n );\n await this.tokenWriter.persistRefresh({\n integrationId,\n accessToken: parsed.accessToken,\n refreshToken: parsed.refreshToken ?? undefined,\n expiresAt: newExpiresAt,\n });\n\n return this.buildCredentials(parsed.accessToken, integration, raw);\n }\n\n protected abstract tokenEndpoint(): string;\n protected abstract refreshBodyExtras(): Record<string, string>;\n protected abstract parseRefreshResponse(raw: unknown): ParsedRefreshResponse;\n protected abstract buildCredentials(\n accessToken: string,\n integration: DecryptedIntegration,\n refreshRaw?: unknown,\n ): AuthCredentials;\n\n private async executeRefresh(\n integrationId: string,\n refreshToken: string,\n ): Promise<{ parsed: ParsedRefreshResponse; raw: unknown }> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n ...this.refreshBodyExtras(),\n });\n const response = await this.fetchImpl(this.tokenEndpoint(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!response.ok) {\n const err = (await safeJson(response)) as Partial<{\n error: string;\n error_description: string;\n message: string;\n }>;\n if (\n response.status === 400 &&\n (err.error === 'invalid_grant' || err.error === 'invalid_token')\n ) {\n throw new IntegrationBrokenError(\n integrationId,\n err.error ?? 'invalid_grant',\n err.error_description ?? err.message ?? 'refresh token rejected',\n );\n }\n throw new Error(\n `${this.provider} token refresh failed: ${response.status} ${err.error ?? ''} ${err.error_description ?? err.message ?? ''}`.trim(),\n );\n }\n const raw = await response.json();\n return { parsed: this.parseRefreshResponse(raw), raw };\n }\n\n private isExpiring(expiresAt: Date | null): boolean {\n if (!expiresAt) return true;\n return expiresAt.getTime() - this.now() < REFRESH_SAFETY_MS;\n }\n}\n\nasync function safeJson(response: Response): Promise<unknown> {\n try {\n return await response.clone().json();\n } catch {\n return {};\n }\n}\n","/**\n * Provider-agnostic marker for \"the access token was rejected; a forced\n * refresh may recover.\"\n *\n * Concrete provider error classes (e.g. SalesforceSessionExpiredError,\n * HubSpotUnauthorizedError) either extend `SessionExpiredError` directly or\n * set `isSessionExpired === true` on their instances. `withAuthRetry` uses\n * the `isSessionExpiredError` predicate to decide whether to force-refresh\n * and retry once.\n *\n * This discriminator replaces the SFDC-only `instanceof` check from the\n * extraction-source app's original `withAuthRetry`. See\n * `docs/gate-1-auth-extraction-findings.md` (recommendation 4).\n */\nexport class SessionExpiredError extends Error {\n /** Duck-type marker — works across package boundaries where `instanceof` fails. */\n readonly isSessionExpired = true as const;\n\n constructor(message = 'Access token rejected by provider') {\n super(message);\n this.name = 'SessionExpiredError';\n }\n}\n\n/**\n * Predicate used by `withAuthRetry` by default.\n *\n * Matches any error that either `instanceof SessionExpiredError` or carries\n * the `isSessionExpired === true` marker property. Provider adapters that\n * want their existing error classes to participate can simply add the\n * marker property without touching the class hierarchy.\n */\nexport function isSessionExpiredError(err: unknown): boolean {\n if (err instanceof SessionExpiredError) return true;\n if (err !== null && typeof err === 'object' && 'isSessionExpired' in err) {\n return (err as { isSessionExpired?: unknown }).isSessionExpired === true;\n }\n return false;\n}\n","/**\n * Run `op` with auth-aware retry-once on session-expired errors.\n *\n * Pattern: resolve creds → run op → if `isSessionExpired(e)` → resolve with\n * `forceRefresh: true` → retry → propagate. A second session-expired error\n * on the refreshed token propagates rather than looping, so transient\n * adapter bugs can't hang the caller.\n *\n * Generalisation over the extraction source's SFDC-specific original: the\n * session-expired classifier is injected. Providers mark their session-\n * expired errors (via `instanceof` of a marker class, or by setting a known\n * property) and pass a classifier matching that shape.\n *\n * Default classifier recognises the marker interface `SessionExpiredError`\n * shipped in `session-expired.error.ts` — concrete provider errors that\n * extend it (or set `isSessionExpired === true`) get retried without any\n * further wiring.\n */\nimport type {\n AuthCredentials,\n IAuthStrategy,\n} from '../protocols/auth-strategy';\nimport { isSessionExpiredError } from './session-expired.error';\n\nexport interface WithAuthRetryOptions {\n /**\n * Classifier that decides whether a thrown error is a session-expired\n * signal worth retrying once with a fresh token. Defaults to the marker-\n * interface check in `session-expired.error.ts`.\n */\n isSessionExpired?: (err: unknown) => boolean;\n}\n\nexport async function withAuthRetry<T>(\n authStrategy: IAuthStrategy,\n integrationId: string,\n op: (credentials: AuthCredentials) => Promise<T>,\n options: WithAuthRetryOptions = {},\n): Promise<T> {\n const classify = options.isSessionExpired ?? isSessionExpiredError;\n\n let creds = await authStrategy.resolve(integrationId);\n try {\n return await op(creds);\n } catch (e) {\n if (!classify(e)) throw e;\n creds = await authStrategy.resolve(integrationId, { forceRefresh: true });\n return op(creds);\n }\n}\n","/**\n * Drizzle schema for the `auth_oauth_state` table — backs the\n * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).\n *\n * One row per outstanding /connect → /callback dance. Single-use; rows are\n * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`\n * filter on read) clears abandoned rows.\n *\n * Columns:\n * - `state` — opaque random token, primary key.\n * - `user_id` — text (matches the consumer-defined user-id shape;\n * the auth subsystem doesn't constrain this to UUID\n * because some apps key users by external id).\n * - `redirect` — optional post-callback redirect path.\n * - `expires_at` — TTL boundary; entries past this are treated as absent.\n *\n * Convention: schema files live at the root of the subsystem dir\n * (mirrors `cache.schema.ts`, `sync-audit.schema.ts`, `domain-events.schema.ts`).\n */\nimport { pgTable, text, timestamp } from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const authOAuthState = pgTable('auth_oauth_state', {\n state: text('state').primaryKey(),\n userId: text('user_id').notNull(),\n redirect: text('redirect'),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n});\n\nexport type AuthOAuthState = InferSelectModel<typeof authOAuthState>;\n","/**\n * Env-backed AES-256-GCM encryption.\n *\n * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce\n * per call means two encryptions of the same plaintext produce different\n * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;\n * any tampering throws on decrypt.\n *\n * Key source: `INTEGRATION_TOKEN_ENCRYPTION_KEY` env var, 32 bytes base64-encoded.\n * Generate via `openssl rand -base64 32`.\n *\n * Future backend: `kms.ts` (AWS/GCP KMS) for production deployments that\n * need key rotation + audit trails.\n */\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport type { IEncryptionKey } from '../../protocols/encryption-key';\n\nexport interface EnvEncryptionKeyOptions {\n /** Defaults to `process.env`. Tests inject a fixture. */\n env?: NodeJS.ProcessEnv;\n /** Defaults to `'INTEGRATION_TOKEN_ENCRYPTION_KEY'`. */\n envVar?: string;\n}\n\nconst ALGO = 'aes-256-gcm';\nconst NONCE_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport class EnvEncryptionKey implements IEncryptionKey {\n private readonly key: Buffer;\n\n constructor(opts: EnvEncryptionKeyOptions = {}) {\n const env = opts.env ?? process.env;\n const envVar = opts.envVar ?? 'INTEGRATION_TOKEN_ENCRYPTION_KEY';\n const raw = env[envVar];\n if (!raw) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} is not set. Generate with: openssl rand -base64 32`,\n );\n }\n const decoded = Buffer.from(raw, 'base64');\n if (decoded.length !== KEY_BYTES) {\n throw new Error(\n `EnvEncryptionKey: ${envVar} must decode to ${KEY_BYTES} bytes (got ${decoded.length}). Use: openssl rand -base64 32`,\n );\n }\n this.key = decoded;\n }\n\n async encrypt(plaintext: string): Promise<string> {\n const nonce = randomBytes(NONCE_BYTES);\n const cipher = createCipheriv(ALGO, this.key, nonce);\n const ciphertext = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([nonce, ciphertext, authTag]).toString('base64');\n }\n\n async decrypt(ciphertext: string): Promise<string> {\n const buf = Buffer.from(ciphertext, 'base64');\n if (buf.length < NONCE_BYTES + TAG_BYTES) {\n throw new Error('EnvEncryptionKey: ciphertext too short');\n }\n const nonce = buf.subarray(0, NONCE_BYTES);\n const authTag = buf.subarray(buf.length - TAG_BYTES);\n const body = buf.subarray(NONCE_BYTES, buf.length - TAG_BYTES);\n\n const decipher = createDecipheriv(ALGO, this.key, nonce);\n decipher.setAuthTag(authTag);\n const plain = Buffer.concat([decipher.update(body), decipher.final()]);\n return plain.toString('utf8');\n }\n}\n","/**\n * In-memory `IOAuthStateStore` backend.\n *\n * Single-process store — Map<state, { record, expiresAt }>. Suitable for\n * tests and single-worker dev. Production deployments select the drizzle\n * backend so state survives restarts and is shared across workers.\n *\n * Single-use semantics:\n * - `generate(record)` mints a 256-bit random token (base64url, opaque).\n * - `consume(state)` deletes the entry on read. A second call with the\n * same state throws `OAuthStateError('replay')`.\n * - Expired entries also throw (`'expired'`); the entry is deleted as a\n * side effect so a later replay still surfaces correctly.\n *\n * TTL defaults to 10 minutes — long enough for a user to complete the\n * provider's consent screen, short enough that abandoned states age out.\n */\nimport { randomBytes } from 'node:crypto';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface MemoryOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\ninterface Slot {\n record: OAuthStateRecord;\n expiresAt: number;\n}\n\nexport class MemoryOAuthStateStore implements IOAuthStateStore {\n private readonly store = new Map<string, Slot>();\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(opts: MemoryOAuthStateStoreOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n this.store.set(state, {\n record: { ...record },\n expiresAt: this.now() + this.ttlMs,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const slot = this.store.get(state);\n if (!slot) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n // Delete first so a concurrent consume can't replay.\n this.store.delete(state);\n if (slot.expiresAt <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return slot.record;\n }\n}\n","/**\n * Drizzle-backed `IOAuthStateStore`.\n *\n * Uses the `auth_oauth_state` table (see `auth-oauth-state.schema.ts`).\n * Single-use semantics enforced via `DELETE ... RETURNING`: the consume\n * path atomically deletes and returns the row, so a concurrent /callback\n * with the same state cannot replay.\n *\n * Behaviour:\n * - `generate(record)` mints a 256-bit base64url token, INSERTs the row\n * with `expires_at = now() + ttlMs`.\n * - `consume(state)` runs `DELETE ... WHERE state = $1 RETURNING ...`\n * once. Throws `OAuthStateError('missing')` if no row was deleted\n * (unknown or already consumed) and `OAuthStateError('expired')` if\n * the deleted row was past its `expires_at`.\n */\nimport { randomBytes } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../../types/drizzle';\nimport { authOAuthState } from '../auth-oauth-state.schema';\nimport {\n type IOAuthStateStore,\n type OAuthStateRecord,\n OAuthStateError,\n} from '../protocols/oauth-state-store';\n\nexport interface DrizzleOAuthStateStoreOptions {\n /** TTL in ms. Default 10 minutes. */\n ttlMs?: number;\n /** Injectable clock for tests. Default `Date.now`. */\n now?: () => number;\n /** Injectable token generator for tests. Default 32-byte base64url. */\n generateToken?: () => string;\n}\n\nexport class DrizzleOAuthStateStore implements IOAuthStateStore {\n private readonly ttlMs: number;\n private readonly now: () => number;\n private readonly generateToken: () => string;\n\n constructor(\n private readonly db: DrizzleClient,\n opts: DrizzleOAuthStateStoreOptions = {},\n ) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n this.generateToken =\n opts.generateToken ?? (() => randomBytes(32).toString('base64url'));\n }\n\n async generate(record: OAuthStateRecord): Promise<string> {\n const state = this.generateToken();\n const expiresAt = new Date(this.now() + this.ttlMs);\n await this.db.insert(authOAuthState).values({\n state,\n userId: record.userId,\n redirect: record.redirect ?? null,\n expiresAt,\n });\n return state;\n }\n\n async consume(state: string): Promise<OAuthStateRecord> {\n const rows = await this.db\n .delete(authOAuthState)\n .where(eq(authOAuthState.state, state))\n .returning();\n const row = rows[0];\n if (!row) {\n throw new OAuthStateError(\n `OAuth state token unknown or already consumed`,\n 'missing',\n );\n }\n if (row.expiresAt.getTime() <= this.now()) {\n throw new OAuthStateError(`OAuth state token expired`, 'expired');\n }\n return {\n userId: row.userId,\n redirect: row.redirect ?? undefined,\n };\n }\n}\n","/**\n * AuthController — provider-agnostic OAuth2 connect/callback dance.\n *\n * Mounts two routes:\n * - `GET /auth/:provider/connect?redirect=...` — generates state, builds\n * the provider's authorize-url, 302-redirects the browser there.\n * - `GET /auth/:provider/callback?code=...&state=...` — consumes state,\n * exchanges the code for tokens, hands them to the grant sink, then\n * 302-redirects to the post-connect path.\n *\n * Hexagonal seams:\n * - `STRATEGY_REGISTRY` (ReadonlyMap<slug, IProviderStrategy>) — dispatch.\n * Concrete per-provider strategies live consumer-side and contribute\n * entries via a `useFactory` in the consumer's app module.\n * - `AUTH_USER_CONTEXT` (IUserContext) — resolves \"who is this request\"\n * from the consumer's session/JWT/etc.\n * - `OAUTH_STATE_STORE` (IOAuthStateStore) — CSRF state minting/consume.\n * - `AUTH_INTEGRATION_GRANT_SINK` (IIntegrationGrantSink) — persists the\n * freshly-minted grant. Adapter lives consumer-side (e.g. the\n * auth-integrations starter from #285).\n *\n * The controller never imports `IntegrationsService` or any other concrete\n * consumer type — it goes through ports only.\n */\nimport {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n HttpException,\n HttpStatus,\n} from '@nestjs/common';\nimport {\n AUTH_INTEGRATION_GRANT_SINK,\n AUTH_OPTIONS,\n AUTH_USER_CONTEXT,\n OAUTH_STATE_STORE,\n STRATEGY_REGISTRY,\n} from '../auth.tokens';\nimport type { AuthModuleOptions } from '../auth.module';\nimport type { IOAuthStateStore } from '../protocols/oauth-state-store';\nimport type { IUserContext } from '../protocols/user-context';\nimport type {\n IProviderStrategy,\n ProviderStrategyRegistry,\n} from '../protocols/provider-strategy';\nimport type { IIntegrationGrantSink } from '../protocols/integration-store';\n\n/**\n * Minimal response surface used by the controller — typed loosely so we\n * don't pull a hard dep on `express` or `fastify`. Both popular HTTP\n * adapters expose `redirect(status, url)`.\n */\ninterface RedirectingResponse {\n redirect(statusCode: number, url: string): unknown;\n}\n\n@Controller('auth')\nexport class AuthController {\n constructor(\n @Inject(STRATEGY_REGISTRY)\n private readonly registry: ProviderStrategyRegistry,\n @Inject(AUTH_USER_CONTEXT)\n private readonly userContext: IUserContext,\n @Inject(OAUTH_STATE_STORE)\n private readonly stateStore: IOAuthStateStore,\n @Inject(AUTH_INTEGRATION_GRANT_SINK)\n private readonly grantSink: IIntegrationGrantSink,\n @Inject(AUTH_OPTIONS)\n private readonly options: AuthModuleOptions,\n ) {}\n\n @Get(':provider/connect')\n async connect(\n @Param('provider') slug: string,\n @Query('redirect') redirect: string | undefined,\n @Req() req: unknown,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n const userId = await this.userContext.getCurrentUserId(req);\n const state = await this.stateStore.generate({ userId, redirect });\n const url = strategy.buildAuthorizeUrl({\n state,\n redirectUri: this.redirectUriFor(slug),\n });\n return res.redirect(HttpStatus.FOUND, url);\n }\n\n @Get(':provider/callback')\n async callback(\n @Param('provider') slug: string,\n @Query('code') code: string | undefined,\n @Query('state') state: string | undefined,\n @Res() res: RedirectingResponse,\n ): Promise<unknown> {\n const strategy = this.requireStrategy(slug);\n if (!code) {\n throw new HttpException(\n `Missing 'code' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n if (!state) {\n throw new HttpException(\n `Missing 'state' query param`,\n HttpStatus.BAD_REQUEST,\n );\n }\n const { userId, redirect } = await this.stateStore.consume(state);\n const tokens = await strategy.exchangeCodeForTokens({\n code,\n redirectUri: this.redirectUriFor(slug),\n });\n await this.grantSink.createOrUpdateFromOAuthGrant({\n userId,\n provider: slug,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n externalAccountId: tokens.externalAccountId,\n providerMetadata: tokens.providerMetadata,\n });\n return res.redirect(\n HttpStatus.FOUND,\n redirect ?? `/settings/integrations?connected=${encodeURIComponent(slug)}`,\n );\n }\n\n private requireStrategy(slug: string): IProviderStrategy {\n const strategy = this.registry.get(slug);\n if (!strategy) {\n throw new HttpException(\n `Unknown provider '${slug}'`,\n HttpStatus.NOT_FOUND,\n );\n }\n return strategy;\n }\n\n private redirectUriFor(slug: string): string {\n const base = this.options.redirectUriBase;\n if (!base) {\n throw new Error(\n `AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`,\n );\n }\n const trimmed = base.replace(/\\/+$/, '');\n return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;\n }\n}\n","/**\n * AuthModule — DynamicModule factory for the auth subsystem.\n *\n * Wires the pluggable backends the subsystem ships with:\n * - `ENCRYPTION_KEY` → `EnvEncryptionKey` (AES-256-GCM from env)\n * - `OAUTH_STATE_STORE` → `MemoryOAuthStateStore` (dev/tests) or\n * `DrizzleOAuthStateStore` (prod, requires\n * DRIZZLE provider).\n * - `AUTH_OPTIONS` → resolved options bag (used by AuthController\n * for `redirectUriBase`).\n *\n * The integration-store ports (`AUTH_INTEGRATION_READER`,\n * `AUTH_INTEGRATION_TOKEN_WRITER`, `AUTH_INTEGRATION_GRANT_SINK`),\n * `AUTH_USER_CONTEXT`, and `STRATEGY_REGISTRY` are deliberately **not**\n * wired here — they are always consumer-specific:\n * - integration-store ports adapt the consumer's `integrations` storage;\n * - `IUserContext` adapts the app's session/JWT scheme;\n * - `STRATEGY_REGISTRY` is populated from the per-provider strategy\n * classes the consumer maintains.\n *\n * Consumers provide them in their app module (or by importing the\n * `auth-integrations` starter, which binds the three integration-store\n * ports off a single canonical entity).\n *\n * Usage in AppModule:\n * ```typescript\n * AuthModule.forRoot({\n * encryptionKey: 'env',\n * oauthStateStore: 'memory', // or 'drizzle'\n * enableController: true,\n * redirectUriBase: 'http://localhost:3000',\n * });\n * ```\n *\n * `global: true` means other modules don't need to re-import AuthModule to\n * inject the auth tokens.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n AUTH_OPTIONS,\n ENCRYPTION_KEY,\n OAUTH_STATE_STORE,\n} from './auth.tokens';\nimport { EnvEncryptionKey } from './backends/encryption-key/env';\nimport { MemoryOAuthStateStore } from './backends/state-store.memory-backend';\nimport { DrizzleOAuthStateStore } from './backends/state-store.drizzle-backend';\nimport { AuthController } from './controllers/auth.controller';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type { DrizzleClient } from '../../types/drizzle';\n\ntype EncryptionKeyChoice =\n | 'env'\n | Omit<Provider, 'provide'>;\n\ntype OAuthStateStoreChoice =\n | 'memory'\n | 'drizzle'\n | Omit<Provider, 'provide'>;\n\nexport interface AuthModuleOptions {\n /** `'env'` (default) or a full provider definition (e.g. `{ useClass: MyKmsEncryptionKey }`). */\n encryptionKey?: EncryptionKeyChoice;\n /**\n * `'memory'` (default — tests/dev) or `'drizzle'` (prod, requires DRIZZLE\n * provider) or a full provider definition for a custom impl.\n */\n oauthStateStore?: OAuthStateStoreChoice;\n /**\n * Mount `AuthController` (`/auth/:provider/connect` + `/callback`).\n * Default `false` — apps that hand-roll connect/callback (rare) or that\n * use the subsystem only for the refresh path can opt out.\n */\n enableController?: boolean;\n /**\n * Public base URL of the API server. Used to construct per-provider\n * callback URIs as `${redirectUriBase}/auth/:provider/callback`.\n * Required when `enableController: true`.\n */\n redirectUriBase?: string;\n}\n\nfunction resolveEncryptionKeyProvider(choice: EncryptionKeyChoice): Provider {\n if (choice === 'env') {\n return { provide: ENCRYPTION_KEY, useClass: EnvEncryptionKey };\n }\n return { provide: ENCRYPTION_KEY, ...choice } as Provider;\n}\n\nfunction resolveOAuthStateStoreProvider(\n choice: OAuthStateStoreChoice,\n): Provider {\n if (choice === 'memory') {\n return { provide: OAUTH_STATE_STORE, useClass: MemoryOAuthStateStore };\n }\n if (choice === 'drizzle') {\n return {\n provide: OAUTH_STATE_STORE,\n useFactory: (db: DrizzleClient | null) => {\n if (!db) {\n throw new Error(\n \"AuthModule.forRoot: oauthStateStore: 'drizzle' selected but DRIZZLE provider is not available. \" +\n 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before AuthModule.forRoot.',\n );\n }\n return new DrizzleOAuthStateStore(db);\n },\n inject: [{ token: DRIZZLE, optional: true }],\n };\n }\n return { provide: OAUTH_STATE_STORE, ...choice } as Provider;\n}\n\n@Module({})\nexport class AuthModule {\n static forRoot(options: AuthModuleOptions = {}): DynamicModule {\n const resolved: AuthModuleOptions = {\n encryptionKey: options.encryptionKey ?? 'env',\n oauthStateStore: options.oauthStateStore ?? 'memory',\n enableController: options.enableController ?? false,\n redirectUriBase: options.redirectUriBase,\n };\n\n if (resolved.enableController && !resolved.redirectUriBase) {\n throw new Error(\n 'AuthModule.forRoot: redirectUriBase is required when enableController: true',\n );\n }\n\n const encryptionKeyProvider = resolveEncryptionKeyProvider(\n resolved.encryptionKey!,\n );\n const oauthStateStoreProvider = resolveOAuthStateStoreProvider(\n resolved.oauthStateStore!,\n );\n const optionsProvider: Provider = {\n provide: AUTH_OPTIONS,\n useValue: resolved,\n };\n\n return {\n module: AuthModule,\n global: true,\n providers: [encryptionKeyProvider, oauthStateStoreProvider, optionsProvider],\n controllers: resolved.enableController ? [AuthController] : [],\n exports: [ENCRYPTION_KEY, OAUTH_STATE_STORE, AUTH_OPTIONS],\n };\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n"],"mappings":"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","names":["randomBytes","randomBytes","randomBytes","randomBytes"]}
|