@o3co/create-auth-provider 0.10.0 → 0.12.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 (28) hide show
  1. package/dist/index.d.mts.map +1 -1
  2. package/dist/index.mjs +11 -1
  3. package/package.json +2 -2
  4. package/templates/standalone/.env.example +19 -1
  5. package/templates/standalone/Dockerfile +9 -1
  6. package/templates/standalone/README.ja.md +24 -4
  7. package/templates/standalone/README.md +175 -21
  8. package/templates/standalone/config/application.conf +28 -0
  9. package/templates/standalone/config/clients.yaml.example +15 -2
  10. package/templates/standalone/config/production.conf +9 -3
  11. package/templates/standalone/docker-compose.production.yml +80 -5
  12. package/templates/standalone/docker-compose.yml +24 -0
  13. package/templates/standalone/gitignore +21 -0
  14. package/templates/standalone/package.json +5 -6
  15. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +100 -0
  16. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +71 -4
  17. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +179 -0
  18. package/templates/standalone/src/__tests__/replica-safety.test.mts +456 -0
  19. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +246 -0
  20. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +288 -0
  21. package/templates/standalone/src/__tests__/shutdown.test.mts +324 -0
  22. package/templates/standalone/src/__tests__/smoke.test.mts +36 -0
  23. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +12 -7
  24. package/templates/standalone/src/app.mts +16 -3
  25. package/templates/standalone/src/buildModules.mts +54 -16
  26. package/templates/standalone/src/modules.mts +128 -24
  27. package/templates/standalone/src/shutdown.mts +219 -0
  28. package/templates/versions.json +13 -11
@@ -22,6 +22,8 @@ import {
22
22
  createInMemorySessionFamilyIndex,
23
23
  createInMemorySessionFederationIndex,
24
24
  createInMemorySessionRPRegistry,
25
+ createInMemorySubjectRevocation,
26
+ createInMemorySubjectSessionIndex,
25
27
  createInMemoryUserSessionStore,
26
28
  createKeyStoreFactory,
27
29
  createRepositoryFactories,
@@ -36,7 +38,6 @@ import {
36
38
  } from "@o3co/auth-provider-core";
37
39
  import type { GoogleProviderConfig } from "@o3co/auth-provider-federation-google";
38
40
  import { registerBuiltinAdapters } from "@o3co/auth-provider-foundation";
39
- import { redisFederationTokenStoreBuilder } from "@o3co/auth-provider-redis";
40
41
  import { makeIoredisClients } from "@o3co/auth-provider-redis/ioredis";
41
42
  import { extractFederationSection } from "@o3co/auth-provider-session";
42
43
  // Named import is required here, not default. ioredis is CJS and its entry
@@ -153,6 +154,14 @@ export const repositoriesModule: Module = defineModule({
153
154
  */
154
155
  export const inMemoryCodeRepositoryModule: Module = defineModule({
155
156
  name: "standalone:in-memory-code-repository",
157
+ // #455: the replica-safety guard reads this off the manifest. Before it
158
+ // did, this module booted under `deployment.mode = "multi"` — the guard
159
+ // keyed on core's module names and had never heard of this one.
160
+ replicaSafety: {
161
+ unsafe: true,
162
+ reason:
163
+ "authorization codes fork per replica — a code issued by the replica that served /authorize is unknown to the replica that receives the token request, so the exchange fails with invalid_grant everywhere but one replica, and a code redeemed on one replica can be redeemed again on another",
164
+ },
156
165
  requires: ["config"] as const,
157
166
  optional: ["lifecycleRegistrar"] as const,
158
167
  provides: {
@@ -193,36 +202,75 @@ export const inMemoryCodeRepositoryModule: Module = defineModule({
193
202
  */
194
203
  export const inMemorySessionStoresModule: Module = defineModule({
195
204
  name: "standalone:in-memory-session-stores",
205
+ // #455: the replica-safety guard reads this off the manifest. Before it
206
+ // did, `DEPLOYMENT_MODE=multi` with `USER_SESSION_STORES_ADAPTER=memory`
207
+ // booted — the guard keyed on core's `memorySessionStores`, and this
208
+ // module provides the same six slots under a name it had never heard of.
209
+ replicaSafety: {
210
+ unsafe: true,
211
+ reason:
212
+ "user sessions, RP registrations, family indexes and the subject-level revocation pair fork per replica — back-channel logout reaches only the replica that received it, so a logged-out session stays valid on the others, and a credential change enumerates and watermarks only the replica that handled it (#321)",
213
+ },
196
214
  provides: {
197
215
  userSessionStore: () => createInMemoryUserSessionStore(),
198
216
  sessionRPRegistry: () => createInMemorySessionRPRegistry(),
199
217
  sessionFamilyIndex: () => createInMemorySessionFamilyIndex(),
200
218
  sessionFederationIndex: () => createInMemorySessionFederationIndex(),
219
+ // #406: the two subject-level revocation slots. They were missing here
220
+ // on both branches, so a scaffolded deployment got
221
+ // `subjectRevocation: undefined` — `verifyJwt` skipped the watermark,
222
+ // the #376 refresh gate was inert, and `revokeAllForSubject` reported
223
+ // `unavailable`. Nothing failed and nothing warned, which is what #406
224
+ // is about; the boot guard now refuses that state, and the scaffold's
225
+ // answer is to wire them rather than to declare the capability absent.
226
+ //
227
+ // Single-process only, like every other store on this branch. The
228
+ // redis branch swaps in `redisSessionStoresModule`, which has provided
229
+ // both since #321 — before that there was no distributed adapter to
230
+ // point at, which is why this gap outlived #296.
231
+ subjectSessionIndex: () => createInMemorySubjectSessionIndex(),
232
+ subjectRevocation: () => createInMemorySubjectRevocation(),
201
233
  },
202
234
  });
203
235
 
204
236
  /**
205
- * Federation token store module — always wired (independent of the
206
- * `userSessionStores.adapter` switch). Pre-Wave-5d this slot was bundled
207
- * into the larger `storesModule`; that meant the redis session-stores
208
- * branch dropped the federation-token-store provider entirely and boot
209
- * failed on the missing component (Copilot review on PR #121). Splitting
210
- * the slot out lets both adapters reuse it unchanged.
237
+ * In-memory federation token store module — wired by `buildModules` only
238
+ * when `federationTokenStore.type = "memory"` (the default). The redis
239
+ * branch swaps in `redisFederationTokenStoreModule` from
240
+ * `@o3co/auth-provider-redis` (mutually exclusive — both provide the same
241
+ * `federationTokenStore` slot), off the shared ioredis socket.
242
+ *
243
+ * Until #455 / #456 this was one module for both adapters
244
+ * (`standalone:federation-token-store`), choosing at runtime through the
245
+ * adapter factory. That shape failed twice over: the replica-safety guard
246
+ * could not tell the memory store from the Redis one by name, so
247
+ * `deployment.mode = "multi"` booted with the memory store; and the Redis
248
+ * branch handed the builder no client, so `federationTokenStore.type =
249
+ * "redis"` — documented in the README — was a boot failure rather than a
250
+ * Redis-backed store. One module per adapter, like the session stores.
251
+ *
252
+ * The slot is always wired, independent of the `userSessionStores.adapter`
253
+ * switch: pre-Wave-5d it was bundled into the larger `storesModule`, and the
254
+ * redis session-stores branch dropped the provider entirely (Copilot review
255
+ * on PR #121).
256
+ *
257
+ * The memory adapter still comes through core's factory, unchanged: that is
258
+ * where its "dev/test only" boot warning lives.
211
259
  */
212
- export const federationTokenStoreModule: Module = defineModule({
213
- name: "standalone:federation-token-store",
214
- requires: ["config"] as const,
260
+ export const inMemoryFederationTokenStoreModule: Module = defineModule({
261
+ name: "standalone:in-memory-federation-token-store",
262
+ // #455: read by the replica-safety guard. Before the split this module
263
+ // carried one name for both adapters, so no name could have carried it.
264
+ replicaSafety: {
265
+ unsafe: true,
266
+ reason:
267
+ "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",
268
+ },
215
269
  provides: {
216
- federationTokenStore: async ({ config }) => {
270
+ federationTokenStore: async () => {
217
271
  const factory = createFederationTokenStoreFactory();
218
272
  registerBuiltinFederationTokenStores(factory);
219
- factory.register("redis", redisFederationTokenStoreBuilder);
220
- const slice = (
221
- config as AppConfig & {
222
- federationTokenStore?: { type: string } & Record<string, unknown>;
223
- }
224
- ).federationTokenStore;
225
- return factory.create(slice ? flattenAdapterConfig(slice) : { type: "memory" });
273
+ return factory.create({ type: "memory" });
226
274
  },
227
275
  },
228
276
  });
@@ -289,25 +337,34 @@ export const auditSinkModule: Module = defineModule({
289
337
 
290
338
  /**
291
339
  * @deprecated since Wave 5d (F4 PR2): split into `inMemorySessionStoresModule`
292
- * + `federationTokenStoreModule`. Re-exported for backward compatibility
293
- * with consumers that imported `storesModule` from this file. New code
294
- * should use the two split modules directly.
340
+ * + `inMemoryFederationTokenStoreModule`. Re-exported for backward
341
+ * compatibility with consumers that imported `storesModule` from this file.
342
+ * New code should use the two split modules directly.
295
343
  */
296
344
  export const storesModule: Module = defineModule({
297
345
  name: "standalone:stores",
346
+ // #455: everything this bundle provides lives in process memory, so a
347
+ // composition still on it is refused under `deployment.mode = "multi"`
348
+ // like the split modules it stands in for.
349
+ replicaSafety: {
350
+ unsafe: true,
351
+ reason:
352
+ "user sessions, RP registrations, family indexes, the subject-level revocation pair and upstream federation tokens fork per replica — back-channel logout reaches only the replica that received it, so a logged-out session stays valid on the others",
353
+ },
298
354
  requires: ["config"] as const,
299
355
  provides: {
300
356
  ...inMemorySessionStoresModule.provides,
301
- ...federationTokenStoreModule.provides,
357
+ ...inMemoryFederationTokenStoreModule.provides,
302
358
  },
303
359
  });
304
360
 
305
361
  /**
306
362
  * Shared ioredis clients module — opens ONE long-lived ioredis connection
307
- * per replica, wraps it via `makeIoredisClients()` (returns 11 typed
363
+ * per replica, wraps it via `makeIoredisClients()` (returns 13 typed
308
364
  * per-purpose clients), and exposes the slots consumed by the standalone's
309
365
  * Redis-backed adapters (refresh-token-family + 4 user-session stores +
310
- * rate limiter + code repository + access-token denylist).
366
+ * subject-level revocation index + watermark + rate limiter + code
367
+ * repository + access-token denylist).
311
368
  *
312
369
  * Per F4 PR1 (D-2 v2) + Wave 5d unification: the previous design opened a
313
370
  * separate ioredis socket per Redis-backed module (3+ sockets per replica).
@@ -407,6 +464,53 @@ export const standaloneRedisClientsModule: Module = defineModule({
407
464
  return getOrCreateClients(config as AppConfig, lifecycleRegistrar, readinessRegistrar, logger)
408
465
  .accessTokenDenylistClient;
409
466
  },
467
+ // #321: the two subject-level revocation client slots, off the same
468
+ // shared socket. `redisSessionStoresModule` requires both — the index
469
+ // enumerates what a credential change cascades over and the watermark
470
+ // refuses what the cascade missed — so without them the scaffold's
471
+ // Redis branch had no provider for the slots and stage-1 boot refused
472
+ // the manifest with `missing-required-component`. Nothing in the
473
+ // standalone consumes these slots directly; they exist so the module
474
+ // that does can be selected.
475
+ subjectSessionIndexClient: async ({
476
+ config,
477
+ lifecycleRegistrar,
478
+ readinessRegistrar,
479
+ logger,
480
+ }) => {
481
+ return getOrCreateClients(config as AppConfig, lifecycleRegistrar, readinessRegistrar, logger)
482
+ .subjectSessionIndexClient;
483
+ },
484
+ subjectRevocationClient: async ({ config, lifecycleRegistrar, readinessRegistrar, logger }) => {
485
+ return getOrCreateClients(config as AppConfig, lifecycleRegistrar, readinessRegistrar, logger)
486
+ .subjectRevocationClient;
487
+ },
488
+ // #433: the device-code store client, off the same shared socket.
489
+ // `redisDeviceCodeStoreModule` requires it. This template does not
490
+ // mount the device grant, so nothing in `buildModules` selects that
491
+ // module today; the slot is provided anyway, because a deployment that
492
+ // adds `deviceGrantModule` to this manifest and picks the Redis store
493
+ // must not hit the `missing-required-component` boot failure the
494
+ // subject-level slots above already paid for (#439) — a Redis-branch
495
+ // module whose client slot this module did not provide, caught only by
496
+ // the umbrella e2e.
497
+ deviceCodeStoreClient: async ({ config, lifecycleRegistrar, readinessRegistrar, logger }) => {
498
+ return getOrCreateClients(config as AppConfig, lifecycleRegistrar, readinessRegistrar, logger)
499
+ .deviceCodeStoreClient;
500
+ },
501
+ // #456: the federation-token-store client, off the same shared socket.
502
+ // `redisFederationTokenStoreModule` requires it, and nothing provided
503
+ // it — which is why the Redis branch of the federation store could not
504
+ // boot in this template while the README said it could.
505
+ federationTokenStoreClient: async ({
506
+ config,
507
+ lifecycleRegistrar,
508
+ readinessRegistrar,
509
+ logger,
510
+ }) => {
511
+ return getOrCreateClients(config as AppConfig, lifecycleRegistrar, readinessRegistrar, logger)
512
+ .federationTokenStoreClient;
513
+ },
410
514
  },
411
515
  });
412
516
 
@@ -0,0 +1,219 @@
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
+ import type { Server } from "node:http";
18
+ import type { Logger } from "@o3co/auth-provider-core";
19
+
20
+ /**
21
+ * Graceful shutdown for the scaffolded server (#290).
22
+ *
23
+ * ## Why this is in the template rather than a dependency
24
+ *
25
+ * It used to be `gracefulShutdown` from `@o3co/auth.utils@0.0.4`. The ops
26
+ * review's objection was not that the package is bad — it is that for the
27
+ * component which terminates every user session, "does SIGTERM wait for
28
+ * in-flight requests, and for how long?" has to be answerable from the code an
29
+ * operator deploys, and it was answerable only by reading a pre-1.0 package
30
+ * with no contract pinned here.
31
+ *
32
+ * Reading it answered the question, and the answer was the reason to move it:
33
+ * **there was no deadline**. `server.close()` waits for in-flight requests
34
+ * indefinitely, so a single stuck request meant the process never exited on
35
+ * its own and the orchestrator's SIGKILL took it down mid-flight — the
36
+ * opposite of a graceful shutdown, arriving only under the load that produces
37
+ * a stuck request. The cleanup-failure path also wrote to `console.error`, one
38
+ * bare line in a service whose every other line is NDJSON.
39
+ *
40
+ * Neither is fixed by documenting or pinning the dependency, which is why this
41
+ * is ~40 lines here with tests, rather than a version range and a README
42
+ * paragraph.
43
+ *
44
+ * ## The guarantees, stated
45
+ *
46
+ * 1. **SIGTERM and SIGINT** both start it; the second signal is ignored rather
47
+ * than starting a second dispose over the first one's stores.
48
+ * 2. **New connections stop immediately** (`server.close`), and idle keep-alive
49
+ * sockets are released (`closeIdleConnections`) — they hold the server open
50
+ * with no request behind them, so a quiet server would otherwise wait out
51
+ * the whole deadline for nothing.
52
+ * 3. **In-flight requests are given `drainTimeoutMs`** (default 10s) to finish.
53
+ * 4. **Past the deadline, remaining connections are cut** (`closeAllConnections`)
54
+ * and the process exits **non-zero** — an orchestrator that only ever sees
55
+ * `0` cannot tell a clean drain from one that ran out of time.
56
+ * 5. **`cleanup` runs after draining, before exit**, and its failure is logged
57
+ * through the app logger and reflected in the exit code. It never wedges the
58
+ * process: a dispose that throws still exits, and one that never settles is
59
+ * cut off at `cleanupTimeoutMs`.
60
+ * 6. **A `close` that fails is not reported as a clean drain.** `server.close`
61
+ * reports through its callback, and treating that as success would tell an
62
+ * orchestrator the listener came down when it did not.
63
+ *
64
+ * Size `drainTimeoutMs` **below** the orchestrator's own kill grace period
65
+ * (Kubernetes `terminationGracePeriodSeconds`, compose `stop_grace_period`,
66
+ * both 30s by default) — the point is to close on our terms before SIGKILL
67
+ * arrives on someone else's.
68
+ */
69
+ export interface GracefulShutdownOptions {
70
+ readonly logger: Logger;
71
+ /** Reverse-topological component cleanup — normally `handle.dispose()`. */
72
+ readonly cleanup?: () => void | Promise<void>;
73
+ /** How long in-flight requests get before connections are cut. Default 10s. */
74
+ readonly drainTimeoutMs?: number;
75
+ /**
76
+ * How long `cleanup` gets before the shutdown gives up on it. Defaults to
77
+ * `drainTimeoutMs`, so the worst-case shutdown is the two budgets in
78
+ * sequence — size both against the orchestrator's grace period, not one.
79
+ */
80
+ readonly cleanupTimeoutMs?: number;
81
+ /** Injected in tests; defaults to {@link deferExit}. */
82
+ readonly exit?: (code: number) => void;
83
+ /** Injected in tests; defaults to `process.on`. */
84
+ readonly onSignal?: (signal: NodeJS.Signals, handler: () => void) => void;
85
+ /** Injected in tests; defaults to `process.removeListener`. */
86
+ readonly offSignal?: (signal: NodeJS.Signals, handler: () => void) => void;
87
+ }
88
+
89
+ /**
90
+ * Exit after yielding the loop once.
91
+ *
92
+ * The scaffolded app logs NDJSON through pino, whose default destination is not
93
+ * synchronous, so calling `process.exit` in the same tick as the last
94
+ * `logger.error` can drop exactly the lines that say why the shutdown failed.
95
+ * One turn is a flush window, not a guarantee: a deployment that needs
96
+ * certainty should pass an `exit` that flushes its own transport first.
97
+ */
98
+ export function deferExit(code: number, exitProcess: (code: number) => void = process.exit): void {
99
+ setImmediate(() => exitProcess(code));
100
+ }
101
+
102
+ const DEFAULT_DRAIN_TIMEOUT_MS = 10_000;
103
+ const SIGNALS: readonly NodeJS.Signals[] = ["SIGTERM", "SIGINT"];
104
+
105
+ export function installGracefulShutdown(server: Server, options: GracefulShutdownOptions): void {
106
+ const {
107
+ logger,
108
+ cleanup,
109
+ drainTimeoutMs = DEFAULT_DRAIN_TIMEOUT_MS,
110
+ exit = deferExit,
111
+ onSignal = (signal, handler): void => {
112
+ process.on(signal, handler);
113
+ },
114
+ offSignal = (signal, handler): void => {
115
+ process.removeListener(signal, handler);
116
+ },
117
+ } = options;
118
+
119
+ const cleanupTimeoutMs = options.cleanupTimeoutMs ?? drainTimeoutMs;
120
+
121
+ let shuttingDown = false;
122
+ let finished = false;
123
+
124
+ /** Sentinel so a timed-out cleanup is reported as that, not as a throw. */
125
+ const CLEANUP_TIMED_OUT = Symbol("cleanup-timed-out");
126
+
127
+ /**
128
+ * Await `cleanup`, but not forever. `cleanup()` is invoked inside the async
129
+ * wrapper so a synchronous throw lands in the same rejection path as an
130
+ * async one.
131
+ */
132
+ const runCleanup = async (): Promise<typeof CLEANUP_TIMED_OUT | undefined> => {
133
+ if (!cleanup) return;
134
+ let timer: ReturnType<typeof setTimeout> | undefined;
135
+ try {
136
+ return await Promise.race([
137
+ (async (): Promise<undefined> => {
138
+ await cleanup();
139
+ return undefined;
140
+ })(),
141
+ new Promise<typeof CLEANUP_TIMED_OUT>((resolve) => {
142
+ timer = setTimeout(() => resolve(CLEANUP_TIMED_OUT), cleanupTimeoutMs);
143
+ timer.unref?.();
144
+ }),
145
+ ]);
146
+ } finally {
147
+ if (timer) clearTimeout(timer);
148
+ }
149
+ };
150
+
151
+ /** Run `cleanup` and exit. Called by whichever of drain / deadline wins. */
152
+ const finish = async (code: number, reason: string): Promise<void> => {
153
+ if (finished) return;
154
+ finished = true;
155
+ let exitCode = code;
156
+ // `reason` names whatever decided the exit code, so the line an operator
157
+ // alerts on cannot say "drained" next to a non-zero code. The drain
158
+ // outcome keeps its own key rather than being overwritten — both facts
159
+ // are wanted, and a stable shape is what makes the line queryable.
160
+ let outcome = reason;
161
+ try {
162
+ if ((await runCleanup()) === CLEANUP_TIMED_OUT) {
163
+ logger.error({ cleanupTimeoutMs }, "graceful shutdown: cleanup timed out");
164
+ exitCode = 1;
165
+ outcome = "cleanup-timeout";
166
+ }
167
+ } catch (err) {
168
+ // Through the app logger, not `console.error`: a shutdown that
169
+ // failed to release its Redis connections is exactly the line an
170
+ // operator needs to find later, and a bare write is the one their
171
+ // pipeline drops.
172
+ logger.error({ err }, "graceful shutdown: cleanup failed");
173
+ exitCode = 1;
174
+ outcome = "cleanup-failed";
175
+ }
176
+ logger.info({ reason: outcome, drain: reason, exitCode }, "graceful shutdown: complete");
177
+ exit(exitCode);
178
+ };
179
+
180
+ const handler = (): void => {
181
+ if (shuttingDown) return;
182
+ shuttingDown = true;
183
+ for (const signal of SIGNALS) offSignal(signal, handler);
184
+ logger.info({ drainTimeoutMs }, "graceful shutdown: draining");
185
+
186
+ const deadline = setTimeout(() => {
187
+ logger.error(
188
+ { drainTimeoutMs },
189
+ "graceful shutdown: drain deadline exceeded, closing remaining connections",
190
+ );
191
+ server.closeAllConnections();
192
+ void finish(1, "drain-timeout");
193
+ }, drainTimeoutMs);
194
+ // The deadline must not be what keeps the process alive once the drain
195
+ // has already finished.
196
+ deadline.unref?.();
197
+
198
+ server.close((err) => {
199
+ clearTimeout(deadline);
200
+ if (err) {
201
+ // `close` reports through its callback — "Server is not running"
202
+ // is the common one, but any listener teardown failure lands
203
+ // here. Reporting "drained" and exiting 0 on it would tell an
204
+ // orchestrator the shutdown went cleanly when the listener did
205
+ // not actually come down.
206
+ logger.error({ err }, "graceful shutdown: server close failed");
207
+ void finish(1, "close-failed");
208
+ return;
209
+ }
210
+ void finish(0, "drained");
211
+ });
212
+ // Idle keep-alive sockets have no request behind them, so nothing is
213
+ // lost by releasing them — and without this a quiet server waits out
214
+ // the whole deadline for connections that will never send anything.
215
+ server.closeIdleConnections();
216
+ };
217
+
218
+ for (const signal of SIGNALS) onSignal(signal, handler);
219
+ }
@@ -1,13 +1,15 @@
1
1
  {
2
- "@o3co/auth-provider-core": "0.10.0",
3
- "@o3co/auth-provider-dpop": "0.10.0",
4
- "@o3co/auth-provider-mtls": "0.10.0",
5
- "@o3co/auth-provider-federation-google": "0.10.0",
6
- "@o3co/auth-provider-federation-github": "0.10.0",
7
- "@o3co/auth-provider-oauth": "0.10.0",
8
- "@o3co/auth-provider-oauth-token-exchange": "0.10.0",
9
- "@o3co/auth-provider-session": "0.10.0",
10
- "@o3co/auth-provider-foundation": "0.10.0",
11
- "@o3co/auth-provider-redis": "0.10.0",
12
- "@o3co/auth-provider-webauthn": "0.10.0"
2
+ "@o3co/auth-provider-core": "0.12.0",
3
+ "@o3co/auth-provider-device-grant": "0.12.0",
4
+ "@o3co/auth-provider-dpop": "0.12.0",
5
+ "@o3co/auth-provider-mtls": "0.12.0",
6
+ "@o3co/auth-provider-federation-apple": "0.12.0",
7
+ "@o3co/auth-provider-federation-google": "0.12.0",
8
+ "@o3co/auth-provider-federation-github": "0.12.0",
9
+ "@o3co/auth-provider-oauth": "0.12.0",
10
+ "@o3co/auth-provider-oauth-token-exchange": "0.12.0",
11
+ "@o3co/auth-provider-session": "0.12.0",
12
+ "@o3co/auth-provider-foundation": "0.12.0",
13
+ "@o3co/auth-provider-redis": "0.12.0",
14
+ "@o3co/auth-provider-webauthn": "0.12.0"
13
15
  }