@o3co/create-auth-provider 0.17.0-rc.4 → 0.17.0

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 (32) hide show
  1. package/package.json +1 -1
  2. package/templates/standalone/README.ja.md +2 -3
  3. package/templates/standalone/README.md +2 -3
  4. package/templates/standalone/config/clients.yaml.example +11 -5
  5. package/templates/standalone/config/reference.conf +5 -2
  6. package/templates/standalone/package.json +2 -1
  7. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +6 -10
  8. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +2 -2
  9. package/templates/standalone/src/__tests__/audit-sink.test.mts +13 -4
  10. package/templates/standalone/src/__tests__/backchannel-peer.fixture.mts +64 -0
  11. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +11 -10
  12. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +7 -4
  13. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +7 -4
  14. package/templates/standalone/src/__tests__/frontchannel-logout-csp.test.mts +168 -0
  15. package/templates/standalone/src/__tests__/google-session.fixture.mts +17 -7
  16. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +13 -12
  17. package/templates/standalone/src/__tests__/own-modules.test.mts +3 -35
  18. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +73 -4
  19. package/templates/standalone/src/__tests__/redis-stand-in.fixture.mts +98 -0
  20. package/templates/standalone/src/__tests__/replica-safety.test.mts +7 -4
  21. package/templates/standalone/src/__tests__/sections.test.mts +15 -3
  22. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +8 -10
  23. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +13 -4
  24. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +7 -7
  25. package/templates/standalone/src/__tests__/smoke.test.mts +13 -5
  26. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +24 -8
  27. package/templates/standalone/src/__tests__/store-credential.test.mts +7 -5
  28. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +7 -4
  29. package/templates/standalone/src/app.mts +2 -11
  30. package/templates/standalone/src/sections.mts +3 -2
  31. package/templates/standalone/src/securityHeaders.mts +36 -0
  32. package/templates/versions.json +18 -18
@@ -36,7 +36,7 @@ import request from "supertest";
36
36
  import { afterEach, describe, expect, it } from "vitest";
37
37
  import { buildModules } from "../buildModules.mjs";
38
38
  import type { Switches } from "../configPath.mjs";
39
- import type { Adapters } from "../sections.mjs";
39
+ import { type Adapters, keyStoreSectionSchema } from "../sections.mjs";
40
40
  import {
41
41
  capturedRenames,
42
42
  inProcessAdapters,
@@ -110,6 +110,12 @@ const config: AppConfig & Record<string, unknown> = {
110
110
  },
111
111
  rateLimit: { failMode: "open" },
112
112
  "standalone-in-memory-code-repository": { defaultExpiresIn: 600 },
113
+ // The in-process limiter's section as core's `reference.conf` ships it.
114
+ "core-rate-limiter-memory": {
115
+ maxBuckets: 10000,
116
+ defaultLimit: { limit: 60, windowSeconds: 60 },
117
+ limits: {},
118
+ },
113
119
  };
114
120
 
115
121
  /** What phase one hands `buildModules`: the configuration, with `changes` over every store in process. */
@@ -165,16 +171,18 @@ const testRepositoriesModule = defineModule({
165
171
  },
166
172
  });
167
173
 
174
+ // The template's `key-store` section, read by its own schema, without the
175
+ // section's relocation bridge.
168
176
  const testKeyStoreModule = defineModule({
169
- name: "test:key-store",
170
- requires: ["config"] as const,
177
+ name: "key-store",
178
+ section: { schema: keyStoreSectionSchema },
171
179
  provides: {
172
- keyStore: async ({ config: c }) => {
180
+ keyStore: async ({ section }) => {
173
181
  const factory = createKeyStoreFactory();
174
182
  registerBuiltinKeyStores(factory);
175
183
  return factory.create({
176
184
  type: "local",
177
- ...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
185
+ ...(section.local ?? {}),
178
186
  });
179
187
  },
180
188
  },
@@ -28,7 +28,8 @@ import { createRequire } from "node:module";
28
28
  import { tmpdir } from "node:os";
29
29
  import { join } from "node:path";
30
30
  import { fileURLToPath } from "node:url";
31
- import { describe, expect, it } from "vitest";
31
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
32
+ import { listeningRedis } from "./redis-stand-in.fixture.mjs";
32
33
 
33
34
  const templateRoot = fileURLToPath(new URL("../..", import.meta.url));
34
35
  const tsxCli = createRequire(import.meta.url).resolve("tsx/cli");
@@ -45,14 +46,29 @@ const clientsFile = (() => {
45
46
  return file;
46
47
  })();
47
48
 
49
+ /**
50
+ * The Redis the refresh-token family store's shared socket dials: a stand-in
51
+ * that reports `noeviction`, so the store passes its eviction gate and the run
52
+ * reaches no real server.
53
+ */
54
+ let redis: Awaited<ReturnType<typeof listeningRedis>>;
55
+
56
+ beforeAll(async () => {
57
+ redis = await listeningRedis();
58
+ });
59
+
60
+ afterAll(async () => {
61
+ await redis?.close();
62
+ });
63
+
48
64
  /**
49
65
  * Development, with every store the `adapters` section selects in memory.
50
66
  * The refresh-token family store has no memory choice in the template; its
51
- * shared Redis socket points at a port nothing answers, so the run reaches no
52
- * real server. Nothing is inherited from the test runner's environment but
53
- * PATH, so its NODE_ENV and LOGGING_LEVEL do not reach the child.
67
+ * shared Redis socket points at the stand-in above. Nothing is inherited from
68
+ * the test runner's environment but PATH, so its NODE_ENV and LOGGING_LEVEL do
69
+ * not reach the child.
54
70
  */
55
- const ENV: Readonly<Record<string, string>> = {
71
+ const env = (): Readonly<Record<string, string>> => ({
56
72
  PATH: process.env.PATH ?? "",
57
73
  CONFIG_ENV: "development",
58
74
  NODE_ENV: "development",
@@ -74,11 +90,11 @@ const ENV: Readonly<Record<string, string>> = {
74
90
  ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
75
91
  ADAPTERS_MFA_FACTOR_STORE: "memory",
76
92
  ADAPTERS_MFA_TRANSACTION_STORE: "memory",
77
- REDIS_CLIENTS_URL: "redis://127.0.0.1:9",
93
+ REDIS_CLIENTS_URL: `redis://127.0.0.1:${redis.port}`,
78
94
  REPOSITORIES_CLIENT_YAML_PATH: clientsFile,
79
95
  REPOSITORIES_USER_HTTP_AUTHENTICATE_URL: "http://127.0.0.1:9/authenticate",
80
96
  REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL: "http://127.0.0.1:9/authenticate-by-token",
81
- };
97
+ });
82
98
 
83
99
  interface Run {
84
100
  readonly stdout: string;
@@ -91,7 +107,7 @@ function startAndStop(timeoutMs: number): Promise<Run> {
91
107
  return new Promise((resolve, reject) => {
92
108
  const child: ChildProcess = spawn(process.execPath, [tsxCli, "src/app.mts"], {
93
109
  cwd: templateRoot,
94
- env: ENV,
110
+ env: env(),
95
111
  stdio: ["ignore", "pipe", "pipe"],
96
112
  });
97
113
  let stdout = "";
@@ -48,7 +48,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
48
48
  import { buildModules } from "../buildModules.mjs";
49
49
  import { resolveConfigPaths } from "../configPath.mjs";
50
50
  import { repositoriesModuleFor, templateReference } from "../modules.mjs";
51
- import { repositoriesSectionSchema } from "../sections.mjs";
51
+ import { keyStoreSectionSchema, repositoriesSectionSchema } from "../sections.mjs";
52
52
  import {
53
53
  type BothPhases,
54
54
  bothPhasesOf,
@@ -186,16 +186,18 @@ describe("a token the Store refuses, seen from outside the booted app", () => {
186
186
  ADAPTERS_FEDERATION_GRANT_INTENT_STORE: "memory",
187
187
  };
188
188
 
189
+ // The template's `key-store` section, read by its own schema, without the
190
+ // section's relocation bridge.
189
191
  const testKeyStoreModule = defineModule({
190
- name: "test:key-store",
191
- requires: ["config"] as const,
192
+ name: "key-store",
193
+ section: { schema: keyStoreSectionSchema },
192
194
  provides: {
193
- keyStore: async ({ config: c }) => {
195
+ keyStore: async ({ section }) => {
194
196
  const factory = createKeyStoreFactory();
195
197
  registerBuiltinKeyStores(factory);
196
198
  return factory.create({
197
199
  type: "local",
198
- ...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
200
+ ...(section.local ?? {}),
199
201
  });
200
202
  },
201
203
  },
@@ -50,6 +50,7 @@ import { afterEach, describe, expect, it } from "vitest";
50
50
  import { buildModules } from "#/buildModules.mjs";
51
51
  import { resolveConfigPaths } from "#/configPath.mjs";
52
52
  import { templateReference } from "../modules.mjs";
53
+ import { keyStoreSectionSchema } from "../sections.mjs";
53
54
  import {
54
55
  type BothPhases,
55
56
  bothPhasesOf,
@@ -181,16 +182,18 @@ const testRepositoriesModule = defineModule({
181
182
  } as never,
182
183
  });
183
184
 
185
+ // The template's `key-store` section, read by its own schema, without the
186
+ // section's relocation bridge.
184
187
  const testKeyStoreModule = defineModule({
185
- name: "test:key-store",
186
- requires: ["config"] as const,
188
+ name: "key-store",
189
+ section: { schema: keyStoreSectionSchema },
187
190
  provides: {
188
- keyStore: async ({ config: c }) => {
191
+ keyStore: async ({ section }) => {
189
192
  const factory = createKeyStoreFactory();
190
193
  registerBuiltinKeyStores(factory);
191
194
  return factory.create({
192
195
  type: "local",
193
- ...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
196
+ ...(section.local ?? {}),
194
197
  });
195
198
  },
196
199
  },
@@ -16,7 +16,6 @@
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { createApp } from "@o3co/auth-provider-core";
18
18
  import express from "express";
19
- import helmet from "helmet";
20
19
  import { buildModules } from "./buildModules.mjs";
21
20
  import {
22
21
  configDefaultsFor,
@@ -31,6 +30,7 @@ import { listen } from "./listen.mjs";
31
30
  import { createAppLogger } from "./logger.mjs";
32
31
  import { createMetrics } from "./metrics.mjs";
33
32
  import { mountRoutes } from "./routes.mjs";
33
+ import { securityHeaders } from "./securityHeaders.mjs";
34
34
  import { installGracefulShutdown } from "./shutdown.mjs";
35
35
 
36
36
  // Step 1: the configuration, in two phases (`configPath.mts`; template
@@ -57,16 +57,7 @@ await (async (): Promise<void> => {
57
57
  // `trust proxy` is set from the `http` module's `httpSettings` once boot
58
58
  // has it (step 3), before a request can arrive.
59
59
  const app = express();
60
- app.use(
61
- helmet({
62
- contentSecurityPolicy: {
63
- directives: {
64
- defaultSrc: ["'none'"],
65
- frameAncestors: ["'none'"],
66
- },
67
- },
68
- }),
69
- );
60
+ app.use(securityHeaders());
70
61
 
71
62
  // Timing middleware goes ahead of everything so the histogram covers the
72
63
  // whole stack — including responses produced by middleware that short-circuits
@@ -244,13 +244,14 @@ export const keyStoreSectionSchema = z
244
244
  * `redis-clients`: the one Redis connection every Redis-backed store shares.
245
245
  * `assumeNoEviction` is the operator's assertion that the server runs
246
246
  * `maxmemory-policy noeviction`, read by the Redis stores' eviction gate only
247
- * where the server will not report its policy.
247
+ * where the server will not report its policy; the template's
248
+ * `config/reference.conf` ships it false.
248
249
  */
249
250
  export const redisClientsSectionSchema = z
250
251
  .object({
251
252
  url: z.string(),
252
253
  password: z.string().optional(),
253
- assumeNoEviction: coerceBooleanFromEnv.default(false),
254
+ assumeNoEviction: coerceBooleanFromEnv,
254
255
  })
255
256
  .strict();
256
257
 
@@ -0,0 +1,36 @@
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
+ }
@@ -1,20 +1,20 @@
1
1
  {
2
- "@o3co/auth-provider-core": "0.17.0-rc.4",
3
- "@o3co/auth-provider-device-grant": "0.17.0-rc.4",
4
- "@o3co/auth-provider-dpop": "0.17.0-rc.4",
5
- "@o3co/auth-provider-mtls": "0.17.0-rc.4",
6
- "@o3co/auth-provider-federation-apple": "0.17.0-rc.4",
7
- "@o3co/auth-provider-federation-google": "0.17.0-rc.4",
8
- "@o3co/auth-provider-federation-github": "0.17.0-rc.4",
9
- "@o3co/auth-provider-federation-grants": "0.17.0-rc.4",
10
- "@o3co/auth-provider-federation-oidc": "0.17.0-rc.4",
11
- "@o3co/auth-provider-oauth": "0.17.0-rc.4",
12
- "@o3co/auth-provider-oauth-token-exchange": "0.17.0-rc.4",
13
- "@o3co/auth-provider-session": "0.17.0-rc.4",
14
- "@o3co/auth-provider-foundation": "0.17.0-rc.4",
15
- "@o3co/auth-provider-mfa": "0.17.0-rc.4",
16
- "@o3co/auth-provider-redis": "0.17.0-rc.4",
17
- "@o3co/auth-provider-standard": "0.17.0-rc.4",
18
- "@o3co/auth-provider-test-kit": "0.17.0-rc.4",
19
- "@o3co/auth-provider-webauthn": "0.17.0-rc.4"
2
+ "@o3co/auth-provider-core": "0.17.0",
3
+ "@o3co/auth-provider-device-grant": "0.17.0",
4
+ "@o3co/auth-provider-dpop": "0.17.0",
5
+ "@o3co/auth-provider-mtls": "0.17.0",
6
+ "@o3co/auth-provider-federation-apple": "0.17.0",
7
+ "@o3co/auth-provider-federation-google": "0.17.0",
8
+ "@o3co/auth-provider-federation-github": "0.17.0",
9
+ "@o3co/auth-provider-federation-grants": "0.17.0",
10
+ "@o3co/auth-provider-federation-oidc": "0.17.0",
11
+ "@o3co/auth-provider-oauth": "0.17.0",
12
+ "@o3co/auth-provider-oauth-token-exchange": "0.17.0",
13
+ "@o3co/auth-provider-session": "0.17.0",
14
+ "@o3co/auth-provider-foundation": "0.17.0",
15
+ "@o3co/auth-provider-mfa": "0.17.0",
16
+ "@o3co/auth-provider-redis": "0.17.0",
17
+ "@o3co/auth-provider-standard": "0.17.0",
18
+ "@o3co/auth-provider-test-kit": "0.17.0",
19
+ "@o3co/auth-provider-webauthn": "0.17.0"
20
20
  }