@qauth-labs/plugin-sdk 0.0.0 → 0.2.0-rc.1

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,390 @@
1
+ import { AUTH_METHOD_ROUTE_PATH_PATTERN, authMethodRoutePrefix } from './contract.js';
2
+ import { AuthMethodConfigError } from './errors.js';
3
+ const EGRESS_METHODS = ['GET', 'POST'];
4
+ const EGRESS_SOURCES = ['operator', 'admin', 'remote_document'];
5
+ const EGRESS_REDIRECTS = [undefined, 'follow', 'error'];
6
+ const ASSERTION_ALGORITHMS = ['RS256', 'ES256', 'EdDSA'];
7
+ /**
8
+ * The longest a client assertion signed through the context stays valid, in
9
+ * seconds: its `exp` is lowered to at most this far past the moment it is
10
+ * signed. Five minutes covers a token request and clock skew; an assertion
11
+ * that outlives it is a replayable credential at the provider.
12
+ *
13
+ * @experimental
14
+ */
15
+ export const MAX_AUTH_METHOD_CLIENT_ASSERTION_LIFETIME_SECONDS = 300;
16
+ /**
17
+ * How far a client assertion's `iat` may lie in the future, in seconds, for
18
+ * the clock skew between the method's idea of now and the host's.
19
+ */
20
+ const ASSERTION_IAT_SKEW_SECONDS = 60;
21
+ /**
22
+ * The fields a method's log line may not carry, at any depth, matched without
23
+ * regard to case, `_` or `-` (`clientSecret` matches `client_secret`): the
24
+ * value is replaced with `[Redacted]`. Codes, verifiers, client secrets and
25
+ * assertions, tokens, nonces and passwords: what an upstream sign-in handles
26
+ * and a log must never hold. The last segment of each of the method's own
27
+ * secret paths joins them (`AuthMethodContextParts.secrets`), so does every
28
+ * occurrence of a secret's value.
29
+ *
30
+ * @experimental
31
+ */
32
+ export const AUTH_METHOD_LOG_REDACTED_FIELDS = Object.freeze([
33
+ 'code',
34
+ 'code_verifier',
35
+ 'client_secret',
36
+ 'client_assertion',
37
+ 'refresh_token',
38
+ 'access_token',
39
+ 'id_token',
40
+ 'nonce',
41
+ 'password',
42
+ ]);
43
+ /**
44
+ * The key a method's log fields are nested under, so they never overwrite
45
+ * the host's own fields of the line (`reqId`, `realm`, …).
46
+ *
47
+ * @experimental
48
+ */
49
+ export const AUTH_METHOD_LOG_FIELDS_KEY = 'authMethod';
50
+ /** What a redacted log value is replaced with. */
51
+ const LOG_CENSOR = '[Redacted]';
52
+ /** How deep a method's log fields are copied; anything deeper is cut. */
53
+ const MAX_LOG_DEPTH = 8;
54
+ /**
55
+ * Build the context a method is handed for one realm (ADR-019 Decision 12).
56
+ *
57
+ * The context holds exactly five members: the config, parsed by the method's
58
+ * `configSchema`, copied and deep-frozen (it must be plain data: objects,
59
+ * arrays and primitives, so a Map, Set, Date or other class instance is
60
+ * refused, as none of them freezes); `egress`; `signer`; `log`; and
61
+ * `routeUrl`. Each is an adapter that calls the host's object with the members
62
+ * the contract names and nothing else:
63
+ *
64
+ * - `egress.request` passes `method` (`GET` or `POST`), `url`, `headers`,
65
+ * `body`, `purpose` and `source`, and adds this realm's name. One cap only,
66
+ * `maxRedirects: 0` when the method asks for `redirect: 'error'`; no other
67
+ * cap, no other realm, no other method. `source` is `operator` or `admin` (the
68
+ * labels the realm's egress allowlist applies to, for non-public
69
+ * addresses) only when the URL's origin is the origin of a URL among the
70
+ * plain members of the method's parsed config, never a secret one; any
71
+ * other call goes as `remote_document`, so a method cannot reach an
72
+ * allowlisted address its config does not name.
73
+ * - `signer.signClientAssertion` passes `audience`, `claims` and `algorithm`,
74
+ * with this realm, this provider and the host's caller role. It sets `iat`
75
+ * when the claims carry none, and lowers `exp` to at most
76
+ * {@link MAX_AUTH_METHOD_CLIENT_ASSERTION_LIFETIME_SECONDS} from now.
77
+ * `signer.getUpstreamJwks` takes no argument and asks for this realm and
78
+ * provider.
79
+ * - `log` calls the logger's four levels with a message and the method's
80
+ * fields nested under {@link AUTH_METHOD_LOG_FIELDS_KEY}, as a copy in which
81
+ * every field {@link AUTH_METHOD_LOG_REDACTED_FIELDS} names, or a secret
82
+ * path's last segment, is redacted at any depth, and every secret value is
83
+ * cut out of the message and of every string.
84
+ * - `routeUrl` answers URLs only for the paths the method's own `routes`
85
+ * declare.
86
+ *
87
+ * So the host can hand over its whole egress client, signer and logger, and a
88
+ * method still reaches no `validateDestination`, no `decrypt`, no `child`, no
89
+ * database or cache handle and no key material.
90
+ *
91
+ * @throws AuthMethodConfigError when the config does not pass `configSchema`,
92
+ * or its parsed form is not plain data; the error names the failing
93
+ * members, never their values
94
+ * @experimental
95
+ */
96
+ export function createAuthMethodContext(method, parts) {
97
+ const type = method.type;
98
+ const config = parseAuthMethodConfig(method, parts.config);
99
+ const origin = new URL(parts.origin);
100
+ if (origin.origin !== parts.origin) {
101
+ throw new TypeError('origin must be a bare origin, such as https://auth.example.com');
102
+ }
103
+ const prefix = authMethodRoutePrefix(type);
104
+ const routePaths = declaredRoutePaths(method.routes);
105
+ const secrets = parts.secrets ?? new Map();
106
+ const secretValues = [...secrets.values()].filter((value) => value !== '');
107
+ const configOrigins = urlOrigins(config, new Set(secrets.keys()), new Set(secretValues));
108
+ const redaction = {
109
+ fields: new Set([
110
+ ...AUTH_METHOD_LOG_REDACTED_FIELDS.map(fieldKey),
111
+ ...[...secrets.keys()].map((path) => fieldKey(path.split('.').at(-1) ?? path)),
112
+ ]),
113
+ // The longest first, so no shorter value cuts a longer one apart.
114
+ values: [...secretValues].sort((a, b) => b.length - a.length),
115
+ };
116
+ const { egress, signer, log, realmName, realmId, providerId, signerCaller } = parts;
117
+ const logged = (fields) => ({
118
+ [AUTH_METHOD_LOG_FIELDS_KEY]: redactedCopy(fields, 0, new WeakSet(), redaction),
119
+ });
120
+ const message = (text) => withoutSecrets(String(text), redaction);
121
+ return Object.freeze({
122
+ config: deepFreeze(config),
123
+ egress: Object.freeze({
124
+ request(request) {
125
+ // Each member read once: what is checked is what is sent.
126
+ const { method: verb, url, headers, body, purpose, source, redirect } = request;
127
+ if (!EGRESS_METHODS.includes(verb)) {
128
+ return Promise.reject(new TypeError('egress method must be GET or POST'));
129
+ }
130
+ if (!EGRESS_SOURCES.includes(source)) {
131
+ return Promise.reject(new TypeError('egress source must be operator, admin or remote_document'));
132
+ }
133
+ if (!EGRESS_REDIRECTS.includes(redirect)) {
134
+ return Promise.reject(new TypeError('egress redirect must be follow or error'));
135
+ }
136
+ return egress.request({
137
+ method: verb,
138
+ url,
139
+ ...(headers === undefined ? {} : { headers: { ...headers } }),
140
+ ...(body === undefined ? {} : { body }),
141
+ purpose,
142
+ source: source === 'remote_document' || configOrigins.has(originOf(url))
143
+ ? source
144
+ : 'remote_document',
145
+ realm: realmName,
146
+ // The one cap a method may set: zero redirects, the floor.
147
+ ...(redirect === 'error' ? { limits: { maxRedirects: 0 } } : {}),
148
+ });
149
+ },
150
+ }),
151
+ signer: Object.freeze({
152
+ signClientAssertion(request) {
153
+ const { audience, claims, algorithm } = request;
154
+ if (algorithm !== undefined && !ASSERTION_ALGORITHMS.includes(algorithm)) {
155
+ return Promise.reject(new TypeError('algorithm must be RS256, ES256 or EdDSA'));
156
+ }
157
+ if (typeof claims !== 'object' || claims === null || Array.isArray(claims)) {
158
+ return Promise.reject(new TypeError('claims must be an object'));
159
+ }
160
+ const now = Math.floor(Date.now() / 1000);
161
+ const bounded = { ...claims };
162
+ const { iat, exp } = bounded;
163
+ if (iat === undefined) {
164
+ bounded['iat'] = now;
165
+ }
166
+ else if (typeof iat !== 'number' ||
167
+ !Number.isFinite(iat) ||
168
+ iat > now + ASSERTION_IAT_SKEW_SECONDS) {
169
+ return Promise.reject(new TypeError('iat must be a time, in seconds, not in the future'));
170
+ }
171
+ // A missing or non-numeric exp goes on as it is: the signer refuses it.
172
+ const latest = now + MAX_AUTH_METHOD_CLIENT_ASSERTION_LIFETIME_SECONDS;
173
+ if (typeof exp === 'number' && exp > latest)
174
+ bounded['exp'] = latest;
175
+ return signer.signClientAssertion(signerCaller, {
176
+ realmId,
177
+ providerId,
178
+ audience,
179
+ claims: bounded,
180
+ ...(algorithm === undefined ? {} : { algorithm }),
181
+ });
182
+ },
183
+ async getUpstreamJwks() {
184
+ const jwks = await signer.getUpstreamJwks(signerCaller, realmId, providerId);
185
+ return { keys: jwks.keys };
186
+ },
187
+ }),
188
+ log: Object.freeze({
189
+ debug: (fields, text) => log.debug(logged(fields), message(text)),
190
+ info: (fields, text) => log.info(logged(fields), message(text)),
191
+ warn: (fields, text) => log.warn(logged(fields), message(text)),
192
+ error: (fields, text) => log.error(logged(fields), message(text)),
193
+ }),
194
+ routeUrl(path) {
195
+ if (typeof path !== 'string' ||
196
+ !AUTH_METHOD_ROUTE_PATH_PATTERN.test(path) ||
197
+ !routePaths.has(path)) {
198
+ throw new TypeError("routeUrl takes the path of one of the method's own routes, such as /callback");
199
+ }
200
+ return `${origin.origin}${prefix}${path}`;
201
+ },
202
+ });
203
+ }
204
+ /** The paths of a method's routes, read once by index. */
205
+ function declaredRoutePaths(routes) {
206
+ const paths = new Set();
207
+ if (!Array.isArray(routes))
208
+ return paths;
209
+ const length = routes.length;
210
+ if (typeof length !== 'number' || !Number.isSafeInteger(length))
211
+ return paths;
212
+ for (let index = 0; index < length; index += 1) {
213
+ const route = routes[index];
214
+ if (typeof route !== 'object' || route === null)
215
+ continue;
216
+ const path = route['path'];
217
+ if (typeof path === 'string')
218
+ paths.add(path);
219
+ }
220
+ return paths;
221
+ }
222
+ /**
223
+ * The origins of every absolute URL among the strings of a (plain) config's
224
+ * plain members: a member at one of `secretPaths`, and a string that is one
225
+ * of `secretValues`, is skipped, so a secret that parses as a URL opens no
226
+ * destination.
227
+ */
228
+ function urlOrigins(config, secretPaths, secretValues) {
229
+ const origins = new Set();
230
+ const walk = (value, path, depth) => {
231
+ if (path !== '' && secretPaths.has(path))
232
+ return;
233
+ if (typeof value === 'string') {
234
+ if (secretValues.has(value))
235
+ return;
236
+ const found = originOf(value);
237
+ if (found !== '')
238
+ origins.add(found);
239
+ return;
240
+ }
241
+ if (typeof value !== 'object' || value === null || depth > 32)
242
+ return;
243
+ // A list's items have no member path; a secret is never inside one.
244
+ const inList = Array.isArray(value);
245
+ for (const [key, item] of Object.entries(value)) {
246
+ walk(item, inList ? `${path}[]` : path === '' ? key : `${path}.${key}`, depth + 1);
247
+ }
248
+ };
249
+ walk(config, '', 0);
250
+ return origins;
251
+ }
252
+ /** The origin of an absolute http(s) URL, or `''`. */
253
+ function originOf(value) {
254
+ if (typeof value !== 'string')
255
+ return '';
256
+ let url;
257
+ try {
258
+ url = new URL(value);
259
+ }
260
+ catch {
261
+ return '';
262
+ }
263
+ return url.protocol === 'https:' || url.protocol === 'http:' ? url.origin : '';
264
+ }
265
+ /**
266
+ * A method's config as a context holds it: parsed by its `configSchema`, and
267
+ * a copy of plain data (objects with the plain prototype or none, arrays,
268
+ * primitives). The admin API checks a plugin's config with it before it
269
+ * writes it (#600, #639), so a config the context would refuse is refused at
270
+ * the configure, never at every sign-in.
271
+ *
272
+ * @throws AuthMethodConfigError when the config does not pass the schema, or
273
+ * its parsed form is not plain data; the error names the failing member's
274
+ * path, never its value
275
+ * @experimental
276
+ */
277
+ export function parseAuthMethodConfig(method, raw) {
278
+ const type = method.type;
279
+ const parsed = method.configSchema.safeParse(raw);
280
+ if (!parsed.success) {
281
+ throw new AuthMethodConfigError(type, parsed.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.code}`));
282
+ }
283
+ const notPlain = notPlainDataPath(parsed.data, [], 0);
284
+ if (notPlain !== undefined) {
285
+ throw new AuthMethodConfigError(type, [
286
+ `${notPlain.join('.') || '(root)'}: not plain data (a Map, Set, Date or class instance does not freeze)`,
287
+ ]);
288
+ }
289
+ try {
290
+ // A copy, so freezing it leaves the caller's objects alone.
291
+ return structuredClone(parsed.data);
292
+ }
293
+ catch {
294
+ throw new AuthMethodConfigError(type, ['(root): the parsed config is not plain data']);
295
+ }
296
+ }
297
+ /**
298
+ * The path of the first member of `value` that is not plain data (an object
299
+ * with the plain prototype or none, an array, or a primitive), or
300
+ * `undefined` when there is none.
301
+ */
302
+ function notPlainDataPath(value, path, depth) {
303
+ if (typeof value !== 'object' || value === null)
304
+ return undefined;
305
+ if (depth > 32)
306
+ return path;
307
+ if (!Array.isArray(value)) {
308
+ const prototype = Object.getPrototypeOf(value);
309
+ if (prototype !== Object.prototype && prototype !== null)
310
+ return path;
311
+ }
312
+ for (const [key, item] of Object.entries(value)) {
313
+ const found = notPlainDataPath(item, [...path, key], depth + 1);
314
+ if (found !== undefined)
315
+ return found;
316
+ }
317
+ return undefined;
318
+ }
319
+ /** A field name as redaction compares it: lower case, without `_` or `-`. */
320
+ function fieldKey(name) {
321
+ return name.toLowerCase().replace(/[_-]/g, '');
322
+ }
323
+ /** `text` with every occurrence of a secret value replaced by the censor. */
324
+ function withoutSecrets(text, redaction) {
325
+ let result = text;
326
+ for (const value of redaction.values) {
327
+ if (result.includes(value))
328
+ result = result.split(value).join(LOG_CENSOR);
329
+ }
330
+ return result;
331
+ }
332
+ /**
333
+ * A copy of a method's log fields with every field `redaction` names redacted,
334
+ * at any depth, and every secret value cut out of every string. An Error is
335
+ * copied as its name, message, stack, cause and own fields, as the host's
336
+ * error serializer would write it; binary data as its size.
337
+ */
338
+ function redactedCopy(value, depth, seen, redaction) {
339
+ if (typeof value !== 'object' || value === null) {
340
+ if (typeof value === 'string')
341
+ return withoutSecrets(value, redaction);
342
+ return typeof value === 'function' || typeof value === 'symbol'
343
+ ? withoutSecrets(String(value), redaction)
344
+ : value;
345
+ }
346
+ if (depth >= MAX_LOG_DEPTH)
347
+ return '[Truncated]';
348
+ if (seen.has(value))
349
+ return '[Circular]';
350
+ seen.add(value);
351
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
352
+ return `[${value.byteLength} bytes]`;
353
+ }
354
+ if (value instanceof Date)
355
+ return value.toISOString();
356
+ if (Array.isArray(value)) {
357
+ const copy = [];
358
+ const length = value.length;
359
+ for (let index = 0; index < length; index += 1) {
360
+ copy[index] = redactedCopy(value[index], depth + 1, seen, redaction);
361
+ }
362
+ return copy;
363
+ }
364
+ // No prototype: a field named `__proto__` stays a field.
365
+ const copy = Object.create(null);
366
+ if (value instanceof Error) {
367
+ copy['type'] = redactedCopy(value.name, depth + 1, seen, redaction);
368
+ copy['message'] = redactedCopy(value.message, depth + 1, seen, redaction);
369
+ copy['stack'] = redactedCopy(value.stack, depth + 1, seen, redaction);
370
+ if (value.cause !== undefined) {
371
+ copy['cause'] = redactedCopy(value.cause, depth + 1, seen, redaction);
372
+ }
373
+ }
374
+ for (const [key, item] of Object.entries(value)) {
375
+ copy[key] = redaction.fields.has(fieldKey(key))
376
+ ? LOG_CENSOR
377
+ : redactedCopy(item, depth + 1, seen, redaction);
378
+ }
379
+ return copy;
380
+ }
381
+ /** Freeze `value` and everything it holds; `seen` stops at cycles. */
382
+ function deepFreeze(value, seen = new WeakSet()) {
383
+ if (typeof value !== 'object' || value === null || seen.has(value))
384
+ return value;
385
+ seen.add(value);
386
+ for (const item of Object.values(value))
387
+ deepFreeze(item, seen);
388
+ Object.freeze(value);
389
+ return value;
390
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * The fixed values of the AuthMethod contract (#599, ADR-003 as amended,
3
+ * ADR-018 §4, ADR-019 Decision 12).
4
+ *
5
+ * EXPERIMENTAL. The AuthMethod contract is the plugin API of ADR-018 §4: it is
6
+ * public and documented, so others can write methods against it, but it stays
7
+ * outside the 1.0 stability promise and may change in a minor release. It
8
+ * becomes stable in 1.x. QAuth's experimental-feature registry lists it as
9
+ * `plugin-api`.
10
+ *
11
+ * This package imports no other QAuth package, so the few values it shares
12
+ * with the server's other contracts (the Interaction API's version label and
13
+ * display rule, the credential types QAuth writes itself) are written out
14
+ * here. The server checks at boot, and in its tests, that each still equals
15
+ * its source.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ /**
20
+ * The contract's name, as it appears in {@link AUTH_METHOD_CONTRACT}.
21
+ *
22
+ * @experimental
23
+ */
24
+ export declare const AUTH_METHOD_CONTRACT_NAME = "qauth-auth-method";
25
+ /**
26
+ * The contract's version: a release candidate until QAuth 1.0.0
27
+ * (ADR-018 §5). A breaking change raises the release-candidate number, so one
28
+ * identifier never names two contracts.
29
+ *
30
+ * @experimental
31
+ */
32
+ export declare const AUTH_METHOD_CONTRACT_VERSION = "1-rc.0";
33
+ /**
34
+ * The contract a method declares it is written against, as a format id:
35
+ * `qauth-auth-method/1-rc.0`. The registry refuses a method that declares
36
+ * another one, so a method built for another version fails the boot instead of
37
+ * running against a contract it does not know.
38
+ *
39
+ * @experimental
40
+ */
41
+ export declare const AUTH_METHOD_CONTRACT: "qauth-auth-method/1-rc.0";
42
+ /**
43
+ * The export a plugin package names its method by: `authMethod`, an
44
+ * AuthMethod object. A CommonJS package may put it under `default`.
45
+ *
46
+ * ```ts
47
+ * // index.ts of @example/qauth-plugin-corp-sso
48
+ * export const authMethod: AuthMethod<CorpSsoConfig, CorpSsoTransaction> = { … };
49
+ * ```
50
+ *
51
+ * @experimental
52
+ */
53
+ export declare const AUTH_METHOD_PLUGIN_EXPORT = "authMethod";
54
+ /**
55
+ * The upstream step: the Interaction API step through which a person signs in
56
+ * at an upstream identity provider. The method starts an upstream transaction
57
+ * (`AuthMethod.start`), the browser goes to the provider, and the
58
+ * method's callback route hands the provider's answer back to the engine.
59
+ *
60
+ * Step names are QAuth identifiers that carry the Interaction API's version
61
+ * (ADR-018 §5): `urn:qauth:ia:upstream:v1-rc.0` until 1.0.0. The step's params
62
+ * and its place in ADR-017's step table come with the upstream sign-in flow
63
+ * (#639).
64
+ *
65
+ * @experimental
66
+ */
67
+ export declare const UPSTREAM_STEP_URN: "urn:qauth:ia:upstream:v1-rc.0";
68
+ /**
69
+ * Every step a method may contribute in this version of the contract, by URN.
70
+ * Only the upstream step today; a later version may add others (an LDAP form,
71
+ * for instance).
72
+ *
73
+ * @experimental
74
+ */
75
+ export declare const AUTH_METHOD_STEP_URNS: readonly ["urn:qauth:ia:upstream:v1-rc.0"];
76
+ /**
77
+ * A step a method may contribute: one of {@link AUTH_METHOD_STEP_URNS}.
78
+ *
79
+ * @experimental
80
+ */
81
+ export type AuthMethodStepUrn = (typeof AUTH_METHOD_STEP_URNS)[number];
82
+ /**
83
+ * The root every method's routes are mounted under, on the realm host. A
84
+ * method's own prefix is `/upstream/<type>` ({@link authMethodRoutePrefix}),
85
+ * so each provider has its own callback URL, and a response meant for one
86
+ * provider cannot be replayed at another.
87
+ *
88
+ * No version in the path: the callback URL is registered at the upstream
89
+ * provider, and a path that changed at 1.0 would break every registration.
90
+ *
91
+ * @experimental
92
+ */
93
+ export declare const AUTH_METHOD_ROUTE_ROOT = "/upstream";
94
+ /**
95
+ * The prefix a method's routes are mounted under: `/upstream/<type>`.
96
+ *
97
+ * @experimental
98
+ */
99
+ export declare function authMethodRoutePrefix(type: string): string;
100
+ /**
101
+ * An AuthMethod's `type`: lower-case ASCII letters, digits and `_`, starting
102
+ * with a letter, at most 63 characters. It is the registry key, the value
103
+ * written to `user_credentials.provider_type`, the `source` of the attributes
104
+ * the method extracts and a segment of its route prefix, so it must be safe in
105
+ * all four. A type with `:` or `/` (an ID-JAG issuer type, `oidc_<issuer URL>`)
106
+ * can never match.
107
+ *
108
+ * @experimental
109
+ */
110
+ export declare const AUTH_METHOD_TYPE_PATTERN: RegExp;
111
+ /**
112
+ * An AuthMethod's `id`: the stable name of the method's implementation, such
113
+ * as `oidc` or `corp-sso`. Lower-case ASCII letters, digits, `_` and `-`,
114
+ * starting with a letter, at most 63 characters.
115
+ *
116
+ * @experimental
117
+ */
118
+ export declare const AUTH_METHOD_ID_PATTERN: RegExp;
119
+ /**
120
+ * Types an AuthMethod may not take: the credential types and the attribute
121
+ * source QAuth writes itself, built or planned. A method under one of them
122
+ * would read as that built-in credential, and `wallet` would outrank every
123
+ * other attribute source in claim resolution (`rankAttributeSource`).
124
+ *
125
+ * @experimental
126
+ */
127
+ export declare const RESERVED_AUTH_METHOD_TYPES: readonly string[];
128
+ /**
129
+ * A route path below a method's prefix: one to four segments of lower-case
130
+ * ASCII letters, digits, `_` and `-`, such as `/callback`. No dot segment, no
131
+ * encoded character, no route parameter and no wildcard can match, so a path
132
+ * cannot leave the prefix it is mounted under.
133
+ *
134
+ * @experimental
135
+ */
136
+ export declare const AUTH_METHOD_ROUTE_PATH_PATTERN: RegExp;
137
+ /**
138
+ * The display rule of the Interaction API (ADR-019 Decision 6), which a
139
+ * method's default name follows: no markup bracket, no control, format,
140
+ * private-use or unassigned character, no line separator and no space at
141
+ * either end.
142
+ *
143
+ * @experimental
144
+ */
145
+ export declare const AUTH_METHOD_DISPLAY_NAME_PATTERN: RegExp;
146
+ /**
147
+ * The longest default display name, in UTF-16 code units.
148
+ *
149
+ * @experimental
150
+ */
151
+ export declare const MAX_AUTH_METHOD_DISPLAY_NAME_LENGTH = 64;
152
+ /**
153
+ * The largest default icon, in bytes.
154
+ *
155
+ * @experimental
156
+ */
157
+ export declare const MAX_AUTH_METHOD_ICON_BYTES: number;
158
+ /**
159
+ * The media types a default icon may have: raster PNG only. An SVG can carry
160
+ * script, and the icon is served from the realm origin.
161
+ *
162
+ * @experimental
163
+ */
164
+ export declare const AUTH_METHOD_ICON_MEDIA_TYPES: readonly ["image/png"];
165
+ /**
166
+ * The largest upstream transaction a method may ask the engine to keep, in
167
+ * bytes of its JSON form. The engine stores it server-side, bound to the
168
+ * interaction and its flow cookie, for the lifetime of the interaction.
169
+ *
170
+ * @experimental
171
+ */
172
+ export declare const MAX_UPSTREAM_TRANSACTION_BYTES = 4096;
173
+ /**
174
+ * A binding (#599): the value `start()` answers beside its transaction, which
175
+ * the authorization request carries to the provider (as `state`, usually)
176
+ * and the provider's callback carries back. 22 to 512 base64url characters:
177
+ * 22 is the length of 128 random bits in base64url. The pattern checks the
178
+ * length and the alphabet only; the method makes the value random.
179
+ *
180
+ * @experimental
181
+ */
182
+ export declare const AUTH_METHOD_BINDING_PATTERN: RegExp;
183
+ /**
184
+ * The parameter a callback route carries the binding back in when the route
185
+ * names none: `state` (RFC 6749 §4.1.2).
186
+ *
187
+ * @experimental
188
+ */
189
+ export declare const DEFAULT_AUTH_METHOD_BINDING_PARAMETER = "state";
190
+ /**
191
+ * A parameter name a callback route may name as its binding parameter:
192
+ * ASCII letters, digits, `_`, `.` and `-`, at most 64 characters.
193
+ *
194
+ * @experimental
195
+ */
196
+ export declare const AUTH_METHOD_BINDING_PARAMETER_PATTERN: RegExp;
197
+ /**
198
+ * The longest URL a method may send the browser to when it starts.
199
+ *
200
+ * @experimental
201
+ */
202
+ export declare const MAX_UPSTREAM_REDIRECT_LENGTH = 8192;
203
+ /**
204
+ * The longest `externalSub` a method may resolve: 255 characters, the limit
205
+ * OpenID Connect Core 1.0 §2 sets on `sub`.
206
+ *
207
+ * @experimental
208
+ */
209
+ export declare const MAX_AUTH_METHOD_EXTERNAL_SUB_LENGTH = 255;
210
+ /**
211
+ * The largest `rawClaims` a method's `verify()` may resolve, in bytes of its
212
+ * JSON form. The engine keeps a frozen copy.
213
+ *
214
+ * @experimental
215
+ */
216
+ export declare const MAX_AUTH_METHOD_RAW_CLAIMS_BYTES: number;
217
+ /**
218
+ * An attribute key a method may extract: lower-case ASCII letters, digits
219
+ * and `_`, starting with a letter, at most 64 characters (`email`,
220
+ * `given_name`).
221
+ *
222
+ * @experimental
223
+ */
224
+ export declare const AUTH_METHOD_ATTRIBUTE_KEY_PATTERN: RegExp;
225
+ /**
226
+ * The longest attribute value a method may extract, in UTF-16 code units.
227
+ *
228
+ * @experimental
229
+ */
230
+ export declare const MAX_AUTH_METHOD_ATTRIBUTE_VALUE_LENGTH = 8192;
231
+ /**
232
+ * The most parameters a method's route handler sees from one query or one
233
+ * form; the rest are dropped.
234
+ *
235
+ * @experimental
236
+ */
237
+ export declare const MAX_AUTH_METHOD_PARAMETERS = 64;
238
+ /**
239
+ * The longest parameter value a method's route handler sees, in UTF-16 code
240
+ * units; a parameter with a longer value is dropped.
241
+ *
242
+ * @experimental
243
+ */
244
+ export declare const MAX_AUTH_METHOD_PARAMETER_LENGTH = 8192;
245
+ /**
246
+ * The most values of one repeated parameter a method's route handler sees;
247
+ * a parameter repeated more often is dropped.
248
+ *
249
+ * @experimental
250
+ */
251
+ export declare const MAX_AUTH_METHOD_PARAMETER_VALUES = 16;
252
+ /**
253
+ * Why a method's callback did not produce a result, other than the person
254
+ * declining at the provider:
255
+ *
256
+ * - `transaction_mismatch`: no transaction is pending for this browser, or the
257
+ * callback does not belong to it (its binding, `state` by default, is
258
+ * missing, repeated or another's). A callback replayed in another browser,
259
+ * or forged, ends here; the host answers it before the method runs.
260
+ * - `provider_error`: the provider answered with an error, other than the
261
+ * person declining (`access_denied`).
262
+ * - `invalid_response`: the provider's answer failed a check, such as the ID
263
+ * token's signature, `iss`, `aud` or `nonce`.
264
+ * - `upstream_unavailable`: the provider could not be reached in time.
265
+ *
266
+ * @experimental
267
+ */
268
+ export declare const UPSTREAM_FAILURE_REASONS: readonly ["transaction_mismatch", "provider_error", "invalid_response", "upstream_unavailable"];
269
+ /**
270
+ * One of {@link UPSTREAM_FAILURE_REASONS}.
271
+ *
272
+ * @experimental
273
+ */
274
+ export type UpstreamFailureReason = (typeof UPSTREAM_FAILURE_REASONS)[number];