@pattern-stack/codegen 0.6.4 → 0.6.6

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -0
  3. package/dist/runtime/subsystems/auth/auth-oauth-state.schema.d.ts +81 -0
  4. package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js +12 -0
  5. package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js.map +1 -0
  6. package/dist/runtime/subsystems/auth/auth.module.d.ts +39 -24
  7. package/dist/runtime/subsystems/auth/auth.module.js +247 -14
  8. package/dist/runtime/subsystems/auth/auth.module.js.map +1 -1
  9. package/dist/runtime/subsystems/auth/auth.tokens.d.ts +15 -2
  10. package/dist/runtime/subsystems/auth/auth.tokens.js +9 -1
  11. package/dist/runtime/subsystems/auth/auth.tokens.js.map +1 -1
  12. package/dist/runtime/subsystems/auth/backends/encryption-key/env.d.ts +1 -1
  13. package/dist/runtime/subsystems/auth/backends/encryption-key/env.js +1 -1
  14. package/dist/runtime/subsystems/auth/backends/encryption-key/env.js.map +1 -1
  15. package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.d.ts +23 -0
  16. package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js +68 -0
  17. package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js.map +1 -0
  18. package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.d.ts +21 -0
  19. package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.js +51 -0
  20. package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.js.map +1 -0
  21. package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +31 -0
  22. package/dist/runtime/subsystems/auth/controllers/auth.controller.js +137 -0
  23. package/dist/runtime/subsystems/auth/controllers/auth.controller.js.map +1 -0
  24. package/dist/runtime/subsystems/auth/index.d.ts +13 -4
  25. package/dist/runtime/subsystems/auth/index.js +254 -15
  26. package/dist/runtime/subsystems/auth/index.js.map +1 -1
  27. package/dist/runtime/subsystems/auth/protocols/auth-strategy.d.ts +1 -1
  28. package/dist/runtime/subsystems/auth/protocols/integration-store.d.ts +37 -2
  29. package/dist/runtime/subsystems/auth/protocols/oauth-state-store.d.ts +33 -7
  30. package/dist/runtime/subsystems/auth/protocols/oauth-state-store.js +12 -0
  31. package/dist/runtime/subsystems/auth/protocols/oauth-state-store.js.map +1 -1
  32. package/dist/runtime/subsystems/auth/protocols/provider-strategy.d.ts +54 -0
  33. package/dist/runtime/subsystems/auth/protocols/provider-strategy.js +1 -0
  34. package/dist/runtime/subsystems/auth/protocols/provider-strategy.js.map +1 -0
  35. package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +24 -0
  36. package/dist/runtime/subsystems/auth/protocols/user-context.js +1 -0
  37. package/dist/runtime/subsystems/auth/protocols/user-context.js.map +1 -0
  38. package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.d.ts +2 -2
  39. package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.js.map +1 -1
  40. package/dist/runtime/subsystems/auth/runtime/session-expired.error.d.ts +2 -2
  41. package/dist/runtime/subsystems/auth/runtime/session-expired.error.js.map +1 -1
  42. package/dist/runtime/subsystems/auth/runtime/with-auth-retry.d.ts +1 -1
  43. package/dist/runtime/subsystems/auth/runtime/with-auth-retry.js.map +1 -1
  44. package/dist/runtime/subsystems/index.d.ts +9 -4
  45. package/dist/runtime/subsystems/index.js +248 -15
  46. package/dist/runtime/subsystems/index.js.map +1 -1
  47. package/dist/runtime/subsystems/sync/deep-equal.differ.js.map +1 -1
  48. package/dist/runtime/subsystems/sync/execute-sync.use-case.js.map +1 -1
  49. package/dist/runtime/subsystems/sync/index.js.map +1 -1
  50. package/dist/runtime/subsystems/sync/sync-change-source.protocol.d.ts +1 -1
  51. package/dist/runtime/subsystems/sync/sync-cursor-store.memory-backend.js.map +1 -1
  52. package/dist/runtime/subsystems/sync/sync-loopback.protocol.d.ts +3 -4
  53. package/dist/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.js.map +1 -1
  54. package/dist/runtime/subsystems/sync/sync.module.js.map +1 -1
  55. package/dist/src/cli/index.js +574 -142
  56. package/dist/src/cli/index.js.map +1 -1
  57. package/dist/src/index.js.map +1 -1
  58. package/package.json +1 -1
  59. package/runtime/subsystems/auth/auth-oauth-state.schema.ts +30 -0
  60. package/runtime/subsystems/auth/auth.module.ts +89 -32
  61. package/runtime/subsystems/auth/auth.tokens.ts +14 -1
  62. package/runtime/subsystems/auth/backends/encryption-key/env.ts +3 -3
  63. package/runtime/subsystems/auth/backends/state-store.drizzle-backend.ts +83 -0
  64. package/runtime/subsystems/auth/backends/state-store.memory-backend.ts +76 -0
  65. package/runtime/subsystems/auth/controllers/auth.controller.ts +155 -0
  66. package/runtime/subsystems/auth/index.ts +43 -4
  67. package/runtime/subsystems/auth/protocols/auth-strategy.ts +1 -1
  68. package/runtime/subsystems/auth/protocols/integration-store.ts +38 -1
  69. package/runtime/subsystems/auth/protocols/oauth-state-store.ts +38 -6
  70. package/runtime/subsystems/auth/protocols/provider-strategy.ts +48 -0
  71. package/runtime/subsystems/auth/protocols/user-context.ts +22 -0
  72. package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +2 -2
  73. package/runtime/subsystems/auth/runtime/session-expired.error.ts +2 -2
  74. package/runtime/subsystems/auth/runtime/with-auth-retry.ts +1 -1
  75. package/runtime/subsystems/index.ts +17 -2
  76. package/runtime/subsystems/sync/deep-equal.differ.ts +1 -1
  77. package/runtime/subsystems/sync/execute-sync.use-case.ts +1 -1
  78. package/runtime/subsystems/sync/sync-change-source.protocol.ts +1 -1
  79. package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +1 -1
  80. package/runtime/subsystems/sync/sync-loopback.protocol.ts +3 -4
  81. package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +1 -1
  82. package/templates/subsystem/auth/app-module-hook.ejs.t +21 -0
  83. package/templates/subsystem/auth/auth-oauth-state.schema.ejs.t +35 -0
  84. package/templates/subsystem/auth/env-config.ejs.t +20 -0
  85. package/templates/subsystem/auth/prompt.js +46 -0
  86. package/templates/subsystem/auth-config/codegen-config-auth-block.ejs.t +20 -0
  87. package/templates/subsystem/auth-config/prompt.js +20 -0
  88. package/templates/subsystem/auth-integrations/app-module-hook.ejs.t +16 -0
  89. package/templates/subsystem/auth-integrations/prompt.js +23 -0
  90. package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.d.ts +0 -24
  91. package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.js +0 -24
  92. package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.js.map +0 -1
  93. package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +0 -42
@@ -6,19 +6,32 @@
6
6
  * ```typescript
7
7
  * import {
8
8
  * AuthModule,
9
+ * AuthController,
9
10
  * ENCRYPTION_KEY,
10
11
  * OAUTH_STATE_STORE,
11
12
  * AUTH_INTEGRATION_READER,
12
13
  * AUTH_INTEGRATION_TOKEN_WRITER,
14
+ * AUTH_INTEGRATION_GRANT_SINK,
15
+ * AUTH_USER_CONTEXT,
16
+ * STRATEGY_REGISTRY,
17
+ * AUTH_OPTIONS,
13
18
  * OAuth2RefreshStrategy,
14
19
  * withAuthRetry,
15
20
  * IntegrationBrokenError,
16
21
  * SessionExpiredError,
22
+ * OAuthStateError,
17
23
  * type IAuthStrategy,
18
24
  * type IEncryptionKey,
19
25
  * type IOAuthStateStore,
26
+ * type OAuthStateRecord,
20
27
  * type IIntegrationReader,
21
28
  * type IIntegrationTokenWriter,
29
+ * type IIntegrationGrantSink,
30
+ * type IntegrationGrantInput,
31
+ * type IUserContext,
32
+ * type IProviderStrategy,
33
+ * type ProviderStrategyRegistry,
34
+ * type ExchangedTokens,
22
35
  * } from '@pattern-stack/codegen/runtime/subsystems/auth';
23
36
  * ```
24
37
  */
@@ -32,14 +45,23 @@ export type {
32
45
  export type { IEncryptionKey } from './protocols/encryption-key';
33
46
  export type {
34
47
  IOAuthStateStore,
35
- OAuthStateEntry,
48
+ OAuthStateRecord,
36
49
  } from './protocols/oauth-state-store';
50
+ export { OAuthStateError } from './protocols/oauth-state-store';
37
51
  export type {
38
52
  DecryptedIntegration,
39
53
  IIntegrationReader,
40
54
  IIntegrationTokenWriter,
41
55
  IntegrationTokenUpdate,
56
+ IIntegrationGrantSink,
57
+ IntegrationGrantInput,
42
58
  } from './protocols/integration-store';
59
+ export type { IUserContext } from './protocols/user-context';
60
+ export type {
61
+ IProviderStrategy,
62
+ ProviderStrategyRegistry,
63
+ ExchangedTokens,
64
+ } from './protocols/provider-strategy';
43
65
 
44
66
  // Tokens
45
67
  export {
@@ -47,6 +69,10 @@ export {
47
69
  OAUTH_STATE_STORE,
48
70
  AUTH_INTEGRATION_READER,
49
71
  AUTH_INTEGRATION_TOKEN_WRITER,
72
+ AUTH_INTEGRATION_GRANT_SINK,
73
+ AUTH_USER_CONTEXT,
74
+ STRATEGY_REGISTRY,
75
+ AUTH_OPTIONS,
50
76
  } from './auth.tokens';
51
77
 
52
78
  // Runtime
@@ -63,15 +89,28 @@ export {
63
89
  isSessionExpiredError,
64
90
  } from './runtime/session-expired.error';
65
91
 
92
+ // Schema (drizzle backend)
93
+ export {
94
+ authOAuthState,
95
+ type AuthOAuthState,
96
+ } from './auth-oauth-state.schema';
97
+
66
98
  // Backends
67
99
  export {
68
100
  EnvEncryptionKey,
69
101
  type EnvEncryptionKeyOptions,
70
102
  } from './backends/encryption-key/env';
71
103
  export {
72
- InMemoryOAuthStateStore,
73
- type InMemoryOAuthStateStoreOptions,
74
- } from './backends/oauth-state-store/in-memory';
104
+ MemoryOAuthStateStore,
105
+ type MemoryOAuthStateStoreOptions,
106
+ } from './backends/state-store.memory-backend';
107
+ export {
108
+ DrizzleOAuthStateStore,
109
+ type DrizzleOAuthStateStoreOptions,
110
+ } from './backends/state-store.drizzle-backend';
111
+
112
+ // Controller
113
+ export { AuthController } from './controllers/auth.controller';
75
114
 
76
115
  // Module
77
116
  export { AuthModule, type AuthModuleOptions } from './auth.module';
@@ -7,7 +7,7 @@
7
7
  * `OAuth2RefreshStrategy` template-method base in `../runtime/`.
8
8
  *
9
9
  * See `docs/adrs/ADR-031-auth-subsystem.md` and
10
- * `docs/gate-1-auth-extraction-findings.md` (inbound from dealbrain-v2) for
10
+ * `docs/gate-1-auth-extraction-findings.md` (extraction-source findings) for
11
11
  * the rationale.
12
12
  */
13
13
 
@@ -6,7 +6,7 @@
6
6
  * those rows — consumers implement these narrow ports against whatever
7
7
  * `integrations` table their app uses.
8
8
  *
9
- * In dealbrain-v2 (the extraction source) both ports are satisfied by a
9
+ * In the extraction-source app both ports are satisfied by a
10
10
  * pair of thin adapters over `IntegrationService` + `RefreshIntegrationUseCase`.
11
11
  * The codegen-patterns `examples/auth-integrations/` starter (separate PR)
12
12
  * ships a canonical `integration.yaml` whose generated service + use case
@@ -64,3 +64,40 @@ export interface IntegrationTokenUpdate {
64
64
  export interface IIntegrationTokenWriter {
65
65
  persistRefresh(update: IntegrationTokenUpdate): Promise<void>;
66
66
  }
67
+
68
+ /**
69
+ * Grant-sink port — persists a freshly-minted OAuth2 grant from the
70
+ * authorize-code callback (i.e. the user just connected a new provider, or
71
+ * re-connected an existing one).
72
+ *
73
+ * `AuthController.callback` invokes this after `IProviderStrategy.exchangeCodeForTokens`.
74
+ * The subsystem itself never imports a concrete `IntegrationsService` — the
75
+ * consumer's `auth-integrations` starter (or any equivalent) adapts this
76
+ * port. Keeps the auth subsystem standalone: a non-codegen consumer can
77
+ * satisfy the port against its own integrations storage.
78
+ *
79
+ * Semantics:
80
+ * - Upserts on `(userId, provider)`. Repeated grants for the same pair
81
+ * replace the prior tokens (re-connect flow).
82
+ * - Implementations are responsible for encrypting tokens at rest.
83
+ * - `expiresAt` / `refreshToken` / `scope` / `externalAccountId` /
84
+ * `providerMetadata` are optional because not every provider supplies
85
+ * them (e.g. some providers omit `expires_in`; not every flow returns
86
+ * a refresh token on first grant).
87
+ */
88
+ export interface IntegrationGrantInput {
89
+ userId: string;
90
+ /** Provider slug — must match the strategy's `provider`. */
91
+ provider: string;
92
+ accessToken: string;
93
+ refreshToken?: string;
94
+ expiresAt?: Date;
95
+ scope?: string[];
96
+ externalAccountId?: string;
97
+ /** Provider-specific bag (SFDC `instance_url`, Google `sub`, …). */
98
+ providerMetadata?: Record<string, unknown>;
99
+ }
100
+
101
+ export interface IIntegrationGrantSink {
102
+ createOrUpdateFromOAuthGrant(input: IntegrationGrantInput): Promise<void>;
103
+ }
@@ -2,15 +2,47 @@
2
2
  * Auth subsystem — `IOAuthStateStore` port.
3
3
  *
4
4
  * CSRF protection for the OAuth2 authorize-code callback. Generic across
5
- * providers. Concrete backends live under `../backends/oauth-state-store/`.
5
+ * providers. The store mints opaque state tokens at /connect time and
6
+ * single-use consumes them at /callback time, returning the original
7
+ * record (userId + optional post-callback redirect path).
8
+ *
9
+ * Concrete backends live under `../backends/`:
10
+ * - `state-store.memory-backend.ts` — in-process Map (tests/dev).
11
+ * - `state-store.drizzle-backend.ts` — Postgres (prod).
12
+ *
13
+ * Semantics:
14
+ * - `generate(record)` → returns an opaque state token; record is stored
15
+ * under that token until consumed or until TTL expires.
16
+ * - `consume(state)` → atomically deletes the entry and returns the
17
+ * record. Throws on missing, expired, or replayed state. Never returns
18
+ * null — a missing/expired state is a CSRF signal.
6
19
  */
7
- export interface OAuthStateEntry {
20
+ export interface OAuthStateRecord {
8
21
  userId: string;
9
- createdAt: Date;
22
+ /** Optional post-callback redirect path (relative URL). */
23
+ redirect?: string;
10
24
  }
11
25
 
12
26
  export interface IOAuthStateStore {
13
- put(state: string, entry: OAuthStateEntry): Promise<void>;
14
- /** Single-use consume: returns entry if present + valid, deletes it. */
15
- consume(state: string): Promise<OAuthStateEntry | null>;
27
+ /** Mint an opaque state token bound to `record`. Single-use. */
28
+ generate(record: OAuthStateRecord): Promise<string>;
29
+ /**
30
+ * Atomically consume `state`, returning the bound record. Throws on
31
+ * missing / expired / replayed state.
32
+ */
33
+ consume(state: string): Promise<OAuthStateRecord>;
34
+ }
35
+
36
+ /**
37
+ * Thrown by `IOAuthStateStore.consume` when the state token is unknown,
38
+ * expired, or has already been consumed (replay attempt).
39
+ */
40
+ export class OAuthStateError extends Error {
41
+ constructor(
42
+ message: string,
43
+ public readonly reason: 'missing' | 'expired',
44
+ ) {
45
+ super(message);
46
+ this.name = 'OAuthStateError';
47
+ }
16
48
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Auth subsystem — `IProviderStrategy` contract.
3
+ *
4
+ * Extension of `OAuth2RefreshStrategy` (which already covers the refresh
5
+ * path) that adds the two methods needed by the connect/callback dance:
6
+ *
7
+ * - `buildAuthorizeUrl({ state, redirectUri })` → consent-page URL.
8
+ * - `exchangeCodeForTokens({ code, redirectUri })` → tokens after consent.
9
+ *
10
+ * Concrete per-provider strategies (HubSpot, SFDC, Google, Gong, Fathom, …)
11
+ * stay consumer-side per ADR-031 ("every app has different combinations").
12
+ * They typically subclass `OAuth2RefreshStrategy` for the refresh path and
13
+ * implement these two methods structurally — that satisfies
14
+ * `IProviderStrategy` because TS lets interfaces extend classes by type.
15
+ *
16
+ * AuthController never imports a concrete strategy — it injects the
17
+ * `STRATEGY_REGISTRY` (a `ReadonlyMap<provider-slug, IProviderStrategy>`)
18
+ * and dispatches by slug.
19
+ *
20
+ * **Naming convention:** interfaces that describe behavioral ports use the
21
+ * `I` prefix (`IProviderStrategy`, `IIntegrationReader`, `IUserContext`,
22
+ * `IOAuthStateStore`, `IEncryptionKey`). Plain data types / DTOs (e.g.
23
+ * `ExchangedTokens`, `DecryptedIntegration`, `IntegrationGrantInput`) do
24
+ * not. Abstract template-method classes (e.g. `OAuth2RefreshStrategy`) also
25
+ * do not — the `I` is for interfaces only.
26
+ */
27
+ import type { OAuth2RefreshStrategy } from '../runtime/oauth2-refresh.strategy';
28
+
29
+ export interface ExchangedTokens {
30
+ accessToken: string;
31
+ refreshToken?: string;
32
+ expiresAt?: Date;
33
+ scope?: string[];
34
+ externalAccountId?: string;
35
+ /** Provider-specific bag (SFDC `instance_url`, Google `sub`, …). */
36
+ providerMetadata?: Record<string, unknown>;
37
+ }
38
+
39
+ export interface IProviderStrategy extends OAuth2RefreshStrategy {
40
+ buildAuthorizeUrl(args: { state: string; redirectUri: string }): string;
41
+ exchangeCodeForTokens(args: {
42
+ code: string;
43
+ redirectUri: string;
44
+ }): Promise<ExchangedTokens>;
45
+ }
46
+
47
+ /** The DI value type behind the `STRATEGY_REGISTRY` token. */
48
+ export type ProviderStrategyRegistry = ReadonlyMap<string, IProviderStrategy>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Auth subsystem — `IUserContext` port.
3
+ *
4
+ * Resolves "who is the current user" from a request. The shape is
5
+ * universal; the implementation is always app-specific:
6
+ *
7
+ * - WorkOS session: `req.session.user.id`
8
+ * - JWT bearer: `decode(req.headers.authorization).sub`
9
+ * - Test fixture: hardcoded UUID
10
+ *
11
+ * The auth subsystem cannot ship a default — every app does auth differently —
12
+ * but the port is universal, so the contract ships here. Consumers bind a
13
+ * concrete implementation under the `AUTH_USER_CONTEXT` token in their app
14
+ * module.
15
+ *
16
+ * `req` is typed as `unknown` deliberately: this protocol must not pull a
17
+ * dependency on `express` / `fastify` / NestJS request types. The concrete
18
+ * adapter narrows it (e.g. via a `Request` import).
19
+ */
20
+ export interface IUserContext {
21
+ getCurrentUserId(req: unknown): Promise<string>;
22
+ }
@@ -2,8 +2,8 @@
2
2
  * Abstract base class for OAuth2 refresh-token strategies.
3
3
  *
4
4
  * Template-method pattern: `resolve()` is concrete; four small hooks inject
5
- * provider specifics. Validated across two providers in dealbrain-v2
6
- * (SalesforceAuthStrategy, HubSpotAuthStrategy) before extraction here — see
5
+ * provider specifics. Validated across two providers (Salesforce, HubSpot)
6
+ * in the extraction-source app before being extracted here — see
7
7
  * `docs/gate-1-auth-extraction-findings.md` for the "build first, extract
8
8
  * later" evidence.
9
9
  *
@@ -8,8 +8,8 @@
8
8
  * the `isSessionExpiredError` predicate to decide whether to force-refresh
9
9
  * and retry once.
10
10
  *
11
- * This discriminator replaces the SFDC-only `instanceof` check from
12
- * dealbrain-v2's original `withAuthRetry`. See
11
+ * This discriminator replaces the SFDC-only `instanceof` check from the
12
+ * extraction-source app's original `withAuthRetry`. See
13
13
  * `docs/gate-1-auth-extraction-findings.md` (recommendation 4).
14
14
  */
15
15
  export class SessionExpiredError extends Error {
@@ -6,7 +6,7 @@
6
6
  * on the refreshed token propagates rather than looping, so transient
7
7
  * adapter bugs can't hang the caller.
8
8
  *
9
- * Generalisation over dealbrain's original SFDC-specific version: the
9
+ * Generalisation over the extraction source's SFDC-specific original: the
10
10
  * session-expired classifier is injected. Providers mark their session-
11
11
  * expired errors (via `instanceof` of a marker class, or by setting a known
12
12
  * property) and pass a classifier matching that shape.
@@ -56,14 +56,22 @@ export {
56
56
  OAUTH_STATE_STORE,
57
57
  AUTH_INTEGRATION_READER,
58
58
  AUTH_INTEGRATION_TOKEN_WRITER,
59
+ AUTH_INTEGRATION_GRANT_SINK,
60
+ AUTH_USER_CONTEXT,
61
+ STRATEGY_REGISTRY,
62
+ AUTH_OPTIONS,
59
63
  AuthModule,
64
+ AuthController,
60
65
  OAuth2RefreshStrategy,
61
66
  withAuthRetry,
62
67
  IntegrationBrokenError,
63
68
  SessionExpiredError,
64
69
  isSessionExpiredError,
70
+ OAuthStateError,
65
71
  EnvEncryptionKey,
66
- InMemoryOAuthStateStore,
72
+ MemoryOAuthStateStore,
73
+ DrizzleOAuthStateStore,
74
+ authOAuthState,
67
75
  } from './auth';
68
76
  export type {
69
77
  IAuthStrategy,
@@ -71,10 +79,17 @@ export type {
71
79
  IOAuthStateStore,
72
80
  IIntegrationReader,
73
81
  IIntegrationTokenWriter,
82
+ IIntegrationGrantSink,
83
+ IntegrationGrantInput,
84
+ IUserContext,
85
+ IProviderStrategy,
86
+ ProviderStrategyRegistry,
87
+ ExchangedTokens,
74
88
  AuthCredentials,
75
89
  AuthResolveOptions,
76
90
  DecryptedIntegration,
77
- OAuthStateEntry,
91
+ OAuthStateRecord,
78
92
  IntegrationTokenUpdate,
79
93
  ParsedRefreshResponse,
94
+ AuthOAuthState,
80
95
  } from './auth';
@@ -5,7 +5,7 @@
5
5
  * per-field diff (`{ from, to }`) for every field whose value changed.
6
6
  * Returns `'noop'` when the record is unchanged.
7
7
  *
8
- * Design decisions (extracted from dealbrain-v2 + HS-9 findings):
8
+ * Design decisions (extracted from the upstream consumer + HS-9 findings):
9
9
  *
10
10
  * 1. **Ignore list** — row metadata that sinks/services stamp unconditionally
11
11
  * so upstream cannot reasonably disagree:
@@ -41,7 +41,7 @@
41
41
  * is strictly provider-agnostic:
42
42
  * - `entityType` is `string` throughout; no `'opportunity' | 'account' | ...`
43
43
  * narrowing leaks into the use case
44
- * - dealbrain's `SyncRunRecorderService` class injection replaced with the
44
+ * - the upstream consumer's `SyncRunRecorderService` class injection replaced with the
45
45
  * `ISyncRunRecorder` protocol (backend lands in SYNC-4)
46
46
  */
47
47
  import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
@@ -6,7 +6,7 @@
6
6
  * depend on a specific backend implementation.
7
7
  *
8
8
  * Three detection modes (poll / cdc / webhook) converge on this single port
9
- * per ADR-0002 (dealbrain-v2). Per-mode differences live in the
9
+ * per ADR-0002 (the upstream consumer). Per-mode differences live in the
10
10
  * `Change.source` / `dedupKey` / `providerChangedFields` metadata fields,
11
11
  * not in separate ports.
12
12
  *
@@ -21,7 +21,7 @@
21
21
  * could occur. Tests that want to assert per-tenant isolation should
22
22
  * target the Drizzle backend.
23
23
  *
24
- * Not shipped in dealbrain-v2; this is a subsystem-first addition for the
24
+ * Not shipped in the upstream consumer; this is a subsystem-first addition for the
25
25
  * test surface. Consumed by:
26
26
  * - SYNC-5 unit tests (`ExecuteSyncUseCase` against synthetic sources)
27
27
  * - SYNC-6 module tests (`SyncModule.forRoot({ backend: 'memory' })`)
@@ -14,10 +14,9 @@
14
14
  * a backend in Phase 1; consumers that need loopback suppression provide
15
15
  * their own (redis-hashed, memory TTL, etc.).
16
16
  *
17
- * `entityType` is `string` (not a union) — per dealbrain-v2's HS-9 findings
18
- * the CRM-specific narrowing `'opportunity' | 'account' | 'contact'` bled
19
- * into the port and had to be removed. Consumers narrow internally if they
20
- * want.
17
+ * `entityType` is `string` (not a union) — per HS-9 findings the
18
+ * CRM-specific narrowing `'opportunity' | 'account' | 'contact'` bled into
19
+ * the port and had to be removed. Consumers narrow internally if they want.
21
20
  */
22
21
 
23
22
  export interface ILoopbackFingerprintStore<T = unknown> {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * DrizzleSyncRunRecorder — Drizzle-backed `ISyncRunRecorder` (SYNC-4).
3
3
  *
4
- * Generic write path only — extracted from dealbrain-v2's
4
+ * Generic write path only — extracted from the source app's
5
5
  * `SyncRunRecorderService`, minus CRM-specific convenience methods. Those
6
6
  * stay consumer-owned; the subsystem ships the substrate.
7
7
  *
@@ -0,0 +1,21 @@
1
+ ---
2
+ to: "<%= appModulePath %>"
3
+ inject: true
4
+ append: true
5
+ skip_if: "AuthModule"
6
+ ---
7
+
8
+ // TODO: Register AuthModule (auth subsystem)
9
+ // Add to AppModule.imports:
10
+ //
11
+ // import { AuthModule } from '@shared/subsystems/auth';
12
+ // // ...
13
+ // AuthModule.forRoot({
14
+ // encryptionKey: 'env',
15
+ // oauthStateStore: 'drizzle',
16
+ // enableController: true,
17
+ // redirectUriBase: process.env.AUTH_REDIRECT_URI_BASE ?? '<%= redirectUriBase %>',
18
+ // }),
19
+ //
20
+ // Requires INTEGRATION_TOKEN_ENCRYPTION_KEY in your environment (see .env.config).
21
+ // Provide an IUserContext adapter (your app's session/JWT scheme).
@@ -0,0 +1,35 @@
1
+ ---
2
+ to: "<%= schemaPath %>"
3
+ force: true
4
+ ---
5
+ // Schema barrel append tracked in #284 — same gap as events/jobs/sync today.
6
+ /**
7
+ * Drizzle schema for the `auth_oauth_state` table — backs the
8
+ * `DrizzleOAuthStateStore` (`state-store.drizzle-backend.ts`).
9
+ *
10
+ * One row per outstanding /connect → /callback dance. Single-use; rows are
11
+ * deleted on consume. A periodic sweep (or a `WHERE expires_at < now()`
12
+ * filter on read) clears abandoned rows.
13
+ *
14
+ * Columns:
15
+ * - `state` — opaque random token, primary key.
16
+ * - `user_id` — text (matches the consumer-defined user-id shape;
17
+ * the auth subsystem doesn't constrain this to UUID
18
+ * because some apps key users by external id).
19
+ * - `redirect` — optional post-callback redirect path.
20
+ * - `expires_at` — TTL boundary; entries past this are treated as absent.
21
+ *
22
+ * Convention: schema files live at the root of the subsystem dir
23
+ * (mirrors `cache.schema.ts`, `sync-audit.schema.ts`, `domain-events.schema.ts`).
24
+ */
25
+ import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
26
+ import type { InferSelectModel } from 'drizzle-orm';
27
+
28
+ export const authOAuthState = pgTable('auth_oauth_state', {
29
+ state: text('state').primaryKey(),
30
+ userId: text('user_id').notNull(),
31
+ redirect: text('redirect'),
32
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
33
+ });
34
+
35
+ export type AuthOAuthState = InferSelectModel<typeof authOAuthState>;
@@ -0,0 +1,20 @@
1
+ ---
2
+ to: "<%= envConfigPath %>"
3
+ inject: true
4
+ append: true
5
+ skip_if: "INTEGRATION_TOKEN_ENCRYPTION_KEY"
6
+ ---
7
+
8
+ # OAuth integration token encryption key — 32 bytes base64 (AES-256-GCM).
9
+ # DO NOT REUSE this dev value in prod. To regenerate locally:
10
+ # node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
11
+ # Re-running `codegen subsystem install auth` does NOT rotate this key —
12
+ # `skip_if: "INTEGRATION_TOKEN_ENCRYPTION_KEY"` blocks the inject intentionally,
13
+ # because silently rotating would invalidate every encrypted token already in
14
+ # the consumer's `integrations` table. Rotation is a separate, auditable op.
15
+ # In production: store the key in `secrets/secrets.yaml` (or your secret
16
+ # manager) and have your deploy pipeline materialise it into the env at
17
+ # boot — this `.env.config` line should be a placeholder, not the source.
18
+ INTEGRATION_TOKEN_ENCRYPTION_KEY=<%= tokenEncryptionKey %>
19
+ # Public base URL where OAuth providers redirect back. Override in staging/prod.
20
+ AUTH_REDIRECT_URI_BASE=<%= redirectUriBase %>
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Hygen prompt.js — #287 auth subsystem scaffold.
3
+ *
4
+ * Locals are resolved by the CLI (src/cli/shared/auth-scaffold-locals.ts)
5
+ * and forwarded as CLI args. This prompt.js just coerces and forwards;
6
+ * no interactive prompts.
7
+ *
8
+ * Invoked via:
9
+ * bunx hygen subsystem auth \
10
+ * --appName <string> \
11
+ * --configPath <abs> \
12
+ * --schemaPath <abs> \
13
+ * --appModulePath <abs> \
14
+ * --envConfigPath <abs> \
15
+ * --redirectUriBase <url> \
16
+ * --tokenEncryptionKey <b64-32-bytes>
17
+ *
18
+ * The three templates this folder steers:
19
+ * - `auth-oauth-state.schema.ejs.t` — emits the `auth_oauth_state`
20
+ * drizzle schema (sole emitter; copyRuntime skips the runtime source).
21
+ * - `app-module-hook.ejs.t` — appends a TODO comment block to
22
+ * app.module.ts directing the human to register
23
+ * `AuthModule.forRoot({ ... })`. Same convention as observability.
24
+ * - `env-config.ejs.t` — appends `INTEGRATION_TOKEN_ENCRYPTION_KEY=<b64>`
25
+ * and `AUTH_REDIRECT_URI_BASE=<url>` to `.env.config`. Idempotent via
26
+ * `skip_if: "INTEGRATION_TOKEN_ENCRYPTION_KEY"` — re-running install does NOT
27
+ * regenerate the key (rotation is a separate operation).
28
+ *
29
+ * Auth has NO `multi_tenant` knob (see auth-scaffold-locals.ts docstring).
30
+ */
31
+
32
+ export default {
33
+ prompt: async ({ args }) => {
34
+ return {
35
+ appName: args.appName ?? "",
36
+ configPath: args.configPath ?? "codegen.config.yaml",
37
+ schemaPath:
38
+ args.schemaPath ??
39
+ "src/shared/subsystems/auth/auth-oauth-state.schema.ts",
40
+ appModulePath: args.appModulePath ?? "src/app.module.ts",
41
+ envConfigPath: args.envConfigPath ?? ".env.config",
42
+ redirectUriBase: args.redirectUriBase ?? "http://localhost:3000",
43
+ tokenEncryptionKey: args.tokenEncryptionKey ?? "",
44
+ };
45
+ },
46
+ };
@@ -0,0 +1,20 @@
1
+ ---
2
+ to: "<%= configPath %>"
3
+ inject: true
4
+ append: true
5
+ skip_if: "auth:"
6
+ ---
7
+
8
+ auth:
9
+ # ── Encryption key (INTEGRATION_TOKEN_ENCRYPTION_KEY from .env.config) ──
10
+ encryption_key: env
11
+
12
+ # ── OAuth state store (drizzle for prod, memory for tests) ──
13
+ oauth_state_store: drizzle
14
+
15
+ # ── Public base URL — providers redirect back here ──
16
+ # Override in staging/prod via AUTH_REDIRECT_URI_BASE env var.
17
+ redirect_uri_base: http://localhost:3000
18
+
19
+ # ── Mount AuthController under /auth/:provider ──
20
+ enable_controller: true
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Hygen prompt.js — #287 auth config-block scaffold.
3
+ *
4
+ * Split from `templates/subsystem/auth/` so the CLI can invoke the
5
+ * config-block inject step independently — `subsystem install auth --force`
6
+ * preserves an existing `auth:` block by skipping this action; pass
7
+ * `--force-config` to opt into regeneration. Mirrors `events-config` /
8
+ * `sync-config` / `observability-config` exactly.
9
+ *
10
+ * Invoked via:
11
+ * bunx hygen subsystem auth-config --configPath <abs>
12
+ */
13
+
14
+ export default {
15
+ prompt: async ({ args }) => {
16
+ return {
17
+ configPath: args.configPath ?? "codegen.config.yaml",
18
+ };
19
+ },
20
+ };
@@ -0,0 +1,16 @@
1
+ ---
2
+ to: "<%= appModulePath %>"
3
+ inject: true
4
+ append: true
5
+ skip_if: "IntegrationsAuthModule"
6
+ ---
7
+
8
+ // TODO: Register IntegrationsAuthModule (vendored from auth-integrations starter)
9
+ // Add to AppModule.imports AFTER AuthModule:
10
+ //
11
+ // import { IntegrationsAuthModule } from '@shared/integrations/integrations-auth.module';
12
+ // // ...
13
+ // IntegrationsAuthModule,
14
+ //
15
+ // Requires AuthModule.forRoot(...) registered first (provides ENCRYPTION_KEY).
16
+ // Run `cdp entity new integration` to scaffold the codegen layer the adapters import.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Hygen prompt.js — #287 auth-integrations starter scaffold.
3
+ *
4
+ * Locals are resolved by the CLI
5
+ * (src/cli/shared/auth-integrations-scaffold-locals.ts) and forwarded as
6
+ * CLI args. The vendor copies (runtime/integrations/** + integration.yaml)
7
+ * happen in subsystem.ts (`runAuthIntegrationsScaffold`); this template
8
+ * folder only injects the TODO comment block into app.module.ts.
9
+ *
10
+ * Invoked via:
11
+ * bunx hygen subsystem auth-integrations \
12
+ * --appName <string> \
13
+ * --appModulePath <abs>
14
+ */
15
+
16
+ export default {
17
+ prompt: async ({ args }) => {
18
+ return {
19
+ appName: args.appName ?? "",
20
+ appModulePath: args.appModulePath ?? "src/app.module.ts",
21
+ };
22
+ },
23
+ };