@o3co/create-auth-provider 0.17.0 → 0.18.0-rc.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.
Files changed (114) hide show
  1. package/README.md +2 -198
  2. package/main.js +4 -0
  3. package/package.json +8 -30
  4. package/README.ja.md +0 -200
  5. package/dist/cli.d.mts +0 -3
  6. package/dist/cli.d.mts.map +0 -1
  7. package/dist/cli.mjs +0 -18
  8. package/dist/index.d.mts +0 -48
  9. package/dist/index.d.mts.map +0 -1
  10. package/dist/index.mjs +0 -325
  11. package/dist/internal/template-filter.d.mts +0 -14
  12. package/dist/internal/template-filter.d.mts.map +0 -1
  13. package/dist/internal/template-filter.mjs +0 -42
  14. package/templates/standalone/.dockerignore +0 -23
  15. package/templates/standalone/.env.example +0 -152
  16. package/templates/standalone/Dockerfile +0 -125
  17. package/templates/standalone/Makefile +0 -12
  18. package/templates/standalone/README.ja.md +0 -912
  19. package/templates/standalone/README.md +0 -1536
  20. package/templates/standalone/config/application.conf +0 -351
  21. package/templates/standalone/config/clients.yaml +0 -4
  22. package/templates/standalone/config/clients.yaml.example +0 -124
  23. package/templates/standalone/config/development.conf +0 -17
  24. package/templates/standalone/config/mailpit.conf +0 -13
  25. package/templates/standalone/config/production.conf +0 -15
  26. package/templates/standalone/config/reference.conf +0 -427
  27. package/templates/standalone/docker-compose.mailpit.yml +0 -39
  28. package/templates/standalone/docker-compose.production.yml +0 -229
  29. package/templates/standalone/docker-compose.test.yml +0 -16
  30. package/templates/standalone/docker-compose.yml +0 -67
  31. package/templates/standalone/gitignore +0 -23
  32. package/templates/standalone/package.json +0 -56
  33. package/templates/standalone/src/__tests__/adapters.test.mts +0 -425
  34. package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +0 -87
  35. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +0 -1188
  36. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +0 -285
  37. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +0 -1154
  38. package/templates/standalone/src/__tests__/audit-sink.test.mts +0 -355
  39. package/templates/standalone/src/__tests__/backchannel-peer.fixture.mts +0 -64
  40. package/templates/standalone/src/__tests__/config-notices.test.mts +0 -157
  41. package/templates/standalone/src/__tests__/configPath.test.mts +0 -69
  42. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +0 -48
  43. package/templates/standalone/src/__tests__/core-federations.test.mts +0 -252
  44. package/templates/standalone/src/__tests__/core-sections.test.mts +0 -192
  45. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +0 -95
  46. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +0 -1291
  47. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +0 -787
  48. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +0 -464
  49. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +0 -141
  50. package/templates/standalone/src/__tests__/federation-types.test.mts +0 -239
  51. package/templates/standalone/src/__tests__/frontchannel-logout-csp.test.mts +0 -168
  52. package/templates/standalone/src/__tests__/google-session.fixture.mts +0 -318
  53. package/templates/standalone/src/__tests__/host-routes.test.mts +0 -134
  54. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +0 -29
  55. package/templates/standalone/src/__tests__/library-references.fixture.mts +0 -188
  56. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +0 -217
  57. package/templates/standalone/src/__tests__/listen.test.mts +0 -121
  58. package/templates/standalone/src/__tests__/logger-projection.test.mts +0 -77
  59. package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +0 -113
  60. package/templates/standalone/src/__tests__/mail-sender.test.mts +0 -134
  61. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -393
  62. package/templates/standalone/src/__tests__/metrics.test.mts +0 -253
  63. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +0 -122
  64. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +0 -127
  65. package/templates/standalone/src/__tests__/mfa-switch.test.mts +0 -883
  66. package/templates/standalone/src/__tests__/own-modules.test.mts +0 -1480
  67. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +0 -183
  68. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +0 -283
  69. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +0 -135
  70. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +0 -214
  71. package/templates/standalone/src/__tests__/redis-stand-in.fixture.mts +0 -98
  72. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +0 -249
  73. package/templates/standalone/src/__tests__/replica-safety.test.mts +0 -657
  74. package/templates/standalone/src/__tests__/repositories-logger.test.mts +0 -119
  75. package/templates/standalone/src/__tests__/router-error-answers.test.mts +0 -146
  76. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +0 -610
  77. package/templates/standalone/src/__tests__/sections.test.mts +0 -516
  78. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +0 -213
  79. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +0 -305
  80. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +0 -130
  81. package/templates/standalone/src/__tests__/session-requirements.test.mts +0 -382
  82. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +0 -274
  83. package/templates/standalone/src/__tests__/shutdown.test.mts +0 -622
  84. package/templates/standalone/src/__tests__/smoke.test.mts +0 -642
  85. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +0 -164
  86. package/templates/standalone/src/__tests__/store-credential.test.mts +0 -314
  87. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +0 -296
  88. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +0 -265
  89. package/templates/standalone/src/__tests__/two-phase-config.test.mts +0 -605
  90. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +0 -385
  91. package/templates/standalone/src/adapters.mts +0 -162
  92. package/templates/standalone/src/app.mts +0 -127
  93. package/templates/standalone/src/bootRefusal.mts +0 -117
  94. package/templates/standalone/src/buildModules.mts +0 -358
  95. package/templates/standalone/src/configPath.mts +0 -487
  96. package/templates/standalone/src/listen.mts +0 -60
  97. package/templates/standalone/src/logger.mts +0 -80
  98. package/templates/standalone/src/metrics.mts +0 -167
  99. package/templates/standalone/src/mfaSwitch.mts +0 -284
  100. package/templates/standalone/src/modules.mts +0 -705
  101. package/templates/standalone/src/rootRenames.mts +0 -95
  102. package/templates/standalone/src/routes.mts +0 -89
  103. package/templates/standalone/src/sections.mts +0 -378
  104. package/templates/standalone/src/securityHeaders.mts +0 -36
  105. package/templates/standalone/src/shutdown.mts +0 -213
  106. package/templates/standalone/tests/Makefile +0 -5
  107. package/templates/standalone/tests/index.test.js +0 -283
  108. package/templates/standalone/tests/package-lock.json +0 -1617
  109. package/templates/standalone/tests/package.json +0 -16
  110. package/templates/standalone/tests/vitest.config.mjs +0 -26
  111. package/templates/standalone/tsconfig.json +0 -31
  112. package/templates/standalone/vitest.config.mts +0 -31
  113. package/templates/standalone/vitest.supertest-loopback.mts +0 -278
  114. package/templates/versions.json +0 -20
@@ -1,95 +0,0 @@
1
- /*
2
- * Copyright 2026 1o1 Co. Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- /**
18
- * The variables renamed with a path of the composition root's own layers,
19
- * which no module declares: the adapter selections and the federations the
20
- * template ships. Phase one refuses an old name set, alone or beside its new
21
- * name at any value, before any module is chosen, as boot refuses a
22
- * variable a module declares renamed (`environment-variable-renamed`).
23
- * Boot's own check cannot hold these: an adapter selection decides which
24
- * modules are chosen before boot, and a federation's key sits in core's
25
- * section, whose renames core alone declares.
26
- */
27
-
28
- import { variablesRenamed } from "./bootRefusal.mjs";
29
-
30
- /**
31
- * A variable renamed with a path of the composition root's own: its old name,
32
- * its new one, the path the new one binds, and the module that path's section
33
- * is named after in a refusal's details (`bootRefusal.mts`).
34
- */
35
- export interface RootRename {
36
- readonly module: string;
37
- readonly from: string;
38
- readonly to: string;
39
- readonly path: string;
40
- }
41
-
42
- /**
43
- * Refuses, with an `environment-variable-renamed` `BootError`, an old name in
44
- * `env` set alone (`unset`), or beside its new name (`different`, whatever
45
- * either holds), in the words core refuses a module's rename in. Every such
46
- * rename is named at once; no value is compared, quoted or carried.
47
- */
48
- export function refuseRenamedVariables(
49
- env: Readonly<Record<string, string>>,
50
- renames: readonly RootRename[],
51
- ): void {
52
- const refused = renames.flatMap((rename) => {
53
- const { from, to } = rename;
54
- if (env[from] === undefined) return [];
55
- return [
56
- { ...rename, state: env[to] === undefined ? ("unset" as const) : ("different" as const) },
57
- ];
58
- });
59
- if (refused.length === 0) return;
60
- const named = refused.map(({ from, to, path, state }) => {
61
- const renamed = `${from} was renamed ${to}, the variable ${path} is bound to; see the upgrade guide (docs/upgrading-from-v0.16.0.md).`;
62
- return state === "unset"
63
- ? `${renamed} Set ${to} instead and unset ${from}.`
64
- : `${renamed} ${to} is set as well: keep the value you mean in ${to} and unset ${from}.`;
65
- });
66
- throw variablesRenamed(
67
- `The environment sets ${refused.length} variable(s) that were renamed: ${named.join(" ")}`,
68
- refused.map(({ module, from, to, path, state }) => ({ module, from, to, path, state })),
69
- );
70
- }
71
-
72
- /**
73
- * The variables `config/application.conf` binds for the two federations the
74
- * template ships, each renamed after its path under `core.federations`: in
75
- * core's section, so named under module "core" in a refusal's details.
76
- */
77
- export const SHIPPED_FEDERATION_RENAMES: readonly RootRename[] = (
78
- [
79
- ["GOOGLE", "google", ["ENABLED", "enabled"]],
80
- ["GOOGLE", "google", ["CLIENT_ID", "clientId"]],
81
- ["GOOGLE", "google", ["CLIENT_SECRET", "clientSecret"]],
82
- ["GOOGLE", "google", ["CALLBACK_URL", "callbackURL"]],
83
- ["GOOGLE", "google", ["ACCESS_TYPE", "accessType"]],
84
- ["OIDC", "oidc", ["ENABLED", "enabled"]],
85
- ["OIDC", "oidc", ["ISSUER", "issuer"]],
86
- ["OIDC", "oidc", ["CLIENT_ID", "clientId"]],
87
- ["OIDC", "oidc", ["CLIENT_SECRET", "clientSecret"]],
88
- ["OIDC", "oidc", ["CALLBACK_URL", "callbackURL"]],
89
- ] as const
90
- ).map(([federation, name, [key, path]]) => ({
91
- module: "core",
92
- from: `FEDERATIONS_${federation}_${key}`,
93
- to: `CORE_FEDERATIONS_${federation}_${key}`,
94
- path: `core.federations.${name}.${path}`,
95
- }));
@@ -1,89 +0,0 @@
1
- /*
2
- * Copyright 2026 1o1 Co. Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- /*
18
- * The host's routes, in the order `app.mts` mounts them: liveness, readiness
19
- * and the metrics scrape ahead of the composed auth router, so they keep
20
- * answering while the auth pipeline is degraded (which is when an operator
21
- * needs them); then that router; then core's terminal error handler, last,
22
- * because Express hands an error only to handlers mounted after the route
23
- * that raised it. A function rather than inline in `app.mts`, so the order
24
- * and the handler can be tested as the process mounts them.
25
- *
26
- * The handler is core's `terminalErrorHandler`, the one that already ends the
27
- * composed router, so an error a host route lets through is answered as every
28
- * other route's is (see core's `middleware/terminalError.mts`).
29
- */
30
-
31
- import {
32
- createHealthcheckRouter,
33
- createReadinessRouter,
34
- type Logger,
35
- type ReadinessProbe,
36
- terminalErrorHandler,
37
- } from "@o3co/auth-provider-core";
38
- import express, { type Express, type Router } from "express";
39
- import type { Metrics } from "./metrics.mjs";
40
-
41
- export interface MountRoutesOptions {
42
- /** The router `createApp` composed (`handle.router`). */
43
- readonly router: Router;
44
- /** The readiness probes the builders registered (`handle.readinessProbes`). */
45
- readonly probes: readonly ReadinessProbe[];
46
- /** Per-probe deadline, in milliseconds: the `http` module's `readinessTimeoutMs`. */
47
- readonly readinessTimeoutMs: number;
48
- readonly metrics: Metrics;
49
- readonly logger: Logger;
50
- }
51
-
52
- /** Mount the host's routes, the composed router and the terminal error handler on `app`. */
53
- export function mountRoutes(app: Express, options: MountRoutesOptions): void {
54
- // Liveness: the process is up and its event loop is turning. Deliberately
55
- // static — restarting the process would not bring Redis back, so a Redis
56
- // outage must not read as "this container is broken, kill it".
57
- app.use(createHealthcheckRouter(express));
58
-
59
- // Readiness: can this replica serve right now? Redis backs sessions,
60
- // authorization codes and refresh-token families in the deployable
61
- // defaults, so a replica that has lost it answers 503 here and should be
62
- // taken out of rotation. Probes are contributed by the builders that own
63
- // each connection; a memory-only deployment registers none and is always
64
- // ready.
65
- app.use(
66
- createReadinessRouter(express, {
67
- probes: options.probes,
68
- timeoutMs: options.readinessTimeoutMs,
69
- logger: options.logger,
70
- }),
71
- );
72
-
73
- // Prometheus scrape endpoint. Same reasoning as the probes above: it has to
74
- // keep answering while the auth pipeline is degraded, because that is when
75
- // the series matter.
76
- app.use(
77
- options.metrics.route(express, {
78
- probes: options.probes,
79
- probeTimeoutMs: options.readinessTimeoutMs,
80
- }),
81
- );
82
-
83
- // The composed auth router, which ends in core's terminal handler.
84
- app.use(options.router);
85
- // Core's terminal handler again, LAST, for the host routes
86
- // above: Express routes an error only to handlers registered after the
87
- // route that threw it. See the file header.
88
- app.use(terminalErrorHandler(options.logger));
89
- }
@@ -1,378 +0,0 @@
1
- /*
2
- * Copyright 2026 1o1 Co. Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- /**
18
- * The schemas of the template's own sections: its modules' — `logging`,
19
- * `http` (with its CORS list), `key-store`, `redis-clients`, `repositories`,
20
- * the in-process code repository's and `audit-sink` — and the composition
21
- * root's own `adapters` and `mfaMode`. Each is strict, a key it does not
22
- * declare refused;
23
- * each reads the strings an environment variable carries; none holds a
24
- * default, which lives in `config/reference.conf`. The rules a value is held
25
- * to are core's shared vocabulary (trusted-proxy entries, serialized origins,
26
- * key ids), applied here, never restated.
27
- */
28
-
29
- import {
30
- checkSerializedOrigin,
31
- checkTrustedProxyEntry,
32
- coerceBooleanFromEnv,
33
- describeSerializedOriginRejection,
34
- describeTrustedProxyEntryRejection,
35
- isWellFormedKid,
36
- MAX_KID_LENGTH,
37
- MAX_TRUST_PROXY_HOPS,
38
- normalizeAllowedOrigins,
39
- wholeNumberFromEnv,
40
- wholeNumberInRangeFromEnv,
41
- } from "@o3co/auth-provider-core";
42
- import { mfaConfigSchema } from "@o3co/auth-provider-mfa";
43
- import { z } from "zod";
44
-
45
- /** `logging`: the level the process logs at. `silent` is a threshold, not a level. */
46
- export const loggingSectionSchema = z
47
- .object({ level: z.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]) })
48
- .strict();
49
-
50
- /** What the logger is built from: the `logging` module's section. */
51
- export type LoggingSettings = z.output<typeof loggingSectionSchema>;
52
-
53
- /**
54
- * A decimal, optionally signed or fractional: the variable's shapes meant as a
55
- * hop count, so `-1` and `1.5` fail as hop counts instead of reading as
56
- * one-entry address lists.
57
- */
58
- const NUMERIC_STRING = /^-?[0-9]+(\.[0-9]+)?$/;
59
-
60
- /**
61
- * A `trust proxy` value in one of Express's shapes: the variable's string read
62
- * as `true` / `false` / a hop count / a comma-separated list, a list's entries
63
- * trimmed. `1` and `0` are hop counts here, not booleans. An exported-but-empty
64
- * variable is `false`: trust nothing.
65
- */
66
- const normalizeTrustProxy = (raw: unknown): unknown => {
67
- if (Array.isArray(raw)) {
68
- return raw.map((entry) => (typeof entry === "string" ? entry.trim() : entry));
69
- }
70
- if (typeof raw !== "string") return raw;
71
- const value = raw.trim();
72
- if (value === "") return false;
73
- const lower = value.toLowerCase();
74
- if (lower === "true") return true;
75
- if (lower === "false") return false;
76
- if (NUMERIC_STRING.test(value)) return Number(value);
77
- return value
78
- .split(",")
79
- .map((entry) => entry.trim())
80
- .filter((entry) => entry !== "");
81
- };
82
-
83
- /**
84
- * `http.trustProxy`, handed to Express's `trust proxy`: `false` (trust
85
- * nothing), `true` (trust every hop), a hop count up to
86
- * `MAX_TRUST_PROXY_HOPS`, or a non-empty list of IP literals, CIDR ranges and
87
- * named ranges, each held to core's trusted-proxy vocabulary so a typo fails
88
- * at boot, naming its index, instead of never matching.
89
- */
90
- const trustProxySchema = z
91
- .preprocess(
92
- normalizeTrustProxy,
93
- z.union([
94
- z.boolean(),
95
- z.number().int().min(0).max(MAX_TRUST_PROXY_HOPS),
96
- z
97
- .array(z.string())
98
- .min(
99
- 1,
100
- "must list at least one address, CIDR range, or named range — use `false` to trust no forwarding hop",
101
- ),
102
- ]),
103
- )
104
- .superRefine((value, ctx) => {
105
- if (!Array.isArray(value)) return;
106
- value.forEach((entry, index) => {
107
- const rejection = checkTrustedProxyEntry(entry);
108
- if (rejection !== null) {
109
- ctx.addIssue({
110
- code: "custom",
111
- message: `http.trustProxy[${index}] ${describeTrustedProxyEntryRejection(rejection)}`,
112
- path: [index],
113
- });
114
- }
115
- });
116
- });
117
-
118
- /**
119
- * `http.cors.allowedOrigins`: the browser origins allowed to read the token,
120
- * userinfo, revocation and discovery/JWKS responses; empty is CORS off. A list,
121
- * or the one comma-separated string `HTTP_CORS_ALLOWED_ORIGINS` carries
122
- * (`normalizeAllowedOrigins`, core's reading of an origin list);
123
- * `null` reads as no origins, any other shape is refused. Each entry must be a
124
- * serialized origin, matched by exact string equality: one that could never
125
- * match is refused, naming its index.
126
- */
127
- const allowedOriginsSchema = z
128
- .preprocess((raw, ctx) => {
129
- if (raw === undefined) return raw;
130
- if (raw !== null && typeof raw !== "string" && !Array.isArray(raw)) {
131
- ctx.addIssue({
132
- code: "custom",
133
- message: `http.cors.allowedOrigins must be a list of origins, or one comma-separated string of them (HTTP_CORS_ALLOWED_ORIGINS); got ${typeof raw === "object" ? "an object" : `a ${typeof raw}`}`,
134
- });
135
- return raw;
136
- }
137
- return normalizeAllowedOrigins(raw);
138
- }, z.array(z.string()))
139
- .superRefine((value, ctx) => {
140
- value.forEach((entry, index) => {
141
- const rejection = checkSerializedOrigin(entry);
142
- if (rejection !== null) {
143
- ctx.addIssue({
144
- code: "custom",
145
- message: `http.cors.allowedOrigins[${index}] ${describeSerializedOriginRejection(rejection)}`,
146
- path: [index],
147
- });
148
- }
149
- });
150
- });
151
-
152
- const PORT_RULE =
153
- "http.port must be a port number from 0 to 65535 in decimal digits, 0 letting the OS choose a free port (HTTP_PORT); an exported-but-empty HTTP_PORT is not one";
154
-
155
- /**
156
- * `http.port`, read strictly: an empty or non-decimal value fails boot instead
157
- * of reading as 0, which would listen on a port the OS picks at random.
158
- */
159
- const portSchema = wholeNumberFromEnv(
160
- z
161
- .number({ error: PORT_RULE })
162
- .int({ error: PORT_RULE })
163
- .min(0, { error: PORT_RULE })
164
- .max(65_535, { error: PORT_RULE }),
165
- );
166
-
167
- /**
168
- * `http`: the listener's port, the trusted forwarding hops, the readiness
169
- * deadline, and the CORS list. The deadline is bounded both ways, because
170
- * `setTimeout` turns 0 and anything above 2^31-1 into 1 ms: every probe would
171
- * time out.
172
- */
173
- export const httpSectionSchema = z
174
- .object({
175
- port: portSchema,
176
- trustProxy: trustProxySchema,
177
- readinessTimeoutMs: wholeNumberInRangeFromEnv(1, 2_147_483_647),
178
- cors: z.object({ allowedOrigins: allowedOriginsSchema }).strict(),
179
- })
180
- .strict();
181
-
182
- /** A key id `verifyJwt` accepts: one it would refuse would fail every token signed under it. */
183
- const kidSchema = z.string().refine(isWellFormedKid, {
184
- message: `must be a key id: a string of 1 to ${MAX_KID_LENGTH} characters with no control character`,
185
- });
186
-
187
- /** HS256: a shared secret, rotated through `previousSecrets`. */
188
- const hs256LocalSchema = z
189
- .object({
190
- algorithm: z.literal("HS256"),
191
- kid: kidSchema,
192
- secret: z.string().optional(),
193
- previousSecrets: z
194
- .array(z.object({ kid: kidSchema, secret: z.string(), expiresAt: z.string() }).strict())
195
- .optional(),
196
- })
197
- .strict();
198
-
199
- /**
200
- * RS256, ES256, EdDSA: a key pair, rotated through `previousKeys`. `secret`,
201
- * which the reference binds for every algorithm, and `previousSecrets` are
202
- * accepted and left to the key store, which names what an asymmetric key
203
- * cannot use them for.
204
- */
205
- const asymmetricLocalSchema = z
206
- .object({
207
- algorithm: z.enum(["RS256", "ES256", "EdDSA"]),
208
- kid: kidSchema,
209
- privateKey: z.string().optional(),
210
- privateKeyPath: z.string().optional(),
211
- publicKey: z.string().optional(),
212
- publicKeyPath: z.string().optional(),
213
- previousKeys: z
214
- .array(
215
- z
216
- .object({
217
- kid: kidSchema,
218
- publicKey: z.string().optional(),
219
- publicKeyPath: z.string().optional(),
220
- expiresAt: z.string(),
221
- })
222
- .strict(),
223
- )
224
- .optional(),
225
- secret: z.string().optional(),
226
- previousSecrets: z.unknown().optional(),
227
- })
228
- .strict();
229
-
230
- /**
231
- * `key-store`: which key store builds the signing key (`provider`, `local` the
232
- * one this template registers) and the local store's settings, shape only:
233
- * whether the key material is there is the key store's check, when it is
234
- * built.
235
- */
236
- export const keyStoreSectionSchema = z
237
- .object({
238
- provider: z.string(),
239
- local: z.discriminatedUnion("algorithm", [hs256LocalSchema, asymmetricLocalSchema]).optional(),
240
- })
241
- .strict();
242
-
243
- /**
244
- * `redis-clients`: the one Redis connection every Redis-backed store shares.
245
- * `assumeNoEviction` is the operator's assertion that the server runs
246
- * `maxmemory-policy noeviction`, read by the Redis stores' eviction gate only
247
- * where the server will not report its policy; the template's
248
- * `config/reference.conf` ships it false.
249
- */
250
- export const redisClientsSectionSchema = z
251
- .object({
252
- url: z.string(),
253
- password: z.string().optional(),
254
- assumeNoEviction: coerceBooleanFromEnv,
255
- })
256
- .strict();
257
-
258
- /**
259
- * `adapters`: which adapter fills each slot, the composition root's own
260
- * choice. `userSessionStores` switches the four user-session stores and the
261
- * subject-level revocation pair together; the repositories take core's
262
- * `static`, an alias of `yaml` with a block of its own; `mfaFactorStore` and
263
- * `mfaTransactionStore` are read by a composition that installs MFA;
264
- * `auditSink` names a sink the audit-sink module's factory registers.
265
- */
266
- export const adaptersSchema = z
267
- .object({
268
- rateLimiter: z.enum(["memory", "redis"]),
269
- attemptCounter: z.enum(["memory", "redis"]),
270
- userSessionStores: z.enum(["memory", "redis"]),
271
- accessTokenDenylist: z.enum(["memory", "redis"]),
272
- replaySeenSet: z.enum(["memory", "redis"]),
273
- consentStore: z.enum(["none", "memory", "redis"]),
274
- federationTokenStore: z.enum(["memory", "redis"]),
275
- federationGrantStore: z.enum(["none", "memory", "redis"]),
276
- federationGrantIntentStore: z.enum(["none", "memory", "redis"]),
277
- mfaFactorStore: z.enum(["memory", "redis", "store"]),
278
- mfaTransactionStore: z.enum(["memory", "redis"]),
279
- codeRepository: z.enum(["memory", "redis"]),
280
- clientRepository: z.enum(["yaml", "static"]),
281
- userRepository: z.enum(["yaml", "static", "http"]),
282
- auditSink: z.string().min(1),
283
- })
284
- .strict();
285
-
286
- /** The composition root's adapter selections, as `adaptersSchema` reads them. */
287
- export type Adapters = z.output<typeof adaptersSchema>;
288
-
289
- /** A section of keys: an object whose prototype is `Object.prototype` or none. */
290
- export function isPlainSection(value: unknown): value is Readonly<Record<string, unknown>> {
291
- if (typeof value !== "object" || value === null) return false;
292
- const prototype: unknown = Object.getPrototypeOf(value);
293
- return prototype === Object.prototype || prototype === null;
294
- }
295
-
296
- /**
297
- * `mfaMode`: whether the composition root installs MFA, its own choice — the
298
- * MFA package's mode, read with that package's schema for `mfa.mode`. `off`
299
- * installs nothing of MFA; `optional` and `required` install it and are
300
- * written to `mfa.mode`.
301
- */
302
- export const mfaSwitchSchema = mfaConfigSchema.shape.mode;
303
-
304
- /** The composition root's MFA switch, as `mfaSwitchSchema` reads it. */
305
- export type MfaSwitch = z.output<typeof mfaSwitchSchema>;
306
-
307
- /** A YAML file's path: a repository reads its entries from it when it is built. */
308
- const yamlSchema = z.object({ path: z.string() }).strict();
309
-
310
- /**
311
- * `repositories`: the YAML client registry, and the user repository's
312
- * settings for each adapter it may be — the YAML file, or the Store's HTTP
313
- * endpoints. `static`, core's alias of `yaml`, reads the same shape from a
314
- * block of its own, which has no default. The HTTP settings are shape only: the repository holds each
315
- * value to its rules (https or loopback, a credential's strength, positive
316
- * whole numbers) when it is built, and names what it refuses.
317
- */
318
- export const repositoriesSectionSchema = z
319
- .object({
320
- client: z.object({ yaml: yamlSchema, static: yamlSchema.optional() }).strict(),
321
- user: z
322
- .object({
323
- yaml: yamlSchema,
324
- static: yamlSchema.optional(),
325
- http: z
326
- .object({
327
- authenticateUrl: z.unknown().optional(),
328
- authenticateByTokenUrl: z.unknown().optional(),
329
- linkFederatedIdentityUrl: z.unknown().optional(),
330
- findSubjectByFederatedIdentityUrl: z.unknown().optional(),
331
- markMfaEnrolledUrl: z.unknown().optional(),
332
- federatedIdentityLookupCoverage: z.unknown().optional(),
333
- bearerToken: z.unknown().optional(),
334
- timeout: z.unknown().optional(),
335
- maxResponseBytes: z.unknown().optional(),
336
- })
337
- .strict(),
338
- })
339
- .strict(),
340
- })
341
- .strict();
342
-
343
- /**
344
- * `repositories` for the adapters `selection` names: `repositoriesSectionSchema`,
345
- * and the `static` block's path required for a repository that selects
346
- * `static`, since the block has no default. Refused at the section's parse,
347
- * naming the path, before any repository is built.
348
- */
349
- export function repositoriesSectionSchemaFor(selection: {
350
- readonly client: Adapters["clientRepository"];
351
- readonly user: Adapters["userRepository"];
352
- }) {
353
- return repositoriesSectionSchema.superRefine((section, ctx) => {
354
- for (const repository of ["client", "user"] as const) {
355
- if (selection[repository] === "static" && section[repository].static === undefined) {
356
- ctx.addIssue({
357
- code: "custom",
358
- path: [repository, "static", "path"],
359
- message: `required when adapters.${repository}Repository is "static": the static block has no default`,
360
- });
361
- }
362
- }
363
- });
364
- }
365
-
366
- /** The in-process code repository's section: the default lifetime, in positive whole seconds. */
367
- export const inMemoryCodeRepositorySectionSchema = z
368
- .object({ defaultExpiresIn: wholeNumberInRangeFromEnv(1) })
369
- .strict();
370
-
371
- /**
372
- * `audit-sink`: each sink's options, keyed by the sink's name, for the sink
373
- * `adapters.auditSink` selects. Open: the sinks are the factory's, and each
374
- * builder holds its own options to its rules.
375
- */
376
- export const auditSinkSectionSchema = z
377
- .record(z.string(), z.record(z.string(), z.unknown()))
378
- .optional();
@@ -1,36 +0,0 @@
1
- /*
2
- * Copyright 2026 1o1 Co. Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- /**
18
- * The template's security headers, on every response: helmet's defaults with
19
- * a Content-Security-Policy that allows nothing by default and no framing. A
20
- * route whose page needs more sets that response's own policy in place of
21
- * this one.
22
- */
23
-
24
- import type { RequestHandler } from "express";
25
- import helmet from "helmet";
26
-
27
- export function securityHeaders(): RequestHandler {
28
- return helmet({
29
- contentSecurityPolicy: {
30
- directives: {
31
- defaultSrc: ["'none'"],
32
- frameAncestors: ["'none'"],
33
- },
34
- },
35
- });
36
- }