@ever-works/oidc-identity 1.0.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.
@@ -0,0 +1,1468 @@
1
+ import { IdentityTokenRejectionCode, JsonSchema, IPlugin, IIdentityProviderPlugin, PluginCategory, PluginContext, IdentityProviderCheck, VerifiedIdTokenClaims, VerifiedAccessTokenClaims, VerifiedLogoutTokenClaims, PluginHealthCheck, PluginManifest } from '@ever-works/plugin';
2
+ import { JWSHeaderParameters, CryptoKey, KeyObject } from 'jose';
3
+
4
+ /**
5
+ * APW-12 T6 — the discovery half of the `identity-provider` capability: reading
6
+ * the provider's OpenID Connect discovery document, and the outbound rule every
7
+ * call to Ever ID obeys.
8
+ *
9
+ * Spec: FR-3 (what **Test connection** reads and reports), FR-14 (the discovery
10
+ * document is cached for 3,600 seconds; a changed issuer turns sign-in off until
11
+ * an administrator re-tests), FR-15 (5,000 ms timeout; discovery and key fetches
12
+ * retry once after 1,000 ms) and FR-16 (nothing here may reach a log with token
13
+ * material in it). Plan §4.3 is the table these numbers come from.
14
+ *
15
+ * ## Why this reads the document itself instead of `openid-client.discovery()`
16
+ *
17
+ * Plan §4.2 names `openid-client` as the flow's tool, and it stays that: T7 and
18
+ * T8 build the `Configuration` (`buildAuthorizationUrl`,
19
+ * `authorizationCodeGrant`) from the document this module read. The diagnostic
20
+ * read cannot be that call, for two measured reasons:
21
+ *
22
+ * 1. **FR-3 wants a row per check, not an exception.** `openid-client`'s
23
+ * `discovery()` rejects when the discovered `issuer` differs from the
24
+ * address it fetched (`build/index.js` — "discovered metadata issuer does
25
+ * not match the expected issuer"), and that rejection is precisely the
26
+ * `issuerMatch` row FR-3 asks **Test connection** to render. A call that
27
+ * throws cannot report "the endpoints are present but S256 is missing"
28
+ * either: it is all-or-nothing.
29
+ * 2. **FR-15's retry and testability.** `discovery()` bounds itself with
30
+ * `AbortSignal.timeout()` — a native timer a fake clock cannot drive — and
31
+ * performs no retry; §4.3 asks for one retry after 1,000 ms.
32
+ *
33
+ * Both reasons are about the *diagnostic* path only. Nothing here duplicates a
34
+ * security decision: the document this module returns is the document T7/T8 feed
35
+ * to `openid-client`, and the numbers below are the same §4.3 numbers.
36
+ *
37
+ * ## The numbers, and the one place they come from
38
+ *
39
+ * The constants below mirror `EVER_ID_LIMITS` in
40
+ * `packages/contracts/src/apps/ever-id.ts` — plan §4.3's single source. This
41
+ * package does not depend on `@ever-works/contracts` (T5 transcribed the
42
+ * settings-schema bounds the same way and said so), so the values are
43
+ * transcribed here rather than imported, and
44
+ * `src/__tests__/test-connection.spec.ts` reads the contracts source off disk and
45
+ * asserts the two agree, so a drift on either side reddens a test instead of
46
+ * shipping.
47
+ */
48
+ /**
49
+ * FR-15 — every call to Ever ID times out after 5,000 ms.
50
+ *
51
+ * This is the per-attempt bound. For **Test connection** it is also the bound on
52
+ * the whole run (FR-3, ACC-12-03): `testConnection` passes a deadline so the
53
+ * retry can never push the answer past 5 seconds.
54
+ */
55
+ declare const OIDC_OUTBOUND_TIMEOUT_MS = 5000;
56
+ /** FR-15 — discovery and key fetches retry **once** after 1,000 ms. */
57
+ declare const OIDC_OUTBOUND_RETRY_DELAY_MS = 1000;
58
+ /** FR-15 — "retry once": two attempts, never three. */
59
+ declare const OIDC_OUTBOUND_MAX_ATTEMPTS = 2;
60
+ /** FR-14 — the discovery document is cached for 3,600 seconds. */
61
+ declare const OIDC_DISCOVERY_CACHE_SECONDS = 3600;
62
+ /** FR-13 — signing keys are cached for 600 seconds. */
63
+ declare const OIDC_JWKS_CACHE_SECONDS = 600;
64
+ /** FR-13 — an unknown key id triggers at most one refresh per 30 seconds. */
65
+ declare const OIDC_JWKS_UNKNOWN_KID_COOLDOWN_SECONDS = 30;
66
+ /** FR-13 — cached keys stay usable for at most 21,600 seconds, then fail closed. */
67
+ declare const OIDC_JWKS_MAX_STALE_SECONDS = 21600;
68
+ /**
69
+ * FR-11 / FR-3 — the only signing algorithms Ever ID may use, in one place.
70
+ *
71
+ * FR-11 accepts an ID token signed with one of these three; FR-3 reports failure
72
+ * when the provider advertises none of them. The key cache (§4.3) rejects `none`
73
+ * and `HS*` **before** it looks a key up, which is this same list read as an
74
+ * allow-list.
75
+ */
76
+ declare const OIDC_IDENTITY_SIGNING_ALGS: readonly ["RS256", "ES256", "EdDSA"];
77
+ /** One of {@link OIDC_IDENTITY_SIGNING_ALGS}. */
78
+ type OidcIdentitySigningAlg = (typeof OIDC_IDENTITY_SIGNING_ALGS)[number];
79
+ /**
80
+ * The subset of the token-rejection vocabulary (`@ever-works/plugin`) this module
81
+ * can raise.
82
+ *
83
+ * `providerUnavailable` is the honest answer when the provider cannot be reached
84
+ * or its keys can no longer be trusted; T7/T8 rethrow it as
85
+ * `IdentityTokenRejectedError` with the same code, so the closed set the contract
86
+ * publishes stays the only vocabulary a caller branches on.
87
+ */
88
+ type OidcProviderUnavailableCode = Extract<IdentityTokenRejectionCode, 'providerUnavailable'>;
89
+ /**
90
+ * Why the provider is unavailable — a closed set, never a provider's own text.
91
+ *
92
+ * FR-16 forbids a message carrying token material, and the cheapest way to keep
93
+ * that promise in a path that has a client secret in scope is to have no free
94
+ * text at all: `OidcProviderUnavailableError.message` **is** the code, and this
95
+ * reason names which rule failed. T12 maps it onto the facade's
96
+ * `IdentityProviderUnavailableError`; T32 records it in `availability`.
97
+ */
98
+ type OidcProviderUnavailableReason =
99
+ /** FR-2/FR-3 — the integration is not configured, so nothing can be read. */
100
+ 'notConfigured'
101
+ /** FR-14 — the discovery document could not be read. */
102
+ | 'discoveryFailed'
103
+ /** FR-14 — the provider advertises a different issuer than the configured one. */
104
+ | 'issuerDrift'
105
+ /** FR-3 — discovery answered, but a required capability is missing. */
106
+ | 'discoveryIncomplete'
107
+ /** FR-13 — no key set has ever been fetched, so nothing may be verified. */
108
+ | 'keysUnavailable'
109
+ /** FR-13 — the last good key fetch is older than 21,600 seconds: fail closed. */
110
+ | 'keysStale';
111
+ /**
112
+ * The single error a discovery or key-set failure raises (FR-14, FR-13).
113
+ *
114
+ * `message` is the code and nothing else — no issuer echo, no HTTP body, no
115
+ * client secret (FR-16). Extending `Error` is load-bearing exactly as it is for
116
+ * `IdentityTokenRejectedError`: callers catch it as an `Error` and branch on
117
+ * `code`, and a reason they do not recognise must degrade to "unavailable",
118
+ * never to a pass.
119
+ */
120
+ declare class OidcProviderUnavailableError extends Error {
121
+ readonly reason: OidcProviderUnavailableReason;
122
+ readonly code: OidcProviderUnavailableCode;
123
+ constructor(reason: OidcProviderUnavailableReason);
124
+ }
125
+ /**
126
+ * The discovery document, as far as this package reads it.
127
+ *
128
+ * Only the fields FR-3 and T7/T8's flow need are named; the index signature keeps
129
+ * every other claim (`userinfo_endpoint`, `end_session_endpoint`,
130
+ * `revocation_endpoint`, `token_endpoint_auth_methods_supported`, …) verbatim, so
131
+ * a later task reads a claim without re-fetching and without this interface
132
+ * having to grow first.
133
+ */
134
+ interface OidcDiscoveryDocument {
135
+ /** The issuer the provider claims — FR-3 compares it to the configured one, exactly. */
136
+ issuer: string;
137
+ authorization_endpoint?: string;
138
+ token_endpoint?: string;
139
+ jwks_uri?: string;
140
+ end_session_endpoint?: string;
141
+ device_authorization_endpoint?: string;
142
+ code_challenge_methods_supported?: string[];
143
+ id_token_signing_alg_values_supported?: string[];
144
+ backchannel_logout_supported?: boolean;
145
+ backchannel_logout_session_supported?: boolean;
146
+ [claim: string]: unknown;
147
+ }
148
+ /** The shape of a response this module needs — a structural subset of `Response`. */
149
+ interface OidcHttpResponse {
150
+ readonly ok: boolean;
151
+ readonly status: number;
152
+ json(): Promise<unknown>;
153
+ }
154
+ /**
155
+ * What this module hands the fetch seam.
156
+ *
157
+ * `signal` and `headers` are what T6 needed; `method` and `body` were added by
158
+ * **T7**, because the code exchange is the one call that is not a GET — and a
159
+ * spec that cannot see the request it is asserting on cannot prove FR-15's
160
+ * `client_secret_basic`, the four form fields of RFC 6749 §4.1.3 or the absence
161
+ * of a retry. Both are optional and both default to a bodyless GET, so every
162
+ * T6 fake that ignores them keeps compiling and keeps its behaviour.
163
+ */
164
+ interface OidcFetchInit {
165
+ readonly signal: AbortSignal;
166
+ readonly headers: Record<string, string>;
167
+ /** The HTTP method; the callers that pass one pass `POST`. Defaults to a GET. */
168
+ readonly method?: string;
169
+ /** The already-encoded request body, sent only when present. */
170
+ readonly body?: string;
171
+ }
172
+ /**
173
+ * The seam every outbound call goes through.
174
+ *
175
+ * Optional everywhere it is used, and defaulting to the runtime's `fetch`: the
176
+ * platform's plugin loader constructs a plugin with no arguments
177
+ * (`PluginLoaderService.loadPluginModule`, `plugin-loader.service.ts:364` —
178
+ * `new PluginClass()`), so an injected fetch is a test's business and never a
179
+ * production requirement.
180
+ */
181
+ type OidcFetchImpl = (url: string, init: OidcFetchInit) => Promise<OidcHttpResponse>;
182
+ /** One JSON request. */
183
+ interface OidcOutboundRequest {
184
+ readonly url: string;
185
+ /** Extra headers; `accept: application/json` is always sent. */
186
+ readonly headers?: Record<string, string>;
187
+ /**
188
+ * The HTTP method. Absent means a GET — the discovery read and the key fetch
189
+ * (FR-3, FR-13). T7's code exchange passes `POST`, the one call that is not.
190
+ */
191
+ readonly method?: string;
192
+ /** The request body, encoded by the caller; sent only when present. */
193
+ readonly body?: string;
194
+ /**
195
+ * Sent as `content-type` when a body is sent.
196
+ *
197
+ * The token endpoint of RFC 6749 §4.1.3 takes
198
+ * `application/x-www-form-urlencoded`; a provider that checks the header — as
199
+ * ZITADEL and Keycloak both do — answers `415` without it.
200
+ */
201
+ readonly contentType?: string;
202
+ }
203
+ /** Every injectable knob of an outbound call. */
204
+ interface OidcOutboundOptions {
205
+ readonly fetchImpl?: OidcFetchImpl;
206
+ /** The clock, in epoch milliseconds. Defaults to `Date.now`. */
207
+ readonly now?: () => number;
208
+ readonly timeoutMs?: number;
209
+ readonly retryDelayMs?: number;
210
+ /**
211
+ * Absolute epoch-ms deadline the **whole** call must fit inside.
212
+ *
213
+ * FR-3 gives **Test connection** 5 seconds for every check, while FR-15 gives
214
+ * discovery one retry after 1 second — with a 5,000 ms per-attempt timeout the
215
+ * two together would allow 11 seconds. The deadline is how the two are kept:
216
+ * the retry happens whenever it fits, and the answer is reported at the
217
+ * deadline either way. Callers with no such bound (the key fetch, FR-13) leave
218
+ * it undefined and get §4.3 literally.
219
+ */
220
+ readonly deadlineAt?: number;
221
+ }
222
+ /** Why a JSON GET did not produce a body. */
223
+ type OidcOutboundFailure = 'timeout' | 'httpStatus' | 'network' | 'invalidBody';
224
+ /** The outcome of {@link fetchJsonWithOneRetry}. */
225
+ interface OidcOutboundResult {
226
+ readonly ok: boolean;
227
+ /** Present as soon as a response was received, pass or fail. */
228
+ readonly status?: number;
229
+ /** The parsed body, when `ok`. */
230
+ readonly body?: unknown;
231
+ /** Why it failed, when not `ok`. */
232
+ readonly failure?: OidcOutboundFailure;
233
+ /** How many attempts were started — 1 or 2 (FR-15). */
234
+ readonly attempts: number;
235
+ }
236
+ /**
237
+ * One request, **never retried** — FR-15's "the code exchange is never retried".
238
+ *
239
+ * Same per-attempt bound as {@link fetchJsonWithOneRetry}
240
+ * ({@link OIDC_OUTBOUND_TIMEOUT_MS}, on a `setTimeout` so an injected clock and
241
+ * vitest's fake timers can drive it) and the same never-rejects contract, but a
242
+ * single attempt by construction rather than by option: a security-relevant
243
+ * "no second try" is safer as a different function than as a flag a caller can
244
+ * pass wrongly. FR-15 gives the token endpoint 5,000 ms and one attempt, and
245
+ * this is that rule.
246
+ *
247
+ * `deadlineAt` is honoured when a caller passes one, and reports how many
248
+ * attempts had started when the bound was reached — `1` once the request is in
249
+ * flight, `0` if it never was.
250
+ */
251
+ declare function fetchJsonOnce(request: OidcOutboundRequest, options?: OidcOutboundOptions): Promise<OidcOutboundResult>;
252
+ /** Why a discovery read produced no document. */
253
+ type OidcDiscoveryFailure = OidcOutboundFailure
254
+ /** The configured issuer is not an address a discovery document can be read from. */
255
+ | 'invalidIssuer'
256
+ /** A 200 answered with a body that is not an object carrying a string `issuer`. */
257
+ | 'invalidDocument';
258
+ /** The outcome of one discovery read (FR-3, FR-14). */
259
+ type OidcDiscoveryRead = {
260
+ readonly ok: true;
261
+ readonly document: OidcDiscoveryDocument;
262
+ readonly fetchedAt: number;
263
+ /**
264
+ * FR-3: whether the document's `issuer` equals the configured issuer
265
+ * **exactly**. A `false` here is a reported check row, not an exception —
266
+ * and every other read path in this class refuses to hand the document on
267
+ * when it is false (FR-14's disable).
268
+ */
269
+ readonly issuerMatches: boolean;
270
+ readonly attempts: number;
271
+ } | {
272
+ readonly ok: false;
273
+ readonly failure: OidcDiscoveryFailure;
274
+ readonly attempts: number;
275
+ readonly status?: number;
276
+ };
277
+ /** Construction options for {@link OidcDiscoveryReader}. */
278
+ interface OidcDiscoveryOptions extends OidcOutboundOptions {
279
+ /** The administrator's configured issuer address (FR-2), compared exactly. */
280
+ readonly issuerUrl: string;
281
+ /** FR-14's 3,600 seconds; overridable so a spec can pin the boundary. */
282
+ readonly cacheSeconds?: number;
283
+ }
284
+ /**
285
+ * The discovery document, read, cached for 3,600 seconds and refused on drift
286
+ * (FR-14).
287
+ *
288
+ * Two entry points, and the difference between them is the point:
289
+ *
290
+ * - {@link read} **always** fetches. It is what **Test connection** calls, because
291
+ * an administrator pressing the button is asking the provider a question and a
292
+ * cached answer would defeat FR-14's "until an administrator re-tests".
293
+ * - {@link get} serves the cache and refreshes it when it has expired. It is what
294
+ * the flow calls, and it fails closed — `providerUnavailable` — whenever the
295
+ * document could not be read or the provider's issuer has drifted, so a
296
+ * sign-in can never proceed against a provider whose identity changed under
297
+ * it.
298
+ */
299
+ declare class OidcDiscoveryReader {
300
+ private readonly options;
301
+ private document;
302
+ private fetchedAtMs;
303
+ constructor(options: OidcDiscoveryOptions);
304
+ /** The configured issuer this reader answers for (FR-7: one issuer set per installation). */
305
+ get issuerUrl(): string;
306
+ /**
307
+ * When the last successful read happened, in epoch ms, or `null`.
308
+ *
309
+ * This is the in-process source of plan §4.2's `availability.discoveryRefreshedAt`
310
+ * and of plan §9.1's health view; T32 persists it.
311
+ */
312
+ get lastRefreshedAt(): number | null;
313
+ /** The cached document while FR-14's 3,600 seconds have not elapsed, else `null`. */
314
+ cachedDocument(): OidcDiscoveryDocument | null;
315
+ /**
316
+ * Read the document from the provider now (FR-3's read, FR-14's 3,600-second cache
317
+ * entry point).
318
+ *
319
+ * A successful read replaces the cache even when the issuer drifts: the drift is
320
+ * what an administrator needs to see, and {@link get} refuses it either way.
321
+ */
322
+ read(options?: {
323
+ deadlineAt?: number;
324
+ }): Promise<OidcDiscoveryRead>;
325
+ /**
326
+ * The document, from the cache while it is fresh and from the provider once it is
327
+ * not (FR-14). Throws {@link OidcProviderUnavailableError} rather than returning
328
+ * `null`: every caller of this method is on a path that must not continue without
329
+ * a document.
330
+ */
331
+ get(): Promise<OidcDiscoveryDocument>;
332
+ /** FR-14: a different issuer turns sign-in off until an administrator re-tests. */
333
+ private assertIssuerMatches;
334
+ private now;
335
+ }
336
+
337
+ /**
338
+ * APW-12 T5 — the `oidc-identity` plugin settings (plan §4.2, setting key by
339
+ * setting key; spec FR-2 holds the same list in prose).
340
+ *
341
+ * Every constraint below is transcribed from plan §4.2's table — the same
342
+ * numbers as `EVER_ID_LIMITS` in `packages/contracts/src/apps/ever-id.ts`,
343
+ * which plan §4.3 makes the single source for the *runtime* checks. Nothing is
344
+ * invented here: no key, bound, default or requirement appears that the table
345
+ * does not state, so a tightening of the schema cannot silently refuse a
346
+ * configuration the plan allows.
347
+ *
348
+ * Three properties of this schema are load-bearing, and are pinned by
349
+ * `src/__tests__/settings.schema.spec.ts` rather than asserted in prose here:
350
+ *
351
+ * - **`clientSecret` is `x-secret`** (Constitution VII, spec FR-16 / ACC-12-03).
352
+ * The marker is the only thing that makes the platform treat the value as a
353
+ * secret: `packages/plugin/src/api/api-response.types.ts:129` turns it into
354
+ * `secret: true` on the descriptor every API response is built from, and
355
+ * `packages/agent/src/plugins/services/plugin-operations.service.ts:1792`
356
+ * and `:1932` mask exactly those fields. Drop the marker and the secret
357
+ * becomes an ordinary setting, readable back out of the settings API.
358
+ * - **every key is `x-scope: 'global'`** (plan §4.2, FR-7). One issuer set per
359
+ * installation, configured by a platform admin: the schema is admin-tier, so
360
+ * `SettingsSchemaValidatorService.filterSchemaByScope`
361
+ * (`packages/agent/src/plugins/services/settings-schema-validator.service.ts:247`)
362
+ * never offers these keys to a user- or work-scoped read or write.
363
+ * - **the issuer rule is split across schema and runtime.** JSON Schema is
364
+ * static, so the schema carries the half that does not depend on the
365
+ * environment — `https` anywhere, `http` only for `localhost`/`127.0.0.1` —
366
+ * while the plan's remaining half (`http` is refused outside development)
367
+ * is a `NODE_ENV` check.
368
+ *
369
+ * **That runtime half now exists** (2026-09-21), in
370
+ * `oidc-identity.plugin.ts`: `allowsInsecureIssuer` is an allow-list of
371
+ * `development` / `test`, applied in `resolveSettings` (the chokepoint every
372
+ * public method goes through, so a refused issuer refuses discovery, the key
373
+ * fetch, the code exchange, both verifiers and `testConnection` together) and
374
+ * again where `allowInsecureRequests` is called. Until then the sentence above
375
+ * said T6 owned the check, T6 never wrote it, and a comment at the
376
+ * `allowInsecureRequests` call site asserted the gate as though it were in
377
+ * place — which is what kept it unnoticed. Do not restate a gate here without
378
+ * naming the function that enforces it.
379
+ *
380
+ * **A bound in this schema is a write-time check, and for an `x-envVar` key that
381
+ * is not `x-secret` it never runs.** `PluginSettingsService.filterEnvVarFields`
382
+ * strips every key that is `x-envVar` and not `x-secret` from settings writes, so
383
+ * the validator never sees it. (`clientSecret` is both, so it is kept and IS
384
+ * validated on write.) The value is read from its environment variable by `parseEnvValue`,
385
+ * which turns an `integer` key into `Number(value)` and checks nothing else. So
386
+ * `clockSkewSeconds`'s `minimum: 0` / `maximum: 120` (FR-2) does not reach the
387
+ * value the plugin reads. That bound is enforced at run time by
388
+ * `isClockSkewInBounds` in `oidc-identity.plugin.ts`, applied in
389
+ * `resolveSettings` against `OIDC_MAX_CLOCK_SKEW_SECONDS` (2026-09-25). A value
390
+ * outside it turns the integration off as `notConfigured`, with the field name
391
+ * logged and the value not logged. `src/__tests__/clock-skew-gate.spec.ts` pins
392
+ * that constant against this schema's `maximum` and `EVER_ID_LIMITS`. Until then
393
+ * the plugin applied the stored value unchecked, and an out-of-range, `NaN` or
394
+ * string value widened or switched off every token time check.
395
+ */
396
+ /**
397
+ * `https://…`, or `http://localhost…` / `http://127.0.0.1…` with an optional
398
+ * port and path (plan §4.2 `issuerUrl`, spec FR-2). Anything else is refused by
399
+ * the settings validator before it is ever persisted.
400
+ */
401
+ declare const oidcIdentityIssuerUrlPattern = "^(https://[^\\s]+|http://(localhost|127\\.0\\.0\\.1)(:\\d{1,5})?(/[^\\s]*)?)$";
402
+ /** `https://…` only — the **Manage in Ever ID ↗** target (plan §4.2). */
403
+ declare const oidcIdentityHttpsUrlPattern = "^https://[^\\s]+$";
404
+ /** One local client allowed to exchange an Ever ID token for a session (FR-39). */
405
+ interface OidcIdentityLocalClient {
406
+ kind: 'cli' | 'node';
407
+ clientId: string;
408
+ }
409
+ /** A remembered delegated client and the name the card shows it under (FR-48). */
410
+ interface OidcIdentityDelegatedClientName {
411
+ clientId: string;
412
+ displayName?: string;
413
+ }
414
+ /**
415
+ * Platform-written availability state (plan §4.2, §9.2). Stored in the plugin's
416
+ * own settings row so every replica reads the same state — a re-test on one
417
+ * replica clears the flag on all of them (FR-5). Never user-writable: the
418
+ * property is `x-hidden`.
419
+ */
420
+ interface OidcIdentityAvailability {
421
+ unavailableSince?: string;
422
+ discoveryRefreshedAt?: string;
423
+ jwksRefreshedAt?: string;
424
+ lastLogoutNoticeAt?: string;
425
+ }
426
+ /** The settings this plugin reads once resolved (plan §4.2). */
427
+ interface OidcIdentitySettings {
428
+ issuerUrl: string;
429
+ clientId: string;
430
+ clientSecret: string;
431
+ allowedIssuers?: string[];
432
+ apiAudience?: string;
433
+ localClients?: OidcIdentityLocalClient[];
434
+ delegatedClientNames?: OidcIdentityDelegatedClientName[];
435
+ accountManagementUrl?: string;
436
+ signUpAllowed?: boolean;
437
+ clockSkewSeconds?: number;
438
+ displayName?: string;
439
+ availability?: OidcIdentityAvailability;
440
+ }
441
+ /** Every top-level key of plan §4.2, in the table's own order. */
442
+ declare const OIDC_IDENTITY_SETTING_KEYS: readonly ["issuerUrl", "clientId", "clientSecret", "allowedIssuers", "apiAudience", "localClients", "delegatedClientNames", "accountManagementUrl", "signUpAllowed", "clockSkewSeconds", "displayName", "availability"];
443
+ /** Union of {@link OIDC_IDENTITY_SETTING_KEYS}. */
444
+ type OidcIdentitySettingKey = (typeof OIDC_IDENTITY_SETTING_KEYS)[number];
445
+ declare const oidcIdentitySettingsSchema: JsonSchema;
446
+
447
+ /**
448
+ * FR-9 — the `state` a sign-in carries is **at least 32 random bytes**
449
+ * (`EVER_ID_LIMITS.stateBytes`; plan §3.5 seals it in the transaction cookie).
450
+ *
451
+ * Exported so the number is pinnable by a spec that reads the contract off disk,
452
+ * exactly as T6 pinned FR-13's three numbers: the behavioural cases in
453
+ * `src/__tests__/authorization-request.spec.ts` assert against the literals
454
+ * `32`/`32`/`64` in FR-9's own words, and this constant is asserted against
455
+ * `EVER_ID_LIMITS` separately.
456
+ */
457
+ declare const OIDC_STATE_BYTES = 32;
458
+ /** FR-9 — the `nonce` a sign-in carries is **at least 32 random bytes** (`EVER_ID_LIMITS.nonceBytes`). */
459
+ declare const OIDC_NONCE_BYTES = 32;
460
+ /**
461
+ * FR-9 — the PKCE code verifier is **64 characters** (`EVER_ID_LIMITS.codeVerifierLength`).
462
+ *
463
+ * Randomness is drawn as {@link OIDC_CODE_VERIFIER_BYTES} bytes and encoded
464
+ * base64url, which is 64 characters for 48 bytes. The two constants are separate
465
+ * because they are two different requirements — "64 characters" is FR-9's, and
466
+ * the byte count is this package's choice of an encoding that produces exactly
467
+ * that — and the spec asserts that the encoding of 48 bytes really is 64
468
+ * characters rather than assuming it.
469
+ */
470
+ declare const OIDC_CODE_VERIFIER_LENGTH = 64;
471
+ /** How many random bytes base64url encode to {@link OIDC_CODE_VERIFIER_LENGTH} characters. */
472
+ declare const OIDC_CODE_VERIFIER_BYTES = 48;
473
+ /**
474
+ * FR-2 — the default clock-skew tolerance, in seconds
475
+ * (`EVER_ID_LIMITS.defaultClockSkewSeconds`; the schema's `clockSkewSeconds`
476
+ * default is the same number, and the spec asserts the two agree).
477
+ */
478
+ declare const OIDC_DEFAULT_CLOCK_SKEW_SECONDS = 60;
479
+ /**
480
+ * FR-2 — the widest clock-skew tolerance an administrator may configure, in
481
+ * seconds (`EVER_ID_LIMITS.maxClockSkewSeconds`). The schema's `clockSkewSeconds`
482
+ * maximum is the same number, and `src/__tests__/clock-skew-gate.spec.ts` asserts
483
+ * that all three agree.
484
+ *
485
+ * {@link isClockSkewInBounds} applies it at run time. The schema bound alone never
486
+ * reaches the value this plugin reads (see that function).
487
+ */
488
+ declare const OIDC_MAX_CLOCK_SKEW_SECONDS = 120;
489
+ /**
490
+ * FR-11 — an `iat` may be **no earlier than 600 seconds ago**
491
+ * (`EVER_ID_LIMITS.idTokenMaxAgeSeconds`; plan §4.3's "ID token times" row).
492
+ */
493
+ declare const OIDC_ID_TOKEN_MAX_AGE_SECONDS = 600;
494
+ /**
495
+ * FR-11 — `sub` is **1–255 characters**.
496
+ *
497
+ * The bound is asymmetric on purpose: an empty subject identifies nobody, and a
498
+ * longer one is not a subject any provider's directory would mint — FR-31 stores
499
+ * the value, so the column's width is a decision this check is allowed to make.
500
+ */
501
+ declare const OIDC_SUBJECT_MAX_LENGTH = 255;
502
+ /**
503
+ * FR-2 — the audience a delegated or exchanged access token must carry when an
504
+ * administrator configured none (`EVER_ID_DEFAULT_API_AUDIENCE`; plan §4.2's
505
+ * `apiAudience` row, APW-11 §2.4).
506
+ *
507
+ * Until T8 this literal lived only inside `getPublicConfig`; it is a constant here
508
+ * because two methods now read the same default — that projection and
509
+ * {@link OidcIdentityPlugin.verifyAccessToken}'s audience rule — and a default
510
+ * spelled twice is a default that can drift between what the web is told and what
511
+ * a token is checked against.
512
+ */
513
+ declare const OIDC_DEFAULT_API_AUDIENCE = "ever-works";
514
+ /**
515
+ * FR-45 — a delegated token's lifetime (`exp` − `iat`) is **at most 3,600 seconds**
516
+ * (`EVER_ID_LIMITS.delegatedTokenMaxLifetimeSeconds`; plan §4.3's "Access token
517
+ * (delegated)" row).
518
+ *
519
+ * Exported although `verifyAccessToken` takes the ceiling as an argument rather
520
+ * than applying a constant: the number belongs to the caller's token family, and
521
+ * this is the value that family is capped at. A client that mints a longer-lived
522
+ * token is refused with `lifetimeTooLong` (ACC-12-35).
523
+ */
524
+ declare const OIDC_ACCESS_TOKEN_MAX_LIFETIME_SECONDS = 3600;
525
+ /**
526
+ * FR-40 — an exchange token's `iat` may be **no older than 300 seconds**
527
+ * (`EVER_ID_LIMITS.exchangeTokenMaxAgeSeconds`).
528
+ *
529
+ * Passed by the local-client exchange as `maxAgeSeconds`; a plain delegated read
530
+ * (FR-45) passes none, because that row has no age rule — its bound is the
531
+ * lifetime ceiling above.
532
+ */
533
+ declare const OIDC_ACCESS_TOKEN_MAX_AGE_SECONDS = 300;
534
+ /**
535
+ * FR-33 — a sign-out notice's `iat` may be **no older than 300 seconds**
536
+ * (`EVER_ID_LIMITS.logoutTokenMaxAgeSeconds`).
537
+ *
538
+ * Unlike the two numbers above this one is not a caller's choice: FR-33 fixes it
539
+ * for every back-channel logout notice, so {@link OidcIdentityPlugin.verifyLogoutToken}
540
+ * applies it itself.
541
+ */
542
+ declare const OIDC_LOGOUT_TOKEN_MAX_AGE_SECONDS = 300;
543
+ /**
544
+ * FR-33 — a `jti` must not have been seen in the last **600 seconds**
545
+ * (`EVER_ID_LIMITS.replayWindowSeconds`).
546
+ *
547
+ * Exported although this plugin deliberately does **not** enforce it: the window
548
+ * needs a store, plan §4.1 leaves it with the caller ("the caller keeps the
549
+ * 600-second replay window, not the plugin"), and T13's replay service is where it
550
+ * lives. The constant is here so the one number has one spelling in this package
551
+ * — the alternative is a literal in the API that nothing cross-checks.
552
+ */
553
+ declare const OIDC_LOGOUT_TOKEN_REPLAY_WINDOW_SECONDS = 600;
554
+ /**
555
+ * FR-33 — the `events` member a back-channel logout notice must carry
556
+ * (OpenID Connect Back-Channel Logout 1.0 §2.4's event identifier).
557
+ *
558
+ * One spelling, because three things have to agree on it: this plugin's
559
+ * acceptance rule, the fake provider that mints notices, and any future caller
560
+ * that has to explain a refusal.
561
+ */
562
+ declare const OIDC_BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
563
+ /**
564
+ * The seams a spec (or a future caller) may inject.
565
+ *
566
+ * All three are optional and all three default to the runtime: the plugin loader
567
+ * constructs a plugin with no arguments (`PluginLoaderService.loadPluginModule`,
568
+ * `plugin-loader.service.ts:364`), so nothing here is a production requirement.
569
+ * They exist because T6's two properties — FR-3's 5-second bound and FR-13's
570
+ * 600/30/21,600-second ladder — are only provable against a clock and a socket the
571
+ * test owns; a spec that really waited 21,600 seconds is not a spec.
572
+ *
573
+ * `randomBytes` is T7's addition, and for the same reason: FR-9's "fresh `state`
574
+ * and `nonce` of at least 32 random bytes each" is a claim about *freshness* and
575
+ * *byte count*, and neither can be asserted against a value the spec cannot see
576
+ * the source of. With the seam injected, a case can pin the exact bytes the
577
+ * request was built from; with it left alone, the default is the platform
578
+ * CSPRNG and the case asserts what can be asserted of any output — that two
579
+ * calls never agree, and that each value decodes to 32 bytes.
580
+ */
581
+ interface OidcIdentityPluginOptions {
582
+ /** The outbound HTTP seam; defaults to the runtime `fetch` (see `discovery.ts`). */
583
+ readonly fetchImpl?: OidcFetchImpl;
584
+ /** The clock, in epoch milliseconds; defaults to `Date.now`. */
585
+ readonly now?: () => number;
586
+ /** The randomness seam, in bytes; defaults to `node:crypto`'s `randomBytes`. */
587
+ readonly randomBytes?: (length: number) => Uint8Array;
588
+ /**
589
+ * The deployment environment, for the ONE rule that depends on it: whether a
590
+ * non-TLS (`http://localhost`) issuer is usable at all
591
+ * ({@link allowsInsecureIssuer}). Defaults to `process.env.NODE_ENV` read at call
592
+ * time, so a spec that flips the variable does not have to construct the
593
+ * plugin again. A spec that wants the refusing branch passes any value that is
594
+ * not in {@link NON_PRODUCTION_NODE_ENVS} — `nodeEnv: 'production'` is the
595
+ * readable one.
596
+ */
597
+ readonly nodeEnv?: string;
598
+ }
599
+ /**
600
+ * APW-12 T5 — the `oidc-identity` plugin, scaffolded as an identity and completed
601
+ * by T6, T7 and T8 (see the history paragraph below).
602
+ *
603
+ * What the scaffold itself had to get right, and what every later task kept:
604
+ *
605
+ * - the manifest the `everworks.plugin` block in `package.json` also declares
606
+ * (they must agree — `packages/agent/src/plugins/services/plugin-manifest-validator.service.ts`
607
+ * extracts the block from disk and `PluginClassValidatorService` compares the
608
+ * loaded class against it);
609
+ * - the settings schema of plan §4.2;
610
+ * - the two lifecycle hooks the loader calls. Both are **prototype methods on
611
+ * purpose**: `PluginClassValidatorService.isPluginClass`
612
+ * (`packages/agent/src/plugins/services/plugin-class-validator.service.ts:42-55`)
613
+ * accepts a class only when `onLoad` and `onUnload` sit on its prototype, so
614
+ * an arrow-function property would make this plugin undiscoverable.
615
+ *
616
+ * What did NOT exist before **T8**, and why the docstring is kept as history: three
617
+ * of the seven methods of `IIdentityProviderPlugin` — `verifyAccessToken`,
618
+ * `verifyLogoutToken` and `buildEndSessionUrl` — and `healthCheck`. T6 landed the
619
+ * two configuration methods — `testConnection` (FR-3) and `getPublicConfig`
620
+ * (FR-2) — plus the discovery reader and the key cache they and the flow share
621
+ * (`src/discovery.ts`, `src/jwks-cache.ts`); **T7** landed the two sign-in
622
+ * methods, `buildAuthorizationRequest` (FR-8/FR-9/FR-10) and
623
+ * `exchangeAuthorizationCode` (FR-11/FR-12/FR-19's single completion is the
624
+ * caller's), plus `src/scopes.ts`. **T8** landed the three token/sign-out methods
625
+ * (FR-40/FR-45's access tokens, FR-33's sign-out notice, FR-36's end-session
626
+ * address) and `healthCheck`, so the class now claims the interface below rather
627
+ * than five of its seven methods — and
628
+ * `isIdentityProviderPlugin` (`packages/plugin/src/contracts/capabilities/identity-provider.interface.ts:284`)
629
+ * answers `true` for it, which is what it was written to answer once every method
630
+ * exists.
631
+ *
632
+ * Claiming `IIdentityProviderPlugin` in the `implements` clause is not decoration:
633
+ * it makes `tsc` refuse a member whose name, arity or nullability has drifted from
634
+ * plan §4.1, which is the failure the guard above cannot see. What it does **not**
635
+ * change is the plugin's posture: `autoEnable: false`, so the installation still
636
+ * opts in explicitly (FR-5's kill switch is the plugin toggle, plan §9.2).
637
+ */
638
+ declare class OidcIdentityPlugin implements IPlugin, IIdentityProviderPlugin {
639
+ readonly id = "oidc-identity";
640
+ readonly name = "OpenID Connect identity (Ever ID)";
641
+ readonly version = "1.0.0";
642
+ /**
643
+ * The plan §4.2 category, `'identity'`.
644
+ *
645
+ * The assertion is needed only because this package lands **before T4**,
646
+ * which appends `'identity'` to `PLUGIN_CATEGORIES`
647
+ * (`packages/plugin/src/contracts/plugin-manifest.types.ts:5-117` — the tuple
648
+ * currently closes with `'build'`). Until that append lands the loader's own
649
+ * `isPluginCategory` check rejects the manifest, so this plugin is
650
+ * discoverable only after T4; the value itself is already exact, and the
651
+ * assertion is the one place the type union is narrower than the plan.
652
+ *
653
+ * **Landed 2026-09-18 (T4).** `'identity'` is now the **last** member of
654
+ * `PLUGIN_CATEGORIES`, so the loader's `isPluginCategory` accepts this
655
+ * manifest and discovery finds the package instead of skipping it with
656
+ * `Invalid category` (T4's report measures it: 103 → 104 plugins). The
657
+ * paragraph above is kept because that is what this file had to say while
658
+ * the append was missing, and the cast below stays — it resolves to the
659
+ * same member, and dropping it would remove an assertion for nothing.
660
+ */
661
+ readonly category: PluginCategory;
662
+ /** Plan §4.2: exactly one capability, `identity-provider`. */
663
+ readonly capabilities: readonly string[];
664
+ /** One issuer set per installation, configured by a platform admin (FR-7, FR-2). */
665
+ readonly configurationMode = "admin-only";
666
+ readonly settingsSchema: JsonSchema;
667
+ /**
668
+ * The context `onLoad` was handed — the only way this plugin reads its own
669
+ * resolved settings (plan §4.1: no method takes a configuration object, so a
670
+ * caller cannot hand the plugin a different issuer, client id or secret than the
671
+ * one an administrator saved; FR-7).
672
+ */
673
+ private context?;
674
+ /** The discovery reader for the currently configured issuer (FR-14's cache lives in it). */
675
+ private discoveryReader?;
676
+ /** Which issuer {@link discoveryReader} was built for — a settings change replaces it. */
677
+ private discoveryIssuerUrl?;
678
+ /** The key cache for the currently configured `jwks_uri` (FR-13's ladder lives in it). */
679
+ private jwksCache?;
680
+ /** Which `jwks_uri` {@link jwksCache} was built for — a provider that moves its keys replaces it. */
681
+ private jwksUri?;
682
+ /** FR-13 / plan §9.2: when the key set was last fetched successfully, in epoch ms. T32 persists it. */
683
+ private jwksRefreshedAtMs;
684
+ /** FR-14/FR-5: when the provider was first seen unusable, in epoch ms. T32 persists it. */
685
+ private unavailableSinceMs;
686
+ /** Which rule made it unusable — a closed set, never a provider's own text (FR-16). */
687
+ private unavailableReason;
688
+ private readonly fetchImpl?;
689
+ private readonly now;
690
+ private readonly randomBytes?;
691
+ private readonly nodeEnv?;
692
+ constructor(options?: OidcIdentityPluginOptions);
693
+ onLoad(context: PluginContext): Promise<void>;
694
+ onUnload(): Promise<void>;
695
+ /**
696
+ * FR-3 — read the provider's discovery document and report every check.
697
+ *
698
+ * The rules this method exists to keep, in the order they matter:
699
+ *
700
+ * - **5 seconds, always** (FR-3, ACC-12-03). The read is handed a deadline
701
+ * computed when the run starts, so FR-15's single retry can only ever fit
702
+ * inside that budget, and the answer is reported at the deadline whether or
703
+ * not the provider answered in time.
704
+ * - **A row per check, always** — the same seven ids in FR-3's order, an
705
+ * unconfigured integration and an unreachable provider included. A short or
706
+ * varying list would make spec §6.7's admin surface render differently
707
+ * depending on what went wrong.
708
+ * - **Never the secret** (FR-16). No `detail` quotes a value the provider sent
709
+ * except a numeric HTTP status; every one of them names a field, and the
710
+ * settings object — which is where `clientSecret` lives — is never returned,
711
+ * logged or embedded.
712
+ *
713
+ * The run also writes the availability record plan §4.2 keeps: the five checks
714
+ * of {@link FR3_REQUIRED_CHECK_IDS} passing clears `unavailableSince`, any of them
715
+ * failing sets it (FR-14's "until an administrator re-tests"). T32 persists it;
716
+ * {@link getAvailability} is the in-process view until then.
717
+ */
718
+ testConnection(): Promise<IdentityProviderCheck[]>;
719
+ /**
720
+ * FR-2's non-secret projection — what the web needs to render the button and
721
+ * what a local client needs before it can sign in.
722
+ *
723
+ * The contract's five fields and nothing else: the client secret is not among
724
+ * them (FR-16), and neither is anything that would let a caller address the
725
+ * provider differently from the configuration an administrator saved (FR-7). The
726
+ * defaults are FR-2's: display name `Ever ID`, audience `ever-works`, sign-up on.
727
+ */
728
+ getPublicConfig(): Promise<{
729
+ issuer: string;
730
+ displayName: string;
731
+ localClients: OidcIdentityLocalClient[];
732
+ apiAudience: string;
733
+ signUpAllowed: boolean;
734
+ }>;
735
+ /**
736
+ * FR-8/FR-9/FR-10/FR-12's first half — the authorization request a browser is
737
+ * sent to, and the three secrets the transaction cookie carries.
738
+ *
739
+ * What this method is responsible for, and what it is deliberately not:
740
+ *
741
+ * - **PKCE S256 only** (FR-8). `code_challenge_method=S256` is a literal in
742
+ * the parameters below and the challenge is the SHA-256 of the verifier,
743
+ * computed by `openid-client`'s `calculatePKCECodeChallenge` — the same
744
+ * transformation the provider will apply to the verifier at the token
745
+ * endpoint. `plain` is never offered and never sent; a provider that
746
+ * advertises only `plain` is refused by FR-3's check row and by the
747
+ * availability record, not by this method.
748
+ * - **Fresh `state` and `nonce` of 32 random bytes, and a 64-character
749
+ * verifier, per call** (FR-9). Every call draws new bytes from
750
+ * {@link OidcIdentityPluginOptions.randomBytes} (the platform CSPRNG by
751
+ * default) and encodes them base64url: 32 bytes → a 43-character value
752
+ * whose decoded length is exactly the 32 FR-9 asks for, 48 bytes → exactly
753
+ * the 64 characters FR-9 asks of the verifier. Nothing is reused between
754
+ * calls and nothing is derived from the clock, the issuer or the client
755
+ * id — a `state` an attacker can predict is a `state` that binds nothing.
756
+ * - **The exact redirect address, never rebuilt** (FR-10). `redirectUri` is
757
+ * copied into the query parameter byte-for-byte: no trailing-slash
758
+ * normalisation, no parsing and re-serialising, no default. Deriving that
759
+ * address from the configured public web address — and refusing anything
760
+ * that came from request input — is the caller's rule (plan §5.1's
761
+ * `POST /authorize`), and this method cannot enforce it because the
762
+ * address is not part of the plugin's settings (plan §4.2 has no redirect
763
+ * key: the one address per deployment lives in platform configuration).
764
+ * - **`openid email profile`, and never `offline_access`** (plan §4.2,
765
+ * FR-38) — the scope comes from `src/scopes.ts` as one constant rather
766
+ * than being assembled here.
767
+ *
768
+ * The endpoint comes from the discovery document this plugin already read
769
+ * (FR-14's 3,600-second cache, and its issuer-drift refusal): a request is
770
+ * never built against an issuer whose document advertises a different issuer,
771
+ * so a provider cannot move this installation's sign-in by publishing a
772
+ * document somewhere else.
773
+ *
774
+ * Errors are T6's availability vocabulary rather than token rejections —
775
+ * nothing here verifies a token, and a caller needs to know *which*
776
+ * configuration or document problem stopped the sign-in from starting:
777
+ * `notConfigured`, `discoveryFailed`, `issuerDrift` or `discoveryIncomplete`
778
+ * (a document with no `authorization_endpoint`). Every one of them maps to
779
+ * plan §5.2's `404 everIdDisabled` / `503 providerUnavailable` at the façade.
780
+ */
781
+ buildAuthorizationRequest(input: {
782
+ redirectUri: string;
783
+ prompt?: 'login';
784
+ maxAgeSeconds?: number;
785
+ }): Promise<{
786
+ url: string;
787
+ state: string;
788
+ nonce: string;
789
+ codeVerifier: string;
790
+ }>;
791
+ /**
792
+ * FR-11/FR-12 — redeem an authorization code and answer the ID token's claims,
793
+ * or refuse.
794
+ *
795
+ * The order below is the spec's order, and each step is load-bearing:
796
+ *
797
+ * 1. **the settings resolve** — an unconfigured integration cannot exchange
798
+ * anything, and this method has no way to say so except
799
+ * `providerUnavailable` (see the error contract below);
800
+ * 2. **FR-12's `iss` equality, before anything is redeemed.** When the
801
+ * authorization response carries `iss`, it must equal the configured
802
+ * issuer exactly. Checking it first is not an optimisation: a code whose
803
+ * response named another issuer is a code this installation must not
804
+ * spend, and a refusal here touches the network zero times;
805
+ * 3. **the discovery document** (FR-14's cache, and its issuer-drift
806
+ * refusal) — the token endpoint and `jwks_uri` come from it;
807
+ * 4. **the code exchange** — one POST, `client_secret_basic`, 5,000 ms, and
808
+ * **no retry** (FR-15's "the code exchange is never retried": a second
809
+ * attempt would be a second redemption of a single-use code, which is
810
+ * FR-19's "a sign-in transaction completes at most once" seen from the
811
+ * provider's side);
812
+ * 5. **the signature and the algorithm**, through T6's key cache
813
+ * (`jwks-cache.ts`), whose three codes and whose 21,600-second
814
+ * fail-closed end are reported with the same codes here;
815
+ * 6. **FR-11's claim rules** (plan §4.3), against the injected clock and the
816
+ * administrator's `clockSkewSeconds` (default
817
+ * {@link OIDC_DEFAULT_CLOCK_SKEW_SECONDS}).
818
+ *
819
+ * ## The error contract, and why it differs from {@link buildAuthorizationRequest}
820
+ *
821
+ * **Everything this method throws is an `IdentityTokenRejectedError`** — the
822
+ * contract's single refusal type, `message` equal to `code` and nothing else
823
+ * (FR-16). That includes the provider-unavailable conditions: T6's discovery
824
+ * and key-cache errors are caught below and rethrown as
825
+ * `providerUnavailable`, and any `jwks-cache` refusal keeps its own code
826
+ * (`badAlg`, `badSignature`). The reason is the caller: the callback handler
827
+ * has one decision to make — plan §5.2's "ID token rejected → 401" versus
828
+ * "endpoint unavailable → 503" — and one error type with a closed code set is
829
+ * the shape that decision is written against. The builder, which answers a
830
+ * question rather than a verdict, keeps the precise availability reason
831
+ * instead.
832
+ *
833
+ * A refusal never partially succeeds: nothing is returned, nothing is stored,
834
+ * and no claim of a refused token reaches the caller.
835
+ *
836
+ * ## `maxAuthAgeSeconds` (FR-25)
837
+ *
838
+ * Supplied, it applies FR-25's rule — `auth_time` no older than 300 seconds
839
+ * plus the skew — and answers `tooOld`, including when the token carries no
840
+ * `auth_time` at all: a connect flow that cannot show the authentication was
841
+ * recent must re-authenticate rather than proceed. Absent (a plain sign-in),
842
+ * `auth_time` is not required and is passed through as `null` when missing.
843
+ */
844
+ exchangeAuthorizationCode(input: {
845
+ code: string;
846
+ redirectUri: string;
847
+ codeVerifier: string;
848
+ expectedNonce: string;
849
+ receivedIssuer?: string;
850
+ maxAuthAgeSeconds?: number;
851
+ }): Promise<VerifiedIdTokenClaims>;
852
+ /**
853
+ * FR-40 (the local-client exchange) and FR-45 (the App Launcher's delegated
854
+ * read) — verify an Ever ID access token and answer the claims both callers
855
+ * branch on.
856
+ *
857
+ * The rules, in the order they are applied, and where each one comes from:
858
+ *
859
+ * 1. **the settings resolve** — as in {@link exchangeAuthorizationCode}, an
860
+ * unconfigured integration answers `providerUnavailable`, the only code
861
+ * this method has for "the plugin cannot ask the provider anything";
862
+ * 2. **the discovery document** (FR-14's cache and issuer-drift refusal) and
863
+ * its `jwks_uri` — the same read the exchange performs, and the same
864
+ * `providerUnavailable` when the document names no key set;
865
+ * 3. **the signature and the algorithm** through T6's key cache, including
866
+ * FR-13's ladder and its fail-closed end (`badAlg`, `badSignature`,
867
+ * `providerUnavailable`);
868
+ * 4. **FR-11's issuer rule**, which FR-45 makes a rule for access tokens too:
869
+ * `iss` equals the discovery issuer **and** is allow-listed (`badIssuer`);
870
+ * 5. **the audience** — `aud` contains the configured API audience, or plan
871
+ * §4.2's `ever-works` default (`badAudience`);
872
+ * 6. **the scopes** — every entry of `requiredScopes` is present (`missingScope`,
873
+ * FR-46's code: a valid token that lacks the scope its caller needs);
874
+ * 7. **the times** — a readable `exp` that has not passed (with the skew), an
875
+ * `iat` no later than now plus the skew, and an `iat` no older than
876
+ * `maxAgeSeconds` when the caller gives one (`expired`, `notYetValid`,
877
+ * `tooOld`);
878
+ * 8. **the lifetime ceiling** — `exp − iat ≤ maxLifetimeSeconds`
879
+ * (`lifetimeTooLong`; ACC-12-35's "lifetime over 3,600 seconds");
880
+ * 9. **the authorised party** — when `allowedAuthorizedParties` is given (the
881
+ * exchange path, FR-40), `azp` must be one of them
882
+ * (`badAuthorizedParty`); absent, the claim is not read at all, which is
883
+ * FR-45's rule set exactly.
884
+ *
885
+ * ## The two arguments that make one method serve both token families
886
+ *
887
+ * The contract has one `verifyAccessToken` and two rules of §4.3: the delegated
888
+ * read passes `{ requiredScopes: ['apps:read'], maxLifetimeSeconds: 3_600 }`
889
+ * (FR-44/FR-45), the exchange passes
890
+ * `{ requiredScopes: ['ever-works:session'], maxLifetimeSeconds: 3_600, maxAgeSeconds: 300, allowedAuthorizedParties: <the configured local client ids> }`
891
+ * (FR-40). Nothing here decides which family a token belongs to — that is the
892
+ * caller's, and it is a decision about *why* the token is being presented, not
893
+ * about the token.
894
+ *
895
+ * ## What is deliberately left to the caller
896
+ *
897
+ * **The `jti` replay window** (FR-40/FR-33's 600 seconds). The parsed `jti` is
898
+ * returned and never enforced: it needs a store with a unique index, which is
899
+ * T13's `ever-id-replay.service.ts`, and a plugin that held that state in
900
+ * process memory would enforce a different window per replica. Plan §4.1 says
901
+ * the same thing from the contract side ("the caller keeps the 600-second replay
902
+ * window, not the plugin").
903
+ *
904
+ * **Whether the subject is a connected identity of an active account** — FR-40's
905
+ * last clause and FR-45's. That is a database question and it belongs to the
906
+ * services that own those rows.
907
+ *
908
+ * A refusal never partially succeeds: no claim of a refused token is returned.
909
+ */
910
+ verifyAccessToken(token: string, input: {
911
+ requiredScopes: string[];
912
+ maxLifetimeSeconds: number;
913
+ maxAgeSeconds?: number;
914
+ allowedAuthorizedParties?: string[];
915
+ }): Promise<VerifiedAccessTokenClaims>;
916
+ /**
917
+ * FR-33 — verify a back-channel logout notice and answer the three claims the
918
+ * session teardown keys on.
919
+ *
920
+ * Validated exactly like an ID token (FR-33's "validated like FR-11 and FR-13":
921
+ * the algorithm allow-list, the signature against Ever ID's published keys,
922
+ * FR-13's ladder, FR-11's issuer rule, and the times), plus FR-33's own five:
923
+ *
924
+ * - the **back-channel logout event** is present ({@link OIDC_BACKCHANNEL_LOGOUT_EVENT});
925
+ * - **no `nonce`** — the claim's mere presence is refused
926
+ * (`nonceInLogoutToken`), because a notice is not an authentication and a
927
+ * token that can be replayed as one is a token that should never verify;
928
+ * - **`sid` or `sub`**, at least one, or there is no session to end
929
+ * (`badLogoutEvent`);
930
+ * - **`jti` required** (`badLogoutEvent`) — without it the 600-second replay
931
+ * window has no key, and a notice that cannot be bounded must be refused
932
+ * rather than treated as new;
933
+ * - **`iat` within the skew and no older than 300 seconds** (`notYetValid`,
934
+ * `tooOld`).
935
+ *
936
+ * ## `exp`, and the one place a logout token differs from an ID token
937
+ *
938
+ * Plan §4.3's ID-token row requires a readable `exp`; FR-33's logout-token row
939
+ * does not mention one, and OpenID Connect Back-Channel Logout 1.0 §2.4 does not
940
+ * put `exp` in the claim set a logout token **must** carry (`iss`, `aud`, `iat`,
941
+ * `jti`, `events`). FR-53 makes that difference load-bearing: a provider that
942
+ * publishes a standards-compliant notice would have every sign-out refused as
943
+ * `expired` if this method demanded an `exp` the provider never promised, and a
944
+ * refused sign-out is a session that stays open. So `exp` is **checked when it
945
+ * is there** and not required, and the freshness bound FR-33 does state — `iat`
946
+ * within the skew and no older than 300 seconds — is enforced either way. This
947
+ * is a deliberate reading and is pinned by a case in
948
+ * `src/__tests__/logout-token.spec.ts` in both directions.
949
+ *
950
+ * ## What is deliberately left to the caller
951
+ *
952
+ * The `jti` **replay window** (FR-33's 600 seconds; see
953
+ * {@link verifyAccessToken}) and everything that happens to a session once this
954
+ * method has answered — FR-34's "ends the sessions carrying it", FR-35's "never
955
+ * ends a session opened by another method" and the 5-second bound are the
956
+ * API's, not a token verifier's.
957
+ */
958
+ verifyLogoutToken(token: string): Promise<VerifiedLogoutTokenClaims>;
959
+ /**
960
+ * FR-36 — the address that ends the person's Ever ID session, or **`null`** when
961
+ * the provider publishes no end-session endpoint.
962
+ *
963
+ * The `null` is the one answer in this plugin that is neither a set of claims
964
+ * nor a refusal, and it means one thing only: the discovery document the plugin
965
+ * already read has no `end_session_endpoint`, so there is nowhere to send the
966
+ * person and the caller renders "sign out of Ever Works" alone. It is **not**
967
+ * an error path — inventing a URL, or answering the issuer's address and hoping,
968
+ * would send a browser somewhere the provider never agreed to serve.
969
+ *
970
+ * What is sent, and what is not:
971
+ *
972
+ * - `client_id` — how the provider knows which relying party is asking. The
973
+ * alternative identifier, `id_token_hint`, is deliberately **not** sent:
974
+ * FR-31 forbids this platform from storing an Ever ID token, so there is no
975
+ * ID token to hint with, and OpenID Connect RP-Initiated Logout 1.0 names
976
+ * `client_id` as the parameter to use in exactly that case.
977
+ * - `post_logout_redirect_uri` — copied **byte for byte**, the same rule
978
+ * FR-10 puts on the authorization request's `redirect_uri` and for the same
979
+ * reason: the address an administrator registered is the address that
980
+ * travels, never one this package rebuilt.
981
+ * - `state` — carried verbatim. "Validated" in FR-36 is about the value the
982
+ * API sealed into the sign-out transaction (plan §3.4/§3.5, T13's seal
983
+ * service) being the value that comes back; the plugin neither mints nor
984
+ * checks it, and must not, because it holds no per-session state.
985
+ *
986
+ * Errors are {@link OidcProviderUnavailableError} — T6's availability
987
+ * vocabulary, exactly as in {@link buildAuthorizationRequest} — rather than an
988
+ * `IdentityTokenRejectedError`: nothing here verifies a token, and a caller
989
+ * needs to know which configuration or document problem stopped the sign-out:
990
+ * `notConfigured`, `discoveryFailed`, `issuerDrift`, or `discoveryIncomplete`
991
+ * (an `end_session_endpoint` that is published but is not a URL).
992
+ */
993
+ buildEndSessionUrl(input: {
994
+ postLogoutRedirectUri: string;
995
+ state: string;
996
+ }): Promise<string | null>;
997
+ /**
998
+ * Plan §9.2's plugin health view, in the platform's own lifecycle shape
999
+ * (`PluginHealthCheck`, `packages/plugin/src/contracts/lifecycle.types.ts:24`).
1000
+ *
1001
+ * The three states, and why each one is the honest answer:
1002
+ *
1003
+ * - **`unhealthy`** — an administrator's configuration could not be read (a
1004
+ * required setting is missing or blank, or `resolveSettings` refused a set
1005
+ * value: a non-TLS issuer outside development, or a `clockSkewSeconds`
1006
+ * outside FR-2's bound — the `configuration` row names the field, never the
1007
+ * value), or FR-14/FR-5's availability record
1008
+ * says the provider was last seen unusable. The second is a real outage of
1009
+ * this capability: sign-in is off until a **Test connection** passes.
1010
+ * - **`healthy`** — the settings resolve and this process has successfully
1011
+ * read the discovery document or the key set.
1012
+ * - **`unknown`** — the settings resolve but nothing has been read yet. A
1013
+ * plugin that has not been called is not a broken plugin, and reporting
1014
+ * `unhealthy` for it would make every installation that does not use Ever ID
1015
+ * render a red health row (the plugin ships `autoEnable: false`).
1016
+ *
1017
+ * Everything it answers is a **name, a closed code or a number** (FR-16): the
1018
+ * check ids are this file's own, the unavailability reason is
1019
+ * {@link OidcProviderUnavailableReason}'s closed set, the timestamps are epoch
1020
+ * milliseconds, and no message carries the issuer, the client id, the client
1021
+ * secret or any part of a token. `duration` is measured on the injected clock
1022
+ * around the one await this method performs, so a caller (and a spec) can see
1023
+ * how long the settings read took.
1024
+ */
1025
+ healthCheck(): Promise<PluginHealthCheck>;
1026
+ /**
1027
+ * The in-process availability record plan §4.2 stores under `availability`.
1028
+ *
1029
+ * Additive surface for T32, which persists these into the plugin's settings row so
1030
+ * every replica agrees within 60 seconds (FR-5): this is the same record one
1031
+ * process holds, exposed so the persistence task does not have to re-derive when a
1032
+ * run passed.
1033
+ *
1034
+ * `jwksRefreshedAt` is T7's addition — plan §9.2's health view has carried the
1035
+ * field since T5 wrote it into the settings schema
1036
+ * (`settings.schema.ts:262`) and nothing produced it until the key cache was
1037
+ * wired to the flow; it is `null` until a key set has been fetched, which is
1038
+ * exactly what plan §9.2's admin surface renders as "unknown".
1039
+ */
1040
+ getAvailability(): {
1041
+ unavailableSince: string | null;
1042
+ unavailableReason: OidcProviderUnavailableReason | null;
1043
+ discoveryRefreshedAt: string | null;
1044
+ jwksRefreshedAt: string | null;
1045
+ discoveryCacheSeconds: number;
1046
+ };
1047
+ /**
1048
+ * The settings this plugin reads, or the field names that are missing.
1049
+ *
1050
+ * `context.getSettings()` resolves the admin-tier values, secrets included
1051
+ * (`SettingsSchemaValidatorService`, plan §4.2); a throw from the platform is
1052
+ * treated exactly like an absent configuration, because the fail-closed answer is
1053
+ * the same and neither path may surface the platform's own error text (FR-16).
1054
+ */
1055
+ private resolveSettings;
1056
+ /** One reader per configured issuer: a settings change starts a fresh cache (FR-14). */
1057
+ private readerFor;
1058
+ /**
1059
+ * The discovery document the exchange and T8's two verifiers run against, with
1060
+ * T6's availability errors translated to the one refusal type their callers
1061
+ * branch on (see {@link exchangeAuthorizationCode}'s error contract).
1062
+ *
1063
+ * Named for the verification path rather than the exchange since T8: the read is
1064
+ * the same one FR-14's cache and issuer-drift refusal describe, and an access
1065
+ * token or a sign-out notice needs the same document for the same reason — the
1066
+ * `jwks_uri` and the issuer its claims are compared against.
1067
+ */
1068
+ private discoveryForVerification;
1069
+ /**
1070
+ * The one POST of the sign-in flow (FR-15, FR-19).
1071
+ *
1072
+ * `client_secret_basic` is plan §4.2's authentication method for this provider,
1073
+ * and `ClientSecretBasic` from `openid-client` is what produces the header —
1074
+ * the same function the library's own grant calls use, so the encoding of the
1075
+ * credentials (`application/x-www-form-urlencoded` then base64, RFC 6749
1076
+ * §2.3.1) is not re-implemented here. `client_id` is therefore **not** in the
1077
+ * body: RFC 6749 §4.1.3 requires it only "if the client is not authenticating
1078
+ * with the authorization server", and sending it twice is a habit providers
1079
+ * tolerate rather than a requirement they state.
1080
+ *
1081
+ * The response is read as JSON and the `id_token` taken from it. Every way the
1082
+ * call can fail is one code — `providerUnavailable` — because the closed
1083
+ * vocabulary has no code for "the provider refused the grant" and a precise
1084
+ * code with the wrong meaning is worse than a coarse one that is right (see the
1085
+ * report for T4/T25: an `invalid_grant` therefore reaches the API as 503 today).
1086
+ */
1087
+ private redeemAuthorizationCode;
1088
+ /**
1089
+ * FR-11's first two clauses, through T6's key cache: the algorithm allow-list
1090
+ * and the signature, with the cache's own ladder (600 s, 30 s, 21,600 s) and
1091
+ * its `providerUnavailable` end.
1092
+ *
1093
+ * **One implementation for all three token kinds.** T7 wrote it for the ID token
1094
+ * and named it for one; T8's `verifyAccessToken` and `verifyLogoutToken` call the
1095
+ * same method, because "the algorithm is one of RS256/ES256/EdDSA and the
1096
+ * signature verifies against a key Ever ID published, inside FR-13's ladder" is
1097
+ * one rule and FR-45/FR-33 both adopt it by reference ("validated like FR-11 and
1098
+ * FR-13"). A second copy would be a second place for the ladder to drift.
1099
+ *
1100
+ * Both of T6's error types become `IdentityTokenRejectedError`: `badAlg` and
1101
+ * `badSignature` keep their code, and anything the cache could not answer
1102
+ * because the provider is unreachable or its keys are stale is
1103
+ * `providerUnavailable`. An error that is neither is a bug in this package
1104
+ * rather than a verdict about a token, so it propagates instead of being
1105
+ * disguised as a provider outage — no claims are returned either way, which is
1106
+ * what fail-closed means here.
1107
+ */
1108
+ private verifyProviderSignature;
1109
+ /** One key cache per resolved `jwks_uri`; a provider that moves its keys starts a fresh one (FR-13). */
1110
+ private jwksFor;
1111
+ /**
1112
+ * The `openid-client` configuration for one document and one resolved settings
1113
+ * object — used for `buildAuthorizationUrl`, which is a pure URL builder (no
1114
+ * socket, no timer), and for nothing else.
1115
+ *
1116
+ * The rest of the flow does not go through `openid-client`'s `authorizationCodeGrant`,
1117
+ * and that is a measured decision rather than an omission: that call validates
1118
+ * the ID token itself against its own remote key set, which caches for 600
1119
+ * seconds and **keeps using a stale set indefinitely** — the opposite of FR-13's
1120
+ * 21,600-second fail-closed end that `jwks-cache.ts` exists to enforce — and it
1121
+ * applies its own `clockTolerance` to the time claims, which would decide
1122
+ * FR-11's skew edges instead of the administrator's configured
1123
+ * `clockSkewSeconds`. Plan §4.2 names the library for this flow; where its
1124
+ * built-in policy contradicts the spec, the spec's own module wins and the
1125
+ * library keeps the parts that are exactly right (`buildAuthorizationUrl`,
1126
+ * `calculatePKCECodeChallenge`, `ClientSecretBasic`) — the same trade T6
1127
+ * documented for `discovery()`.
1128
+ */
1129
+ private configurationFor;
1130
+ /** Exactly `length` random bytes, from the injected seam or the platform CSPRNG (FR-9). */
1131
+ private random;
1132
+ /** Set or clear plan §4.2's `unavailableSince` from the five checks that gate it. */
1133
+ private applyAvailability;
1134
+ /** FR-14/FR-5: the provider is off from the first failing run until a passing one. */
1135
+ private recordUnavailable;
1136
+ /**
1137
+ * One log line per run, and it carries counts and check **ids** only.
1138
+ *
1139
+ * Ids come from the closed contract set, never from the provider, so nothing a
1140
+ * provider chose to send can reach a log through this path (FR-16).
1141
+ */
1142
+ private logChecks;
1143
+ getManifest(): PluginManifest;
1144
+ }
1145
+
1146
+ /**
1147
+ * APW-12 T7 — the scopes a sign-in asks Ever ID for, and the one it never asks for.
1148
+ *
1149
+ * Plan §4.2: "`src/scopes.ts`: sign-in scopes `openid email profile`; the plugin
1150
+ * never requests `offline_access` (spec FR-38)."
1151
+ *
1152
+ * Both halves of that sentence are requirements, and they are not symmetrical:
1153
+ *
1154
+ * - **`openid email profile`** is what a sign-in needs. `openid` is what makes
1155
+ * the response an OpenID Connect authentication (and what obliges the
1156
+ * provider to return an ID token at all, FR-11); `email` and `profile` carry
1157
+ * `email`, `email_verified` and `name`, which FR-23/FR-24/FR-25 branch on
1158
+ * when an unknown pair is offered account creation or a connection. Asking
1159
+ * for anything else would be asking for access the sign-in path does not
1160
+ * read.
1161
+ * - **`offline_access` is never requested.** FR-38 states the reason in one
1162
+ * line — "Ever Works never stores Ever ID refresh tokens in P1" — and the
1163
+ * cheapest way to keep that promise is not to be able to receive one: a
1164
+ * provider only issues a refresh token to a client that asked for
1165
+ * `offline_access`, so a request that never carries the scope can never
1166
+ * produce a token this platform would have to decide what to do with.
1167
+ *
1168
+ * ## Why this is its own module
1169
+ *
1170
+ * The scope string is a wire value three separate places need to agree on: the
1171
+ * authorization request this package builds (T7), the `GET /client-config`
1172
+ * answer a local client reads before it signs in (FR-39, plan §5.1) and the
1173
+ * documentation that tells a person what Ever Works asks for. It is a constant
1174
+ * rather than a literal because a second spelling of `'openid email profile'`
1175
+ * is a second thing that can drift.
1176
+ *
1177
+ * **T8** added the two scopes on the verifying side —
1178
+ * {@link OIDC_DELEGATED_READ_SCOPE} (`apps:read`, FR-44) and
1179
+ * {@link OIDC_SESSION_EXCHANGE_SCOPE} (`ever-works:session`, FR-39/FR-40) — for
1180
+ * the same reason, one step further along the flow: `verifyAccessToken` is handed
1181
+ * `requiredScopes` by its caller, and the caller and the provider's registration
1182
+ * have to spell them identically.
1183
+ *
1184
+ * Nothing here is a secret and nothing here is provider-specific: the same three
1185
+ * sign-in scopes are what any OpenID Connect relying party asks for, which is why
1186
+ * this file names no issuer.
1187
+ */
1188
+ /**
1189
+ * The three sign-in scopes, in the order the authorization request sends them.
1190
+ *
1191
+ * The order is fixed rather than incidental: the request is asserted
1192
+ * byte-for-byte against this list by `src/__tests__/authorization-request.spec.ts`,
1193
+ * so a reordering is a visible change rather than a silent one.
1194
+ */
1195
+ declare const OIDC_SIGN_IN_SCOPES: readonly ["openid", "email", "profile"];
1196
+ /** Union of {@link OIDC_SIGN_IN_SCOPES}. */
1197
+ type OidcSignInScope = (typeof OIDC_SIGN_IN_SCOPES)[number];
1198
+ /**
1199
+ * The `scope` parameter of every authorization request: `openid email profile`.
1200
+ *
1201
+ * Space-delimited, per RFC 6749 §3.3 — and `buildAuthorizationRequest` sends
1202
+ * **this** string, never one assembled at the call site.
1203
+ */
1204
+ declare const OIDC_SIGN_IN_SCOPE: string;
1205
+ /**
1206
+ * The scopes Ever Works never asks for (FR-38).
1207
+ *
1208
+ * Named rather than inlined so the refusal is testable: `isSignInScope` answers
1209
+ * `false` for a request that carries one, and the spec asserts both that a
1210
+ * built request does not, and that the guard refuses a string that does.
1211
+ */
1212
+ declare const OIDC_NEVER_REQUESTED_SCOPES: readonly ["offline_access"];
1213
+ /** One of {@link OIDC_NEVER_REQUESTED_SCOPES}. */
1214
+ type OidcNeverRequestedScope = (typeof OIDC_NEVER_REQUESTED_SCOPES)[number];
1215
+ /**
1216
+ * APW-12 **T8** — the two scopes Ever Works *verifies* rather than requests.
1217
+ *
1218
+ * They are the other side of the same wire vocabulary: a sign-in **asks** for
1219
+ * `openid email profile`, and a token that arrives from Ever ID is **checked**
1220
+ * against one of these. Both spellings live in this file because a scope string
1221
+ * three components have to agree on is exactly the thing that should have one
1222
+ * definition — FR-40's exchange scope is read by the local client, by the API
1223
+ * endpoint that verifies the token and by the provider's own registration, and
1224
+ * FR-44's delegated scope by the App Launcher, the marked endpoint and the
1225
+ * provider.
1226
+ *
1227
+ * The values are `EVER_ID_SCOPES` in `packages/contracts/src/apps/ever-id.ts`,
1228
+ * transcribed rather than imported for the reason this package depends on
1229
+ * `@ever-works/plugin` alone (see `discovery.ts`), and
1230
+ * `src/__tests__/access-token.spec.ts` reads that file off disk and asserts the
1231
+ * two agree.
1232
+ */
1233
+ declare const OIDC_DELEGATED_READ_SCOPE = "apps:read";
1234
+ /**
1235
+ * FR-39/FR-40 — the scope a local client's access token must carry to be
1236
+ * exchanged for an Ever Works session (`EVER_ID_SCOPES.SESSION_EXCHANGE`).
1237
+ */
1238
+ declare const OIDC_SESSION_EXCHANGE_SCOPE = "ever-works:session";
1239
+ /** The two scopes {@link OIDC_DELEGATED_READ_SCOPE} and {@link OIDC_SESSION_EXCHANGE_SCOPE} name, in one list. */
1240
+ declare const OIDC_TOKEN_SCOPES: readonly ["apps:read", "ever-works:session"];
1241
+ /** Union of {@link OIDC_TOKEN_SCOPES}. */
1242
+ type OidcTokenScope = (typeof OIDC_TOKEN_SCOPES)[number];
1243
+ /**
1244
+ * Is `value` exactly the sign-in scope set — the three of
1245
+ * {@link OIDC_SIGN_IN_SCOPES}, and nothing else?
1246
+ *
1247
+ * Deliberately an equality rather than a "contains" test: a request carrying an
1248
+ * extra scope (a refresh-token scope, a delegated-read scope, a scope a caller
1249
+ * appended) is not the sign-in request this package means to send, and the
1250
+ * honest answer for it is `false`. It is exported so the spec — and any future
1251
+ * caller that has a scope string in hand rather than the constant — can ask the
1252
+ * question the plugin answers by construction.
1253
+ */
1254
+ declare function isSignInScope(value: string): boolean;
1255
+
1256
+ /**
1257
+ * APW-12 T6 — the signing-key cache of the `identity-provider` capability.
1258
+ *
1259
+ * Spec FR-13, in its own words and in one place, because every number below is
1260
+ * one of its clauses:
1261
+ *
1262
+ * - "Signing keys are cached for **600 seconds**";
1263
+ * - "An unknown key ID triggers **at most one refresh per 30 seconds**";
1264
+ * - "While Ever ID is unreachable, cached keys stay usable for at most
1265
+ * **21,600 seconds** after the last successful refresh; after that,
1266
+ * validation **fails closed**";
1267
+ * - "A key Ever ID removes stops validating at the next successful refresh."
1268
+ *
1269
+ * FR-15 adds the shape of the fetch itself: 5,000 ms timeout, one retry after
1270
+ * 1,000 ms. Plan §4.3 is the same table; `EVER_ID_LIMITS.jwksCacheSeconds`,
1271
+ * `.jwksUnknownKidCooldownSeconds` and `.jwksMaxStaleSeconds` are the same numbers
1272
+ * in `packages/contracts/src/apps/ever-id.ts`, and
1273
+ * `src/__tests__/jwks-cache.spec.ts` reads that file off disk and asserts the two
1274
+ * agree (this package deliberately does not depend on `@ever-works/contracts` —
1275
+ * see `discovery.ts`).
1276
+ *
1277
+ * ## What is deliberately *not* here
1278
+ *
1279
+ * Claim validation. `iss`, `aud`, `azp`, `exp`, `iat`, `nonce` and the scope rules
1280
+ * of §4.3 belong to the methods that know which kind of token they were handed
1281
+ * (T7's `exchangeAuthorizationCode`, T8's `verifyAccessToken` /
1282
+ * `verifyLogoutToken`). This class answers one question — "is this signature good
1283
+ * under a key Ever ID published, and may we still trust the set we have?" — and
1284
+ * answers it with the three codes of the closed vocabulary those methods rethrow:
1285
+ * `badAlg`, `badSignature` and `providerUnavailable`.
1286
+ *
1287
+ * ## Why the ladder is implemented here rather than left to `jose`
1288
+ *
1289
+ * `jose`'s `createRemoteJWKSet` already defaults to a 600-second cache and a
1290
+ * 30-second cooldown, but it *keeps using a stale set indefinitely* when the
1291
+ * refetch fails — the opposite of FR-13's 21,600-second fail-closed end. It also
1292
+ * owns its own timer, which FR-15's 5,000 ms bound and a fake clock both need to
1293
+ * see. So the fetch, the clock and the ladder are ours, and `jose` does the two
1294
+ * things it is the dependency for: selecting a key from a JWK Set
1295
+ * (`createLocalJWKSet`) and verifying a compact JWS (`compactVerify`).
1296
+ */
1297
+ /**
1298
+ * Every code this class can raise, taken from the contract's closed set.
1299
+ *
1300
+ * Typed as a subset of `IdentityTokenRejectionCode` on purpose: T7/T8 rethrow
1301
+ * these as `IdentityTokenRejectedError` with the same code, so a code invented
1302
+ * here would not compile.
1303
+ */
1304
+ type OidcJwksFailureCode = Extract<IdentityTokenRejectionCode, 'badAlg' | 'badSignature'>;
1305
+ /**
1306
+ * The error a refused token raises (FR-11's signature and algorithm rules).
1307
+ *
1308
+ * `message` is the code and nothing else (FR-16). `providerUnavailable` is not
1309
+ * here because it is not about the token: {@link OidcProviderUnavailableError}
1310
+ * already carries it, with the reason that produced it.
1311
+ */
1312
+ declare class OidcJwksVerificationError extends Error {
1313
+ readonly code: OidcJwksFailureCode;
1314
+ constructor(code: OidcJwksFailureCode);
1315
+ }
1316
+ /** Construction options for {@link OidcJwksCache}. */
1317
+ interface OidcJwksCacheOptions {
1318
+ /** `jwks_uri` from the discovery document (FR-3's third endpoint check). */
1319
+ readonly jwksUri: string;
1320
+ /** The fetch seam, defaulting to the runtime `fetch` (see `discovery.ts`). */
1321
+ readonly fetchImpl?: OidcFetchImpl;
1322
+ /** The clock, in epoch milliseconds. Defaults to `Date.now`. */
1323
+ readonly now?: () => number;
1324
+ /** FR-15's 5,000 ms per attempt. */
1325
+ readonly timeoutMs?: number;
1326
+ /** FR-15's 1,000 ms before the single retry. */
1327
+ readonly retryDelayMs?: number;
1328
+ /** FR-13's 600 seconds. */
1329
+ readonly cacheSeconds?: number;
1330
+ /** FR-13's 30 seconds, applied to the unknown-`kid` refetch. */
1331
+ readonly unknownKidCooldownSeconds?: number;
1332
+ /** FR-13's 21,600 seconds: the point past which validation fails closed. */
1333
+ readonly maxStaleSeconds?: number;
1334
+ /** FR-11's algorithm allow-list; `none` and `HS*` are refused before any lookup. */
1335
+ readonly algorithms?: readonly string[];
1336
+ /**
1337
+ * Called after every **successful** key fetch, with the epoch-ms instant.
1338
+ *
1339
+ * This is the in-process source of plan §4.2's `availability.jwksRefreshedAt`
1340
+ * (plan §9.2's health view); T32 persists it, and the plugin owns the wiring.
1341
+ */
1342
+ readonly onRefreshed?: (refreshedAt: number) => void;
1343
+ }
1344
+ /** What {@link OidcJwksCache.verify} answers: the header it verified and the claims it carried. */
1345
+ interface OidcJwksVerification {
1346
+ readonly header: JWSHeaderParameters;
1347
+ readonly payload: Record<string, unknown>;
1348
+ /** The `kid` the signature was verified with, when the token named one. */
1349
+ readonly kid: string | null;
1350
+ }
1351
+ /**
1352
+ * The cached signing keys, and the rules that decide when they may still be used
1353
+ * (FR-13).
1354
+ *
1355
+ * One instance per resolved `jwks_uri`. Everything is per-process by design: keys
1356
+ * are public material a provider republishes on demand, so a replica that has not
1357
+ * fetched yet fetches on first use rather than sharing cache state (contrast the
1358
+ * *availability* record, which plan §9.2 keeps in persisted settings precisely
1359
+ * because replicas must agree on it).
1360
+ */
1361
+ declare class OidcJwksCache {
1362
+ private readonly options;
1363
+ private jwks;
1364
+ private resolveKey;
1365
+ private refreshedAtMs;
1366
+ private lastAttemptAtMs;
1367
+ private lastUnknownKidRefreshAtMs;
1368
+ private lastFailure;
1369
+ private availableKeyCount;
1370
+ constructor(options: OidcJwksCacheOptions);
1371
+ /**
1372
+ * When the last **successful** fetch happened, in epoch ms, or `null`.
1373
+ *
1374
+ * FR-13's staleness clock starts here: an attempt that failed does not move
1375
+ * this value, which is exactly why cached keys survive a provider outage.
1376
+ */
1377
+ get lastRefreshedAt(): number | null;
1378
+ /** When the last fetch was **attempted**, successful or not, or `null`. */
1379
+ get lastAttemptAt(): number | null;
1380
+ /** Why the last fetch failed, or `null` when the last one succeeded. */
1381
+ get lastFetchFailure(): OidcOutboundFailure | 'invalidDocument' | null;
1382
+ /** How many keys the cached set holds; `0` until the first successful fetch. */
1383
+ get keyCount(): number;
1384
+ /**
1385
+ * Verify a compact JWS against the cached key set, and answer its claims.
1386
+ *
1387
+ * The order is the plan §4.3 order and each step is load-bearing:
1388
+ *
1389
+ * 1. the protected header must parse (a truncated token is `badSignature`);
1390
+ * 2. `alg` must be one of FR-11's three — `none` and `HS256` are refused
1391
+ * **before** any key lookup, so a token can never talk us into treating a
1392
+ * public key as a shared secret;
1393
+ * 3. the key comes from the cache, refreshing once under FR-13's rules;
1394
+ * 4. `compactVerify` checks the signature, with the same allow-list handed to
1395
+ * it so the two can never disagree.
1396
+ *
1397
+ * Claim rules are the caller's (see the class docstring).
1398
+ */
1399
+ verify(token: string): Promise<OidcJwksVerification>;
1400
+ /**
1401
+ * The key for a token's header, refreshing at most once when the `kid` is unknown
1402
+ * (FR-13).
1403
+ *
1404
+ * A `kid` nobody published cannot be served from the cache, and the two ways out
1405
+ * are ranked: refresh if FR-13's cooldown allows it, and otherwise refuse. A
1406
+ * refusal is `badSignature` — never `providerUnavailable` — while the cached set
1407
+ * is still inside its 21,600 seconds, because the set *is* trustworthy and the
1408
+ * token is simply not signed by anything in it.
1409
+ */
1410
+ resolveSigningKey(header: JWSHeaderParameters, token: string): Promise<CryptoKey | KeyObject | Uint8Array>;
1411
+ /**
1412
+ * Fetch the key set now, replacing the cache **only** on success.
1413
+ *
1414
+ * Answers whether a usable set is in place afterwards. A failed fetch leaves the
1415
+ * previous set exactly as it was, which is what FR-13's 21,600-second window
1416
+ * describes; it never clears the cache and it never throws to a caller that has a
1417
+ * token to refuse.
1418
+ */
1419
+ refresh(): Promise<boolean>;
1420
+ /**
1421
+ * Make sure the cache is fresh enough to answer from (FR-13's 600 seconds).
1422
+ *
1423
+ * An expired cache is refreshed; a cache that cannot be refreshed stays usable
1424
+ * while {@link assertTrusted} allows it, and throws `providerUnavailable` once it
1425
+ * does not.
1426
+ */
1427
+ private ensureUsableKeySet;
1428
+ /** FR-13's 600 seconds: `age < cacheSeconds`. */
1429
+ private isCacheFresh;
1430
+ /**
1431
+ * FR-13's 21,600 seconds, and the fail-closed end of the ladder.
1432
+ *
1433
+ * Two distinct refusals, because they are two distinct operational facts: no key
1434
+ * set has ever been fetched (`keysUnavailable` — usually a provider that has
1435
+ * never answered), or the last good fetch is older than plan §4.3's window
1436
+ * (`keysStale` — plan §9.2's "JWKS stale beyond 21,600 s: every validation fails
1437
+ * closed").
1438
+ */
1439
+ private assertTrusted;
1440
+ /**
1441
+ * FR-13's "at most one refresh per 30 seconds" for an unknown key id.
1442
+ *
1443
+ * Read as the rule it protects: the unknown-`kid` refetch is the one an attacker
1444
+ * can trigger at will by presenting a token with a made-up `kid`, so that trigger
1445
+ * is the one the cooldown bounds. A *successful* cache refresh is not counted
1446
+ * against it, because the 600-second cache already bounds those — counting them
1447
+ * would refuse the first token signed by a freshly rotated key for up to 30
1448
+ * seconds after any refresh, which is the opposite of ACC-12-08 ("a token signed
1449
+ * with a newly rotated key validates after one key refresh").
1450
+ */
1451
+ private canRefreshForUnknownKid;
1452
+ /**
1453
+ * Ask jose for the key matching a header, or `null` when nothing matches.
1454
+ *
1455
+ * `JWKSNoMatchingKey` is the expected "unknown kid" answer and becomes `null` so
1456
+ * the caller can try FR-13's one refresh. Every other jose failure — a malformed
1457
+ * set, several keys matching one header — is a refusal, and none of them ever
1458
+ * surfaces with jose's message attached (FR-16).
1459
+ */
1460
+ private findKey;
1461
+ private algorithms;
1462
+ private cacheSeconds;
1463
+ private maxStaleSeconds;
1464
+ private unknownKidCooldownSeconds;
1465
+ private now;
1466
+ }
1467
+
1468
+ export { OIDC_ACCESS_TOKEN_MAX_AGE_SECONDS, OIDC_ACCESS_TOKEN_MAX_LIFETIME_SECONDS, OIDC_BACKCHANNEL_LOGOUT_EVENT, OIDC_CODE_VERIFIER_BYTES, OIDC_CODE_VERIFIER_LENGTH, OIDC_DEFAULT_API_AUDIENCE, OIDC_DEFAULT_CLOCK_SKEW_SECONDS, OIDC_DELEGATED_READ_SCOPE, OIDC_DISCOVERY_CACHE_SECONDS, OIDC_IDENTITY_SETTING_KEYS, OIDC_IDENTITY_SIGNING_ALGS, OIDC_ID_TOKEN_MAX_AGE_SECONDS, OIDC_JWKS_CACHE_SECONDS, OIDC_JWKS_MAX_STALE_SECONDS, OIDC_JWKS_UNKNOWN_KID_COOLDOWN_SECONDS, OIDC_LOGOUT_TOKEN_MAX_AGE_SECONDS, OIDC_LOGOUT_TOKEN_REPLAY_WINDOW_SECONDS, OIDC_MAX_CLOCK_SKEW_SECONDS, OIDC_NEVER_REQUESTED_SCOPES, OIDC_NONCE_BYTES, OIDC_OUTBOUND_MAX_ATTEMPTS, OIDC_OUTBOUND_RETRY_DELAY_MS, OIDC_OUTBOUND_TIMEOUT_MS, OIDC_SESSION_EXCHANGE_SCOPE, OIDC_SIGN_IN_SCOPE, OIDC_SIGN_IN_SCOPES, OIDC_STATE_BYTES, OIDC_SUBJECT_MAX_LENGTH, OIDC_TOKEN_SCOPES, type OidcDiscoveryDocument, type OidcDiscoveryFailure, type OidcDiscoveryRead, OidcDiscoveryReader, type OidcFetchImpl, type OidcFetchInit, type OidcHttpResponse, type OidcIdentityAvailability, type OidcIdentityDelegatedClientName, type OidcIdentityLocalClient, OidcIdentityPlugin, type OidcIdentitySettingKey, type OidcIdentitySettings, type OidcIdentitySigningAlg, OidcJwksCache, type OidcJwksCacheOptions, type OidcJwksFailureCode, type OidcJwksVerification, OidcJwksVerificationError, type OidcNeverRequestedScope, type OidcOutboundFailure, OidcProviderUnavailableError, type OidcProviderUnavailableReason, type OidcSignInScope, type OidcTokenScope, OidcIdentityPlugin as default, fetchJsonOnce, isSignInScope, oidcIdentityHttpsUrlPattern, oidcIdentityIssuerUrlPattern, oidcIdentitySettingsSchema };