@o3co/create-auth-provider 0.16.0 → 0.17.0-rc.2
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.ja.md +66 -21
- package/README.md +76 -31
- package/dist/index.d.mts +33 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +118 -59
- package/dist/internal/template-filter.d.mts +7 -10
- package/dist/internal/template-filter.d.mts.map +1 -1
- package/dist/internal/template-filter.mjs +7 -10
- package/package.json +6 -4
- package/templates/standalone/.dockerignore +15 -2
- package/templates/standalone/.env.example +59 -29
- package/templates/standalone/Dockerfile +6 -2
- package/templates/standalone/README.ja.md +324 -169
- package/templates/standalone/README.md +618 -208
- package/templates/standalone/config/application.conf +211 -352
- package/templates/standalone/config/clients.yaml +2 -0
- package/templates/standalone/config/clients.yaml.example +4 -0
- package/templates/standalone/config/development.conf +13 -4
- package/templates/standalone/config/mailpit.conf +8 -0
- package/templates/standalone/config/production.conf +5 -6
- package/templates/standalone/config/reference.conf +418 -0
- package/templates/standalone/docker-compose.mailpit.yml +36 -0
- package/templates/standalone/docker-compose.production.yml +63 -32
- package/templates/standalone/docker-compose.yml +13 -3
- package/templates/standalone/gitignore +1 -0
- package/templates/standalone/package.json +9 -6
- package/templates/standalone/src/__tests__/adapters.test.mts +419 -0
- package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +287 -198
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +71 -35
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +148 -94
- package/templates/standalone/src/__tests__/audit-sink.test.mts +84 -81
- package/templates/standalone/src/__tests__/config-notices.test.mts +154 -0
- package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
- package/templates/standalone/src/__tests__/core-federations.test.mts +249 -0
- package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +10 -15
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +970 -288
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +421 -86
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +87 -72
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +16 -8
- package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +97 -93
- package/templates/standalone/src/__tests__/host-routes.test.mts +4 -11
- package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
- package/templates/standalone/src/__tests__/library-references.fixture.mts +188 -0
- package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
- package/templates/standalone/src/__tests__/listen.test.mts +6 -13
- package/templates/standalone/src/__tests__/logger-projection.test.mts +1 -1
- package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +326 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +4 -9
- package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +825 -0
- package/templates/standalone/src/__tests__/own-modules.test.mts +1486 -0
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +10 -14
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +1 -5
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +116 -81
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
- package/templates/standalone/src/__tests__/replica-safety.test.mts +265 -146
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +4 -7
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +278 -103
- package/templates/standalone/src/__tests__/sections.test.mts +480 -0
- package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +69 -61
- package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
- package/templates/standalone/src/__tests__/session-requirements.test.mts +382 -0
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +270 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +163 -96
- package/templates/standalone/src/__tests__/smoke.test.mts +176 -258
- package/templates/standalone/src/__tests__/store-credential.test.mts +88 -52
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +131 -102
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +604 -0
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +382 -0
- package/templates/standalone/src/adapters.mts +162 -0
- package/templates/standalone/src/app.mts +65 -79
- package/templates/standalone/src/bootRefusal.mts +117 -0
- package/templates/standalone/src/buildModules.mts +220 -309
- package/templates/standalone/src/configPath.mts +449 -29
- package/templates/standalone/src/listen.mts +12 -18
- package/templates/standalone/src/logger.mts +31 -60
- package/templates/standalone/src/metrics.mts +24 -47
- package/templates/standalone/src/mfaSwitch.mts +285 -0
- package/templates/standalone/src/modules.mts +423 -629
- package/templates/standalone/src/rootRenames.mts +96 -0
- package/templates/standalone/src/routes.mts +8 -14
- package/templates/standalone/src/sections.mts +367 -0
- package/templates/standalone/src/shutdown.mts +45 -123
- package/templates/standalone/tests/index.test.js +6 -5
- package/templates/standalone/tests/package-lock.json +1615 -2223
- package/templates/standalone/tests/package.json +2 -2
- package/templates/standalone/tests/vitest.config.mjs +26 -0
- package/templates/standalone/vitest.config.mts +8 -7
- package/templates/standalone/vitest.supertest-loopback.mts +97 -69
- package/templates/versions.json +18 -15
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -135
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -199
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -138
|
@@ -29,14 +29,23 @@ import {
|
|
|
29
29
|
memoryRefreshTokenFamilyStoreModule,
|
|
30
30
|
registerBuiltinKeyStores,
|
|
31
31
|
} from "@o3co/auth-provider-core";
|
|
32
|
+
import { coreConfigForTests } from "@o3co/auth-provider-core/testing";
|
|
33
|
+
import type { OAuthSection } from "@o3co/auth-provider-oauth";
|
|
32
34
|
import express from "express";
|
|
33
35
|
import request from "supertest";
|
|
34
|
-
import { afterEach, describe, expect, it
|
|
36
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
35
37
|
import { buildModules } from "../buildModules.mjs";
|
|
38
|
+
import type { Switches } from "../configPath.mjs";
|
|
39
|
+
import type { Adapters } from "../sections.mjs";
|
|
40
|
+
import {
|
|
41
|
+
capturedRenames,
|
|
42
|
+
inProcessAdapters,
|
|
43
|
+
shippedAdapters,
|
|
44
|
+
} from "./library-references.fixture.mjs";
|
|
36
45
|
|
|
37
46
|
const dockerfile = readFileSync(new URL("../../Dockerfile", import.meta.url), "utf8");
|
|
38
47
|
|
|
39
|
-
//
|
|
48
|
+
// The template's shipped default is EdDSA with a published JWKS, so the
|
|
40
49
|
// smoke test signs the way a real deployment of this scaffold does. Generated
|
|
41
50
|
// per run — no key material is committed.
|
|
42
51
|
const smokeKeyPair = generateKeyPairSync("ed25519", {
|
|
@@ -44,41 +53,49 @@ const smokeKeyPair = generateKeyPairSync("ed25519", {
|
|
|
44
53
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
45
54
|
});
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
/** The `http` module's section, as the hand-built configuration below carries it. */
|
|
57
|
+
const HTTP = {
|
|
58
|
+
port: 0,
|
|
59
|
+
trustProxy: false,
|
|
60
|
+
readinessTimeoutMs: 1000,
|
|
61
|
+
cors: { allowedOrigins: [] as string[] },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** The template modules' sections sit beside core's, so the configuration is wider than `AppConfig`. */
|
|
65
|
+
const config: AppConfig & Record<string, unknown> = {
|
|
66
|
+
// What a resolution under an environment that sets none captures of
|
|
67
|
+
// core's renamed variables.
|
|
68
|
+
...{
|
|
69
|
+
"renamed-variables": capturedRenames({}),
|
|
70
|
+
},
|
|
71
|
+
http: HTTP,
|
|
49
72
|
logging: { level: "silent" },
|
|
73
|
+
"key-store": {
|
|
74
|
+
provider: "local",
|
|
75
|
+
local: {
|
|
76
|
+
algorithm: "EdDSA",
|
|
77
|
+
kid: "v0",
|
|
78
|
+
privateKey: smokeKeyPair.privateKey,
|
|
79
|
+
publicKey: smokeKeyPair.publicKey,
|
|
80
|
+
previousKeys: [],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
// A composition with a consumer of session admission states what it
|
|
84
|
+
// expects (ADR 2026-09-28-session-admission): the shipped
|
|
85
|
+
// `application.conf` expects none, and so does this hand-built config.
|
|
86
|
+
...coreConfigForTests({ federations: { google: { enabled: false, type: "google" } } }),
|
|
50
87
|
oauth: {
|
|
51
88
|
jwt: {
|
|
52
89
|
issuer: "https://auth.test",
|
|
53
|
-
signingKey: {
|
|
54
|
-
provider: "local",
|
|
55
|
-
local: {
|
|
56
|
-
algorithm: "EdDSA",
|
|
57
|
-
kid: "v0",
|
|
58
|
-
privateKey: smokeKeyPair.privateKey,
|
|
59
|
-
publicKey: smokeKeyPair.publicKey,
|
|
60
|
-
previousKeys: [],
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
accessToken: { expiresIn: 3600 },
|
|
65
|
-
refreshToken: {
|
|
66
|
-
expiresIn: 86400,
|
|
67
|
-
unknownFamilyPolicy: "reject" as const,
|
|
68
|
-
legacyRtPolicy: "reject" as const,
|
|
69
90
|
},
|
|
91
|
+
accessToken: { defaultExpiresIn: 3600 },
|
|
92
|
+
refreshToken: { expiresIn: 86400 },
|
|
70
93
|
grants: {},
|
|
71
94
|
oidcMode: "oidc-required",
|
|
72
|
-
//
|
|
73
|
-
// flag was removed; the first-party invariant is unconditional.
|
|
74
|
-
// OR-9: explicit memory adapter for the smoke test. The legacy
|
|
75
|
-
// `repositories.code.type` fallback would also pick memory here,
|
|
76
|
-
// but the explicit setting documents the intent and avoids
|
|
77
|
-
// console.warn deprecation noise in the test output.
|
|
78
|
-
code: { adapter: "memory" as const },
|
|
95
|
+
// No `authorize` section: the first-party invariant is unconditional.
|
|
79
96
|
},
|
|
80
|
-
session: {
|
|
81
|
-
//
|
|
97
|
+
"session-store": {
|
|
98
|
+
// `session-store.secret` carries a 256-bit entropy floor.
|
|
82
99
|
secret: "test-session-secret.at-least-32-bytes.ok",
|
|
83
100
|
name: "auth.sid",
|
|
84
101
|
maxAge: 3600000,
|
|
@@ -87,53 +104,45 @@ const config: AppConfig = {
|
|
|
87
104
|
domain: null,
|
|
88
105
|
storage: { type: "memory", redis: { url: "redis://localhost:6379" } },
|
|
89
106
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
},
|
|
94
|
-
federations: {
|
|
95
|
-
google: { enabled: false },
|
|
96
|
-
},
|
|
97
|
-
repositories: {
|
|
98
|
-
client: { type: "yaml", path: "./config/clients.yaml" },
|
|
99
|
-
user: { type: "yaml", path: "./config/users.yaml", timeout: 5000 },
|
|
100
|
-
code: { type: "memory", defaultExpiresIn: 600 },
|
|
101
|
-
},
|
|
102
|
-
endpoints: {
|
|
103
|
-
login: { url: "/login" },
|
|
107
|
+
session: {
|
|
108
|
+
loginPage: { url: "/login" },
|
|
109
|
+
rateLimit: { login: { windowMs: 60000, limit: 10 } },
|
|
104
110
|
},
|
|
105
|
-
|
|
111
|
+
rateLimit: { failMode: "open" },
|
|
112
|
+
"standalone-in-memory-code-repository": { defaultExpiresIn: 600 },
|
|
106
113
|
};
|
|
107
114
|
|
|
115
|
+
/** What phase one hands `buildModules`: the configuration, with `changes` over every store in process. */
|
|
116
|
+
const switchesWith = (changes: Partial<Adapters> = {}): Switches =>
|
|
117
|
+
({
|
|
118
|
+
...config,
|
|
119
|
+
adapters: { ...inProcessAdapters(), ...changes },
|
|
120
|
+
mfaMode: "off",
|
|
121
|
+
}) as unknown as Switches;
|
|
122
|
+
|
|
123
|
+
const switches = switchesWith();
|
|
124
|
+
|
|
108
125
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* Post-OR-9 (Wave 5d): `codeRepository` is no longer provided here — it is
|
|
114
|
-
* wired by `inMemoryCodeRepositoryModule` (or `redisCodeRepositoryModule`)
|
|
115
|
-
* via the `oauth.code.adapter` switch in `buildModules`. The smoke test
|
|
116
|
-
* config sets `oauth.code.adapter = "memory"`, so `inMemoryCodeRepositoryModule`
|
|
117
|
-
* is added automatically and provides the slot.
|
|
118
|
-
*
|
|
119
|
-
* D-6 (v0.5.1): a single confidential client is registered so the
|
|
120
|
-
* `clientAuthMw` middleware in front of `/oauth/token` has a record to
|
|
121
|
-
* authenticate against. Smoke tests that hit `/oauth/token` send
|
|
122
|
-
* `Authorization: ${SMOKE_BASIC_AUTH}`.
|
|
126
|
+
* The one confidential client, so the `clientAuthMw` middleware in front of
|
|
127
|
+
* `/oauth/token` has a record to authenticate against. Smoke tests that hit
|
|
128
|
+
* `/oauth/token` send `Authorization: ${SMOKE_BASIC_AUTH}`.
|
|
123
129
|
*/
|
|
124
130
|
const SMOKE_CLIENT_ID = "smoke-client";
|
|
125
131
|
const SMOKE_CLIENT_SECRET = "smoke-secret";
|
|
126
132
|
const SMOKE_BASIC_AUTH = `Basic ${Buffer.from(`${SMOKE_CLIENT_ID}:${SMOKE_CLIENT_SECRET}`).toString("base64")}`;
|
|
127
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Test-only repository module in place of the file-system-backed repositories
|
|
136
|
+
* `repositoriesModule` provides in production: the smoke tests verify the
|
|
137
|
+
* boot pipeline shape, not the data layer. `codeRepository` is not provided
|
|
138
|
+
* here; the `adapters.codeRepository` selection in `buildModules` wires it.
|
|
139
|
+
*/
|
|
128
140
|
const testRepositoriesModule = defineModule({
|
|
129
141
|
name: "test:repositories",
|
|
130
142
|
provides: {
|
|
131
|
-
// `
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// output even though they are optional on input. The fixture supplies
|
|
135
|
-
// every field explicitly so the literal satisfies the output shape
|
|
136
|
-
// without requiring a cast.
|
|
143
|
+
// The map takes `ClientEntry`, the schema's OUTPUT type, where fields
|
|
144
|
+
// with `.default(...)` are required; every field is supplied so the
|
|
145
|
+
// literal needs no cast.
|
|
137
146
|
clientRepository: () =>
|
|
138
147
|
new InMemoryClientRepository(
|
|
139
148
|
new Map([
|
|
@@ -165,7 +174,7 @@ const testKeyStoreModule = defineModule({
|
|
|
165
174
|
registerBuiltinKeyStores(factory);
|
|
166
175
|
return factory.create({
|
|
167
176
|
type: "local",
|
|
168
|
-
...((c as
|
|
177
|
+
...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
|
|
169
178
|
});
|
|
170
179
|
},
|
|
171
180
|
},
|
|
@@ -176,14 +185,11 @@ describe("standalone smoke test", () => {
|
|
|
176
185
|
|
|
177
186
|
async function buildApp() {
|
|
178
187
|
const handle = await createApp({
|
|
179
|
-
modules: buildModules(
|
|
188
|
+
modules: buildModules(switches, {
|
|
180
189
|
keyStoreModule: testKeyStoreModule,
|
|
181
190
|
repositoriesModule: testRepositoriesModule,
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
// with `redisRefreshTokenFamilyStoreModule`; this override replaces
|
|
185
|
-
// the pair entirely with a single memory-backed store module so no
|
|
186
|
-
// real ioredis connection is opened during CI unit tests.
|
|
191
|
+
// A memory RT family store in place of the default Redis store and
|
|
192
|
+
// its client, so no real ioredis connection is opened.
|
|
187
193
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
188
194
|
}),
|
|
189
195
|
bootstrapComponents: { config, pathResolver: (s) => s },
|
|
@@ -194,7 +200,7 @@ describe("standalone smoke test", () => {
|
|
|
194
200
|
app.use(
|
|
195
201
|
createReadinessRouter(express, {
|
|
196
202
|
probes: handle.readinessProbes,
|
|
197
|
-
timeoutMs:
|
|
203
|
+
timeoutMs: HTTP.readinessTimeoutMs,
|
|
198
204
|
}),
|
|
199
205
|
);
|
|
200
206
|
app.use(handle.router);
|
|
@@ -207,16 +213,11 @@ describe("standalone smoke test", () => {
|
|
|
207
213
|
});
|
|
208
214
|
|
|
209
215
|
it("Dockerfile declares runtime metadata and runs install/build steps as node", () => {
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// default.
|
|
216
|
-
// The literal `${HTTP_PORT}` token from the Dockerfile is split across
|
|
217
|
-
// concatenations to sidestep biome's `noTemplateCurlyInString` rule
|
|
218
|
-
// (it cannot statically tell that this string is intentionally NOT
|
|
219
|
-
// a JS template placeholder — it is a Dockerfile env-substitution).
|
|
216
|
+
// HEALTHCHECK and EXPOSE must read the port from ${HTTP_PORT} so an
|
|
217
|
+
// operator overriding it keeps the app, EXPOSE and the healthcheck in
|
|
218
|
+
// sync; they are asserted by that token, not the literal port. The token
|
|
219
|
+
// is built by concatenation to sidestep biome's
|
|
220
|
+
// `noTemplateCurlyInString`: it is a Dockerfile substitution, not JS.
|
|
220
221
|
const HTTP_PORT_VAR = `$${"{"}HTTP_PORT}`;
|
|
221
222
|
expect(dockerfile).toContain("ENV HTTP_PORT=3000");
|
|
222
223
|
expect(dockerfile).toContain(`EXPOSE ${HTTP_PORT_VAR}`);
|
|
@@ -233,7 +234,7 @@ describe("standalone smoke test", () => {
|
|
|
233
234
|
/FROM node-base AS deps[\s\S]*USER node[\s\S]*RUN pnpm install --frozen-lockfile/,
|
|
234
235
|
);
|
|
235
236
|
expect(dockerfile).toMatch(/FROM deps AS builder[\s\S]*USER node[\s\S]*RUN pnpm run build/);
|
|
236
|
-
//
|
|
237
|
+
// Dependencies are resolved exactly once, in `deps`. The runtime
|
|
237
238
|
// stage must take its node_modules from a prune of that same tree — a
|
|
238
239
|
// second `pnpm install` would be a second resolution, so its absence
|
|
239
240
|
// after the runtime FROM is part of the contract.
|
|
@@ -248,12 +249,12 @@ describe("standalone smoke test", () => {
|
|
|
248
249
|
);
|
|
249
250
|
});
|
|
250
251
|
|
|
251
|
-
it("Dockerfile pins its mutable build inputs
|
|
252
|
+
it("Dockerfile pins its mutable build inputs", () => {
|
|
252
253
|
// Base image by digest — the tag alone is a moving pointer, and
|
|
253
254
|
// Dependabot's docker ecosystem bumps tag and digest together.
|
|
254
255
|
expect(dockerfile).toMatch(/FROM node:26-alpine@sha256:[0-9a-f]{64} AS node-base/);
|
|
255
256
|
// Global corepack by version — `npm install -g corepack` with no pin
|
|
256
|
-
//
|
|
257
|
+
// resolves whatever is latest at build time.
|
|
257
258
|
expect(dockerfile).toMatch(/npm install -g corepack@\d+\.\d+\.\d+ --force/);
|
|
258
259
|
// The lockfile is a required build input for --frozen-lockfile.
|
|
259
260
|
expect(dockerfile).toMatch(/COPY --chown=node:node package\.json pnpm-lock\.yaml/);
|
|
@@ -290,7 +291,7 @@ describe("standalone smoke test", () => {
|
|
|
290
291
|
},
|
|
291
292
|
},
|
|
292
293
|
],
|
|
293
|
-
timeoutMs:
|
|
294
|
+
timeoutMs: HTTP.readinessTimeoutMs,
|
|
294
295
|
}),
|
|
295
296
|
);
|
|
296
297
|
const res = await request(app).get("/readyz");
|
|
@@ -300,24 +301,20 @@ describe("standalone smoke test", () => {
|
|
|
300
301
|
});
|
|
301
302
|
|
|
302
303
|
it("issuer-configured buildModules serves the advertised jwks_uri (discovery <-> JWKS presence contract)", async () => {
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
// presence contract at boot (missing `jwks_uri`) — this test fails (RED) in
|
|
309
|
-
// that case. (The oauth package's integration test hand-composes both
|
|
310
|
-
// modules, so only a scaffold-level test like this can catch the
|
|
311
|
-
// composition root forgetting to wire jwksModule.)
|
|
304
|
+
// Against the REAL composition root: core's `jwksModule` contributes
|
|
305
|
+
// `jwks_uri` to discovery AND mounts the JWKS route. Without it the
|
|
306
|
+
// issuer-configured composition fails the discovery presence contract
|
|
307
|
+
// at boot. The oauth package's integration test composes both modules by
|
|
308
|
+
// hand, so only a scaffold-level test catches `buildModules` dropping it.
|
|
312
309
|
const issuerConfig: AppConfig = {
|
|
313
310
|
...config,
|
|
314
311
|
oauth: {
|
|
315
|
-
...config.oauth,
|
|
316
|
-
jwt: { ...config.oauth.jwt, issuer: "https://auth.example.com" },
|
|
312
|
+
...(config.oauth as OAuthSection),
|
|
313
|
+
jwt: { ...(config.oauth as OAuthSection).jwt, issuer: "https://auth.example.com" },
|
|
317
314
|
},
|
|
318
315
|
};
|
|
319
316
|
const handle = await createApp({
|
|
320
|
-
modules: buildModules(
|
|
317
|
+
modules: buildModules(switches, {
|
|
321
318
|
keyStoreModule: testKeyStoreModule,
|
|
322
319
|
repositoriesModule: testRepositoriesModule,
|
|
323
320
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
@@ -333,7 +330,7 @@ describe("standalone smoke test", () => {
|
|
|
333
330
|
const res = await request(app).get(jwksPath);
|
|
334
331
|
expect(res.status).toBe(200);
|
|
335
332
|
expect(Array.isArray(res.body.keys)).toBe(true);
|
|
336
|
-
//
|
|
333
|
+
// Presence is not enough — the advertised jwks_uri must
|
|
337
334
|
// publish an actual verification key, and never the private half.
|
|
338
335
|
expect(res.body.keys).toHaveLength(1);
|
|
339
336
|
expect(res.body.keys[0].alg).toBe("EdDSA");
|
|
@@ -364,70 +361,58 @@ describe("standalone smoke test", () => {
|
|
|
364
361
|
expect(res.headers["cache-control"]).not.toBe("no-store");
|
|
365
362
|
});
|
|
366
363
|
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
// `extractFederationSection` returned undefined and threw at boot. The
|
|
372
|
-
// fix moved the manifest assembly into `buildModules`, which conditionally
|
|
373
|
-
// includes the federation pair. The first sub-assertion asserts directly
|
|
374
|
-
// on the manifest list — a regression that bypasses the gate fails the
|
|
375
|
-
// `not.toContain` even if `googleFederationConfigModule` is later made
|
|
376
|
-
// tolerant of `undefined` (which would otherwise mask the gating bug).
|
|
377
|
-
// The second sub-assertion verifies the resulting handle still boots
|
|
378
|
-
// (Codex Round 1 P1 reproducibility).
|
|
364
|
+
// A freshly scaffolded app must boot under the default
|
|
365
|
+
// `core.federations.google.enabled = false`. The federation types the
|
|
366
|
+
// template bundles are loaded whatever the configuration says, and with no
|
|
367
|
+
// entry enabled they handle nothing: no federation is registered.
|
|
379
368
|
it("boots when google federation is disabled (default scaffold config)", async () => {
|
|
380
|
-
const modules = buildModules(
|
|
369
|
+
const modules = buildModules(switches, {
|
|
381
370
|
keyStoreModule: testKeyStoreModule,
|
|
382
371
|
repositoriesModule: testRepositoriesModule,
|
|
383
372
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
384
373
|
});
|
|
385
374
|
const moduleNames = modules.map((m) => m.name);
|
|
386
|
-
expect(moduleNames).
|
|
387
|
-
expect(moduleNames).
|
|
375
|
+
expect(moduleNames).toContain("federation-google-type");
|
|
376
|
+
expect(moduleNames).toContain("federation-oidc");
|
|
388
377
|
|
|
389
378
|
const handle = await createApp({
|
|
390
379
|
modules,
|
|
391
380
|
bootstrapComponents: { config, pathResolver: (s) => s },
|
|
392
381
|
});
|
|
393
382
|
handleRef = handle;
|
|
394
|
-
expect(handle).
|
|
383
|
+
expect(handle.components.federationProviders?.size ?? 0).toBe(0);
|
|
395
384
|
});
|
|
396
385
|
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
// above (which substitute the memory store for CI).
|
|
403
|
-
describe("D-2 v2 + Wave 5d: redis-clients + adapter wiring", () => {
|
|
386
|
+
// The composition root must default to the Redis-backed RT family store
|
|
387
|
+
// and the ioredis client module, NOT the in-memory store: multi-replica
|
|
388
|
+
// deployments lose RT family persistence when each replica holds families
|
|
389
|
+
// in-process. The smoke tests above exercise the memory override.
|
|
390
|
+
describe("redis-clients + adapter wiring", () => {
|
|
404
391
|
it("buildModules includes the shared redis-clients module + redis store by default", () => {
|
|
405
|
-
const modules = buildModules(
|
|
392
|
+
const modules = buildModules(switches, {
|
|
406
393
|
keyStoreModule: testKeyStoreModule,
|
|
407
394
|
repositoriesModule: testRepositoriesModule,
|
|
408
395
|
});
|
|
409
396
|
const names = modules.map((m) => m.name);
|
|
410
|
-
|
|
411
|
-
// `standalone:refresh-token-family-client` in PR1).
|
|
412
|
-
expect(names).toContain("standalone:redis-clients");
|
|
397
|
+
expect(names).toContain("redis-clients");
|
|
413
398
|
expect(names).toContain("redis-refresh-token-family-store");
|
|
414
399
|
expect(names).not.toContain("core-refresh-token-family-store-memory");
|
|
415
400
|
});
|
|
416
401
|
|
|
417
402
|
it("buildModules drops the shared redis-clients module when override forces memory-only", () => {
|
|
418
|
-
const modules = buildModules(
|
|
403
|
+
const modules = buildModules(switches, {
|
|
419
404
|
keyStoreModule: testKeyStoreModule,
|
|
420
405
|
repositoriesModule: testRepositoriesModule,
|
|
421
406
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
422
407
|
});
|
|
423
408
|
const names = modules.map((m) => m.name);
|
|
424
409
|
expect(names).toContain("core-refresh-token-family-store-memory");
|
|
425
|
-
expect(names).not.toContain("
|
|
410
|
+
expect(names).not.toContain("redis-clients");
|
|
426
411
|
expect(names).not.toContain("redis-refresh-token-family-store");
|
|
427
412
|
});
|
|
428
413
|
|
|
429
|
-
it("buildModules wires memoryRateLimiterModule by default
|
|
430
|
-
const modules = buildModules(
|
|
414
|
+
it("buildModules wires memoryRateLimiterModule by default", () => {
|
|
415
|
+
const modules = buildModules(switches, {
|
|
431
416
|
keyStoreModule: testKeyStoreModule,
|
|
432
417
|
repositoriesModule: testRepositoriesModule,
|
|
433
418
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
@@ -437,12 +422,8 @@ describe("standalone smoke test", () => {
|
|
|
437
422
|
expect(names).not.toContain("redis-rate-limiter");
|
|
438
423
|
});
|
|
439
424
|
|
|
440
|
-
it("buildModules switches to redisRateLimiterModule when rateLimiter
|
|
441
|
-
const
|
|
442
|
-
...config,
|
|
443
|
-
rateLimiter: { adapter: "redis" as const },
|
|
444
|
-
};
|
|
445
|
-
const modules = buildModules(redisRlConfig, {
|
|
425
|
+
it("buildModules switches to redisRateLimiterModule when adapters.rateLimiter = 'redis'", () => {
|
|
426
|
+
const modules = buildModules(switchesWith({ rateLimiter: "redis" }), {
|
|
446
427
|
keyStoreModule: testKeyStoreModule,
|
|
447
428
|
repositoriesModule: testRepositoriesModule,
|
|
448
429
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
@@ -451,44 +432,35 @@ describe("standalone smoke test", () => {
|
|
|
451
432
|
expect(names).toContain("redis-rate-limiter");
|
|
452
433
|
expect(names).not.toContain("core-rate-limiter-memory");
|
|
453
434
|
// Adapter = redis pulls in the shared redis-clients module too.
|
|
454
|
-
expect(names).toContain("
|
|
435
|
+
expect(names).toContain("redis-clients");
|
|
455
436
|
});
|
|
456
437
|
|
|
457
|
-
it("buildModules switches to redisSessionStoresModule when userSessionStores
|
|
458
|
-
const
|
|
459
|
-
...config,
|
|
460
|
-
userSessionStores: { adapter: "redis" as const },
|
|
461
|
-
};
|
|
462
|
-
const modules = buildModules(redisSessConfig, {
|
|
438
|
+
it("buildModules switches to redisSessionStoresModule when adapters.userSessionStores = 'redis'", () => {
|
|
439
|
+
const modules = buildModules(switchesWith({ userSessionStores: "redis" }), {
|
|
463
440
|
keyStoreModule: testKeyStoreModule,
|
|
464
441
|
repositoriesModule: testRepositoriesModule,
|
|
465
442
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
466
443
|
});
|
|
467
444
|
const names = modules.map((m) => m.name);
|
|
468
|
-
expect(names).toContain("
|
|
469
|
-
expect(names).not.toContain("standalone
|
|
470
|
-
expect(names).toContain("
|
|
445
|
+
expect(names).toContain("redis-session-stores");
|
|
446
|
+
expect(names).not.toContain("standalone-in-memory-session-stores");
|
|
447
|
+
expect(names).toContain("redis-clients");
|
|
471
448
|
});
|
|
472
449
|
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
rateLimiter: { adapter: "redis" as const },
|
|
488
|
-
accessTokenDenylist: { adapter: "redis" as const },
|
|
489
|
-
consentStore: { adapter: "redis" as const },
|
|
490
|
-
oauth: { ...config.oauth, code: { adapter: "redis" as const } },
|
|
491
|
-
};
|
|
450
|
+
// The tests above check which modules are selected, not whether the
|
|
451
|
+
// selection is satisfiable: a `*Client` slot with no provider (such as
|
|
452
|
+
// `redisSessionStoresModule`'s `subjectSessionIndexClient`) fails
|
|
453
|
+
// stage-1 boot with `missing-required-component`. The invariant the
|
|
454
|
+
// shared clients module exists for: every `*Client` slot any selected
|
|
455
|
+
// module requires is one it provides.
|
|
456
|
+
it("redis-clients provides every *Client slot the Redis branches require", () => {
|
|
457
|
+
const allRedisConfig = switchesWith({
|
|
458
|
+
userSessionStores: "redis",
|
|
459
|
+
rateLimiter: "redis",
|
|
460
|
+
accessTokenDenylist: "redis",
|
|
461
|
+
consentStore: "redis",
|
|
462
|
+
codeRepository: "redis",
|
|
463
|
+
});
|
|
492
464
|
// No `refreshTokenFamilyModules` override: the default is the Redis
|
|
493
465
|
// RT-family store, so `refreshTokenFamilyClient` is part of what the
|
|
494
466
|
// invariant covers. Still hermetic — buildModules opens no socket.
|
|
@@ -496,7 +468,7 @@ describe("standalone smoke test", () => {
|
|
|
496
468
|
keyStoreModule: testKeyStoreModule,
|
|
497
469
|
repositoriesModule: testRepositoriesModule,
|
|
498
470
|
});
|
|
499
|
-
const clients = modules.find((m) => m.name === "
|
|
471
|
+
const clients = modules.find((m) => m.name === "redis-clients");
|
|
500
472
|
expect(clients).toBeDefined();
|
|
501
473
|
const provided = new Set(Object.keys(clients?.provides ?? {}));
|
|
502
474
|
const requiredClientSlots = new Set(
|
|
@@ -508,14 +480,14 @@ describe("standalone smoke test", () => {
|
|
|
508
480
|
});
|
|
509
481
|
});
|
|
510
482
|
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
describe("
|
|
483
|
+
// Without an access-token denylist, `/oauth/revoke` would answer 200 for
|
|
484
|
+
// an access token and leave the JWT working until expiry. The template
|
|
485
|
+
// always wires one, and its own application.conf selects the Redis-backed
|
|
486
|
+
// adapter — the memory one forks per replica and `core.deployment.mode =
|
|
487
|
+
// "multi"` refuses it.
|
|
488
|
+
describe("access-token denylist wiring", () => {
|
|
517
489
|
it("always wires a denylist, so /oauth/revoke can keep its promise", () => {
|
|
518
|
-
const modules = buildModules(
|
|
490
|
+
const modules = buildModules(switches, {
|
|
519
491
|
keyStoreModule: testKeyStoreModule,
|
|
520
492
|
repositoriesModule: testRepositoriesModule,
|
|
521
493
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
@@ -527,7 +499,7 @@ describe("standalone smoke test", () => {
|
|
|
527
499
|
});
|
|
528
500
|
|
|
529
501
|
it("defaults to the memory denylist when no adapter is configured", () => {
|
|
530
|
-
const modules = buildModules(
|
|
502
|
+
const modules = buildModules(switches, {
|
|
531
503
|
keyStoreModule: testKeyStoreModule,
|
|
532
504
|
repositoriesModule: testRepositoriesModule,
|
|
533
505
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
@@ -537,12 +509,8 @@ describe("standalone smoke test", () => {
|
|
|
537
509
|
expect(names).not.toContain("redis-access-token-denylist");
|
|
538
510
|
});
|
|
539
511
|
|
|
540
|
-
it("switches to the Redis denylist when accessTokenDenylist
|
|
541
|
-
const
|
|
542
|
-
...config,
|
|
543
|
-
accessTokenDenylist: { adapter: "redis" as const },
|
|
544
|
-
};
|
|
545
|
-
const modules = buildModules(redisDenylistConfig, {
|
|
512
|
+
it("switches to the Redis denylist when adapters.accessTokenDenylist = 'redis'", () => {
|
|
513
|
+
const modules = buildModules(switchesWith({ accessTokenDenylist: "redis" }), {
|
|
546
514
|
keyStoreModule: testKeyStoreModule,
|
|
547
515
|
repositoriesModule: testRepositoriesModule,
|
|
548
516
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
@@ -553,25 +521,24 @@ describe("standalone smoke test", () => {
|
|
|
553
521
|
// The Redis branch must also pull in the shared ioredis socket, or the
|
|
554
522
|
// `accessTokenDenylistClient` slot has no provider and boot fails on a
|
|
555
523
|
// missing component instead of on the thing the operator changed.
|
|
556
|
-
expect(names).toContain("
|
|
524
|
+
expect(names).toContain("redis-clients");
|
|
557
525
|
});
|
|
558
526
|
|
|
559
|
-
it("the
|
|
527
|
+
it("the template's reference.conf selects the replica-safe adapter", () => {
|
|
560
528
|
// The template's own config is the artifact operators deploy. It runs
|
|
561
|
-
// with `deployment.mode = multi` in the umbrella E2E, which refuses
|
|
529
|
+
// with `core.deployment.mode = multi` in the umbrella E2E, which refuses
|
|
562
530
|
// every in-memory shared store — so shipping the memory denylist here
|
|
563
531
|
// would be a boot failure in the very stack that proves the scaffold
|
|
564
532
|
// works.
|
|
565
|
-
|
|
566
|
-
expect(conf).toMatch(/accessTokenDenylist\s*\{[\s\S]*?adapter\s*=\s*"redis"/);
|
|
533
|
+
expect(shippedAdapters().accessTokenDenylist).toBe("redis");
|
|
567
534
|
});
|
|
568
535
|
|
|
569
536
|
it("keeps the composition boot-valid end to end", async () => {
|
|
570
|
-
// The core boot guard
|
|
537
|
+
// The core boot guard refuses a composition that reads
|
|
571
538
|
// the denylist slot without one. A scaffold that trips its own library's
|
|
572
539
|
// guard is not a scaffold.
|
|
573
540
|
const handle = await createApp({
|
|
574
|
-
modules: buildModules(
|
|
541
|
+
modules: buildModules(switches, {
|
|
575
542
|
keyStoreModule: testKeyStoreModule,
|
|
576
543
|
repositoriesModule: testRepositoriesModule,
|
|
577
544
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
@@ -583,100 +550,51 @@ describe("standalone smoke test", () => {
|
|
|
583
550
|
});
|
|
584
551
|
});
|
|
585
552
|
|
|
586
|
-
//
|
|
587
|
-
// the
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
it("buildModules wires inMemoryCodeRepositoryModule by default (oauth.code.adapter = 'memory')", () => {
|
|
593
|
-
const modules = buildModules(config, {
|
|
553
|
+
// The OAuth code repository's selection: the memory branch wires
|
|
554
|
+
// `inMemoryCodeRepositoryModule`; the redis branch wires
|
|
555
|
+
// `redisCodeRepositoryModule` against the shared ioredis socket.
|
|
556
|
+
describe("code-repository adapter wiring", () => {
|
|
557
|
+
it("buildModules wires inMemoryCodeRepositoryModule when adapters.codeRepository = 'memory'", () => {
|
|
558
|
+
const modules = buildModules(switches, {
|
|
594
559
|
keyStoreModule: testKeyStoreModule,
|
|
595
560
|
repositoriesModule: testRepositoriesModule,
|
|
596
561
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
597
562
|
});
|
|
598
563
|
const names = modules.map((m) => m.name);
|
|
599
|
-
expect(names).toContain("standalone
|
|
564
|
+
expect(names).toContain("standalone-in-memory-code-repository");
|
|
600
565
|
expect(names).not.toContain("redis-code-repository");
|
|
601
566
|
});
|
|
602
567
|
|
|
603
|
-
it("buildModules switches to redisCodeRepositoryModule when
|
|
604
|
-
const
|
|
605
|
-
...config,
|
|
606
|
-
oauth: { ...config.oauth, code: { adapter: "redis" as const } },
|
|
607
|
-
};
|
|
608
|
-
const modules = buildModules(redisCodeConfig, {
|
|
568
|
+
it("buildModules switches to redisCodeRepositoryModule when adapters.codeRepository = 'redis'", () => {
|
|
569
|
+
const modules = buildModules(switchesWith({ codeRepository: "redis" }), {
|
|
609
570
|
keyStoreModule: testKeyStoreModule,
|
|
610
571
|
repositoriesModule: testRepositoriesModule,
|
|
611
572
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
612
573
|
});
|
|
613
574
|
const names = modules.map((m) => m.name);
|
|
614
575
|
expect(names).toContain("redis-code-repository");
|
|
615
|
-
expect(names).not.toContain("standalone
|
|
576
|
+
expect(names).not.toContain("standalone-in-memory-code-repository");
|
|
616
577
|
// adapter = "redis" pulls in the shared ioredis socket so the
|
|
617
578
|
// `codeRepositoryClient` slot is satisfied.
|
|
618
|
-
expect(names).toContain("
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
it("buildModules honors legacy repositories.code.type='redis' with one object-first deprecation warn when oauth.code.adapter is absent", () => {
|
|
622
|
-
const warn = vi.fn();
|
|
623
|
-
const logger = {
|
|
624
|
-
trace: vi.fn(),
|
|
625
|
-
debug: vi.fn(),
|
|
626
|
-
info: vi.fn(),
|
|
627
|
-
warn,
|
|
628
|
-
error: vi.fn(),
|
|
629
|
-
fatal: vi.fn(),
|
|
630
|
-
child: () => logger,
|
|
631
|
-
};
|
|
632
|
-
const legacyConfig = {
|
|
633
|
-
...config,
|
|
634
|
-
oauth: { ...config.oauth, code: undefined },
|
|
635
|
-
repositories: {
|
|
636
|
-
...config.repositories,
|
|
637
|
-
code: { type: "redis" as const, defaultExpiresIn: 600 },
|
|
638
|
-
},
|
|
639
|
-
};
|
|
640
|
-
const modules = buildModules(legacyConfig, {
|
|
641
|
-
keyStoreModule: testKeyStoreModule,
|
|
642
|
-
repositoriesModule: testRepositoriesModule,
|
|
643
|
-
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
644
|
-
logger,
|
|
645
|
-
});
|
|
646
|
-
const names = modules.map((m) => m.name);
|
|
647
|
-
expect(names).toContain("redis-code-repository");
|
|
648
|
-
expect(warn.mock.calls).toEqual([
|
|
649
|
-
[
|
|
650
|
-
{
|
|
651
|
-
key: "repositories.code.type",
|
|
652
|
-
env: "CLIENT_CODE_TYPE",
|
|
653
|
-
replacement: "oauth.code.adapter",
|
|
654
|
-
replacementEnv: "OAUTH_CODE_ADAPTER",
|
|
655
|
-
},
|
|
656
|
-
"config_key_deprecated",
|
|
657
|
-
],
|
|
658
|
-
]);
|
|
659
|
-
for (const level of ["trace", "debug", "info", "error", "fatal"] as const) {
|
|
660
|
-
expect(logger[level]).not.toHaveBeenCalled();
|
|
661
|
-
}
|
|
579
|
+
expect(names).toContain("redis-clients");
|
|
662
580
|
});
|
|
663
581
|
|
|
664
582
|
it("buildModules has only ONE codeRepository provider in the manifest (no slot collision)", () => {
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
// `
|
|
668
|
-
//
|
|
669
|
-
const modules = buildModules(
|
|
583
|
+
// The `repositories` module must NOT provide `codeRepository` — the
|
|
584
|
+
// slot is owned by `inMemoryCodeRepositoryModule` or
|
|
585
|
+
// `redisCodeRepositoryModule` exclusively, selected via
|
|
586
|
+
// `adapters.codeRepository`.
|
|
587
|
+
const modules = buildModules(switches, {
|
|
670
588
|
keyStoreModule: testKeyStoreModule,
|
|
671
|
-
//
|
|
672
|
-
//
|
|
589
|
+
// The production `repositories` module (not the test override), so
|
|
590
|
+
// its own provides are exercised.
|
|
673
591
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
674
592
|
});
|
|
675
593
|
const codeRepoProviders = modules.filter((m) =>
|
|
676
594
|
Object.keys(m.provides ?? {}).includes("codeRepository"),
|
|
677
595
|
);
|
|
678
596
|
expect(codeRepoProviders).toHaveLength(1);
|
|
679
|
-
expect(codeRepoProviders[0]?.name).toBe("standalone
|
|
597
|
+
expect(codeRepoProviders[0]?.name).toBe("standalone-in-memory-code-repository");
|
|
680
598
|
});
|
|
681
599
|
});
|
|
682
600
|
|
|
@@ -688,10 +606,10 @@ describe("standalone smoke test", () => {
|
|
|
688
606
|
registerBuiltinKeyStores(ksf);
|
|
689
607
|
const keyStore = await ksf.create({
|
|
690
608
|
type: "local",
|
|
691
|
-
...(config
|
|
609
|
+
...(config["key-store"] as { local: object }).local,
|
|
692
610
|
});
|
|
693
611
|
// The token must carry the deployment's configured `iss`: introspection
|
|
694
|
-
// pins it (RFC 9068 §4), and
|
|
612
|
+
// pins it (RFC 9068 §4), and every deployment has one.
|
|
695
613
|
const { token } = await generateToken(
|
|
696
614
|
{},
|
|
697
615
|
{
|
|
@@ -699,7 +617,7 @@ describe("standalone smoke test", () => {
|
|
|
699
617
|
subject: "u1",
|
|
700
618
|
expiresIn: 3600,
|
|
701
619
|
tokenType: "at+jwt",
|
|
702
|
-
issuer: config.oauth.jwt.issuer,
|
|
620
|
+
issuer: (config.oauth as OAuthSection).jwt.issuer,
|
|
703
621
|
},
|
|
704
622
|
);
|
|
705
623
|
|