@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.
- package/CHANGELOG.md +20 -0
- package/README.md +2 -0
- package/dist/runtime/subsystems/auth/auth-oauth-state.schema.d.ts +81 -0
- package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js +12 -0
- package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js.map +1 -0
- package/dist/runtime/subsystems/auth/auth.module.d.ts +39 -24
- package/dist/runtime/subsystems/auth/auth.module.js +247 -14
- package/dist/runtime/subsystems/auth/auth.module.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth.tokens.d.ts +15 -2
- package/dist/runtime/subsystems/auth/auth.tokens.js +9 -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/backends/state-store.drizzle-backend.d.ts +23 -0
- package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js +68 -0
- package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js.map +1 -0
- package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.d.ts +21 -0
- package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.js +51 -0
- package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.js.map +1 -0
- package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +31 -0
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js +137 -0
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js.map +1 -0
- package/dist/runtime/subsystems/auth/index.d.ts +13 -4
- package/dist/runtime/subsystems/auth/index.js +254 -15
- 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 +37 -2
- package/dist/runtime/subsystems/auth/protocols/oauth-state-store.d.ts +33 -7
- package/dist/runtime/subsystems/auth/protocols/oauth-state-store.js +12 -0
- package/dist/runtime/subsystems/auth/protocols/oauth-state-store.js.map +1 -1
- package/dist/runtime/subsystems/auth/protocols/provider-strategy.d.ts +54 -0
- package/dist/runtime/subsystems/auth/protocols/provider-strategy.js +1 -0
- package/dist/runtime/subsystems/auth/protocols/provider-strategy.js.map +1 -0
- package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +24 -0
- package/dist/runtime/subsystems/auth/protocols/user-context.js +1 -0
- package/dist/runtime/subsystems/auth/protocols/user-context.js.map +1 -0
- 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 +9 -4
- package/dist/runtime/subsystems/index.js +248 -15
- 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 +574 -142
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/auth/auth-oauth-state.schema.ts +30 -0
- package/runtime/subsystems/auth/auth.module.ts +89 -32
- package/runtime/subsystems/auth/auth.tokens.ts +14 -1
- package/runtime/subsystems/auth/backends/encryption-key/env.ts +3 -3
- package/runtime/subsystems/auth/backends/state-store.drizzle-backend.ts +83 -0
- package/runtime/subsystems/auth/backends/state-store.memory-backend.ts +76 -0
- package/runtime/subsystems/auth/controllers/auth.controller.ts +155 -0
- package/runtime/subsystems/auth/index.ts +43 -4
- package/runtime/subsystems/auth/protocols/auth-strategy.ts +1 -1
- package/runtime/subsystems/auth/protocols/integration-store.ts +38 -1
- package/runtime/subsystems/auth/protocols/oauth-state-store.ts +38 -6
- package/runtime/subsystems/auth/protocols/provider-strategy.ts +48 -0
- package/runtime/subsystems/auth/protocols/user-context.ts +22 -0
- 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 +17 -2
- 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 +21 -0
- package/templates/subsystem/auth/auth-oauth-state.schema.ejs.t +35 -0
- package/templates/subsystem/auth/env-config.ejs.t +20 -0
- package/templates/subsystem/auth/prompt.js +46 -0
- package/templates/subsystem/auth-config/codegen-config-auth-block.ejs.t +20 -0
- package/templates/subsystem/auth-config/prompt.js +20 -0
- package/templates/subsystem/auth-integrations/app-module-hook.ejs.t +16 -0
- package/templates/subsystem/auth-integrations/prompt.js +23 -0
- package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.d.ts +0 -24
- package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.js +0 -24
- package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.js.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
73
|
-
type
|
|
74
|
-
} from './backends/
|
|
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` (
|
|
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
|
|
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.
|
|
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
|
|
20
|
+
export interface OAuthStateRecord {
|
|
8
21
|
userId: string;
|
|
9
|
-
|
|
22
|
+
/** Optional post-callback redirect path (relative URL). */
|
|
23
|
+
redirect?: string;
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
export interface IOAuthStateStore {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
* -
|
|
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 (
|
|
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
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
+
};
|