@pattern-stack/codegen 0.4.1 → 0.4.2

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 (133) hide show
  1. package/package.json +2 -1
  2. package/runtime/analytics/index.ts +31 -0
  3. package/runtime/analytics/metrics.ts +85 -0
  4. package/runtime/analytics/packs/crm-entity-measures.ts +20 -0
  5. package/runtime/analytics/packs/index.ts +5 -0
  6. package/runtime/analytics/packs/monetary-measures.ts +20 -0
  7. package/runtime/analytics/specs.ts +54 -0
  8. package/runtime/analytics/types.ts +105 -0
  9. package/runtime/base-classes/activity-entity-repository.ts +50 -0
  10. package/runtime/base-classes/activity-entity-service.ts +48 -0
  11. package/runtime/base-classes/base-read-use-cases.ts +88 -0
  12. package/runtime/base-classes/base-repository.ts +289 -0
  13. package/runtime/base-classes/base-service.ts +183 -0
  14. package/runtime/base-classes/index.ts +38 -0
  15. package/runtime/base-classes/knowledge-entity-repository.ts +12 -0
  16. package/runtime/base-classes/knowledge-entity-service.ts +14 -0
  17. package/runtime/base-classes/lifecycle-events.ts +152 -0
  18. package/runtime/base-classes/metadata-entity-repository.ts +80 -0
  19. package/runtime/base-classes/metadata-entity-service.ts +48 -0
  20. package/runtime/base-classes/synced-entity-repository.ts +57 -0
  21. package/runtime/base-classes/synced-entity-service.ts +50 -0
  22. package/runtime/base-classes/with-analytics.ts +22 -0
  23. package/runtime/constants/tokens.ts +29 -0
  24. package/runtime/eav-helpers.ts +74 -0
  25. package/runtime/pipes/zod-validation.pipe.ts +64 -0
  26. package/runtime/shared/openapi/error-response.dto.ts +24 -0
  27. package/runtime/shared/openapi/errors.ts +39 -0
  28. package/runtime/shared/openapi/index.ts +20 -0
  29. package/runtime/shared/openapi/registry.tokens.ts +13 -0
  30. package/runtime/shared/openapi/registry.ts +151 -0
  31. package/runtime/subsystems/analytics/analytics-query.protocol.ts +37 -0
  32. package/runtime/subsystems/analytics/analytics.module.ts +64 -0
  33. package/runtime/subsystems/analytics/analytics.tokens.ts +24 -0
  34. package/runtime/subsystems/analytics/cube-backend.ts +75 -0
  35. package/runtime/subsystems/analytics/index.ts +15 -0
  36. package/runtime/subsystems/analytics/noop-backend.ts +27 -0
  37. package/runtime/subsystems/auth/auth.module.ts +91 -0
  38. package/runtime/subsystems/auth/auth.tokens.ts +27 -0
  39. package/runtime/subsystems/auth/backends/encryption-key/env.ts +76 -0
  40. package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +42 -0
  41. package/runtime/subsystems/auth/index.ts +77 -0
  42. package/runtime/subsystems/auth/protocols/auth-strategy.ts +46 -0
  43. package/runtime/subsystems/auth/protocols/encryption-key.ts +21 -0
  44. package/runtime/subsystems/auth/protocols/integration-store.ts +66 -0
  45. package/runtime/subsystems/auth/protocols/oauth-state-store.ts +16 -0
  46. package/runtime/subsystems/auth/runtime/integration-broken.error.ts +21 -0
  47. package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +189 -0
  48. package/runtime/subsystems/auth/runtime/session-expired.error.ts +39 -0
  49. package/runtime/subsystems/auth/runtime/with-auth-retry.ts +50 -0
  50. package/runtime/subsystems/bridge/assert-tenant-id.ts +57 -0
  51. package/runtime/subsystems/bridge/bridge-delivery-handler.ts +220 -0
  52. package/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts +149 -0
  53. package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +140 -0
  54. package/runtime/subsystems/bridge/bridge-delivery.schema.ts +142 -0
  55. package/runtime/subsystems/bridge/bridge-errors.ts +112 -0
  56. package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +175 -0
  57. package/runtime/subsystems/bridge/bridge.module.ts +160 -0
  58. package/runtime/subsystems/bridge/bridge.protocol.ts +351 -0
  59. package/runtime/subsystems/bridge/bridge.tokens.ts +68 -0
  60. package/runtime/subsystems/bridge/event-flow.service.ts +175 -0
  61. package/runtime/subsystems/bridge/generated/.gitkeep +0 -0
  62. package/runtime/subsystems/bridge/generated/registry.ts +6 -0
  63. package/runtime/subsystems/bridge/index.ts +84 -0
  64. package/runtime/subsystems/bridge/reserved-pools.ts +36 -0
  65. package/runtime/subsystems/cache/cache.drizzle-backend.ts +150 -0
  66. package/runtime/subsystems/cache/cache.memory-backend.ts +116 -0
  67. package/runtime/subsystems/cache/cache.module.ts +115 -0
  68. package/runtime/subsystems/cache/cache.protocol.ts +45 -0
  69. package/runtime/subsystems/cache/cache.schema.ts +27 -0
  70. package/runtime/subsystems/cache/cache.tokens.ts +17 -0
  71. package/runtime/subsystems/cache/index.ts +22 -0
  72. package/runtime/subsystems/events/domain-events.schema.ts +77 -0
  73. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +327 -0
  74. package/runtime/subsystems/events/event-bus.memory-backend.ts +142 -0
  75. package/runtime/subsystems/events/event-bus.protocol.ts +86 -0
  76. package/runtime/subsystems/events/event-bus.redis-backend.ts +304 -0
  77. package/runtime/subsystems/events/events-errors.ts +30 -0
  78. package/runtime/subsystems/events/events.module.ts +230 -0
  79. package/runtime/subsystems/events/events.tokens.ts +62 -0
  80. package/runtime/subsystems/events/generated/bus.ts +103 -0
  81. package/runtime/subsystems/events/generated/index.ts +7 -0
  82. package/runtime/subsystems/events/generated/registry.ts +84 -0
  83. package/runtime/subsystems/events/generated/schemas.ts +59 -0
  84. package/runtime/subsystems/events/generated/types.ts +94 -0
  85. package/runtime/subsystems/events/index.ts +21 -0
  86. package/runtime/subsystems/index.ts +63 -0
  87. package/runtime/subsystems/jobs/generated/job-orchestration.schema.multi-tenant.ts +217 -0
  88. package/runtime/subsystems/jobs/generated/job-orchestration.schema.single-tenant.ts +217 -0
  89. package/runtime/subsystems/jobs/generated/scope-entity-type.ts +10 -0
  90. package/runtime/subsystems/jobs/index.ts +120 -0
  91. package/runtime/subsystems/jobs/job-handler.base.ts +206 -0
  92. package/runtime/subsystems/jobs/job-orchestration.schema.ts +217 -0
  93. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +536 -0
  94. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +850 -0
  95. package/runtime/subsystems/jobs/job-orchestrator.protocol.ts +179 -0
  96. package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +171 -0
  97. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +165 -0
  98. package/runtime/subsystems/jobs/job-run-service.protocol.ts +79 -0
  99. package/runtime/subsystems/jobs/job-step-service.drizzle-backend.ts +66 -0
  100. package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +119 -0
  101. package/runtime/subsystems/jobs/job-step-service.protocol.ts +53 -0
  102. package/runtime/subsystems/jobs/job-worker.module.ts +302 -0
  103. package/runtime/subsystems/jobs/job-worker.ts +615 -0
  104. package/runtime/subsystems/jobs/jobs-domain.module.ts +119 -0
  105. package/runtime/subsystems/jobs/jobs-domain.tokens.ts +30 -0
  106. package/runtime/subsystems/jobs/jobs-errors.ts +150 -0
  107. package/runtime/subsystems/jobs/memory-job-store.ts +35 -0
  108. package/runtime/subsystems/jobs/pool-config.loader.ts +218 -0
  109. package/runtime/subsystems/storage/index.ts +18 -0
  110. package/runtime/subsystems/storage/storage.local-backend.ts +113 -0
  111. package/runtime/subsystems/storage/storage.memory-backend.ts +78 -0
  112. package/runtime/subsystems/storage/storage.module.ts +60 -0
  113. package/runtime/subsystems/storage/storage.protocol.ts +78 -0
  114. package/runtime/subsystems/storage/storage.tokens.ts +9 -0
  115. package/runtime/subsystems/storage/storage.utils.ts +20 -0
  116. package/runtime/subsystems/sync/deep-equal.differ.ts +198 -0
  117. package/runtime/subsystems/sync/execute-sync.use-case.ts +334 -0
  118. package/runtime/subsystems/sync/index.ts +98 -0
  119. package/runtime/subsystems/sync/sync-audit.schema.ts +300 -0
  120. package/runtime/subsystems/sync/sync-change-source.protocol.ts +99 -0
  121. package/runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts +104 -0
  122. package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +64 -0
  123. package/runtime/subsystems/sync/sync-cursor-store.protocol.ts +53 -0
  124. package/runtime/subsystems/sync/sync-errors.ts +54 -0
  125. package/runtime/subsystems/sync/sync-field-diff.protocol.ts +61 -0
  126. package/runtime/subsystems/sync/sync-loopback.protocol.ts +33 -0
  127. package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +123 -0
  128. package/runtime/subsystems/sync/sync-run-recorder.memory-backend.ts +143 -0
  129. package/runtime/subsystems/sync/sync-run-recorder.protocol.ts +86 -0
  130. package/runtime/subsystems/sync/sync-sink.protocol.ts +55 -0
  131. package/runtime/subsystems/sync/sync.module.ts +156 -0
  132. package/runtime/subsystems/sync/sync.tokens.ts +57 -0
  133. package/runtime/types/drizzle.ts +23 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Analytics subsystem — protocol (port)
3
+ *
4
+ * IAnalyticsQuery is the hexagonal port. Services inject this interface via
5
+ * the ANALYTICS_QUERY token. They never depend on a specific backend
6
+ * implementation (cube.js, noop, etc.).
7
+ */
8
+
9
+ export interface ResultRow {
10
+ [key: string]: any;
11
+ }
12
+
13
+ export interface AnalyticsQueryOpts {
14
+ /** Include raw entity IDs in the result set. */
15
+ withIds?: boolean;
16
+ /** Maximum number of rows to return. */
17
+ limit?: number;
18
+ }
19
+
20
+ export interface IAnalyticsQuery {
21
+ /**
22
+ * Execute an analytics query against the semantic layer.
23
+ *
24
+ * @param cube - Cube name (e.g., 'Orders')
25
+ * @param measures - Measure names (e.g., ['totalRevenue'])
26
+ * @param dimensions - Dimension names (e.g., ['status', 'createdAt'])
27
+ * @param where - Optional filter conditions
28
+ * @param opts - Query options (limit, withIds)
29
+ */
30
+ execute(
31
+ cube: string,
32
+ measures: string[],
33
+ dimensions: string[],
34
+ where?: Record<string, any>,
35
+ opts?: AnalyticsQueryOpts,
36
+ ): Promise<ResultRow[]>;
37
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * AnalyticsModule — DynamicModule factory for the analytics query subsystem.
3
+ *
4
+ * Register once in AppModule:
5
+ * ```typescript
6
+ * @Module({
7
+ * imports: [
8
+ * AnalyticsModule.forRoot({ backend: 'cube' }),
9
+ * ],
10
+ * })
11
+ * export class AppModule {}
12
+ * ```
13
+ *
14
+ * Tests swap to the noop backend without touching application code:
15
+ * ```typescript
16
+ * Test.createTestingModule({
17
+ * imports: [AnalyticsModule.forRoot({ backend: 'noop' })],
18
+ * });
19
+ * ```
20
+ *
21
+ * `global: true` means entity modules do not need to import AnalyticsModule
22
+ * individually — the ANALYTICS_QUERY token is available project-wide.
23
+ */
24
+ import { Module, type DynamicModule } from '@nestjs/common';
25
+ import { ANALYTICS_QUERY, CUBE_API_URL, CUBE_API_SECRET } from './analytics.tokens';
26
+ import { CubeAnalyticsBackend } from './cube-backend';
27
+ import { NoopAnalyticsBackend } from './noop-backend';
28
+
29
+ export interface AnalyticsModuleOptions {
30
+ backend: 'cube' | 'noop';
31
+ }
32
+
33
+ @Module({})
34
+ export class AnalyticsModule {
35
+ static forRoot(
36
+ options: AnalyticsModuleOptions = { backend: 'noop' },
37
+ ): DynamicModule {
38
+ if (options.backend === 'cube') {
39
+ return {
40
+ module: AnalyticsModule,
41
+ global: true,
42
+ providers: [
43
+ {
44
+ provide: CUBE_API_URL,
45
+ useValue: process.env['CUBE_API_URL'] ?? 'http://localhost:4000/cubejs-api/v1',
46
+ },
47
+ {
48
+ provide: CUBE_API_SECRET,
49
+ useValue: process.env['CUBE_API_SECRET'] ?? '',
50
+ },
51
+ { provide: ANALYTICS_QUERY, useClass: CubeAnalyticsBackend },
52
+ ],
53
+ exports: [ANALYTICS_QUERY],
54
+ };
55
+ }
56
+
57
+ return {
58
+ module: AnalyticsModule,
59
+ global: true,
60
+ providers: [{ provide: ANALYTICS_QUERY, useClass: NoopAnalyticsBackend }],
61
+ exports: [ANALYTICS_QUERY],
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Injection tokens for the analytics subsystem.
3
+ *
4
+ * String constants (not Symbols) so they match by value across import
5
+ * boundaries — same convention as events.tokens.ts.
6
+ *
7
+ * Usage in services:
8
+ * ```typescript
9
+ * constructor(@Inject(ANALYTICS_QUERY) private readonly analytics: IAnalyticsQuery) {}
10
+ * ```
11
+ */
12
+ export const ANALYTICS_QUERY = 'ANALYTICS_QUERY' as const;
13
+
14
+ /**
15
+ * Injection token for the cube.js API URL.
16
+ * Provided automatically by AnalyticsModule.forRoot({ backend: 'cube' }).
17
+ */
18
+ export const CUBE_API_URL = Symbol('CUBE_API_URL');
19
+
20
+ /**
21
+ * Injection token for the cube.js API secret.
22
+ * Provided automatically by AnalyticsModule.forRoot({ backend: 'cube' }).
23
+ */
24
+ export const CUBE_API_SECRET = Symbol('CUBE_API_SECRET');
@@ -0,0 +1,75 @@
1
+ /**
2
+ * CubeAnalyticsBackend — cube.js backend for the analytics query port.
3
+ *
4
+ * Connects to a running cube.js instance via @cubejs-client/core and
5
+ * translates IAnalyticsQuery calls into cube.js query objects.
6
+ *
7
+ * @cubejs-client/core is an optional peer dependency; lazy-imported so
8
+ * the module loads even if the package isn't installed. Consumers who
9
+ * use the cube backend must install it separately:
10
+ * bun add @cubejs-client/core
11
+ *
12
+ * Provided by AnalyticsModule.forRoot({ backend: 'cube' }).
13
+ */
14
+ import { Inject, Injectable, OnModuleInit, Logger } from '@nestjs/common';
15
+ import { CUBE_API_URL, CUBE_API_SECRET } from './analytics.tokens';
16
+ import type {
17
+ AnalyticsQueryOpts,
18
+ IAnalyticsQuery,
19
+ ResultRow,
20
+ } from './analytics-query.protocol';
21
+
22
+ @Injectable()
23
+ export class CubeAnalyticsBackend implements IAnalyticsQuery, OnModuleInit {
24
+ private readonly logger = new Logger(CubeAnalyticsBackend.name);
25
+ private cubejsApi: any;
26
+
27
+ constructor(
28
+ @Inject(CUBE_API_URL) private readonly apiUrl: string,
29
+ @Inject(CUBE_API_SECRET) private readonly apiSecret: string,
30
+ ) {}
31
+
32
+ async onModuleInit(): Promise<void> {
33
+ try {
34
+ const { default: cubejs } = await import('@cubejs-client/core');
35
+ this.cubejsApi = cubejs(this.apiSecret, { apiUrl: this.apiUrl });
36
+ } catch {
37
+ throw new Error(
38
+ 'CubeAnalyticsBackend requires @cubejs-client/core. Install it: bun add @cubejs-client/core',
39
+ );
40
+ }
41
+ }
42
+
43
+ async execute(
44
+ cube: string,
45
+ measures: string[],
46
+ dimensions: string[],
47
+ where?: Record<string, any>,
48
+ opts?: AnalyticsQueryOpts,
49
+ ): Promise<ResultRow[]> {
50
+ if (!this.cubejsApi) {
51
+ this.logger.warn('Cube.js client not initialized — returning empty result');
52
+ return [];
53
+ }
54
+
55
+ const query: Record<string, any> = {
56
+ measures: measures.map((m) => cube + '.' + m),
57
+ dimensions: dimensions.map((d) => cube + '.' + d),
58
+ };
59
+
60
+ if (where && Object.keys(where).length > 0) {
61
+ query.filters = Object.entries(where).map(([member, value]) => ({
62
+ member: cube + '.' + member,
63
+ operator: 'equals',
64
+ values: Array.isArray(value) ? value : [String(value)],
65
+ }));
66
+ }
67
+
68
+ if (opts?.limit) {
69
+ query.limit = opts.limit;
70
+ }
71
+
72
+ const resultSet = await this.cubejsApi.load(query);
73
+ return resultSet.tablePivot() as ResultRow[];
74
+ }
75
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Analytics subsystem — public API
3
+ *
4
+ * Import the module in AppModule, inject the query port via ANALYTICS_QUERY token.
5
+ */
6
+ export type {
7
+ ResultRow,
8
+ AnalyticsQueryOpts,
9
+ IAnalyticsQuery,
10
+ } from './analytics-query.protocol';
11
+ export { ANALYTICS_QUERY, CUBE_API_URL, CUBE_API_SECRET } from './analytics.tokens';
12
+ export { AnalyticsModule } from './analytics.module';
13
+ export type { AnalyticsModuleOptions } from './analytics.module';
14
+ export { CubeAnalyticsBackend } from './cube-backend';
15
+ export { NoopAnalyticsBackend } from './noop-backend';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * NoopAnalyticsBackend — no-op backend for the analytics query port.
3
+ *
4
+ * Returns empty arrays for all queries. Use this backend when analytics
5
+ * is disabled or in tests that don't need real analytics data.
6
+ *
7
+ * Provided by AnalyticsModule.forRoot({ backend: 'noop' }).
8
+ */
9
+ import { Injectable } from '@nestjs/common';
10
+ import type {
11
+ AnalyticsQueryOpts,
12
+ IAnalyticsQuery,
13
+ ResultRow,
14
+ } from './analytics-query.protocol';
15
+
16
+ @Injectable()
17
+ export class NoopAnalyticsBackend implements IAnalyticsQuery {
18
+ async execute(
19
+ _cube: string,
20
+ _measures: string[],
21
+ _dimensions: string[],
22
+ _where?: Record<string, any>,
23
+ _opts?: AnalyticsQueryOpts,
24
+ ): Promise<ResultRow[]> {
25
+ return [];
26
+ }
27
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * AuthModule — DynamicModule factory for the auth subsystem.
3
+ *
4
+ * Wires the two pluggable backends the subsystem ships with:
5
+ * - `ENCRYPTION_KEY` → `EnvEncryptionKey` (AES-256-GCM from env)
6
+ * - `OAUTH_STATE_STORE` → `InMemoryOAuthStateStore` (dev) / custom Redis impl (prod)
7
+ *
8
+ * The two integration-store ports (`AUTH_INTEGRATION_READER`,
9
+ * `AUTH_INTEGRATION_TOKEN_WRITER`) are deliberately **not** wired by this
10
+ * module — they are always consumer-specific (adapters over the app's own
11
+ * integrations entity/service). Consumers provide them in the module that
12
+ * owns the integrations domain, not here.
13
+ *
14
+ * `IAuthStrategy` implementations are also per-provider and live in the
15
+ * integration module that uses them (`SalesforceModule`, `HubSpotModule`, …).
16
+ * The subsystem provides the abstract base class
17
+ * (`OAuth2RefreshStrategy`) — binding concrete strategies is an app concern.
18
+ *
19
+ * Usage in AppModule:
20
+ * ```typescript
21
+ * AuthModule.forRoot({
22
+ * encryptionKey: 'env',
23
+ * oauthStateStore: 'in-memory',
24
+ * });
25
+ * ```
26
+ *
27
+ * Or inject custom providers directly:
28
+ * ```typescript
29
+ * AuthModule.forRoot({
30
+ * encryptionKey: { useClass: MyKmsEncryptionKey },
31
+ * oauthStateStore: { useClass: RedisOAuthStateStore },
32
+ * });
33
+ * ```
34
+ *
35
+ * `global: true` means other modules don't need to re-import AuthModule to
36
+ * inject `ENCRYPTION_KEY` / `OAUTH_STATE_STORE`.
37
+ */
38
+ import { Module, type DynamicModule, type Provider } from '@nestjs/common';
39
+ import { ENCRYPTION_KEY, OAUTH_STATE_STORE } from './auth.tokens';
40
+ import { EnvEncryptionKey } from './backends/encryption-key/env';
41
+ import { InMemoryOAuthStateStore } from './backends/oauth-state-store/in-memory';
42
+
43
+ type EncryptionKeyChoice =
44
+ | 'env'
45
+ | Omit<Provider, 'provide'>;
46
+
47
+ type OAuthStateStoreChoice =
48
+ | 'in-memory'
49
+ | Omit<Provider, 'provide'>;
50
+
51
+ export interface AuthModuleOptions {
52
+ /** `'env'` (default) or a full provider definition (e.g. `{ useClass: MyKmsEncryptionKey }`). */
53
+ encryptionKey?: EncryptionKeyChoice;
54
+ /** `'in-memory'` (default) or a full provider definition for a Redis/DB impl. */
55
+ oauthStateStore?: OAuthStateStoreChoice;
56
+ }
57
+
58
+ function resolveEncryptionKeyProvider(choice: EncryptionKeyChoice): Provider {
59
+ if (choice === 'env') {
60
+ return { provide: ENCRYPTION_KEY, useClass: EnvEncryptionKey };
61
+ }
62
+ return { provide: ENCRYPTION_KEY, ...choice } as Provider;
63
+ }
64
+
65
+ function resolveOAuthStateStoreProvider(
66
+ choice: OAuthStateStoreChoice,
67
+ ): Provider {
68
+ if (choice === 'in-memory') {
69
+ return { provide: OAUTH_STATE_STORE, useClass: InMemoryOAuthStateStore };
70
+ }
71
+ return { provide: OAUTH_STATE_STORE, ...choice } as Provider;
72
+ }
73
+
74
+ @Module({})
75
+ export class AuthModule {
76
+ static forRoot(options: AuthModuleOptions = {}): DynamicModule {
77
+ const encryptionKeyProvider = resolveEncryptionKeyProvider(
78
+ options.encryptionKey ?? 'env',
79
+ );
80
+ const oauthStateStoreProvider = resolveOAuthStateStoreProvider(
81
+ options.oauthStateStore ?? 'in-memory',
82
+ );
83
+
84
+ return {
85
+ module: AuthModule,
86
+ global: true,
87
+ providers: [encryptionKeyProvider, oauthStateStoreProvider],
88
+ exports: [ENCRYPTION_KEY, OAUTH_STATE_STORE],
89
+ };
90
+ }
91
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Auth subsystem — injection tokens.
3
+ *
4
+ * Following ADR-008 guidance: `Symbol()` tokens for type safety and collision
5
+ * avoidance. Consumers inject these via `@Inject(...)` against the matching
6
+ * protocol interface.
7
+ *
8
+ * Usage:
9
+ * ```typescript
10
+ * constructor(
11
+ * @Inject(ENCRYPTION_KEY) private readonly key: IEncryptionKey,
12
+ * @Inject(OAUTH_STATE_STORE) private readonly states: IOAuthStateStore,
13
+ * @Inject(AUTH_INTEGRATION_READER) private readonly reader: IIntegrationReader,
14
+ * @Inject(AUTH_INTEGRATION_TOKEN_WRITER) private readonly writer: IIntegrationTokenWriter,
15
+ * ) {}
16
+ * ```
17
+ *
18
+ * `IAuthStrategy` implementations are provider-specific and registered under
19
+ * provider-specific tokens (e.g. `SALESFORCE_AUTH_STRATEGY`,
20
+ * `HUBSPOT_AUTH_STRATEGY`) by each integration module — this subsystem does
21
+ * not mandate a single `AUTH_STRATEGY` token because an app typically has
22
+ * many concurrent strategies, one per provider.
23
+ */
24
+ export const ENCRYPTION_KEY = Symbol('ENCRYPTION_KEY');
25
+ export const OAUTH_STATE_STORE = Symbol('OAUTH_STATE_STORE');
26
+ export const AUTH_INTEGRATION_READER = Symbol('AUTH_INTEGRATION_READER');
27
+ export const AUTH_INTEGRATION_TOKEN_WRITER = Symbol('AUTH_INTEGRATION_TOKEN_WRITER');
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Env-backed AES-256-GCM encryption.
3
+ *
4
+ * Framing: `base64( nonce(12B) || ciphertext || authTag(16B) )`. Random nonce
5
+ * per call means two encryptions of the same plaintext produce different
6
+ * ciphertexts — prevents replay-style inference. Auth tag enforces integrity;
7
+ * any tampering throws on decrypt.
8
+ *
9
+ * Key source: `TOKEN_ENCRYPTION_KEY` env var, 32 bytes base64-encoded.
10
+ * Generate via `openssl rand -base64 32`.
11
+ *
12
+ * Future backend: `kms.ts` (AWS/GCP KMS) for production deployments that
13
+ * need key rotation + audit trails.
14
+ */
15
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
16
+ import type { IEncryptionKey } from '../../protocols/encryption-key';
17
+
18
+ export interface EnvEncryptionKeyOptions {
19
+ /** Defaults to `process.env`. Tests inject a fixture. */
20
+ env?: NodeJS.ProcessEnv;
21
+ /** Defaults to `'TOKEN_ENCRYPTION_KEY'`. */
22
+ envVar?: string;
23
+ }
24
+
25
+ const ALGO = 'aes-256-gcm';
26
+ const NONCE_BYTES = 12;
27
+ const TAG_BYTES = 16;
28
+ const KEY_BYTES = 32;
29
+
30
+ export class EnvEncryptionKey implements IEncryptionKey {
31
+ private readonly key: Buffer;
32
+
33
+ constructor(opts: EnvEncryptionKeyOptions = {}) {
34
+ const env = opts.env ?? process.env;
35
+ const envVar = opts.envVar ?? 'TOKEN_ENCRYPTION_KEY';
36
+ const raw = env[envVar];
37
+ if (!raw) {
38
+ throw new Error(
39
+ `EnvEncryptionKey: ${envVar} is not set. Generate with: openssl rand -base64 32`,
40
+ );
41
+ }
42
+ const decoded = Buffer.from(raw, 'base64');
43
+ if (decoded.length !== KEY_BYTES) {
44
+ throw new Error(
45
+ `EnvEncryptionKey: ${envVar} must decode to ${KEY_BYTES} bytes (got ${decoded.length}). Use: openssl rand -base64 32`,
46
+ );
47
+ }
48
+ this.key = decoded;
49
+ }
50
+
51
+ async encrypt(plaintext: string): Promise<string> {
52
+ const nonce = randomBytes(NONCE_BYTES);
53
+ const cipher = createCipheriv(ALGO, this.key, nonce);
54
+ const ciphertext = Buffer.concat([
55
+ cipher.update(plaintext, 'utf8'),
56
+ cipher.final(),
57
+ ]);
58
+ const authTag = cipher.getAuthTag();
59
+ return Buffer.concat([nonce, ciphertext, authTag]).toString('base64');
60
+ }
61
+
62
+ async decrypt(ciphertext: string): Promise<string> {
63
+ const buf = Buffer.from(ciphertext, 'base64');
64
+ if (buf.length < NONCE_BYTES + TAG_BYTES) {
65
+ throw new Error('EnvEncryptionKey: ciphertext too short');
66
+ }
67
+ const nonce = buf.subarray(0, NONCE_BYTES);
68
+ const authTag = buf.subarray(buf.length - TAG_BYTES);
69
+ const body = buf.subarray(NONCE_BYTES, buf.length - TAG_BYTES);
70
+
71
+ const decipher = createDecipheriv(ALGO, this.key, nonce);
72
+ decipher.setAuthTag(authTag);
73
+ const plain = Buffer.concat([decipher.update(body), decipher.final()]);
74
+ return plain.toString('utf8');
75
+ }
76
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * In-memory OAuth state store.
3
+ *
4
+ * Single-process dev store. Production deployments need a Redis-backed impl
5
+ * (follow-up) so state survives restarts + is shared across workers.
6
+ */
7
+ import type {
8
+ IOAuthStateStore,
9
+ OAuthStateEntry,
10
+ } from '../../protocols/oauth-state-store';
11
+
12
+ export interface InMemoryOAuthStateStoreOptions {
13
+ /** TTL in ms. Entries older than this are treated as absent. Default 10min. */
14
+ ttlMs?: number;
15
+ now?: () => number;
16
+ }
17
+
18
+ export class InMemoryOAuthStateStore implements IOAuthStateStore {
19
+ private readonly store = new Map<
20
+ string,
21
+ { entry: OAuthStateEntry; expiresAt: number }
22
+ >();
23
+ private readonly ttlMs: number;
24
+ private readonly now: () => number;
25
+
26
+ constructor(opts: InMemoryOAuthStateStoreOptions = {}) {
27
+ this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;
28
+ this.now = opts.now ?? (() => Date.now());
29
+ }
30
+
31
+ async put(state: string, entry: OAuthStateEntry): Promise<void> {
32
+ this.store.set(state, { entry, expiresAt: this.now() + this.ttlMs });
33
+ }
34
+
35
+ async consume(state: string): Promise<OAuthStateEntry | null> {
36
+ const slot = this.store.get(state);
37
+ if (!slot) return null;
38
+ this.store.delete(state);
39
+ if (slot.expiresAt <= this.now()) return null;
40
+ return slot.entry;
41
+ }
42
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Auth subsystem — public API.
3
+ *
4
+ * Protocol → Backend → Factory, per ADR-008 + ADR-031. Imports:
5
+ *
6
+ * ```typescript
7
+ * import {
8
+ * AuthModule,
9
+ * ENCRYPTION_KEY,
10
+ * OAUTH_STATE_STORE,
11
+ * AUTH_INTEGRATION_READER,
12
+ * AUTH_INTEGRATION_TOKEN_WRITER,
13
+ * OAuth2RefreshStrategy,
14
+ * withAuthRetry,
15
+ * IntegrationBrokenError,
16
+ * SessionExpiredError,
17
+ * type IAuthStrategy,
18
+ * type IEncryptionKey,
19
+ * type IOAuthStateStore,
20
+ * type IIntegrationReader,
21
+ * type IIntegrationTokenWriter,
22
+ * } from '@pattern-stack/codegen/runtime/subsystems/auth';
23
+ * ```
24
+ */
25
+
26
+ // Protocols
27
+ export type {
28
+ AuthCredentials,
29
+ AuthResolveOptions,
30
+ IAuthStrategy,
31
+ } from './protocols/auth-strategy';
32
+ export type { IEncryptionKey } from './protocols/encryption-key';
33
+ export type {
34
+ IOAuthStateStore,
35
+ OAuthStateEntry,
36
+ } from './protocols/oauth-state-store';
37
+ export type {
38
+ DecryptedIntegration,
39
+ IIntegrationReader,
40
+ IIntegrationTokenWriter,
41
+ IntegrationTokenUpdate,
42
+ } from './protocols/integration-store';
43
+
44
+ // Tokens
45
+ export {
46
+ ENCRYPTION_KEY,
47
+ OAUTH_STATE_STORE,
48
+ AUTH_INTEGRATION_READER,
49
+ AUTH_INTEGRATION_TOKEN_WRITER,
50
+ } from './auth.tokens';
51
+
52
+ // Runtime
53
+ export {
54
+ OAuth2RefreshStrategy,
55
+ type OAuth2RefreshStrategyOptions,
56
+ type ParsedRefreshResponse,
57
+ type FetchLike,
58
+ } from './runtime/oauth2-refresh.strategy';
59
+ export { withAuthRetry, type WithAuthRetryOptions } from './runtime/with-auth-retry';
60
+ export { IntegrationBrokenError } from './runtime/integration-broken.error';
61
+ export {
62
+ SessionExpiredError,
63
+ isSessionExpiredError,
64
+ } from './runtime/session-expired.error';
65
+
66
+ // Backends
67
+ export {
68
+ EnvEncryptionKey,
69
+ type EnvEncryptionKeyOptions,
70
+ } from './backends/encryption-key/env';
71
+ export {
72
+ InMemoryOAuthStateStore,
73
+ type InMemoryOAuthStateStoreOptions,
74
+ } from './backends/oauth-state-store/in-memory';
75
+
76
+ // Module
77
+ export { AuthModule, type AuthModuleOptions } from './auth.module';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Auth subsystem — `IAuthStrategy` port.
3
+ *
4
+ * The credentials-resolution seam used by every integration adapter. Adapters
5
+ * depend on this interface; concrete strategies (SalesforceAuthStrategy,
6
+ * HubSpotAuthStrategy, future Gmail/Calendar) typically extend the
7
+ * `OAuth2RefreshStrategy` template-method base in `../runtime/`.
8
+ *
9
+ * See `docs/adrs/ADR-031-auth-subsystem.md` and
10
+ * `docs/gate-1-auth-extraction-findings.md` (inbound from dealbrain-v2) for
11
+ * the rationale.
12
+ */
13
+
14
+ /**
15
+ * Credentials the adapter consumes — opaque bag at this boundary. Provider-
16
+ * specific shapes (`instanceUrl` for Salesforce, no host for HubSpot) live
17
+ * inside as extra fields.
18
+ */
19
+ export interface AuthCredentials {
20
+ accessToken: string;
21
+ /** OAuth refresh token if the adapter needs to inspect it. Usually omitted. */
22
+ refreshToken?: string;
23
+ /** Provider-specific extras (instance URL, api version, scope list, …). */
24
+ [extra: string]: unknown;
25
+ }
26
+
27
+ export interface AuthResolveOptions {
28
+ /**
29
+ * Force the strategy to bypass its cache and mint fresh credentials.
30
+ * Callers use this after catching a session-expired error; a second
31
+ * resolve that still returns an expired token fails hard rather than
32
+ * looping.
33
+ */
34
+ forceRefresh?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Auth-strategy contract shared by every integration adapter. Implementations
39
+ * typically extend `OAuth2RefreshStrategy` and override four small hooks.
40
+ */
41
+ export interface IAuthStrategy {
42
+ resolve(
43
+ integrationId: string,
44
+ opts?: AuthResolveOptions,
45
+ ): Promise<AuthCredentials>;
46
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Auth subsystem — `IEncryptionKey` port.
3
+ *
4
+ * Symmetric encryption for secrets at rest (OAuth tokens, API keys).
5
+ * Ciphertexts are opaque strings; implementations embed whatever framing
6
+ * (nonce, auth tag, key version) they need. Callers must not inspect the
7
+ * ciphertext format.
8
+ */
9
+ export interface IEncryptionKey {
10
+ /**
11
+ * Encrypt plaintext. Output is a self-contained string that includes any
12
+ * nonce + auth tag the impl needs for decryption. Safe to persist.
13
+ */
14
+ encrypt(plaintext: string): Promise<string>;
15
+
16
+ /**
17
+ * Decrypt a ciphertext produced by this impl. Throws on tamper (auth tag
18
+ * mismatch) or malformed input.
19
+ */
20
+ decrypt(ciphertext: string): Promise<string>;
21
+ }