nestjs-ddd-cli 3.2.2 → 3.3.1
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/README.md +118 -9
- package/ddd.schema.json +11 -1
- package/dist/commands/doctor-enhanced.d.ts +1 -0
- package/dist/commands/doctor-enhanced.js +94 -79
- package/dist/commands/doctor-enhanced.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +68 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-all.js +54 -28
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-controller.d.ts +1 -0
- package/dist/commands/generate-controller.js +71 -0
- package/dist/commands/generate-controller.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +1 -1
- package/dist/commands/generate-domain-service.js +6 -4
- package/dist/commands/generate-domain-service.js.map +1 -1
- package/dist/commands/generate-dto.d.ts +4 -0
- package/dist/commands/generate-dto.js +162 -0
- package/dist/commands/generate-dto.js.map +1 -0
- package/dist/commands/generate-entity.js +7 -2
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-event.js +10 -4
- package/dist/commands/generate-event.js.map +1 -1
- package/dist/commands/generate-migration-deployment.d.ts +15 -0
- package/dist/commands/generate-migration-deployment.js +326 -0
- package/dist/commands/generate-migration-deployment.js.map +1 -0
- package/dist/commands/generate-migration-deployment.spec.d.ts +1 -0
- package/dist/commands/generate-migration-deployment.spec.js +337 -0
- package/dist/commands/generate-migration-deployment.spec.js.map +1 -0
- package/dist/commands/generate-module.js +4 -2
- package/dist/commands/generate-module.js.map +1 -1
- package/dist/commands/generate-query.js +23 -4
- package/dist/commands/generate-query.js.map +1 -1
- package/dist/commands/generate-repository.d.ts +1 -1
- package/dist/commands/generate-repository.js +17 -34
- package/dist/commands/generate-repository.js.map +1 -1
- package/dist/commands/generate-service.js +19 -4
- package/dist/commands/generate-service.js.map +1 -1
- package/dist/commands/generate-usecase.js +12 -31
- package/dist/commands/generate-usecase.js.map +1 -1
- package/dist/commands/generate-value-object.js +3 -14
- package/dist/commands/generate-value-object.js.map +1 -1
- package/dist/commands/init-project.d.ts +3 -0
- package/dist/commands/init-project.js +19 -9
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.js +10 -0
- package/dist/commands/interactive-scaffold.js.map +1 -1
- package/dist/commands/migration.d.ts +3 -0
- package/dist/commands/migration.js +83 -48
- package/dist/commands/migration.js.map +1 -1
- package/dist/commands/recipe.d.ts +2 -0
- package/dist/commands/recipe.js +229 -85
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipes/banklink-connector-contract.recipe.d.ts +1 -0
- package/dist/commands/recipes/banklink-connector-contract.recipe.js +161 -0
- package/dist/commands/recipes/banklink-connector-contract.recipe.js.map +1 -0
- package/dist/commands/recipes/bounded-json-http.recipe.d.ts +2 -0
- package/dist/commands/recipes/bounded-json-http.recipe.js +53 -0
- package/dist/commands/recipes/bounded-json-http.recipe.js.map +1 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.d.ts +1 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.js +223 -0
- package/dist/commands/recipes/business-reference-identifiers.recipe.js.map +1 -0
- package/dist/commands/recipes/event-backbone.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-backbone.recipe.js +1118 -0
- package/dist/commands/recipes/event-backbone.recipe.js.map +1 -0
- package/dist/commands/recipes/external-projection-worker.recipe.d.ts +6 -0
- package/dist/commands/recipes/external-projection-worker.recipe.js +211 -0
- package/dist/commands/recipes/external-projection-worker.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +11 -0
- package/dist/commands/recipes/index.js +11 -0
- package/dist/commands/recipes/index.js.map +1 -1
- package/dist/commands/recipes/jest-cjs-compat.recipe.d.ts +3 -0
- package/dist/commands/recipes/jest-cjs-compat.recipe.js +130 -0
- package/dist/commands/recipes/jest-cjs-compat.recipe.js.map +1 -0
- package/dist/commands/recipes/joi-env.recipe.js +1 -1
- package/dist/commands/recipes/kafka-consumer.recipe.d.ts +1 -0
- package/dist/commands/recipes/kafka-consumer.recipe.js +65 -0
- package/dist/commands/recipes/kafka-consumer.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.js +60 -0
- package/dist/commands/recipes/oauth2-client-credentials.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.js +67 -0
- package/dist/commands/recipes/oauth2-token-introspection.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.js +57 -0
- package/dist/commands/recipes/oidc-bff-session.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.js +463 -0
- package/dist/commands/recipes/oidc-dashboard.recipe.js.map +1 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.d.ts +1 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.js +85 -0
- package/dist/commands/recipes/oidc-resource-server.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-context.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-context.recipe.js +229 -0
- package/dist/commands/recipes/platform-context.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.js +339 -0
- package/dist/commands/recipes/platform-parc-authorization.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js +56 -0
- package/dist/commands/recipes/platform-parc-bound-authorization.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.js +103 -0
- package/dist/commands/recipes/platform-service-access-request-context.recipe.js.map +1 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.d.ts +1 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.js +415 -0
- package/dist/commands/recipes/platform-service-runtime.recipe.js.map +1 -0
- package/dist/index.js +77 -9
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +39 -3
- package/dist/templates/ai-context/conventions.md.hbs +13 -8
- package/dist/templates/command/create-command.hbs +8 -3
- package/dist/templates/command/delete-command.hbs +1 -1
- package/dist/templates/command/update-command.hbs +1 -1
- package/dist/templates/controller/application-controller.hbs +7 -0
- package/dist/templates/controller/controller.hbs +6 -0
- package/dist/templates/dto/create-dto.hbs +17 -5
- package/dist/templates/dto/filter-dto.hbs +1 -1
- package/dist/templates/dto/response-dto.hbs +1 -1
- package/dist/templates/entity/entity.hbs +4 -0
- package/dist/templates/mapper/mapper.hbs +5 -1
- package/dist/templates/orm-entity/orm-entity.hbs +9 -1
- package/dist/templates/prisma/prisma-mapper.hbs +6 -0
- package/dist/templates/prisma/prisma-repository.hbs +57 -5
- package/dist/templates/prisma/prisma-schema.hbs +2 -0
- package/dist/templates/query/get-all.query.hbs +10 -2
- package/dist/templates/recipes/bounded-json-http/README.md.hbs +36 -0
- package/dist/templates/recipes/external-projection-worker/README.md.hbs +157 -0
- package/dist/templates/recipes/external-projection-worker/canonical-json.ts.hbs +123 -0
- package/dist/templates/recipes/external-projection-worker/enqueuer.ts.hbs +167 -0
- package/dist/templates/recipes/external-projection-worker/index.ts.hbs +6 -0
- package/dist/templates/recipes/external-projection-worker/migration.ts.hbs +88 -0
- package/dist/templates/recipes/external-projection-worker/module.ts.hbs +36 -0
- package/dist/templates/recipes/external-projection-worker/store.ts.hbs +354 -0
- package/dist/templates/recipes/external-projection-worker/types.ts.hbs +134 -0
- package/dist/templates/recipes/external-projection-worker/worker.ts.hbs +296 -0
- package/dist/templates/recipes/kafka-consumer/README.md.hbs +50 -0
- package/dist/templates/recipes/kafka-consumer/kafka-consumer.ts.hbs +135 -0
- package/dist/templates/recipes/kafka-consumer/migration.ts.hbs +17 -0
- package/dist/templates/recipes/oauth2-client-credentials/README.md.hbs +40 -0
- package/dist/templates/recipes/oauth2-client-credentials/client.spec.ts.hbs +234 -0
- package/dist/templates/recipes/oauth2-client-credentials/client.ts.hbs +210 -0
- package/dist/templates/recipes/oauth2-client-credentials/index.ts.hbs +2 -0
- package/dist/templates/recipes/oauth2-client-credentials/module.ts.hbs +58 -0
- package/dist/templates/recipes/oauth2-token-introspection/README.md.hbs +158 -0
- package/dist/templates/recipes/oauth2-token-introspection/client.spec.ts.hbs +350 -0
- package/dist/templates/recipes/oauth2-token-introspection/client.ts.hbs +241 -0
- package/dist/templates/recipes/oauth2-token-introspection/http-client.spec.ts.hbs +411 -0
- package/dist/templates/recipes/oauth2-token-introspection/http-client.ts.hbs +343 -0
- package/dist/templates/recipes/oauth2-token-introspection/index.ts.hbs +3 -0
- package/dist/templates/recipes/oauth2-token-introspection/module.ts.hbs +58 -0
- package/dist/templates/recipes/oidc-bff-session/README.md.hbs +96 -0
- package/dist/templates/recipes/oidc-bff-session/client.test.ts.hbs +663 -0
- package/dist/templates/recipes/oidc-bff-session/client.ts.hbs +635 -0
- package/dist/templates/recipes/oidc-bff-session/index.ts.hbs +3 -0
- package/dist/templates/recipes/oidc-bff-session/store.integration.test.ts.hbs +480 -0
- package/dist/templates/recipes/oidc-bff-session/store.test.ts.hbs +87 -0
- package/dist/templates/recipes/oidc-bff-session/store.ts.hbs +549 -0
- package/dist/templates/recipes/oidc-bff-session/valkey.test.ts.hbs +158 -0
- package/dist/templates/recipes/oidc-bff-session/valkey.ts.hbs +148 -0
- package/dist/templates/recipes/oidc-resource-server/README.md.hbs +74 -0
- package/dist/templates/recipes/oidc-resource-server/index.ts.hbs +2 -0
- package/dist/templates/recipes/oidc-resource-server/module.ts.hbs +51 -0
- package/dist/templates/recipes/oidc-resource-server/verifier.spec.ts.hbs +186 -0
- package/dist/templates/recipes/oidc-resource-server/verifier.ts.hbs +440 -0
- package/dist/templates/recipes/platform-parc-bound-authorization/README.md.hbs +92 -0
- package/dist/templates/recipes/platform-parc-bound-authorization/client.ts.hbs +142 -0
- package/dist/templates/recipes/platform-service-access-request-context/docs.md.hbs +61 -0
- package/dist/templates/recipes/platform-service-access-request-context/env.example.hbs +7 -0
- package/dist/templates/recipes/platform-service-access-request-context/index.ts.hbs +7 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-headers.ts.hbs +61 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.parser.ts.hbs +132 -0
- package/dist/templates/recipes/platform-service-access-request-context/request-context-introspection.types.ts.hbs +40 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.client.ts.hbs +45 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.config.ts.hbs +142 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.guard.ts.hbs +77 -0
- package/dist/templates/recipes/platform-service-access-request-context/service-access-request-context.module.ts.hbs +17 -0
- package/dist/templates/repository/repository.hbs +42 -4
- package/dist/templates/resolver/graphql-input.hbs +23 -6
- package/dist/templates/resolver/resolver.hbs +4 -0
- package/dist/templates/service/domain-service.hbs +2 -2
- package/dist/templates/test/controller.spec.hbs +5 -1
- package/dist/templates/test/prisma-repository.spec.hbs +192 -0
- package/dist/templates/test/repository.spec.hbs +40 -1
- package/dist/templates/test/usecase.spec.hbs +23 -4
- package/dist/templates/usecase/application-usecase.hbs +17 -0
- package/dist/templates/usecase/create-usecase.hbs +34 -3
- package/dist/templates/usecase/update-usecase.hbs +15 -1
- package/dist/utils/config.utils.d.ts +2 -0
- package/dist/utils/config.utils.js +14 -2
- package/dist/utils/config.utils.js.map +1 -1
- package/dist/utils/dependency.utils.d.ts +16 -2
- package/dist/utils/dependency.utils.js +100 -12
- package/dist/utils/dependency.utils.js.map +1 -1
- package/dist/utils/field.utils.d.ts +6 -0
- package/dist/utils/field.utils.js +50 -8
- package/dist/utils/field.utils.js.map +1 -1
- package/dist/utils/field.utils.spec.d.ts +1 -0
- package/dist/utils/field.utils.spec.js +38 -0
- package/dist/utils/field.utils.spec.js.map +1 -0
- package/dist/utils/file.utils.d.ts +23 -1
- package/dist/utils/file.utils.js +37 -1
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/naming.utils.js +5 -4
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/static-recipe.utils.d.ts +4 -0
- package/dist/utils/static-recipe.utils.js +148 -0
- package/dist/utils/static-recipe.utils.js.map +1 -0
- package/package.json +12 -5
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
export interface BoundedJsonHttpOptions {
|
|
2
|
+
/** Exact origin from validated deployment configuration; no path or request data. */
|
|
3
|
+
origin: string;
|
|
4
|
+
requestHeaders?: Readonly<Record<string, string>>;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
maxResponseBytes?: number;
|
|
7
|
+
trustedInternalHttp?: boolean;
|
|
8
|
+
transport?: (url: string, init: RequestInit) => Promise<Response>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class BoundedJsonHttpUnavailableError extends Error {
|
|
12
|
+
constructor(
|
|
13
|
+
readonly reason: 'timeout' | 'network' | 'http' | 'invalid_response',
|
|
14
|
+
readonly status?: number,
|
|
15
|
+
) {
|
|
16
|
+
super('Provider validation unavailable');
|
|
17
|
+
this.name = 'BoundedJsonHttpUnavailableError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function bounded(
|
|
22
|
+
value: number | undefined,
|
|
23
|
+
fallback: number,
|
|
24
|
+
min: number,
|
|
25
|
+
max: number,
|
|
26
|
+
): number {
|
|
27
|
+
const result = value ?? fallback;
|
|
28
|
+
if (!Number.isSafeInteger(result) || result < min || result > max)
|
|
29
|
+
throw new Error();
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function freeze(value: unknown, depth = 0): void {
|
|
34
|
+
if (depth > 32) throw new BoundedJsonHttpUnavailableError('invalid_response');
|
|
35
|
+
if (value !== null && typeof value === 'object') {
|
|
36
|
+
for (const child of Object.values(value)) freeze(child, depth + 1);
|
|
37
|
+
Object.freeze(value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasControls(value: string): boolean {
|
|
42
|
+
return [...value].some((character) => {
|
|
43
|
+
const code = character.charCodeAt(0);
|
|
44
|
+
return code < 32 || code === 127;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pathSegments(value: unknown): value is readonly string[] {
|
|
49
|
+
return (
|
|
50
|
+
Array.isArray(value) &&
|
|
51
|
+
value.length <= 32 &&
|
|
52
|
+
value.every(
|
|
53
|
+
(segment: unknown) =>
|
|
54
|
+
typeof segment === 'string' &&
|
|
55
|
+
segment.length <= 1024 &&
|
|
56
|
+
!['.', '..'].includes(segment) &&
|
|
57
|
+
!hasControls(segment),
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Minimal JSON transport. It neither authenticates a person nor grants permissions. */
|
|
63
|
+
export class BoundedJsonHttpClient {
|
|
64
|
+
private readonly origin: string;
|
|
65
|
+
readonly #headers: Readonly<Record<string, string>>;
|
|
66
|
+
private readonly timeoutMs: number;
|
|
67
|
+
private readonly maxResponseBytes: number;
|
|
68
|
+
private readonly transport: (
|
|
69
|
+
url: string,
|
|
70
|
+
init: RequestInit,
|
|
71
|
+
) => Promise<Response>;
|
|
72
|
+
|
|
73
|
+
constructor(options: BoundedJsonHttpOptions) {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(options.origin);
|
|
76
|
+
if (
|
|
77
|
+
url.username ||
|
|
78
|
+
url.password ||
|
|
79
|
+
url.hash ||
|
|
80
|
+
url.search ||
|
|
81
|
+
url.pathname !== '/' ||
|
|
82
|
+
(url.protocol !== 'https:' &&
|
|
83
|
+
!(url.protocol === 'http:' && options.trustedInternalHttp === true))
|
|
84
|
+
)
|
|
85
|
+
throw new Error();
|
|
86
|
+
this.origin = url.origin;
|
|
87
|
+
const headers = new Headers({ accept: 'application/json' });
|
|
88
|
+
for (const [key, value] of Object.entries(options.requestHeaders ?? {})) {
|
|
89
|
+
if (['accept', 'content-type'].includes(key.toLowerCase()))
|
|
90
|
+
throw new Error();
|
|
91
|
+
headers.set(key, value);
|
|
92
|
+
}
|
|
93
|
+
this.#headers = Object.freeze(Object.fromEntries(headers.entries()));
|
|
94
|
+
this.timeoutMs = bounded(options.timeoutMs, 3000, 10, 30000);
|
|
95
|
+
this.maxResponseBytes = bounded(
|
|
96
|
+
options.maxResponseBytes,
|
|
97
|
+
65536,
|
|
98
|
+
128,
|
|
99
|
+
1048576,
|
|
100
|
+
);
|
|
101
|
+
this.transport = options.transport ?? ((url, init) => fetch(url, init));
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error('Invalid provider HTTP configuration');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get(
|
|
108
|
+
pathSegments: readonly string[],
|
|
109
|
+
query: Readonly<Record<string, string>> = {},
|
|
110
|
+
): Promise<unknown> {
|
|
111
|
+
return this.execute('GET', pathSegments, query);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
postForm(
|
|
115
|
+
pathSegments: readonly string[],
|
|
116
|
+
form: Readonly<Record<string, string>>,
|
|
117
|
+
): Promise<unknown> {
|
|
118
|
+
if (Object.values(form).some((value) => typeof value !== 'string'))
|
|
119
|
+
throw new Error('Invalid provider request configuration');
|
|
120
|
+
const body = new URLSearchParams(form).toString();
|
|
121
|
+
if (new TextEncoder().encode(body).byteLength > 262144)
|
|
122
|
+
throw new Error('Invalid provider request configuration');
|
|
123
|
+
return this.execute(
|
|
124
|
+
'POST',
|
|
125
|
+
pathSegments,
|
|
126
|
+
{},
|
|
127
|
+
body,
|
|
128
|
+
'application/x-www-form-urlencoded',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
postJson(
|
|
133
|
+
pathSegments: readonly string[],
|
|
134
|
+
payload: unknown,
|
|
135
|
+
): Promise<unknown> {
|
|
136
|
+
return this.json('POST', pathSegments, payload);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
patchJson(
|
|
140
|
+
pathSegments: readonly string[],
|
|
141
|
+
payload: unknown,
|
|
142
|
+
): Promise<unknown> {
|
|
143
|
+
return this.json('PATCH', pathSegments, payload);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
putJson(
|
|
147
|
+
pathSegments: readonly string[],
|
|
148
|
+
payload: unknown,
|
|
149
|
+
query: Readonly<Record<string, string>> = {},
|
|
150
|
+
): Promise<unknown> {
|
|
151
|
+
return this.json('PUT', pathSegments, payload, query);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private json(
|
|
155
|
+
method: 'POST' | 'PATCH' | 'PUT',
|
|
156
|
+
pathSegments: readonly string[],
|
|
157
|
+
payload: unknown,
|
|
158
|
+
query: Readonly<Record<string, string>> = {},
|
|
159
|
+
): Promise<unknown> {
|
|
160
|
+
let nodes = 0;
|
|
161
|
+
const seen = new Set<object>();
|
|
162
|
+
const visit = (value: unknown, depth: number): void => {
|
|
163
|
+
if (++nodes > 10000 || depth > 32) throw new Error();
|
|
164
|
+
if (
|
|
165
|
+
value === null ||
|
|
166
|
+
typeof value === 'string' ||
|
|
167
|
+
typeof value === 'boolean'
|
|
168
|
+
)
|
|
169
|
+
return;
|
|
170
|
+
if (typeof value === 'number' && Number.isFinite(value)) return;
|
|
171
|
+
if (typeof value !== 'object' || !value || seen.has(value))
|
|
172
|
+
throw new Error();
|
|
173
|
+
seen.add(value);
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) throw new Error();
|
|
176
|
+
for (let i = 0; i < value.length; i++) {
|
|
177
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
178
|
+
if (!descriptor || !('value' in descriptor)) throw new Error();
|
|
179
|
+
visit(descriptor.value, depth + 1);
|
|
180
|
+
}
|
|
181
|
+
if (Object.getOwnPropertyNames(value).length !== value.length + 1)
|
|
182
|
+
throw new Error();
|
|
183
|
+
} else {
|
|
184
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
185
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
186
|
+
throw new Error();
|
|
187
|
+
for (const descriptor of Object.values(
|
|
188
|
+
Object.getOwnPropertyDescriptors(value),
|
|
189
|
+
)) {
|
|
190
|
+
if (!descriptor.enumerable || !('value' in descriptor))
|
|
191
|
+
throw new Error();
|
|
192
|
+
visit(descriptor.value, depth + 1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (Object.getOwnPropertySymbols(value).length) throw new Error();
|
|
196
|
+
seen.delete(value);
|
|
197
|
+
};
|
|
198
|
+
try {
|
|
199
|
+
visit(payload, 0);
|
|
200
|
+
const body = JSON.stringify(payload);
|
|
201
|
+
if (new TextEncoder().encode(body).byteLength > 262144) throw new Error();
|
|
202
|
+
return this.execute(method, pathSegments, query, body, 'application/json');
|
|
203
|
+
} catch {
|
|
204
|
+
throw new Error('Invalid provider request configuration');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async execute(
|
|
209
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT',
|
|
210
|
+
segments: readonly string[],
|
|
211
|
+
query: Readonly<Record<string, string>>,
|
|
212
|
+
body?: string,
|
|
213
|
+
contentType?: string,
|
|
214
|
+
): Promise<unknown> {
|
|
215
|
+
if (!pathSegments(segments)) {
|
|
216
|
+
throw new Error('Invalid provider request configuration');
|
|
217
|
+
}
|
|
218
|
+
const entries = Object.entries(query);
|
|
219
|
+
if (
|
|
220
|
+
entries.length > 32 ||
|
|
221
|
+
entries.some(
|
|
222
|
+
([key, value]) =>
|
|
223
|
+
!key ||
|
|
224
|
+
key.length > 128 ||
|
|
225
|
+
typeof value !== 'string' ||
|
|
226
|
+
value.length > 2048 ||
|
|
227
|
+
hasControls(key + value),
|
|
228
|
+
)
|
|
229
|
+
) {
|
|
230
|
+
throw new Error('Invalid provider request configuration');
|
|
231
|
+
}
|
|
232
|
+
let url: URL;
|
|
233
|
+
try {
|
|
234
|
+
url = new URL(
|
|
235
|
+
`${this.origin}/${segments.map((segment) => encodeURIComponent(segment)).join('/')}`,
|
|
236
|
+
);
|
|
237
|
+
for (const [key, value] of entries) url.searchParams.set(key, value);
|
|
238
|
+
if (url.origin !== this.origin || url.href.length > 8192)
|
|
239
|
+
throw new Error();
|
|
240
|
+
} catch {
|
|
241
|
+
throw new Error('Invalid provider request configuration');
|
|
242
|
+
}
|
|
243
|
+
const abort = new AbortController();
|
|
244
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
245
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
246
|
+
timer = setTimeout(() => {
|
|
247
|
+
abort.abort();
|
|
248
|
+
reject(new BoundedJsonHttpUnavailableError('timeout'));
|
|
249
|
+
}, this.timeoutMs);
|
|
250
|
+
});
|
|
251
|
+
try {
|
|
252
|
+
return await Promise.race([
|
|
253
|
+
this.request(url.href, method, body, abort.signal, contentType),
|
|
254
|
+
timeout,
|
|
255
|
+
]);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof BoundedJsonHttpUnavailableError) throw error;
|
|
258
|
+
throw new BoundedJsonHttpUnavailableError('network');
|
|
259
|
+
} finally {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
abort.abort();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async request(
|
|
266
|
+
url: string,
|
|
267
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT',
|
|
268
|
+
body: string | undefined,
|
|
269
|
+
signal: AbortSignal,
|
|
270
|
+
contentType?: string,
|
|
271
|
+
): Promise<unknown> {
|
|
272
|
+
const response = await this.transport(url, {
|
|
273
|
+
method,
|
|
274
|
+
body,
|
|
275
|
+
headers:
|
|
276
|
+
method !== 'GET'
|
|
277
|
+
? {
|
|
278
|
+
...this.#headers,
|
|
279
|
+
'content-type':
|
|
280
|
+
contentType ?? 'application/x-www-form-urlencoded',
|
|
281
|
+
}
|
|
282
|
+
: { ...this.#headers },
|
|
283
|
+
redirect: 'error',
|
|
284
|
+
cache: 'no-store',
|
|
285
|
+
signal,
|
|
286
|
+
});
|
|
287
|
+
if (!response.ok || response.redirected) {
|
|
288
|
+
void response.body?.cancel().catch(() => undefined);
|
|
289
|
+
throw new BoundedJsonHttpUnavailableError('http', response.status);
|
|
290
|
+
}
|
|
291
|
+
if (signal.aborted) {
|
|
292
|
+
void response.body?.cancel().catch(() => undefined);
|
|
293
|
+
throw new BoundedJsonHttpUnavailableError('timeout');
|
|
294
|
+
}
|
|
295
|
+
const length = response.headers.get('content-length');
|
|
296
|
+
if (
|
|
297
|
+
length !== null &&
|
|
298
|
+
(!/^\d+$/.test(length) || Number(length) > this.maxResponseBytes)
|
|
299
|
+
) {
|
|
300
|
+
void response.body?.cancel().catch(() => undefined);
|
|
301
|
+
throw new BoundedJsonHttpUnavailableError('invalid_response');
|
|
302
|
+
}
|
|
303
|
+
if (!response.body)
|
|
304
|
+
throw new BoundedJsonHttpUnavailableError('invalid_response');
|
|
305
|
+
const reader = response.body.getReader();
|
|
306
|
+
const cancel = () => {
|
|
307
|
+
void reader.cancel().catch(() => undefined);
|
|
308
|
+
};
|
|
309
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
310
|
+
const chunks: Uint8Array[] = [];
|
|
311
|
+
let size = 0;
|
|
312
|
+
try {
|
|
313
|
+
while (true) {
|
|
314
|
+
const { done, value } = await reader.read();
|
|
315
|
+
if (done) break;
|
|
316
|
+
size += value.byteLength;
|
|
317
|
+
if (size > this.maxResponseBytes)
|
|
318
|
+
throw new BoundedJsonHttpUnavailableError('invalid_response');
|
|
319
|
+
chunks.push(value);
|
|
320
|
+
}
|
|
321
|
+
const bytes = new Uint8Array(size);
|
|
322
|
+
let offset = 0;
|
|
323
|
+
for (const chunk of chunks) {
|
|
324
|
+
bytes.set(chunk, offset);
|
|
325
|
+
offset += chunk.byteLength;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const parsed: unknown = JSON.parse(
|
|
329
|
+
new TextDecoder('utf-8', { fatal: true }).decode(bytes),
|
|
330
|
+
);
|
|
331
|
+
freeze(parsed);
|
|
332
|
+
return parsed;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (error instanceof BoundedJsonHttpUnavailableError) throw error;
|
|
335
|
+
throw new BoundedJsonHttpUnavailableError('invalid_response');
|
|
336
|
+
}
|
|
337
|
+
} finally {
|
|
338
|
+
signal.removeEventListener('abort', cancel);
|
|
339
|
+
void reader.cancel().catch(() => undefined);
|
|
340
|
+
reader.releaseLock();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicModule,
|
|
3
|
+
FactoryProvider,
|
|
4
|
+
Module,
|
|
5
|
+
ModuleMetadata,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import {
|
|
8
|
+
OAUTH2_TOKEN_INTROSPECTION_OPTIONS,
|
|
9
|
+
OAuth2TokenIntrospectionClient,
|
|
10
|
+
OAuth2TokenIntrospectionOptions,
|
|
11
|
+
} from './oauth2-token-introspection.client';
|
|
12
|
+
|
|
13
|
+
export interface OAuth2TokenIntrospectionModuleAsyncOptions extends Pick<
|
|
14
|
+
ModuleMetadata,
|
|
15
|
+
'imports'
|
|
16
|
+
> {
|
|
17
|
+
inject?: FactoryProvider<OAuth2TokenIntrospectionOptions>['inject'];
|
|
18
|
+
useFactory: FactoryProvider<OAuth2TokenIntrospectionOptions>['useFactory'];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const clientProvider: FactoryProvider<OAuth2TokenIntrospectionClient> = {
|
|
22
|
+
provide: OAuth2TokenIntrospectionClient,
|
|
23
|
+
inject: [OAUTH2_TOKEN_INTROSPECTION_OPTIONS],
|
|
24
|
+
useFactory: (options: OAuth2TokenIntrospectionOptions) =>
|
|
25
|
+
new OAuth2TokenIntrospectionClient(options),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
@Module({})
|
|
29
|
+
export class OAuth2TokenIntrospectionModule {
|
|
30
|
+
static register(options: OAuth2TokenIntrospectionOptions): DynamicModule {
|
|
31
|
+
return {
|
|
32
|
+
module: OAuth2TokenIntrospectionModule,
|
|
33
|
+
providers: [
|
|
34
|
+
{ provide: OAUTH2_TOKEN_INTROSPECTION_OPTIONS, useValue: options },
|
|
35
|
+
clientProvider,
|
|
36
|
+
],
|
|
37
|
+
exports: [OAuth2TokenIntrospectionClient],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static registerAsync(
|
|
42
|
+
options: OAuth2TokenIntrospectionModuleAsyncOptions,
|
|
43
|
+
): DynamicModule {
|
|
44
|
+
return {
|
|
45
|
+
module: OAuth2TokenIntrospectionModule,
|
|
46
|
+
imports: options.imports ?? [],
|
|
47
|
+
providers: [
|
|
48
|
+
{
|
|
49
|
+
provide: OAUTH2_TOKEN_INTROSPECTION_OPTIONS,
|
|
50
|
+
inject: options.inject ?? [],
|
|
51
|
+
useFactory: options.useFactory,
|
|
52
|
+
},
|
|
53
|
+
clientProvider,
|
|
54
|
+
],
|
|
55
|
+
exports: [OAuth2TokenIntrospectionClient],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Confidential OIDC BFF sessions
|
|
2
|
+
|
|
3
|
+
Run `ddd recipe oidc-bff-session --path <app> --dry-run` before applying the same
|
|
4
|
+
command without `--dry-run`. Generated files live in `src/shared/oidc-bff`.
|
|
5
|
+
Generation preflights every target, rejects symlinks/different owner content,
|
|
6
|
+
preserves identical reruns and rolls back only unchanged files it created.
|
|
7
|
+
No module, route, cookie, environment variable, business policy, package manifest,
|
|
8
|
+
lockfile or provider configuration is changed automatically.
|
|
9
|
+
|
|
10
|
+
Consumer runtime requirements: Node 20+ or Bun, `jose@6.2.12` and
|
|
11
|
+
`@redis/client@6.3.0`. Add these through the app's approved dependency workflow;
|
|
12
|
+
on the local Bun host use `bun add --exact jose@6.2.12 @redis/client@6.3.0`.
|
|
13
|
+
Preserve an existing CI/container package-manager contract and its lockfile.
|
|
14
|
+
|
|
15
|
+
`createOidcBffClient` takes immutable explicit issuer, client ID/secret, exact
|
|
16
|
+
HTTPS callback, configured scopes, five same-origin HTTPS metadata endpoints
|
|
17
|
+
(discovery, authorization, token, revocation, JWKS), and optional configured
|
|
18
|
+
authorization parameters. Reserved OIDC/PKCE/state parameters cannot be
|
|
19
|
+
overridden. The consumer owns any provider-specific audience parameter and
|
|
20
|
+
exact issuer/path policy. Only `client_secret_basic`, code/S256 and RS256 signed
|
|
21
|
+
ID tokens are supported. Required scopes include `openid` and `offline_access`;
|
|
22
|
+
authenticated output requires a complete access/refresh token pair.
|
|
23
|
+
|
|
24
|
+
Supply the existing server-owned pending record to `beginAuthorization` and
|
|
25
|
+
`exchangeAuthorizationCode`; the client does not create a second pending record.
|
|
26
|
+
Fresh authentication is required. It verifies signed issuer, subject, client
|
|
27
|
+
audience, authorized party, nonce, expiration, issued time and authentication
|
|
28
|
+
time after pending start, with bounded clock tolerance. Optional signed
|
|
29
|
+
access/code hashes must match. Requested prompt alone does not prove fresh
|
|
30
|
+
authentication; the provider integration must actually perform it.
|
|
31
|
+
|
|
32
|
+
Discovery must match every configured endpoint exactly. No redirect, retry,
|
|
33
|
+
fallback or positive-validation cache exists. A total operation deadline covers
|
|
34
|
+
discovery, token exchange, token custody hook, streamed JWKS, verification and
|
|
35
|
+
best-effort rejected-token cleanup. Responses are capped at 64 KiB; safe token
|
|
36
|
+
fields at 16 KiB; public RSA JWKS at 16 keys and bounded moduli. Errors have fixed
|
|
37
|
+
messages and never include provider bodies, credentials or raw exceptions.
|
|
38
|
+
|
|
39
|
+
The async `onTokenPairIssued` hook stages every independently bounded safe
|
|
40
|
+
credential before scope/type/expiry/ID-token admission policy, including
|
|
41
|
+
access-only or refresh-only responses. At least one safe credential is needed.
|
|
42
|
+
Malformed expiry uses `maxAccessTokenSeconds` only as a retirement hint; this
|
|
43
|
+
fallback is never admitted as a verified token set. Persist the retirement
|
|
44
|
+
record before proceeding. Persistence failure rejects sign-in and attempts
|
|
45
|
+
immediate revocation. A wholly malformed/oversize response cannot safely
|
|
46
|
+
disclose token material; it is rejected without session admission.
|
|
47
|
+
|
|
48
|
+
`OpaqueSessionStore` accepts injected `SessionCommands`, a trusted stable client
|
|
49
|
+
namespace, optional clock (integer seconds) and immutable duration settings.
|
|
50
|
+
Browser cookies contain only random handles; pending PKCE and token material
|
|
51
|
+
remain server-side. Client-hashed Redis Cluster slots isolate keys. Lua owns
|
|
52
|
+
one-time pending claim/completion, browser/generation/client binding, fresh
|
|
53
|
+
account switch, current-session index, logout, staged exact-pair transfer and
|
|
54
|
+
durable revocation enqueue/lease/ack/retry. Only successful exact pending and
|
|
55
|
+
pair commit removes the staged ticket. Once any worker claims retirement, a
|
|
56
|
+
durable non-admissible marker prevents later session commit through retries or
|
|
57
|
+
expired leases; only completed retirement removes that marker. Expired access retains refresh custody.
|
|
58
|
+
Restart uses the same backend/client namespace; there is no local fallback.
|
|
59
|
+
|
|
60
|
+
Defaults: session 3300s, pending 600s, browser generation 1 year, provider access
|
|
61
|
+
maximum 3600s, provider refresh maximum 86400s, token custody 90000s, lease 30s,
|
|
62
|
+
retry 60s. These are conservative documented defaults, not discovery facts.
|
|
63
|
+
The owner must configure provider lifetime bounds explicitly and keep custody
|
|
64
|
+
strictly longer than both provider maxima. Options are bounded and cannot
|
|
65
|
+
reduce custody below those bounds. Retirement expiry hints are not session
|
|
66
|
+
admission. Do not expire the whole token row with browser access expiry.
|
|
67
|
+
|
|
68
|
+
`createValkeySessionCommands` takes an explicit trusted URL, timeout and optional
|
|
69
|
+
test connection seam. TLS (`rediss`) is default; plaintext requires an explicit
|
|
70
|
+
trusted-internal opt-in. Connect and command share a bounded deadline, disable
|
|
71
|
+
offline queues/reconnects, and share one connection attempt. No command runs
|
|
72
|
+
after its connection deadline. `close` permanently retires this command owner.
|
|
73
|
+
No URL/token logging occurs. The Redis library owns RESP framing; this adapter
|
|
74
|
+
bounds deadlines/queued commands and request arguments, and the store validates
|
|
75
|
+
returned records. It does not claim streamed HTTP-style response byte control.
|
|
76
|
+
|
|
77
|
+
Each app still owns configuration, cookie flags, same-origin guards, Next
|
|
78
|
+
server-only marker, authorization wrappers, locale/navigation and durable worker
|
|
79
|
+
scheduling. Each request must perform current provider and canonical domain
|
|
80
|
+
admission. Provider traits, JWT subject or an existing browser session are not
|
|
81
|
+
business permissions. Per-app token retirement leaves shared provider SSO;
|
|
82
|
+
global logout requires its explicitly owned provider operation.
|
|
83
|
+
|
|
84
|
+
Run generated unit cases with `bun test src/shared/oidc-bff`. Actual Lua proof
|
|
85
|
+
additionally requires `OIDC_BFF_TEST_VALKEY_URL` pointing to a disposable Redis
|
|
86
|
+
database with a separately authorized profile. Tests use random client
|
|
87
|
+
namespaces and never flush, stop or alter the server. Unit/source evidence is
|
|
88
|
+
separate from CI, deployed provider behavior, browser and authorized actor proof.
|
|
89
|
+
|
|
90
|
+
Storage custody: key hashing and Redis Cluster hash tags do not encrypt the JSON
|
|
91
|
+
values. Access/refresh credentials and PKCE material are plaintext within the
|
|
92
|
+
server store. Production activation requires authenticated app-scoped Redis
|
|
93
|
+
ACLs, verified private/TLS transport, and verified encryption/access controls
|
|
94
|
+
for persistent storage and backups. An unauthenticated shared Redis instance is
|
|
95
|
+
not a suitable production deployment. This recipe does not provision or prove
|
|
96
|
+
those operator controls.
|