@pattern-stack/codegen 0.6.3 → 0.6.5
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 +22 -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 +246 -13
- 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/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 +253 -14
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/protocols/integration-store.d.ts +36 -1
- 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 +47 -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/index.d.ts +9 -4
- package/dist/runtime/subsystems/index.js +247 -14
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/src/cli/index.js +578 -143
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +2 -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/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/integration-store.ts +37 -0
- package/runtime/subsystems/auth/protocols/oauth-state-store.ts +38 -6
- package/runtime/subsystems/auth/protocols/provider-strategy.ts +41 -0
- package/runtime/subsystems/auth/protocols/user-context.ts +22 -0
- package/runtime/subsystems/index.ts +17 -2
- package/templates/entity/new/backend/modules/core/sync-source.ejs.t +2 -2
- package/templates/entity/new/backend/modules/core/sync-source.providers.ejs.t +1 -1
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -0
- package/templates/entity/new/prompt.js +1 -0
- 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
|
@@ -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 `ProviderStrategy.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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth subsystem — `ProviderStrategy` 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
|
+
* `ProviderStrategy` 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, ProviderStrategy>`)
|
|
18
|
+
* and dispatches by slug.
|
|
19
|
+
*/
|
|
20
|
+
import type { OAuth2RefreshStrategy } from '../runtime/oauth2-refresh.strategy';
|
|
21
|
+
|
|
22
|
+
export interface ExchangedTokens {
|
|
23
|
+
accessToken: string;
|
|
24
|
+
refreshToken?: string;
|
|
25
|
+
expiresAt?: Date;
|
|
26
|
+
scope?: string[];
|
|
27
|
+
externalAccountId?: string;
|
|
28
|
+
/** Provider-specific bag (SFDC `instance_url`, Google `sub`, …). */
|
|
29
|
+
providerMetadata?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProviderStrategy extends OAuth2RefreshStrategy {
|
|
33
|
+
buildAuthorizeUrl(args: { state: string; redirectUri: string }): string;
|
|
34
|
+
exchangeCodeForTokens(args: {
|
|
35
|
+
code: string;
|
|
36
|
+
redirectUri: string;
|
|
37
|
+
}): Promise<ExchangedTokens>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The DI value type behind the `STRATEGY_REGISTRY` token. */
|
|
41
|
+
export type ProviderStrategyRegistry = ReadonlyMap<string, ProviderStrategy>;
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
ProviderStrategy,
|
|
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';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
to: "<%= hasDetection ? `${basePaths.backendSrc}/${paths.modules}/${name}-sync-source.module.ts` : null %>"
|
|
2
|
+
to: "<%= hasDetection ? (isCleanLitePs ? clpOutputPaths.syncSourceModule : `${basePaths.backendSrc}/${paths.modules}/${name}-sync-source.module.ts`) : null %>"
|
|
3
3
|
skip_if: <%= !hasDetection %>
|
|
4
4
|
force: true
|
|
5
5
|
---
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
IChangeSource,
|
|
11
11
|
PollFetchCallback,
|
|
12
12
|
} from '@shared/subsystems/sync';
|
|
13
|
-
import type { <%= className %> } from '<%= imports.moduleToDomain %>';
|
|
13
|
+
import type { <%= className %> } from '<%= isCleanLitePs ? clpImports.syncSourceToEntity : imports.moduleToDomain %>';
|
|
14
14
|
|
|
15
15
|
const <%= name.toUpperCase() %>_DETECTION_CONFIGS: Record<string, DetectionConfig> = <%- detectionConfigsLiteral %>;
|
|
16
16
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
to: "<%= hasDetection ? `${basePaths.backendSrc}/${paths.modules}/${name}-sync-source.providers.ts` : null %>"
|
|
2
|
+
to: "<%= hasDetection ? (isCleanLitePs ? clpOutputPaths.syncSourceProviders : `${basePaths.backendSrc}/${paths.modules}/${name}-sync-source.providers.ts`) : null %>"
|
|
3
3
|
skip_if: <%= !hasDetection %>
|
|
4
4
|
force: true
|
|
5
5
|
---
|
|
@@ -825,6 +825,17 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
|
|
|
825
825
|
declarativeQueries: hasDeclarativeQueries
|
|
826
826
|
? `${srcRoot}/modules/${entityNamePlural}/use-cases/declarative-queries.ts`
|
|
827
827
|
: null,
|
|
828
|
+
// ADR-033.1 §8 — sync-source module emission for clean-lite-ps. Co-located
|
|
829
|
+
// with the entity feature module under src/modules/<plural>/. Closes #267.
|
|
830
|
+
syncSourceModule: `${srcRoot}/modules/${entityNamePlural}/${entityName}-sync-source.module.ts`,
|
|
831
|
+
syncSourceProviders: `${srcRoot}/modules/${entityNamePlural}/${entityName}-sync-source.providers.ts`,
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// Architecture-specific imports for clean-lite-ps. The sync-source module
|
|
835
|
+
// imports the entity type sibling-style (`./<entity>.entity`) since the
|
|
836
|
+
// module file lives next to the entity file in the same feature folder.
|
|
837
|
+
const clpImports = {
|
|
838
|
+
syncSourceToEntity: `./${entityName}.entity`,
|
|
828
839
|
};
|
|
829
840
|
|
|
830
841
|
// Class names
|
|
@@ -953,6 +964,9 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
|
|
|
953
964
|
// Output paths
|
|
954
965
|
clpOutputPaths: outputPaths,
|
|
955
966
|
|
|
967
|
+
// Architecture-specific imports (ADR-033.1 §8 — sync-source closes #267)
|
|
968
|
+
clpImports,
|
|
969
|
+
|
|
956
970
|
// Class names
|
|
957
971
|
classNames,
|
|
958
972
|
|
|
@@ -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 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: "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: "TOKEN_ENCRYPTION_KEY"` blocks the inject intentionally, because
|
|
13
|
+
# silently rotating would invalidate every encrypted token already in the
|
|
14
|
+
# 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
|
+
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 `TOKEN_ENCRYPTION_KEY=<b64>` and
|
|
25
|
+
* `AUTH_REDIRECT_URI_BASE=<url>` to `.env.config`. Idempotent via
|
|
26
|
+
* `skip_if: "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 (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
|
+
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { IOAuthStateStore, OAuthStateEntry } from '../../protocols/oauth-state-store.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* In-memory OAuth state store.
|
|
5
|
-
*
|
|
6
|
-
* Single-process dev store. Production deployments need a Redis-backed impl
|
|
7
|
-
* (follow-up) so state survives restarts + is shared across workers.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
interface InMemoryOAuthStateStoreOptions {
|
|
11
|
-
/** TTL in ms. Entries older than this are treated as absent. Default 10min. */
|
|
12
|
-
ttlMs?: number;
|
|
13
|
-
now?: () => number;
|
|
14
|
-
}
|
|
15
|
-
declare class InMemoryOAuthStateStore implements IOAuthStateStore {
|
|
16
|
-
private readonly store;
|
|
17
|
-
private readonly ttlMs;
|
|
18
|
-
private readonly now;
|
|
19
|
-
constructor(opts?: InMemoryOAuthStateStoreOptions);
|
|
20
|
-
put(state: string, entry: OAuthStateEntry): Promise<void>;
|
|
21
|
-
consume(state: string): Promise<OAuthStateEntry | null>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export { InMemoryOAuthStateStore, type InMemoryOAuthStateStoreOptions };
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts
|
|
2
|
-
var InMemoryOAuthStateStore = class {
|
|
3
|
-
store = /* @__PURE__ */ new Map();
|
|
4
|
-
ttlMs;
|
|
5
|
-
now;
|
|
6
|
-
constructor(opts = {}) {
|
|
7
|
-
this.ttlMs = opts.ttlMs ?? 10 * 60 * 1e3;
|
|
8
|
-
this.now = opts.now ?? (() => Date.now());
|
|
9
|
-
}
|
|
10
|
-
async put(state, entry) {
|
|
11
|
-
this.store.set(state, { entry, expiresAt: this.now() + this.ttlMs });
|
|
12
|
-
}
|
|
13
|
-
async consume(state) {
|
|
14
|
-
const slot = this.store.get(state);
|
|
15
|
-
if (!slot) return null;
|
|
16
|
-
this.store.delete(state);
|
|
17
|
-
if (slot.expiresAt <= this.now()) return null;
|
|
18
|
-
return slot.entry;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
export {
|
|
22
|
-
InMemoryOAuthStateStore
|
|
23
|
-
};
|
|
24
|
-
//# sourceMappingURL=in-memory.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts"],"sourcesContent":["/**\n * In-memory OAuth state store.\n *\n * Single-process dev store. Production deployments need a Redis-backed impl\n * (follow-up) so state survives restarts + is shared across workers.\n */\nimport type {\n IOAuthStateStore,\n OAuthStateEntry,\n} from '../../protocols/oauth-state-store';\n\nexport interface InMemoryOAuthStateStoreOptions {\n /** TTL in ms. Entries older than this are treated as absent. Default 10min. */\n ttlMs?: number;\n now?: () => number;\n}\n\nexport class InMemoryOAuthStateStore implements IOAuthStateStore {\n private readonly store = new Map<\n string,\n { entry: OAuthStateEntry; expiresAt: number }\n >();\n private readonly ttlMs: number;\n private readonly now: () => number;\n\n constructor(opts: InMemoryOAuthStateStoreOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 10 * 60 * 1000;\n this.now = opts.now ?? (() => Date.now());\n }\n\n async put(state: string, entry: OAuthStateEntry): Promise<void> {\n this.store.set(state, { entry, expiresAt: this.now() + this.ttlMs });\n }\n\n async consume(state: string): Promise<OAuthStateEntry | null> {\n const slot = this.store.get(state);\n if (!slot) return null;\n this.store.delete(state);\n if (slot.expiresAt <= this.now()) return null;\n return slot.entry;\n }\n}\n"],"mappings":";AAiBO,IAAM,0BAAN,MAA0D;AAAA,EAC9C,QAAQ,oBAAI,IAG3B;AAAA,EACe;AAAA,EACA;AAAA,EAEjB,YAAY,OAAuC,CAAC,GAAG;AACrD,SAAK,QAAQ,KAAK,SAAS,KAAK,KAAK;AACrC,SAAK,MAAM,KAAK,QAAQ,MAAM,KAAK,IAAI;AAAA,EACzC;AAAA,EAEA,MAAM,IAAI,OAAe,OAAuC;AAC9D,SAAK,MAAM,IAAI,OAAO,EAAE,OAAO,WAAW,KAAK,IAAI,IAAI,KAAK,MAAM,CAAC;AAAA,EACrE;AAAA,EAEA,MAAM,QAAQ,OAAgD;AAC5D,UAAM,OAAO,KAAK,MAAM,IAAI,KAAK;AACjC,QAAI,CAAC,KAAM,QAAO;AAClB,SAAK,MAAM,OAAO,KAAK;AACvB,QAAI,KAAK,aAAa,KAAK,IAAI,EAAG,QAAO;AACzC,WAAO,KAAK;AAAA,EACd;AACF;","names":[]}
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
}
|