@o3co/create-auth-provider 0.17.0-rc.2 → 0.17.0-rc.3

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 (39) hide show
  1. package/README.ja.md +3 -1
  2. package/README.md +5 -2
  3. package/package.json +1 -1
  4. package/templates/standalone/.env.example +7 -2
  5. package/templates/standalone/README.ja.md +14 -10
  6. package/templates/standalone/README.md +26 -20
  7. package/templates/standalone/config/application.conf +1 -0
  8. package/templates/standalone/config/clients.yaml.example +8 -0
  9. package/templates/standalone/config/development.conf +2 -2
  10. package/templates/standalone/config/mailpit.conf +10 -5
  11. package/templates/standalone/config/reference.conf +9 -3
  12. package/templates/standalone/docker-compose.mailpit.yml +3 -0
  13. package/templates/standalone/package.json +1 -1
  14. package/templates/standalone/src/__tests__/adapters.test.mts +12 -6
  15. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +13 -5
  16. package/templates/standalone/src/__tests__/config-notices.test.mts +4 -1
  17. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +48 -0
  18. package/templates/standalone/src/__tests__/core-federations.test.mts +9 -6
  19. package/templates/standalone/src/__tests__/core-sections.test.mts +7 -5
  20. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +89 -86
  21. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +4 -0
  22. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +81 -14
  23. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +8 -4
  24. package/templates/standalone/src/__tests__/mfa-switch.test.mts +79 -21
  25. package/templates/standalone/src/__tests__/own-modules.test.mts +38 -12
  26. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +214 -0
  27. package/templates/standalone/src/__tests__/replica-safety.test.mts +4 -0
  28. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +10 -3
  29. package/templates/standalone/src/__tests__/sections.test.mts +24 -0
  30. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +4 -0
  31. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +148 -0
  32. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +8 -4
  33. package/templates/standalone/src/__tests__/two-phase-config.test.mts +2 -1
  34. package/templates/standalone/src/adapters.mts +2 -2
  35. package/templates/standalone/src/mfaSwitch.mts +6 -7
  36. package/templates/standalone/src/modules.mts +12 -6
  37. package/templates/standalone/src/rootRenames.mts +12 -13
  38. package/templates/standalone/src/sections.mts +12 -2
  39. package/templates/versions.json +18 -18
@@ -0,0 +1,148 @@
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
+ * What the template writes while it starts: `src/app.mts` in a process of its
19
+ * own, in development on the memory stores, until it listens and then until
20
+ * it has shut down. Every line on stdout and on stderr is one JSON object from
21
+ * the structured logger — a warning a store gives at boot included.
22
+ */
23
+
24
+ import { type ChildProcess, spawn } from "node:child_process";
25
+ import { generateKeyPairSync } from "node:crypto";
26
+ import { mkdtempSync, writeFileSync } from "node:fs";
27
+ import { createRequire } from "node:module";
28
+ import { tmpdir } from "node:os";
29
+ import { join } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+ import { describe, expect, it } from "vitest";
32
+
33
+ const templateRoot = fileURLToPath(new URL("../..", import.meta.url));
34
+ const tsxCli = createRequire(import.meta.url).resolve("tsx/cli");
35
+
36
+ const keyPair = generateKeyPairSync("ed25519", {
37
+ publicKeyEncoding: { type: "spki", format: "pem" },
38
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
39
+ });
40
+
41
+ /** An empty client registry: a scaffolded project's clone has no `config/clients.yaml`. */
42
+ const clientsFile = (() => {
43
+ const file = join(mkdtempSync(join(tmpdir(), "startup-log-lines-")), "clients.yaml");
44
+ writeFileSync(file, "");
45
+ return file;
46
+ })();
47
+
48
+ /**
49
+ * Development, with every store the `adapters` section selects in memory.
50
+ * 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.
54
+ */
55
+ const ENV: Readonly<Record<string, string>> = {
56
+ PATH: process.env.PATH ?? "",
57
+ CONFIG_ENV: "development",
58
+ NODE_ENV: "development",
59
+ LOGGING_LEVEL: "info",
60
+ HTTP_PORT: "0",
61
+ OAUTH_JWT_ISSUER: "http://localhost:3000",
62
+ KEY_STORE_LOCAL_PRIVATE_KEY: keyPair.privateKey,
63
+ KEY_STORE_LOCAL_PUBLIC_KEY: keyPair.publicKey,
64
+ SESSION_STORE_SECRET: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
65
+ SESSION_STORE_SECURE: "false",
66
+ SESSION_STORE_NAME: "auth.sid",
67
+ SESSION_STORE_STORAGE_TYPE: "memory",
68
+ ADAPTERS_RATE_LIMITER: "memory",
69
+ ADAPTERS_ATTEMPT_COUNTER: "memory",
70
+ ADAPTERS_USER_SESSION_STORES: "memory",
71
+ ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
72
+ ADAPTERS_REPLAY_SEEN_SET: "memory",
73
+ ADAPTERS_CODE_REPOSITORY: "memory",
74
+ ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
75
+ ADAPTERS_MFA_FACTOR_STORE: "memory",
76
+ ADAPTERS_MFA_TRANSACTION_STORE: "memory",
77
+ REDIS_CLIENTS_URL: "redis://127.0.0.1:9",
78
+ REPOSITORIES_CLIENT_YAML_PATH: clientsFile,
79
+ REPOSITORIES_USER_HTTP_AUTHENTICATE_URL: "http://127.0.0.1:9/authenticate",
80
+ REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL: "http://127.0.0.1:9/authenticate-by-token",
81
+ };
82
+
83
+ interface Run {
84
+ readonly stdout: string;
85
+ readonly stderr: string;
86
+ readonly listened: boolean;
87
+ }
88
+
89
+ /** Starts the template, stops it once it listens (or gives up), and returns what it wrote. */
90
+ function startAndStop(timeoutMs: number): Promise<Run> {
91
+ return new Promise((resolve, reject) => {
92
+ const child: ChildProcess = spawn(process.execPath, [tsxCli, "src/app.mts"], {
93
+ cwd: templateRoot,
94
+ env: ENV,
95
+ stdio: ["ignore", "pipe", "pipe"],
96
+ });
97
+ let stdout = "";
98
+ let stderr = "";
99
+ let listened = false;
100
+ const stop = (): void => {
101
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
102
+ };
103
+ const deadline = setTimeout(stop, timeoutMs);
104
+ child.stdout?.setEncoding("utf8").on("data", (chunk: string) => {
105
+ stdout += chunk;
106
+ if (!listened && stdout.includes('"msg":"server_listening"')) {
107
+ listened = true;
108
+ stop();
109
+ }
110
+ });
111
+ child.stderr?.setEncoding("utf8").on("data", (chunk: string) => {
112
+ stderr += chunk;
113
+ });
114
+ child.on("error", (err) => {
115
+ clearTimeout(deadline);
116
+ reject(err);
117
+ });
118
+ child.on("close", () => {
119
+ clearTimeout(deadline);
120
+ resolve({ stdout, stderr, listened });
121
+ });
122
+ });
123
+ }
124
+
125
+ /** Each line of `text` that is not one JSON object. */
126
+ function linesThatAreNotJsonObjects(text: string): string[] {
127
+ return text
128
+ .split("\n")
129
+ .filter((line) => line.trim() !== "")
130
+ .filter((line) => {
131
+ try {
132
+ const parsed: unknown = JSON.parse(line);
133
+ return typeof parsed !== "object" || parsed === null || Array.isArray(parsed);
134
+ } catch {
135
+ return true;
136
+ }
137
+ });
138
+ }
139
+
140
+ describe("the template's startup output", () => {
141
+ it("is one JSON object per line on stdout and stderr, in development on the memory stores", async () => {
142
+ const run = await startAndStop(15_000);
143
+
144
+ expect(run.listened, `the template did not listen; stderr:\n${run.stderr}`).toBe(true);
145
+ expect(linesThatAreNotJsonObjects(run.stdout)).toEqual([]);
146
+ expect(linesThatAreNotJsonObjects(run.stderr)).toEqual([]);
147
+ }, 30_000);
148
+ });
@@ -29,6 +29,7 @@ import {
29
29
  type Switches,
30
30
  } from "../configPath.mjs";
31
31
  import { templateReference } from "../modules.mjs";
32
+ import { configuredMfaMode } from "./configured-mfa-mode.fixture.mjs";
32
33
  import { type BothPhases, withSwitches } from "./library-references.fixture.mjs";
33
34
 
34
35
  // config/ is two levels above this test file:
@@ -109,15 +110,18 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
109
110
  expect(grants(config).authorizationCode?.enabled).toBe("false");
110
111
  });
111
112
 
112
- it("reads the MFA switch, mfaMode, as required where MFA_MODE is unset, and installs no MFA module under MFA_MODE=off", () => {
113
- // The template's reference.conf ships the switch required and binds MFA_MODE.
113
+ it("reads the MFA switch, mfaMode, as the files write it where MFA_MODE is unset, and installs no MFA module under MFA_MODE=off", () => {
114
+ // The template's reference.conf ships the switch required and binds
115
+ // MFA_MODE; a project's application.conf may write it over that.
114
116
  const mfaNames = (config: Switches) =>
115
117
  buildModules(config)
116
118
  .map((m) => m.name)
117
119
  .filter((name) => /mfa/i.test(name));
118
120
  const shipped = buildResolvedConfig("development");
119
- expect(shipped.mfaMode).toBe("required");
120
- expect(mfaNames(shipped)).toContain("mfa");
121
+ const configured = configuredMfaMode("development");
122
+ expect(shipped.mfaMode).toBe(configured);
123
+ if (configured === "off") expect(mfaNames(shipped)).toEqual([]);
124
+ else expect(mfaNames(shipped)).toContain("mfa");
121
125
  const off = buildResolvedConfig("development", { MFA_MODE: "off" });
122
126
  expect(off.mfaMode).toBe("off");
123
127
  expect(mfaNames(off)).toEqual([]);
@@ -69,6 +69,7 @@ import {
69
69
  type Switches,
70
70
  } from "../configPath.mjs";
71
71
  import { MFA_SWITCH } from "../mfaSwitch.mjs";
72
+ import { configuredMfaMode } from "./configured-mfa-mode.fixture.mjs";
72
73
 
73
74
  const configDir = fileURLToPath(new URL("../../config", import.meta.url));
74
75
 
@@ -270,7 +271,7 @@ describe("phase one reads federation-grants.enabled as the federation-grants mod
270
271
  it("reads the MFA switch as its own mfaMode, as the environment sets it, and no key of the MFA module's", () => {
271
272
  for (const [name, env] of Object.entries(ENVIRONMENTS)) {
272
273
  const switches = readSwitches(readOwnLayers(ownFiles("production"), { env }));
273
- expect(switches.mfaMode, name).toBe(env.MFA_MODE ?? "required");
274
+ expect(switches.mfaMode, name).toBe(env.MFA_MODE ?? configuredMfaMode("production"));
274
275
  expect(switches, name).not.toHaveProperty("mfa");
275
276
  }
276
277
  });
@@ -21,7 +21,7 @@
21
21
  * moved from, and the variables renamed with them, are refused here, before
22
22
  * any module is chosen, in the words and under the reasons boot refuses a
23
23
  * module's with (`config-path-relocated`, `environment-variable-renamed`): a
24
- * renamed variable set alone, or beside its new name at a different value, is
24
+ * renamed variable set, alone or beside its new name at any value, is
25
25
  * refused, naming the variables and never a value.
26
26
  */
27
27
 
@@ -128,7 +128,7 @@ export function readAdapters(
128
128
  `Configuration sets ${moved.length} path(s) that moved: ${moved
129
129
  .map(
130
130
  ({ from, to, environmentVariable }) =>
131
- `${from} has moved to ${to}; see CHANGELOG. Write it there (environment variable ${environmentVariable}) and remove this field from your config (or unset the environment variable that sets it).`,
131
+ `${from} has moved to ${to}; see the upgrade guide (docs/upgrading-from-v0.16.0.md). Write it there (environment variable ${environmentVariable}) and remove this field from your config (or unset the environment variable that sets it).`,
132
132
  )
133
133
  .join(" ")}`,
134
134
  moved,
@@ -38,6 +38,7 @@
38
38
  */
39
39
 
40
40
  import {
41
+ isDevelopmentEnvironment,
41
42
  type Module,
42
43
  memoryMfaFactorStoreModule,
43
44
  memoryMfaTransactionStoreModule,
@@ -96,13 +97,11 @@ export function readMfaSwitch(
96
97
  return result.data;
97
98
  }
98
99
 
99
- /** The names the memory MFA stores are let in under. */
100
- const DEVELOPMENT_NAMES: ReadonlySet<string> = new Set(["development", "test"]);
101
-
102
100
  /**
103
101
  * Why the MFA stores may not be kept in memory under `environment` and
104
- * `CONFIG_ENV` and `NODE_ENV` wherever they are set: each name that is not
105
- * development or test, or that nothing is named. None lifts another's.
102
+ * `CONFIG_ENV` and `NODE_ENV` wherever they are set, as core's
103
+ * `isDevelopmentEnvironment` reads them: each name that is not development
104
+ * or test, or that nothing is named. None lifts another's.
106
105
  */
107
106
  function memoryRefusals(environment: string | undefined): string[] {
108
107
  const named: [string, string][] =
@@ -113,10 +112,10 @@ function memoryRefusals(environment: string | undefined): string[] {
113
112
  }
114
113
  if (named.length === 0) return ["no environment is named"];
115
114
  return named.flatMap(([label, value]) => {
115
+ if (isDevelopmentEnvironment([value])) return [];
116
116
  const production = productionEnvironmentIn([value]);
117
117
  if (production !== undefined) return [`${label} is "${production}"`];
118
- const name = readEnvironmentName(value) ?? "";
119
- return DEVELOPMENT_NAMES.has(name) ? [] : [`${label} "${name}" is not development or test`];
118
+ return [`${label} "${readEnvironmentName(value)}" is not development or test`];
120
119
  });
121
120
  }
122
121
 
@@ -338,7 +338,7 @@ export const inMemorySessionStoresModule: Module = defineModule({
338
338
  * the `adapters.userSessionStores` selection.
339
339
  *
340
340
  * The memory adapter comes through core's factory: that is where its
341
- * "dev/test only" boot warning lives.
341
+ * "dev/test only" boot warning lives, said on the composition's `logger`.
342
342
  */
343
343
  export const inMemoryFederationTokenStoreModule: Module = defineModule({
344
344
  name: "standalone-in-memory-federation-token-store",
@@ -347,10 +347,11 @@ export const inMemoryFederationTokenStoreModule: Module = defineModule({
347
347
  reason:
348
348
  "upstream federation tokens fork per replica — a token stored by the replica that completed the federation callback is missing on the others, so a session cannot refresh its upstream token from another replica, and logout removes only the tokens the replica it lands on can see",
349
349
  },
350
+ optional: ["logger"] as const,
350
351
  provides: {
351
- federationTokenStore: async () => {
352
+ federationTokenStore: async ({ logger }) => {
352
353
  const factory = createFederationTokenStoreFactory();
353
- registerBuiltinFederationTokenStores(factory);
354
+ registerBuiltinFederationTokenStores(factory, logger);
354
355
  return factory.create({ type: "memory" });
355
356
  },
356
357
  },
@@ -408,7 +409,8 @@ export function auditSinkModuleFor(sink: string): Module {
408
409
  * `redis-clients`, which moved from `refreshTokenFamilyStore.redis` with its
409
410
  * variables (`REDIS_CLIENTS_*`). Per-store Redis instances belong in a
410
411
  * custom composition root. An empty URL throws rather than falling back to
411
- * localhost. `io.quit()` is registered once with `lifecycleRegistrar`, so
412
+ * localhost. `assumeNoEviction` is handed to every client, for the Redis
413
+ * stores' eviction gate. `io.quit()` is registered once with `lifecycleRegistrar`, so
412
414
  * `handle.dispose()` closes the connection.
413
415
  *
414
416
  * `buildModules` adds this module only when some composed module needs a
@@ -639,7 +641,11 @@ type StandaloneRedisClients = ReturnType<typeof makeIoredisClients> & {
639
641
  // registrar (tests that seed none), each call creates a fresh client.
640
642
  const clientsCache = new WeakMap<LifecycleRegistrar, StandaloneRedisClients>();
641
643
  function getOrCreateClients(
642
- section: { readonly url: string; readonly password?: string | undefined },
644
+ section: {
645
+ readonly url: string;
646
+ readonly password?: string | undefined;
647
+ readonly assumeNoEviction: boolean;
648
+ },
643
649
  lifecycleRegistrar: LifecycleRegistrar | undefined,
644
650
  readinessRegistrar?: ReadinessRegistrar,
645
651
  injectedLogger?: Logger,
@@ -684,7 +690,7 @@ function getOrCreateClients(
684
690
  // (`refreshTokenFamilyClient.duplicate()`), which inherit no listeners from
685
691
  // `io`; passing the logger lets those report through the same channel.
686
692
  const clients: StandaloneRedisClients = {
687
- ...makeIoredisClients(io, { logger }),
693
+ ...makeIoredisClients(io, { logger, assumeNoEviction: section.assumeNoEviction }),
688
694
  federationGrantStoreClient: makeIoredisFederationGrantStoreClient(io),
689
695
  federationGrantIntentStoreClient: makeIoredisFederationGrantIntentStoreClient(io),
690
696
  };
@@ -17,12 +17,12 @@
17
17
  /**
18
18
  * The variables renamed with a path of the composition root's own layers,
19
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 a different value, before any module is chosen, as boot refuses a
22
- * variable a module declares renamed (`environment-variable-renamed`); the
23
- * two at one value are accepted. Boot's own check cannot hold these: an
24
- * adapter selection decides which modules are chosen before boot, and a
25
- * federation's key sits in core's section, whose renames core alone declares.
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
26
  */
27
27
 
28
28
  import { variablesRenamed } from "./bootRefusal.mjs";
@@ -41,9 +41,9 @@ export interface RootRename {
41
41
 
42
42
  /**
43
43
  * Refuses, with an `environment-variable-renamed` `BootError`, an old name in
44
- * `env` set alone (`unset`), or beside its new name at a different value
45
- * (`different`); the two at one value are accepted. Every such rename is
46
- * named at once; no value is quoted or carried.
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
47
  */
48
48
  export function refuseRenamedVariables(
49
49
  env: Readonly<Record<string, string>>,
@@ -51,18 +51,17 @@ export function refuseRenamedVariables(
51
51
  ): void {
52
52
  const refused = renames.flatMap((rename) => {
53
53
  const { from, to } = rename;
54
- const old = env[from];
55
- if (old === undefined || env[to] === old) return [];
54
+ if (env[from] === undefined) return [];
56
55
  return [
57
56
  { ...rename, state: env[to] === undefined ? ("unset" as const) : ("different" as const) },
58
57
  ];
59
58
  });
60
59
  if (refused.length === 0) return;
61
60
  const named = refused.map(({ from, to, path, state }) => {
62
- const renamed = `${from} was renamed ${to}, the variable ${path} is bound to; see CHANGELOG.`;
61
+ const renamed = `${from} was renamed ${to}, the variable ${path} is bound to; see the upgrade guide (docs/upgrading-from-v0.16.0.md).`;
63
62
  return state === "unset"
64
63
  ? `${renamed} Set ${to} instead and unset ${from}.`
65
- : `${renamed} ${to} is set to a different value: keep the one you mean in ${to} and unset ${from}.`;
64
+ : `${renamed} ${to} is set as well: keep the value you mean in ${to} and unset ${from}.`;
66
65
  });
67
66
  throw variablesRenamed(
68
67
  `The environment sets ${refused.length} variable(s) that were renamed: ${named.join(" ")}`,
@@ -29,6 +29,7 @@
29
29
  import {
30
30
  checkSerializedOrigin,
31
31
  checkTrustedProxyEntry,
32
+ coerceBooleanFromEnv,
32
33
  describeSerializedOriginRejection,
33
34
  describeTrustedProxyEntryRejection,
34
35
  isWellFormedKid,
@@ -239,9 +240,18 @@ export const keyStoreSectionSchema = z
239
240
  })
240
241
  .strict();
241
242
 
242
- /** `redis-clients`: the one Redis connection every Redis-backed store shares. */
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.
248
+ */
243
249
  export const redisClientsSectionSchema = z
244
- .object({ url: z.string(), password: z.string().optional() })
250
+ .object({
251
+ url: z.string(),
252
+ password: z.string().optional(),
253
+ assumeNoEviction: coerceBooleanFromEnv.default(false),
254
+ })
245
255
  .strict();
246
256
 
247
257
  /**
@@ -1,20 +1,20 @@
1
1
  {
2
- "@o3co/auth-provider-core": "0.17.0-rc.2",
3
- "@o3co/auth-provider-device-grant": "0.17.0-rc.2",
4
- "@o3co/auth-provider-dpop": "0.17.0-rc.2",
5
- "@o3co/auth-provider-mtls": "0.17.0-rc.2",
6
- "@o3co/auth-provider-federation-apple": "0.17.0-rc.2",
7
- "@o3co/auth-provider-federation-google": "0.17.0-rc.2",
8
- "@o3co/auth-provider-federation-github": "0.17.0-rc.2",
9
- "@o3co/auth-provider-federation-grants": "0.17.0-rc.2",
10
- "@o3co/auth-provider-federation-oidc": "0.17.0-rc.2",
11
- "@o3co/auth-provider-oauth": "0.17.0-rc.2",
12
- "@o3co/auth-provider-oauth-token-exchange": "0.17.0-rc.2",
13
- "@o3co/auth-provider-session": "0.17.0-rc.2",
14
- "@o3co/auth-provider-foundation": "0.17.0-rc.2",
15
- "@o3co/auth-provider-mfa": "0.17.0-rc.2",
16
- "@o3co/auth-provider-redis": "0.17.0-rc.2",
17
- "@o3co/auth-provider-standard": "0.17.0-rc.2",
18
- "@o3co/auth-provider-test-kit": "0.17.0-rc.2",
19
- "@o3co/auth-provider-webauthn": "0.17.0-rc.2"
2
+ "@o3co/auth-provider-core": "0.17.0-rc.3",
3
+ "@o3co/auth-provider-device-grant": "0.17.0-rc.3",
4
+ "@o3co/auth-provider-dpop": "0.17.0-rc.3",
5
+ "@o3co/auth-provider-mtls": "0.17.0-rc.3",
6
+ "@o3co/auth-provider-federation-apple": "0.17.0-rc.3",
7
+ "@o3co/auth-provider-federation-google": "0.17.0-rc.3",
8
+ "@o3co/auth-provider-federation-github": "0.17.0-rc.3",
9
+ "@o3co/auth-provider-federation-grants": "0.17.0-rc.3",
10
+ "@o3co/auth-provider-federation-oidc": "0.17.0-rc.3",
11
+ "@o3co/auth-provider-oauth": "0.17.0-rc.3",
12
+ "@o3co/auth-provider-oauth-token-exchange": "0.17.0-rc.3",
13
+ "@o3co/auth-provider-session": "0.17.0-rc.3",
14
+ "@o3co/auth-provider-foundation": "0.17.0-rc.3",
15
+ "@o3co/auth-provider-mfa": "0.17.0-rc.3",
16
+ "@o3co/auth-provider-redis": "0.17.0-rc.3",
17
+ "@o3co/auth-provider-standard": "0.17.0-rc.3",
18
+ "@o3co/auth-provider-test-kit": "0.17.0-rc.3",
19
+ "@o3co/auth-provider-webauthn": "0.17.0-rc.3"
20
20
  }