@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../../runtime/subsystems/auth/protocols/oauth-state-store.ts"],"sourcesContent":["/**\n * Auth subsystem — `IOAuthStateStore` port.\n *\n * CSRF protection for the OAuth2 authorize-code callback. Generic across\n * providers. The store mints opaque state tokens at /connect time and\n * single-use consumes them at /callback time, returning the original\n * record (userId + optional post-callback redirect path).\n *\n * Concrete backends live under `../backends/`:\n * - `state-store.memory-backend.ts` — in-process Map (tests/dev).\n * - `state-store.drizzle-backend.ts` — Postgres (prod).\n *\n * Semantics:\n * - `generate(record)` → returns an opaque state token; record is stored\n * under that token until consumed or until TTL expires.\n * - `consume(state)` → atomically deletes the entry and returns the\n * record. Throws on missing, expired, or replayed state. Never returns\n * null — a missing/expired state is a CSRF signal.\n */\nexport interface OAuthStateRecord {\n userId: string;\n /** Optional post-callback redirect path (relative URL). */\n redirect?: string;\n}\n\nexport interface IOAuthStateStore {\n /** Mint an opaque state token bound to `record`. Single-use. */\n generate(record: OAuthStateRecord): Promise<string>;\n /**\n * Atomically consume `state`, returning the bound record. Throws on\n * missing / expired / replayed state.\n */\n consume(state: string): Promise<OAuthStateRecord>;\n}\n\n/**\n * Thrown by `IOAuthStateStore.consume` when the state token is unknown,\n * expired, or has already been consumed (replay attempt).\n */\nexport class OAuthStateError extends Error {\n constructor(\n message: string,\n public readonly reason: 'missing' | 'expired',\n ) {\n super(message);\n this.name = 'OAuthStateError';\n }\n}\n"],"mappings":";AAuCO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EACzC,YACE,SACgB,QAChB;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AAAA,EACd;AAAA,EAJkB;AAKpB;","names":[]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { OAuth2RefreshStrategy } from '../runtime/oauth2-refresh.strategy.js';
|
|
2
|
+
import './auth-strategy.js';
|
|
3
|
+
import './integration-store.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auth subsystem — `IProviderStrategy` contract.
|
|
7
|
+
*
|
|
8
|
+
* Extension of `OAuth2RefreshStrategy` (which already covers the refresh
|
|
9
|
+
* path) that adds the two methods needed by the connect/callback dance:
|
|
10
|
+
*
|
|
11
|
+
* - `buildAuthorizeUrl({ state, redirectUri })` → consent-page URL.
|
|
12
|
+
* - `exchangeCodeForTokens({ code, redirectUri })` → tokens after consent.
|
|
13
|
+
*
|
|
14
|
+
* Concrete per-provider strategies (HubSpot, SFDC, Google, Gong, Fathom, …)
|
|
15
|
+
* stay consumer-side per ADR-031 ("every app has different combinations").
|
|
16
|
+
* They typically subclass `OAuth2RefreshStrategy` for the refresh path and
|
|
17
|
+
* implement these two methods structurally — that satisfies
|
|
18
|
+
* `IProviderStrategy` because TS lets interfaces extend classes by type.
|
|
19
|
+
*
|
|
20
|
+
* AuthController never imports a concrete strategy — it injects the
|
|
21
|
+
* `STRATEGY_REGISTRY` (a `ReadonlyMap<provider-slug, IProviderStrategy>`)
|
|
22
|
+
* and dispatches by slug.
|
|
23
|
+
*
|
|
24
|
+
* **Naming convention:** interfaces that describe behavioral ports use the
|
|
25
|
+
* `I` prefix (`IProviderStrategy`, `IIntegrationReader`, `IUserContext`,
|
|
26
|
+
* `IOAuthStateStore`, `IEncryptionKey`). Plain data types / DTOs (e.g.
|
|
27
|
+
* `ExchangedTokens`, `DecryptedIntegration`, `IntegrationGrantInput`) do
|
|
28
|
+
* not. Abstract template-method classes (e.g. `OAuth2RefreshStrategy`) also
|
|
29
|
+
* do not — the `I` is for interfaces only.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
interface ExchangedTokens {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
refreshToken?: string;
|
|
35
|
+
expiresAt?: Date;
|
|
36
|
+
scope?: string[];
|
|
37
|
+
externalAccountId?: string;
|
|
38
|
+
/** Provider-specific bag (SFDC `instance_url`, Google `sub`, …). */
|
|
39
|
+
providerMetadata?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
interface IProviderStrategy extends OAuth2RefreshStrategy {
|
|
42
|
+
buildAuthorizeUrl(args: {
|
|
43
|
+
state: string;
|
|
44
|
+
redirectUri: string;
|
|
45
|
+
}): string;
|
|
46
|
+
exchangeCodeForTokens(args: {
|
|
47
|
+
code: string;
|
|
48
|
+
redirectUri: string;
|
|
49
|
+
}): Promise<ExchangedTokens>;
|
|
50
|
+
}
|
|
51
|
+
/** The DI value type behind the `STRATEGY_REGISTRY` token. */
|
|
52
|
+
type ProviderStrategyRegistry = ReadonlyMap<string, IProviderStrategy>;
|
|
53
|
+
|
|
54
|
+
export type { ExchangedTokens, IProviderStrategy, ProviderStrategyRegistry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=provider-strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
interface IUserContext {
|
|
21
|
+
getCurrentUserId(req: unknown): Promise<string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type { IUserContext };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=user-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -5,8 +5,8 @@ import { IIntegrationReader, IIntegrationTokenWriter, DecryptedIntegration } fro
|
|
|
5
5
|
* Abstract base class for OAuth2 refresh-token strategies.
|
|
6
6
|
*
|
|
7
7
|
* Template-method pattern: `resolve()` is concrete; four small hooks inject
|
|
8
|
-
* provider specifics. Validated across two providers
|
|
9
|
-
*
|
|
8
|
+
* provider specifics. Validated across two providers (Salesforce, HubSpot)
|
|
9
|
+
* in the extraction-source app before being extracted here — see
|
|
10
10
|
* `docs/gate-1-auth-extraction-findings.md` for the "build first, extract
|
|
11
11
|
* later" evidence.
|
|
12
12
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../runtime/subsystems/auth/runtime/integration-broken.error.ts","../../../../../runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts"],"sourcesContent":["/**\n * Thrown when an OAuth2 provider returns `400 invalid_grant`/`invalid_token`\n * on refresh — the refresh token itself is dead (user revoked, org\n * deactivated, token expired beyond the provider's rotation window). The\n * integration should be marked broken so background sync stops picking it\n * up; the user re-initiates OAuth.\n *\n * Shared across every OAuth2 strategy.\n */\nexport class IntegrationBrokenError extends Error {\n constructor(\n readonly integrationId: string,\n readonly errorCode: string,\n readonly errorDescription: string,\n ) {\n super(\n `Integration ${integrationId} broken: ${errorCode} - ${errorDescription}`,\n );\n this.name = 'IntegrationBrokenError';\n }\n}\n","/**\n * Abstract base class for OAuth2 refresh-token strategies.\n *\n * Template-method pattern: `resolve()` is concrete; four small hooks inject\n * provider specifics. Validated across two providers
|
|
1
|
+
{"version":3,"sources":["../../../../../runtime/subsystems/auth/runtime/integration-broken.error.ts","../../../../../runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts"],"sourcesContent":["/**\n * Thrown when an OAuth2 provider returns `400 invalid_grant`/`invalid_token`\n * on refresh — the refresh token itself is dead (user revoked, org\n * deactivated, token expired beyond the provider's rotation window). The\n * integration should be marked broken so background sync stops picking it\n * up; the user re-initiates OAuth.\n *\n * Shared across every OAuth2 strategy.\n */\nexport class IntegrationBrokenError extends Error {\n constructor(\n readonly integrationId: string,\n readonly errorCode: string,\n readonly errorDescription: string,\n ) {\n super(\n `Integration ${integrationId} broken: ${errorCode} - ${errorDescription}`,\n );\n this.name = 'IntegrationBrokenError';\n }\n}\n","/**\n * Abstract base class for OAuth2 refresh-token strategies.\n *\n * Template-method pattern: `resolve()` is concrete; four small hooks inject\n * provider specifics. Validated across two providers (Salesforce, HubSpot)\n * in the extraction-source app before being extracted here — see\n * `docs/gate-1-auth-extraction-findings.md` for the \"build first, extract\n * later\" evidence.\n *\n * Subclass contract:\n * - `provider` — slug matched against `integrations.provider`\n * - `defaultExpiresInSec` — fallback when refresh response omits `expires_in`\n * - `tokenEndpoint()` — URL to POST the refresh grant\n * - `refreshBodyExtras()` — provider-specific body params\n * - `parseRefreshResponse()` — raw JSON → ParsedRefreshResponse\n * - `buildCredentials()` — stored or freshly-refreshed access token +\n * integration + optional raw refresh response\n * → provider credentials\n *\n * Base handles: expiry check w/ 5-min safety window, `forceRefresh` escape\n * hatch, POST form-urlencoded body, OAuth2 error mapping to\n * `IntegrationBrokenError`, refresh-token rotation persistence, fetch +\n * clock injection for tests.\n */\nimport type {\n AuthCredentials,\n AuthResolveOptions,\n IAuthStrategy,\n} from '../protocols/auth-strategy';\nimport type {\n DecryptedIntegration,\n IIntegrationReader,\n IIntegrationTokenWriter,\n} from '../protocols/integration-store';\nimport { IntegrationBrokenError } from './integration-broken.error';\n\nexport type FetchLike = (\n input: string | URL | Request,\n init?: RequestInit,\n) => Promise<Response>;\n\n/** Safety window before expiry that triggers a refresh. */\nconst REFRESH_SAFETY_MS = 5 * 60 * 1000;\n\nexport interface OAuth2RefreshStrategyOptions {\n integrationReader: IIntegrationReader;\n tokenWriter: IIntegrationTokenWriter;\n /** Injectable fetch for tests. Defaults to the global `fetch`. */\n fetch?: FetchLike;\n /** Injectable clock for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\nexport interface ParsedRefreshResponse {\n accessToken: string;\n /**\n * New refresh token if the provider rotated it (HubSpot: always, Salesforce:\n * sometimes). Omit when the provider reused the old refresh token.\n */\n refreshToken?: string;\n /** Seconds from now. If omitted, subclass `defaultExpiresInSec` applies. */\n expiresInSec?: number;\n}\n\nexport abstract class OAuth2RefreshStrategy implements IAuthStrategy {\n protected abstract readonly provider: string;\n protected abstract readonly defaultExpiresInSec: number;\n\n protected readonly integrationReader: IIntegrationReader;\n protected readonly tokenWriter: IIntegrationTokenWriter;\n protected readonly fetchImpl: FetchLike;\n protected readonly now: () => number;\n\n constructor(opts: OAuth2RefreshStrategyOptions) {\n this.integrationReader = opts.integrationReader;\n this.tokenWriter = opts.tokenWriter;\n this.fetchImpl = opts.fetch ?? fetch;\n this.now = opts.now ?? Date.now;\n }\n\n async resolve(\n integrationId: string,\n opts: AuthResolveOptions = {},\n ): Promise<AuthCredentials> {\n const integration =\n await this.integrationReader.findByIdDecrypted(integrationId);\n if (!integration) {\n throw new Error(`Integration ${integrationId} not found`);\n }\n if (integration.provider !== this.provider) {\n throw new Error(\n `${this.constructor.name} called for non-${this.provider} integration ${integrationId} (provider=${integration.provider})`,\n );\n }\n\n const needsRefresh =\n opts.forceRefresh ||\n this.isExpiring(integration.expiresAt) ||\n !integration.accessToken;\n\n if (!needsRefresh) {\n return this.buildCredentials(integration.accessToken, integration);\n }\n\n if (!integration.refreshToken) {\n throw new IntegrationBrokenError(\n integrationId,\n 'no_refresh_token',\n 'Integration has no refresh token; user must reconnect',\n );\n }\n\n const { parsed, raw } = await this.executeRefresh(\n integrationId,\n integration.refreshToken,\n );\n const newExpiresAt = new Date(\n this.now() + (parsed.expiresInSec ?? this.defaultExpiresInSec) * 1000,\n );\n await this.tokenWriter.persistRefresh({\n integrationId,\n accessToken: parsed.accessToken,\n refreshToken: parsed.refreshToken ?? undefined,\n expiresAt: newExpiresAt,\n });\n\n return this.buildCredentials(parsed.accessToken, integration, raw);\n }\n\n protected abstract tokenEndpoint(): string;\n protected abstract refreshBodyExtras(): Record<string, string>;\n protected abstract parseRefreshResponse(raw: unknown): ParsedRefreshResponse;\n protected abstract buildCredentials(\n accessToken: string,\n integration: DecryptedIntegration,\n refreshRaw?: unknown,\n ): AuthCredentials;\n\n private async executeRefresh(\n integrationId: string,\n refreshToken: string,\n ): Promise<{ parsed: ParsedRefreshResponse; raw: unknown }> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n ...this.refreshBodyExtras(),\n });\n const response = await this.fetchImpl(this.tokenEndpoint(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!response.ok) {\n const err = (await safeJson(response)) as Partial<{\n error: string;\n error_description: string;\n message: string;\n }>;\n if (\n response.status === 400 &&\n (err.error === 'invalid_grant' || err.error === 'invalid_token')\n ) {\n throw new IntegrationBrokenError(\n integrationId,\n err.error ?? 'invalid_grant',\n err.error_description ?? err.message ?? 'refresh token rejected',\n );\n }\n throw new Error(\n `${this.provider} token refresh failed: ${response.status} ${err.error ?? ''} ${err.error_description ?? err.message ?? ''}`.trim(),\n );\n }\n const raw = await response.json();\n return { parsed: this.parseRefreshResponse(raw), raw };\n }\n\n private isExpiring(expiresAt: Date | null): boolean {\n if (!expiresAt) return true;\n return expiresAt.getTime() - this.now() < REFRESH_SAFETY_MS;\n }\n}\n\nasync function safeJson(response: Response): Promise<unknown> {\n try {\n return await response.clone().json();\n } catch {\n return {};\n }\n}\n"],"mappings":";AASO,IAAM,yBAAN,cAAqC,MAAM;AAAA,EAChD,YACW,eACA,WACA,kBACT;AACA;AAAA,MACE,eAAe,aAAa,YAAY,SAAS,MAAM,gBAAgB;AAAA,IACzE;AANS;AACA;AACA;AAKT,SAAK,OAAO;AAAA,EACd;AAAA,EARW;AAAA,EACA;AAAA,EACA;AAOb;;;ACsBA,IAAM,oBAAoB,IAAI,KAAK;AAsB5B,IAAe,wBAAf,MAA8D;AAAA,EAIhD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEnB,YAAY,MAAoC;AAC9C,SAAK,oBAAoB,KAAK;AAC9B,SAAK,cAAc,KAAK;AACxB,SAAK,YAAY,KAAK,SAAS;AAC/B,SAAK,MAAM,KAAK,OAAO,KAAK;AAAA,EAC9B;AAAA,EAEA,MAAM,QACJ,eACA,OAA2B,CAAC,GACF;AAC1B,UAAM,cACJ,MAAM,KAAK,kBAAkB,kBAAkB,aAAa;AAC9D,QAAI,CAAC,aAAa;AAChB,YAAM,IAAI,MAAM,eAAe,aAAa,YAAY;AAAA,IAC1D;AACA,QAAI,YAAY,aAAa,KAAK,UAAU;AAC1C,YAAM,IAAI;AAAA,QACR,GAAG,KAAK,YAAY,IAAI,mBAAmB,KAAK,QAAQ,gBAAgB,aAAa,cAAc,YAAY,QAAQ;AAAA,MACzH;AAAA,IACF;AAEA,UAAM,eACJ,KAAK,gBACL,KAAK,WAAW,YAAY,SAAS,KACrC,CAAC,YAAY;AAEf,QAAI,CAAC,cAAc;AACjB,aAAO,KAAK,iBAAiB,YAAY,aAAa,WAAW;AAAA,IACnE;AAEA,QAAI,CAAC,YAAY,cAAc;AAC7B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,IAAI,IAAI,MAAM,KAAK;AAAA,MACjC;AAAA,MACA,YAAY;AAAA,IACd;AACA,UAAM,eAAe,IAAI;AAAA,MACvB,KAAK,IAAI,KAAK,OAAO,gBAAgB,KAAK,uBAAuB;AAAA,IACnE;AACA,UAAM,KAAK,YAAY,eAAe;AAAA,MACpC;AAAA,MACA,aAAa,OAAO;AAAA,MACpB,cAAc,OAAO,gBAAgB;AAAA,MACrC,WAAW;AAAA,IACb,CAAC;AAED,WAAO,KAAK,iBAAiB,OAAO,aAAa,aAAa,GAAG;AAAA,EACnE;AAAA,EAWA,MAAc,eACZ,eACA,cAC0D;AAC1D,UAAM,OAAO,IAAI,gBAAgB;AAAA,MAC/B,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,GAAG,KAAK,kBAAkB;AAAA,IAC5B,CAAC;AACD,UAAM,WAAW,MAAM,KAAK,UAAU,KAAK,cAAc,GAAG;AAAA,MAC1D,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MAC/D,MAAM,KAAK,SAAS;AAAA,IACtB,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,MAAO,MAAM,SAAS,QAAQ;AAKpC,UACE,SAAS,WAAW,QACnB,IAAI,UAAU,mBAAmB,IAAI,UAAU,kBAChD;AACA,cAAM,IAAI;AAAA,UACR;AAAA,UACA,IAAI,SAAS;AAAA,UACb,IAAI,qBAAqB,IAAI,WAAW;AAAA,QAC1C;AAAA,MACF;AACA,YAAM,IAAI;AAAA,QACR,GAAG,KAAK,QAAQ,0BAA0B,SAAS,MAAM,IAAI,IAAI,SAAS,EAAE,IAAI,IAAI,qBAAqB,IAAI,WAAW,EAAE,GAAG,KAAK;AAAA,MACpI;AAAA,IACF;AACA,UAAM,MAAM,MAAM,SAAS,KAAK;AAChC,WAAO,EAAE,QAAQ,KAAK,qBAAqB,GAAG,GAAG,IAAI;AAAA,EACvD;AAAA,EAEQ,WAAW,WAAiC;AAClD,QAAI,CAAC,UAAW,QAAO;AACvB,WAAO,UAAU,QAAQ,IAAI,KAAK,IAAI,IAAI;AAAA,EAC5C;AACF;AAEA,eAAe,SAAS,UAAsC;AAC5D,MAAI;AACF,WAAO,MAAM,SAAS,MAAM,EAAE,KAAK;AAAA,EACrC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
|
|
@@ -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
|
declare class SessionExpiredError extends Error {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../runtime/subsystems/auth/runtime/session-expired.error.ts"],"sourcesContent":["/**\n * Provider-agnostic marker for \"the access token was rejected; a forced\n * refresh may recover.\"\n *\n * Concrete provider error classes (e.g. SalesforceSessionExpiredError,\n * HubSpotUnauthorizedError) either extend `SessionExpiredError` directly or\n * set `isSessionExpired === true` on their instances. `withAuthRetry` uses\n * the `isSessionExpiredError` predicate to decide whether to force-refresh\n * and retry once.\n *\n * This discriminator replaces the SFDC-only `instanceof` check from\n *
|
|
1
|
+
{"version":3,"sources":["../../../../../runtime/subsystems/auth/runtime/session-expired.error.ts"],"sourcesContent":["/**\n * Provider-agnostic marker for \"the access token was rejected; a forced\n * refresh may recover.\"\n *\n * Concrete provider error classes (e.g. SalesforceSessionExpiredError,\n * HubSpotUnauthorizedError) either extend `SessionExpiredError` directly or\n * set `isSessionExpired === true` on their instances. `withAuthRetry` uses\n * the `isSessionExpiredError` predicate to decide whether to force-refresh\n * and retry once.\n *\n * This discriminator replaces the SFDC-only `instanceof` check from the\n * extraction-source app's original `withAuthRetry`. See\n * `docs/gate-1-auth-extraction-findings.md` (recommendation 4).\n */\nexport class SessionExpiredError extends Error {\n /** Duck-type marker — works across package boundaries where `instanceof` fails. */\n readonly isSessionExpired = true as const;\n\n constructor(message = 'Access token rejected by provider') {\n super(message);\n this.name = 'SessionExpiredError';\n }\n}\n\n/**\n * Predicate used by `withAuthRetry` by default.\n *\n * Matches any error that either `instanceof SessionExpiredError` or carries\n * the `isSessionExpired === true` marker property. Provider adapters that\n * want their existing error classes to participate can simply add the\n * marker property without touching the class hierarchy.\n */\nexport function isSessionExpiredError(err: unknown): boolean {\n if (err instanceof SessionExpiredError) return true;\n if (err !== null && typeof err === 'object' && 'isSessionExpired' in err) {\n return (err as { isSessionExpired?: unknown }).isSessionExpired === true;\n }\n return false;\n}\n"],"mappings":";AAcO,IAAM,sBAAN,cAAkC,MAAM;AAAA;AAAA,EAEpC,mBAAmB;AAAA,EAE5B,YAAY,UAAU,qCAAqC;AACzD,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAUO,SAAS,sBAAsB,KAAuB;AAC3D,MAAI,eAAe,oBAAqB,QAAO;AAC/C,MAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,sBAAsB,KAAK;AACxE,WAAQ,IAAuC,qBAAqB;AAAA,EACtE;AACA,SAAO;AACT;","names":[]}
|
|
@@ -8,7 +8,7 @@ import { IAuthStrategy, AuthCredentials } from '../protocols/auth-strategy.js';
|
|
|
8
8
|
* on the refreshed token propagates rather than looping, so transient
|
|
9
9
|
* adapter bugs can't hang the caller.
|
|
10
10
|
*
|
|
11
|
-
* Generalisation over
|
|
11
|
+
* Generalisation over the extraction source's SFDC-specific original: the
|
|
12
12
|
* session-expired classifier is injected. Providers mark their session-
|
|
13
13
|
* expired errors (via `instanceof` of a marker class, or by setting a known
|
|
14
14
|
* property) and pass a classifier matching that shape.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../runtime/subsystems/auth/runtime/session-expired.error.ts","../../../../../runtime/subsystems/auth/runtime/with-auth-retry.ts"],"sourcesContent":["/**\n * Provider-agnostic marker for \"the access token was rejected; a forced\n * refresh may recover.\"\n *\n * Concrete provider error classes (e.g. SalesforceSessionExpiredError,\n * HubSpotUnauthorizedError) either extend `SessionExpiredError` directly or\n * set `isSessionExpired === true` on their instances. `withAuthRetry` uses\n * the `isSessionExpiredError` predicate to decide whether to force-refresh\n * and retry once.\n *\n * This discriminator replaces the SFDC-only `instanceof` check from\n *
|
|
1
|
+
{"version":3,"sources":["../../../../../runtime/subsystems/auth/runtime/session-expired.error.ts","../../../../../runtime/subsystems/auth/runtime/with-auth-retry.ts"],"sourcesContent":["/**\n * Provider-agnostic marker for \"the access token was rejected; a forced\n * refresh may recover.\"\n *\n * Concrete provider error classes (e.g. SalesforceSessionExpiredError,\n * HubSpotUnauthorizedError) either extend `SessionExpiredError` directly or\n * set `isSessionExpired === true` on their instances. `withAuthRetry` uses\n * the `isSessionExpiredError` predicate to decide whether to force-refresh\n * and retry once.\n *\n * This discriminator replaces the SFDC-only `instanceof` check from the\n * extraction-source app's original `withAuthRetry`. See\n * `docs/gate-1-auth-extraction-findings.md` (recommendation 4).\n */\nexport class SessionExpiredError extends Error {\n /** Duck-type marker — works across package boundaries where `instanceof` fails. */\n readonly isSessionExpired = true as const;\n\n constructor(message = 'Access token rejected by provider') {\n super(message);\n this.name = 'SessionExpiredError';\n }\n}\n\n/**\n * Predicate used by `withAuthRetry` by default.\n *\n * Matches any error that either `instanceof SessionExpiredError` or carries\n * the `isSessionExpired === true` marker property. Provider adapters that\n * want their existing error classes to participate can simply add the\n * marker property without touching the class hierarchy.\n */\nexport function isSessionExpiredError(err: unknown): boolean {\n if (err instanceof SessionExpiredError) return true;\n if (err !== null && typeof err === 'object' && 'isSessionExpired' in err) {\n return (err as { isSessionExpired?: unknown }).isSessionExpired === true;\n }\n return false;\n}\n","/**\n * Run `op` with auth-aware retry-once on session-expired errors.\n *\n * Pattern: resolve creds → run op → if `isSessionExpired(e)` → resolve with\n * `forceRefresh: true` → retry → propagate. A second session-expired error\n * on the refreshed token propagates rather than looping, so transient\n * adapter bugs can't hang the caller.\n *\n * Generalisation over the extraction source's SFDC-specific original: the\n * session-expired classifier is injected. Providers mark their session-\n * expired errors (via `instanceof` of a marker class, or by setting a known\n * property) and pass a classifier matching that shape.\n *\n * Default classifier recognises the marker interface `SessionExpiredError`\n * shipped in `session-expired.error.ts` — concrete provider errors that\n * extend it (or set `isSessionExpired === true`) get retried without any\n * further wiring.\n */\nimport type {\n AuthCredentials,\n IAuthStrategy,\n} from '../protocols/auth-strategy';\nimport { isSessionExpiredError } from './session-expired.error';\n\nexport interface WithAuthRetryOptions {\n /**\n * Classifier that decides whether a thrown error is a session-expired\n * signal worth retrying once with a fresh token. Defaults to the marker-\n * interface check in `session-expired.error.ts`.\n */\n isSessionExpired?: (err: unknown) => boolean;\n}\n\nexport async function withAuthRetry<T>(\n authStrategy: IAuthStrategy,\n integrationId: string,\n op: (credentials: AuthCredentials) => Promise<T>,\n options: WithAuthRetryOptions = {},\n): Promise<T> {\n const classify = options.isSessionExpired ?? isSessionExpiredError;\n\n let creds = await authStrategy.resolve(integrationId);\n try {\n return await op(creds);\n } catch (e) {\n if (!classify(e)) throw e;\n creds = await authStrategy.resolve(integrationId, { forceRefresh: true });\n return op(creds);\n }\n}\n"],"mappings":";AAcO,IAAM,sBAAN,cAAkC,MAAM;AAAA;AAAA,EAEpC,mBAAmB;AAAA,EAE5B,YAAY,UAAU,qCAAqC;AACzD,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAUO,SAAS,sBAAsB,KAAuB;AAC3D,MAAI,eAAe,oBAAqB,QAAO;AAC/C,MAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,sBAAsB,KAAK;AACxE,WAAQ,IAAuC,qBAAqB;AAAA,EACtE;AACA,SAAO;AACT;;;ACLA,eAAsB,cACpB,cACA,eACA,IACA,UAAgC,CAAC,GACrB;AACZ,QAAM,WAAW,QAAQ,oBAAoB;AAE7C,MAAI,QAAQ,MAAM,aAAa,QAAQ,aAAa;AACpD,MAAI;AACF,WAAO,MAAM,GAAG,KAAK;AAAA,EACvB,SAAS,GAAG;AACV,QAAI,CAAC,SAAS,CAAC,EAAG,OAAM;AACxB,YAAQ,MAAM,aAAa,QAAQ,eAAe,EAAE,cAAc,KAAK,CAAC;AACxE,WAAO,GAAG,KAAK;AAAA,EACjB;AACF;","names":[]}
|
|
@@ -21,15 +21,20 @@ export { ObservabilityModule, ObservabilityModuleOptions } from './observability
|
|
|
21
21
|
export { ObservabilityError } from './observability/observability-errors.js';
|
|
22
22
|
export { AuthCredentials, AuthResolveOptions, IAuthStrategy } from './auth/protocols/auth-strategy.js';
|
|
23
23
|
export { IEncryptionKey } from './auth/protocols/encryption-key.js';
|
|
24
|
-
export { IOAuthStateStore,
|
|
25
|
-
export { DecryptedIntegration, IIntegrationReader, IIntegrationTokenWriter, IntegrationTokenUpdate } from './auth/protocols/integration-store.js';
|
|
26
|
-
export {
|
|
24
|
+
export { IOAuthStateStore, OAuthStateError, OAuthStateRecord } from './auth/protocols/oauth-state-store.js';
|
|
25
|
+
export { DecryptedIntegration, IIntegrationGrantSink, IIntegrationReader, IIntegrationTokenWriter, IntegrationGrantInput, IntegrationTokenUpdate } from './auth/protocols/integration-store.js';
|
|
26
|
+
export { IUserContext } from './auth/protocols/user-context.js';
|
|
27
|
+
export { ExchangedTokens, IProviderStrategy, ProviderStrategyRegistry } from './auth/protocols/provider-strategy.js';
|
|
28
|
+
export { AUTH_INTEGRATION_GRANT_SINK, AUTH_INTEGRATION_READER, AUTH_INTEGRATION_TOKEN_WRITER, AUTH_OPTIONS, AUTH_USER_CONTEXT, ENCRYPTION_KEY, OAUTH_STATE_STORE, STRATEGY_REGISTRY } from './auth/auth.tokens.js';
|
|
27
29
|
export { OAuth2RefreshStrategy, ParsedRefreshResponse } from './auth/runtime/oauth2-refresh.strategy.js';
|
|
28
30
|
export { withAuthRetry } from './auth/runtime/with-auth-retry.js';
|
|
29
31
|
export { IntegrationBrokenError } from './auth/runtime/integration-broken.error.js';
|
|
30
32
|
export { SessionExpiredError, isSessionExpiredError } from './auth/runtime/session-expired.error.js';
|
|
33
|
+
export { AuthOAuthState, authOAuthState } from './auth/auth-oauth-state.schema.js';
|
|
31
34
|
export { EnvEncryptionKey } from './auth/backends/encryption-key/env.js';
|
|
32
|
-
export {
|
|
35
|
+
export { MemoryOAuthStateStore } from './auth/backends/state-store.memory-backend.js';
|
|
36
|
+
export { DrizzleOAuthStateStore } from './auth/backends/state-store.drizzle-backend.js';
|
|
37
|
+
export { AuthController } from './auth/controllers/auth.controller.js';
|
|
33
38
|
export { AuthModule } from './auth/auth.module.js';
|
|
34
39
|
export { CursorSnapshot } from './sync/sync-cursor-store.protocol.js';
|
|
35
40
|
export { StatusHistogram } from './bridge/bridge.protocol.js';
|
|
@@ -3971,11 +3971,25 @@ var ObservabilityError = class extends Error {
|
|
|
3971
3971
|
cause;
|
|
3972
3972
|
};
|
|
3973
3973
|
|
|
3974
|
+
// runtime/subsystems/auth/protocols/oauth-state-store.ts
|
|
3975
|
+
var OAuthStateError = class extends Error {
|
|
3976
|
+
constructor(message, reason) {
|
|
3977
|
+
super(message);
|
|
3978
|
+
this.reason = reason;
|
|
3979
|
+
this.name = "OAuthStateError";
|
|
3980
|
+
}
|
|
3981
|
+
reason;
|
|
3982
|
+
};
|
|
3983
|
+
|
|
3974
3984
|
// runtime/subsystems/auth/auth.tokens.ts
|
|
3975
3985
|
var ENCRYPTION_KEY = /* @__PURE__ */ Symbol("ENCRYPTION_KEY");
|
|
3976
3986
|
var OAUTH_STATE_STORE = /* @__PURE__ */ Symbol("OAUTH_STATE_STORE");
|
|
3977
3987
|
var AUTH_INTEGRATION_READER = /* @__PURE__ */ Symbol("AUTH_INTEGRATION_READER");
|
|
3978
3988
|
var AUTH_INTEGRATION_TOKEN_WRITER = /* @__PURE__ */ Symbol("AUTH_INTEGRATION_TOKEN_WRITER");
|
|
3989
|
+
var AUTH_INTEGRATION_GRANT_SINK = /* @__PURE__ */ Symbol("AUTH_INTEGRATION_GRANT_SINK");
|
|
3990
|
+
var AUTH_USER_CONTEXT = /* @__PURE__ */ Symbol("AUTH_USER_CONTEXT");
|
|
3991
|
+
var STRATEGY_REGISTRY = /* @__PURE__ */ Symbol("STRATEGY_REGISTRY");
|
|
3992
|
+
var AUTH_OPTIONS = /* @__PURE__ */ Symbol("AUTH_OPTIONS");
|
|
3979
3993
|
|
|
3980
3994
|
// runtime/subsystems/auth/runtime/integration-broken.error.ts
|
|
3981
3995
|
var IntegrationBrokenError = class extends Error {
|
|
@@ -4112,6 +4126,15 @@ async function withAuthRetry(authStrategy, integrationId, op, options = {}) {
|
|
|
4112
4126
|
}
|
|
4113
4127
|
}
|
|
4114
4128
|
|
|
4129
|
+
// runtime/subsystems/auth/auth-oauth-state.schema.ts
|
|
4130
|
+
import { pgTable as pgTable4, text as text4, timestamp as timestamp4 } from "drizzle-orm/pg-core";
|
|
4131
|
+
var authOAuthState = pgTable4("auth_oauth_state", {
|
|
4132
|
+
state: text4("state").primaryKey(),
|
|
4133
|
+
userId: text4("user_id").notNull(),
|
|
4134
|
+
redirect: text4("redirect"),
|
|
4135
|
+
expiresAt: timestamp4("expires_at", { withTimezone: true }).notNull()
|
|
4136
|
+
});
|
|
4137
|
+
|
|
4115
4138
|
// runtime/subsystems/auth/backends/encryption-key/env.ts
|
|
4116
4139
|
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
4117
4140
|
var ALGO = "aes-256-gcm";
|
|
@@ -4122,7 +4145,7 @@ var EnvEncryptionKey = class {
|
|
|
4122
4145
|
key;
|
|
4123
4146
|
constructor(opts = {}) {
|
|
4124
4147
|
const env = opts.env ?? process.env;
|
|
4125
|
-
const envVar = opts.envVar ?? "
|
|
4148
|
+
const envVar = opts.envVar ?? "INTEGRATION_TOKEN_ENCRYPTION_KEY";
|
|
4126
4149
|
const raw = env[envVar];
|
|
4127
4150
|
if (!raw) {
|
|
4128
4151
|
throw new Error(
|
|
@@ -4162,26 +4185,198 @@ var EnvEncryptionKey = class {
|
|
|
4162
4185
|
}
|
|
4163
4186
|
};
|
|
4164
4187
|
|
|
4165
|
-
// runtime/subsystems/auth/backends/
|
|
4166
|
-
|
|
4188
|
+
// runtime/subsystems/auth/backends/state-store.memory-backend.ts
|
|
4189
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
4190
|
+
var MemoryOAuthStateStore = class {
|
|
4167
4191
|
store = /* @__PURE__ */ new Map();
|
|
4168
4192
|
ttlMs;
|
|
4169
4193
|
now;
|
|
4194
|
+
generateToken;
|
|
4170
4195
|
constructor(opts = {}) {
|
|
4171
4196
|
this.ttlMs = opts.ttlMs ?? 10 * 60 * 1e3;
|
|
4172
4197
|
this.now = opts.now ?? (() => Date.now());
|
|
4198
|
+
this.generateToken = opts.generateToken ?? (() => randomBytes2(32).toString("base64url"));
|
|
4173
4199
|
}
|
|
4174
|
-
async
|
|
4175
|
-
|
|
4200
|
+
async generate(record) {
|
|
4201
|
+
const state = this.generateToken();
|
|
4202
|
+
this.store.set(state, {
|
|
4203
|
+
record: { ...record },
|
|
4204
|
+
expiresAt: this.now() + this.ttlMs
|
|
4205
|
+
});
|
|
4206
|
+
return state;
|
|
4176
4207
|
}
|
|
4177
4208
|
async consume(state) {
|
|
4178
4209
|
const slot = this.store.get(state);
|
|
4179
|
-
if (!slot)
|
|
4210
|
+
if (!slot) {
|
|
4211
|
+
throw new OAuthStateError(
|
|
4212
|
+
`OAuth state token unknown or already consumed`,
|
|
4213
|
+
"missing"
|
|
4214
|
+
);
|
|
4215
|
+
}
|
|
4180
4216
|
this.store.delete(state);
|
|
4181
|
-
if (slot.expiresAt <= this.now())
|
|
4182
|
-
|
|
4217
|
+
if (slot.expiresAt <= this.now()) {
|
|
4218
|
+
throw new OAuthStateError(`OAuth state token expired`, "expired");
|
|
4219
|
+
}
|
|
4220
|
+
return slot.record;
|
|
4221
|
+
}
|
|
4222
|
+
};
|
|
4223
|
+
|
|
4224
|
+
// runtime/subsystems/auth/backends/state-store.drizzle-backend.ts
|
|
4225
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
4226
|
+
import { eq as eq7 } from "drizzle-orm";
|
|
4227
|
+
var DrizzleOAuthStateStore = class {
|
|
4228
|
+
constructor(db, opts = {}) {
|
|
4229
|
+
this.db = db;
|
|
4230
|
+
this.ttlMs = opts.ttlMs ?? 10 * 60 * 1e3;
|
|
4231
|
+
this.now = opts.now ?? (() => Date.now());
|
|
4232
|
+
this.generateToken = opts.generateToken ?? (() => randomBytes3(32).toString("base64url"));
|
|
4233
|
+
}
|
|
4234
|
+
db;
|
|
4235
|
+
ttlMs;
|
|
4236
|
+
now;
|
|
4237
|
+
generateToken;
|
|
4238
|
+
async generate(record) {
|
|
4239
|
+
const state = this.generateToken();
|
|
4240
|
+
const expiresAt = new Date(this.now() + this.ttlMs);
|
|
4241
|
+
await this.db.insert(authOAuthState).values({
|
|
4242
|
+
state,
|
|
4243
|
+
userId: record.userId,
|
|
4244
|
+
redirect: record.redirect ?? null,
|
|
4245
|
+
expiresAt
|
|
4246
|
+
});
|
|
4247
|
+
return state;
|
|
4248
|
+
}
|
|
4249
|
+
async consume(state) {
|
|
4250
|
+
const rows = await this.db.delete(authOAuthState).where(eq7(authOAuthState.state, state)).returning();
|
|
4251
|
+
const row = rows[0];
|
|
4252
|
+
if (!row) {
|
|
4253
|
+
throw new OAuthStateError(
|
|
4254
|
+
`OAuth state token unknown or already consumed`,
|
|
4255
|
+
"missing"
|
|
4256
|
+
);
|
|
4257
|
+
}
|
|
4258
|
+
if (row.expiresAt.getTime() <= this.now()) {
|
|
4259
|
+
throw new OAuthStateError(`OAuth state token expired`, "expired");
|
|
4260
|
+
}
|
|
4261
|
+
return {
|
|
4262
|
+
userId: row.userId,
|
|
4263
|
+
redirect: row.redirect ?? void 0
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
};
|
|
4267
|
+
|
|
4268
|
+
// runtime/subsystems/auth/controllers/auth.controller.ts
|
|
4269
|
+
import {
|
|
4270
|
+
Controller,
|
|
4271
|
+
Get,
|
|
4272
|
+
Inject as Inject16,
|
|
4273
|
+
Param,
|
|
4274
|
+
Query,
|
|
4275
|
+
Req,
|
|
4276
|
+
Res,
|
|
4277
|
+
HttpException,
|
|
4278
|
+
HttpStatus
|
|
4279
|
+
} from "@nestjs/common";
|
|
4280
|
+
var AuthController = class {
|
|
4281
|
+
constructor(registry, userContext, stateStore, grantSink, options) {
|
|
4282
|
+
this.registry = registry;
|
|
4283
|
+
this.userContext = userContext;
|
|
4284
|
+
this.stateStore = stateStore;
|
|
4285
|
+
this.grantSink = grantSink;
|
|
4286
|
+
this.options = options;
|
|
4287
|
+
}
|
|
4288
|
+
registry;
|
|
4289
|
+
userContext;
|
|
4290
|
+
stateStore;
|
|
4291
|
+
grantSink;
|
|
4292
|
+
options;
|
|
4293
|
+
async connect(slug, redirect, req, res) {
|
|
4294
|
+
const strategy = this.requireStrategy(slug);
|
|
4295
|
+
const userId = await this.userContext.getCurrentUserId(req);
|
|
4296
|
+
const state = await this.stateStore.generate({ userId, redirect });
|
|
4297
|
+
const url = strategy.buildAuthorizeUrl({
|
|
4298
|
+
state,
|
|
4299
|
+
redirectUri: this.redirectUriFor(slug)
|
|
4300
|
+
});
|
|
4301
|
+
return res.redirect(HttpStatus.FOUND, url);
|
|
4302
|
+
}
|
|
4303
|
+
async callback(slug, code, state, res) {
|
|
4304
|
+
const strategy = this.requireStrategy(slug);
|
|
4305
|
+
if (!code) {
|
|
4306
|
+
throw new HttpException(
|
|
4307
|
+
`Missing 'code' query param`,
|
|
4308
|
+
HttpStatus.BAD_REQUEST
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
if (!state) {
|
|
4312
|
+
throw new HttpException(
|
|
4313
|
+
`Missing 'state' query param`,
|
|
4314
|
+
HttpStatus.BAD_REQUEST
|
|
4315
|
+
);
|
|
4316
|
+
}
|
|
4317
|
+
const { userId, redirect } = await this.stateStore.consume(state);
|
|
4318
|
+
const tokens = await strategy.exchangeCodeForTokens({
|
|
4319
|
+
code,
|
|
4320
|
+
redirectUri: this.redirectUriFor(slug)
|
|
4321
|
+
});
|
|
4322
|
+
await this.grantSink.createOrUpdateFromOAuthGrant({
|
|
4323
|
+
userId,
|
|
4324
|
+
provider: slug,
|
|
4325
|
+
accessToken: tokens.accessToken,
|
|
4326
|
+
refreshToken: tokens.refreshToken,
|
|
4327
|
+
expiresAt: tokens.expiresAt,
|
|
4328
|
+
scope: tokens.scope,
|
|
4329
|
+
externalAccountId: tokens.externalAccountId,
|
|
4330
|
+
providerMetadata: tokens.providerMetadata
|
|
4331
|
+
});
|
|
4332
|
+
return res.redirect(
|
|
4333
|
+
HttpStatus.FOUND,
|
|
4334
|
+
redirect ?? `/settings/integrations?connected=${encodeURIComponent(slug)}`
|
|
4335
|
+
);
|
|
4336
|
+
}
|
|
4337
|
+
requireStrategy(slug) {
|
|
4338
|
+
const strategy = this.registry.get(slug);
|
|
4339
|
+
if (!strategy) {
|
|
4340
|
+
throw new HttpException(
|
|
4341
|
+
`Unknown provider '${slug}'`,
|
|
4342
|
+
HttpStatus.NOT_FOUND
|
|
4343
|
+
);
|
|
4344
|
+
}
|
|
4345
|
+
return strategy;
|
|
4346
|
+
}
|
|
4347
|
+
redirectUriFor(slug) {
|
|
4348
|
+
const base = this.options.redirectUriBase;
|
|
4349
|
+
if (!base) {
|
|
4350
|
+
throw new Error(
|
|
4351
|
+
`AuthModule.forRoot: redirectUriBase is required when AuthController is enabled`
|
|
4352
|
+
);
|
|
4353
|
+
}
|
|
4354
|
+
const trimmed = base.replace(/\/+$/, "");
|
|
4355
|
+
return `${trimmed}/auth/${encodeURIComponent(slug)}/callback`;
|
|
4183
4356
|
}
|
|
4184
4357
|
};
|
|
4358
|
+
__decorateClass([
|
|
4359
|
+
Get(":provider/connect"),
|
|
4360
|
+
__decorateParam(0, Param("provider")),
|
|
4361
|
+
__decorateParam(1, Query("redirect")),
|
|
4362
|
+
__decorateParam(2, Req()),
|
|
4363
|
+
__decorateParam(3, Res())
|
|
4364
|
+
], AuthController.prototype, "connect", 1);
|
|
4365
|
+
__decorateClass([
|
|
4366
|
+
Get(":provider/callback"),
|
|
4367
|
+
__decorateParam(0, Param("provider")),
|
|
4368
|
+
__decorateParam(1, Query("code")),
|
|
4369
|
+
__decorateParam(2, Query("state")),
|
|
4370
|
+
__decorateParam(3, Res())
|
|
4371
|
+
], AuthController.prototype, "callback", 1);
|
|
4372
|
+
AuthController = __decorateClass([
|
|
4373
|
+
Controller("auth"),
|
|
4374
|
+
__decorateParam(0, Inject16(STRATEGY_REGISTRY)),
|
|
4375
|
+
__decorateParam(1, Inject16(AUTH_USER_CONTEXT)),
|
|
4376
|
+
__decorateParam(2, Inject16(OAUTH_STATE_STORE)),
|
|
4377
|
+
__decorateParam(3, Inject16(AUTH_INTEGRATION_GRANT_SINK)),
|
|
4378
|
+
__decorateParam(4, Inject16(AUTH_OPTIONS))
|
|
4379
|
+
], AuthController);
|
|
4185
4380
|
|
|
4186
4381
|
// runtime/subsystems/auth/auth.module.ts
|
|
4187
4382
|
import { Module as Module7 } from "@nestjs/common";
|
|
@@ -4192,24 +4387,54 @@ function resolveEncryptionKeyProvider(choice) {
|
|
|
4192
4387
|
return { provide: ENCRYPTION_KEY, ...choice };
|
|
4193
4388
|
}
|
|
4194
4389
|
function resolveOAuthStateStoreProvider(choice) {
|
|
4195
|
-
if (choice === "
|
|
4196
|
-
return { provide: OAUTH_STATE_STORE, useClass:
|
|
4390
|
+
if (choice === "memory") {
|
|
4391
|
+
return { provide: OAUTH_STATE_STORE, useClass: MemoryOAuthStateStore };
|
|
4392
|
+
}
|
|
4393
|
+
if (choice === "drizzle") {
|
|
4394
|
+
return {
|
|
4395
|
+
provide: OAUTH_STATE_STORE,
|
|
4396
|
+
useFactory: (db) => {
|
|
4397
|
+
if (!db) {
|
|
4398
|
+
throw new Error(
|
|
4399
|
+
"AuthModule.forRoot: oauthStateStore: 'drizzle' selected but DRIZZLE provider is not available. Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before AuthModule.forRoot."
|
|
4400
|
+
);
|
|
4401
|
+
}
|
|
4402
|
+
return new DrizzleOAuthStateStore(db);
|
|
4403
|
+
},
|
|
4404
|
+
inject: [{ token: DRIZZLE, optional: true }]
|
|
4405
|
+
};
|
|
4197
4406
|
}
|
|
4198
4407
|
return { provide: OAUTH_STATE_STORE, ...choice };
|
|
4199
4408
|
}
|
|
4200
4409
|
var AuthModule = class {
|
|
4201
4410
|
static forRoot(options = {}) {
|
|
4411
|
+
const resolved = {
|
|
4412
|
+
encryptionKey: options.encryptionKey ?? "env",
|
|
4413
|
+
oauthStateStore: options.oauthStateStore ?? "memory",
|
|
4414
|
+
enableController: options.enableController ?? false,
|
|
4415
|
+
redirectUriBase: options.redirectUriBase
|
|
4416
|
+
};
|
|
4417
|
+
if (resolved.enableController && !resolved.redirectUriBase) {
|
|
4418
|
+
throw new Error(
|
|
4419
|
+
"AuthModule.forRoot: redirectUriBase is required when enableController: true"
|
|
4420
|
+
);
|
|
4421
|
+
}
|
|
4202
4422
|
const encryptionKeyProvider = resolveEncryptionKeyProvider(
|
|
4203
|
-
|
|
4423
|
+
resolved.encryptionKey
|
|
4204
4424
|
);
|
|
4205
4425
|
const oauthStateStoreProvider = resolveOAuthStateStoreProvider(
|
|
4206
|
-
|
|
4426
|
+
resolved.oauthStateStore
|
|
4207
4427
|
);
|
|
4428
|
+
const optionsProvider = {
|
|
4429
|
+
provide: AUTH_OPTIONS,
|
|
4430
|
+
useValue: resolved
|
|
4431
|
+
};
|
|
4208
4432
|
return {
|
|
4209
4433
|
module: AuthModule,
|
|
4210
4434
|
global: true,
|
|
4211
|
-
providers: [encryptionKeyProvider, oauthStateStoreProvider],
|
|
4212
|
-
|
|
4435
|
+
providers: [encryptionKeyProvider, oauthStateStoreProvider, optionsProvider],
|
|
4436
|
+
controllers: resolved.enableController ? [AuthController] : [],
|
|
4437
|
+
exports: [ENCRYPTION_KEY, OAUTH_STATE_STORE, AUTH_OPTIONS]
|
|
4213
4438
|
};
|
|
4214
4439
|
}
|
|
4215
4440
|
};
|
|
@@ -4217,32 +4442,40 @@ AuthModule = __decorateClass([
|
|
|
4217
4442
|
Module7({})
|
|
4218
4443
|
], AuthModule);
|
|
4219
4444
|
export {
|
|
4445
|
+
AUTH_INTEGRATION_GRANT_SINK,
|
|
4220
4446
|
AUTH_INTEGRATION_READER,
|
|
4221
4447
|
AUTH_INTEGRATION_TOKEN_WRITER,
|
|
4448
|
+
AUTH_OPTIONS,
|
|
4449
|
+
AUTH_USER_CONTEXT,
|
|
4450
|
+
AuthController,
|
|
4222
4451
|
AuthModule,
|
|
4223
4452
|
CACHE,
|
|
4224
4453
|
CacheModule,
|
|
4225
4454
|
DrizzleCacheService,
|
|
4226
4455
|
DrizzleEventBus,
|
|
4456
|
+
DrizzleOAuthStateStore,
|
|
4227
4457
|
ENCRYPTION_KEY,
|
|
4228
4458
|
EVENT_BUS,
|
|
4229
4459
|
EnvEncryptionKey,
|
|
4230
4460
|
EventsModule,
|
|
4231
|
-
InMemoryOAuthStateStore,
|
|
4232
4461
|
IntegrationBrokenError,
|
|
4233
4462
|
LocalStorageBackend,
|
|
4234
4463
|
MemoryCacheService,
|
|
4235
4464
|
MemoryEventBus,
|
|
4465
|
+
MemoryOAuthStateStore,
|
|
4236
4466
|
MemoryStorageBackend,
|
|
4237
4467
|
OAUTH_STATE_STORE,
|
|
4238
4468
|
OAuth2RefreshStrategy,
|
|
4469
|
+
OAuthStateError,
|
|
4239
4470
|
OBSERVABILITY,
|
|
4240
4471
|
OBSERVABILITY_MODULE_OPTIONS,
|
|
4241
4472
|
ObservabilityError,
|
|
4242
4473
|
ObservabilityModule,
|
|
4243
4474
|
STORAGE,
|
|
4475
|
+
STRATEGY_REGISTRY,
|
|
4244
4476
|
SessionExpiredError,
|
|
4245
4477
|
StorageModule,
|
|
4478
|
+
authOAuthState,
|
|
4246
4479
|
collisionModeEnum,
|
|
4247
4480
|
isSessionExpiredError,
|
|
4248
4481
|
jobRunStatusEnum,
|