@qauth-labs/plugin-sdk 0.0.0-stage → 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,285 @@
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 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 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 const AUTH_METHOD_CONTRACT = `${AUTH_METHOD_CONTRACT_NAME}/${AUTH_METHOD_CONTRACT_VERSION}`;
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 const AUTH_METHOD_PLUGIN_EXPORT = 'authMethod';
54
+ /**
55
+ * The Interaction API's version label, which every step name carries
56
+ * (ADR-018 §5): `v1-rc.0` until QAuth 1.0.0.
57
+ */
58
+ const INTERACTION_API_VERSION = 'v1-rc.0';
59
+ /**
60
+ * The upstream step: the Interaction API step through which a person signs in
61
+ * at an upstream identity provider. The method starts an upstream transaction
62
+ * (`AuthMethod.start`), the browser goes to the provider, and the
63
+ * method's callback route hands the provider's answer back to the engine.
64
+ *
65
+ * Step names are QAuth identifiers that carry the Interaction API's version
66
+ * (ADR-018 §5): `urn:qauth:ia:upstream:v1-rc.0` until 1.0.0. The step's params
67
+ * and its place in ADR-017's step table come with the upstream sign-in flow
68
+ * (#639).
69
+ *
70
+ * @experimental
71
+ */
72
+ export const UPSTREAM_STEP_URN = `urn:qauth:ia:upstream:${INTERACTION_API_VERSION}`;
73
+ /**
74
+ * Every step a method may contribute in this version of the contract, by URN.
75
+ * Only the upstream step today; a later version may add others (an LDAP form,
76
+ * for instance).
77
+ *
78
+ * @experimental
79
+ */
80
+ export const AUTH_METHOD_STEP_URNS = Object.freeze([UPSTREAM_STEP_URN]);
81
+ /**
82
+ * The root every method's routes are mounted under, on the realm host. A
83
+ * method's own prefix is `/upstream/<type>` ({@link authMethodRoutePrefix}),
84
+ * so each provider has its own callback URL, and a response meant for one
85
+ * provider cannot be replayed at another.
86
+ *
87
+ * No version in the path: the callback URL is registered at the upstream
88
+ * provider, and a path that changed at 1.0 would break every registration.
89
+ *
90
+ * @experimental
91
+ */
92
+ export const AUTH_METHOD_ROUTE_ROOT = '/upstream';
93
+ /**
94
+ * The prefix a method's routes are mounted under: `/upstream/<type>`.
95
+ *
96
+ * @experimental
97
+ */
98
+ export function authMethodRoutePrefix(type) {
99
+ if (!AUTH_METHOD_TYPE_PATTERN.test(type)) {
100
+ throw new TypeError('not an AuthMethod type');
101
+ }
102
+ return `${AUTH_METHOD_ROUTE_ROOT}/${type}`;
103
+ }
104
+ /**
105
+ * An AuthMethod's `type`: lower-case ASCII letters, digits and `_`, starting
106
+ * with a letter, at most 63 characters. It is the registry key, the value
107
+ * written to `user_credentials.provider_type`, the `source` of the attributes
108
+ * the method extracts and a segment of its route prefix, so it must be safe in
109
+ * all four. A type with `:` or `/` (an ID-JAG issuer type, `oidc_<issuer URL>`)
110
+ * can never match.
111
+ *
112
+ * @experimental
113
+ */
114
+ export const AUTH_METHOD_TYPE_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
115
+ /**
116
+ * An AuthMethod's `id`: the stable name of the method's implementation, such
117
+ * as `oidc` or `corp-sso`. Lower-case ASCII letters, digits, `_` and `-`,
118
+ * starting with a letter, at most 63 characters.
119
+ *
120
+ * @experimental
121
+ */
122
+ export const AUTH_METHOD_ID_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/;
123
+ /**
124
+ * Types an AuthMethod may not take: the credential types and the attribute
125
+ * source QAuth writes itself, built or planned. A method under one of them
126
+ * would read as that built-in credential, and `wallet` would outrank every
127
+ * other attribute source in claim resolution (`rankAttributeSource`).
128
+ *
129
+ * @experimental
130
+ */
131
+ export const RESERVED_AUTH_METHOD_TYPES = Object.freeze([
132
+ 'password',
133
+ 'wallet',
134
+ 'webauthn',
135
+ 'self_reported',
136
+ 'totp',
137
+ 'recovery_code',
138
+ 'email_code',
139
+ ]);
140
+ /**
141
+ * A route path below a method's prefix: one to four segments of lower-case
142
+ * ASCII letters, digits, `_` and `-`, such as `/callback`. No dot segment, no
143
+ * encoded character, no route parameter and no wildcard can match, so a path
144
+ * cannot leave the prefix it is mounted under.
145
+ *
146
+ * @experimental
147
+ */
148
+ export const AUTH_METHOD_ROUTE_PATH_PATTERN = /^(?:\/[a-z0-9][a-z0-9_-]{0,62}){1,4}$/;
149
+ /**
150
+ * The display rule of the Interaction API (ADR-019 Decision 6), which a
151
+ * method's default name follows: no markup bracket, no control, format,
152
+ * private-use or unassigned character, no line separator and no space at
153
+ * either end.
154
+ *
155
+ * @experimental
156
+ */
157
+ export const AUTH_METHOD_DISPLAY_NAME_PATTERN = /^[^<>\p{C}\p{Z}\u115F\u1160\u3164\uFFA0](?:[^<>\p{C}\p{Zl}\p{Zp}\u115F\u1160\u3164\uFFA0]*[^<>\p{C}\p{Z}\u115F\u1160\u3164\uFFA0])?$/u;
158
+ /**
159
+ * The longest default display name, in UTF-16 code units.
160
+ *
161
+ * @experimental
162
+ */
163
+ export const MAX_AUTH_METHOD_DISPLAY_NAME_LENGTH = 64;
164
+ /**
165
+ * The largest default icon, in bytes.
166
+ *
167
+ * @experimental
168
+ */
169
+ export const MAX_AUTH_METHOD_ICON_BYTES = 16 * 1024;
170
+ /**
171
+ * The media types a default icon may have: raster PNG only. An SVG can carry
172
+ * script, and the icon is served from the realm origin.
173
+ *
174
+ * @experimental
175
+ */
176
+ export const AUTH_METHOD_ICON_MEDIA_TYPES = Object.freeze(['image/png']);
177
+ /**
178
+ * The largest upstream transaction a method may ask the engine to keep, in
179
+ * bytes of its JSON form. The engine stores it server-side, bound to the
180
+ * interaction and its flow cookie, for the lifetime of the interaction.
181
+ *
182
+ * @experimental
183
+ */
184
+ export const MAX_UPSTREAM_TRANSACTION_BYTES = 4096;
185
+ /**
186
+ * A binding (#599): the value `start()` answers beside its transaction, which
187
+ * the authorization request carries to the provider (as `state`, usually)
188
+ * and the provider's callback carries back. 22 to 512 base64url characters:
189
+ * 22 is the length of 128 random bits in base64url. The pattern checks the
190
+ * length and the alphabet only; the method makes the value random.
191
+ *
192
+ * @experimental
193
+ */
194
+ export const AUTH_METHOD_BINDING_PATTERN = /^[A-Za-z0-9_-]{22,512}$/;
195
+ /**
196
+ * The parameter a callback route carries the binding back in when the route
197
+ * names none: `state` (RFC 6749 §4.1.2).
198
+ *
199
+ * @experimental
200
+ */
201
+ export const DEFAULT_AUTH_METHOD_BINDING_PARAMETER = 'state';
202
+ /**
203
+ * A parameter name a callback route may name as its binding parameter:
204
+ * ASCII letters, digits, `_`, `.` and `-`, at most 64 characters.
205
+ *
206
+ * @experimental
207
+ */
208
+ export const AUTH_METHOD_BINDING_PARAMETER_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
209
+ /**
210
+ * The longest URL a method may send the browser to when it starts.
211
+ *
212
+ * @experimental
213
+ */
214
+ export const MAX_UPSTREAM_REDIRECT_LENGTH = 8192;
215
+ /**
216
+ * The longest `externalSub` a method may resolve: 255 characters, the limit
217
+ * OpenID Connect Core 1.0 §2 sets on `sub`.
218
+ *
219
+ * @experimental
220
+ */
221
+ export const MAX_AUTH_METHOD_EXTERNAL_SUB_LENGTH = 255;
222
+ /**
223
+ * The largest `rawClaims` a method's `verify()` may resolve, in bytes of its
224
+ * JSON form. The engine keeps a frozen copy.
225
+ *
226
+ * @experimental
227
+ */
228
+ export const MAX_AUTH_METHOD_RAW_CLAIMS_BYTES = 64 * 1024;
229
+ /**
230
+ * An attribute key a method may extract: lower-case ASCII letters, digits
231
+ * and `_`, starting with a letter, at most 64 characters (`email`,
232
+ * `given_name`).
233
+ *
234
+ * @experimental
235
+ */
236
+ export const AUTH_METHOD_ATTRIBUTE_KEY_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
237
+ /**
238
+ * The longest attribute value a method may extract, in UTF-16 code units.
239
+ *
240
+ * @experimental
241
+ */
242
+ export const MAX_AUTH_METHOD_ATTRIBUTE_VALUE_LENGTH = 8192;
243
+ /**
244
+ * The most parameters a method's route handler sees from one query or one
245
+ * form; the rest are dropped.
246
+ *
247
+ * @experimental
248
+ */
249
+ export const MAX_AUTH_METHOD_PARAMETERS = 64;
250
+ /**
251
+ * The longest parameter value a method's route handler sees, in UTF-16 code
252
+ * units; a parameter with a longer value is dropped.
253
+ *
254
+ * @experimental
255
+ */
256
+ export const MAX_AUTH_METHOD_PARAMETER_LENGTH = 8192;
257
+ /**
258
+ * The most values of one repeated parameter a method's route handler sees;
259
+ * a parameter repeated more often is dropped.
260
+ *
261
+ * @experimental
262
+ */
263
+ export const MAX_AUTH_METHOD_PARAMETER_VALUES = 16;
264
+ /**
265
+ * Why a method's callback did not produce a result, other than the person
266
+ * declining at the provider:
267
+ *
268
+ * - `transaction_mismatch`: no transaction is pending for this browser, or the
269
+ * callback does not belong to it (its binding, `state` by default, is
270
+ * missing, repeated or another's). A callback replayed in another browser,
271
+ * or forged, ends here; the host answers it before the method runs.
272
+ * - `provider_error`: the provider answered with an error, other than the
273
+ * person declining (`access_denied`).
274
+ * - `invalid_response`: the provider's answer failed a check, such as the ID
275
+ * token's signature, `iss`, `aud` or `nonce`.
276
+ * - `upstream_unavailable`: the provider could not be reached in time.
277
+ *
278
+ * @experimental
279
+ */
280
+ export const UPSTREAM_FAILURE_REASONS = Object.freeze([
281
+ 'transaction_mismatch',
282
+ 'provider_error',
283
+ 'invalid_response',
284
+ 'upstream_unavailable',
285
+ ]);
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The verification half of a sign-in method (ADR-003): what every provider
3
+ * QAuth registers implements, and what an AuthMethod extends.
4
+ *
5
+ * The auth engine depends only on this contract: it resolves a provider by
6
+ * `type`, calls `verify()`, and upserts the attributes from
7
+ * `extractAttributes()`. It contains no provider-specific logic, so new
8
+ * providers are added without changing the engine.
9
+ *
10
+ * EXPERIMENTAL as part of this package, the plugin API of ADR-018 §4: it may
11
+ * change in a minor release.
12
+ *
13
+ * @see https://docs.qauth.dev/reference/records/adr/003-credential-provider-interface/
14
+ */
15
+ /**
16
+ * Level of assurance for a verified identity, aligned with eIDAS Levels of
17
+ * Assurance (LoA) and ISO/IEC 29115. Propagated downstream as the OIDC `acr`
18
+ * claim; `'low'` credentials (e.g. self-asserted email/password) carry no `acr`
19
+ * claim per ADR-003.
20
+ *
21
+ * @experimental
22
+ */
23
+ export type AssuranceLevel = 'low' | 'substantial' | 'high';
24
+ /**
25
+ * The result of a successful {@link CredentialProvider.verify} call: a
26
+ * provider-scoped subject identifier, the assurance level of the verification,
27
+ * and the raw claims the provider observed.
28
+ *
29
+ * @experimental
30
+ */
31
+ export interface VerifiedIdentity {
32
+ /**
33
+ * Subject identifier as known to the upstream provider (e.g. the normalized
34
+ * email for password, a DID for a wallet). Never used directly as the token
35
+ * `sub` — the engine maps it to the internal `users.id`.
36
+ */
37
+ externalSub: string;
38
+ /** Assurance level of this verification (eIDAS LoA / ISO 29115). */
39
+ assuranceLevel: AssuranceLevel;
40
+ /** Unprocessed claims exactly as returned by the provider, before extraction. */
41
+ rawClaims: Record<string, unknown>;
42
+ }
43
+ /**
44
+ * A single normalized user attribute derived from a {@link VerifiedIdentity},
45
+ * ready to be upserted into `user_attributes`. Trust ordering during claim
46
+ * resolution is driven by `source` and `verified`.
47
+ *
48
+ * @experimental
49
+ */
50
+ export interface UserAttribute {
51
+ /**
52
+ * Origin of the attribute (e.g. `'self_reported'`, `'wallet'`,
53
+ * `'oidc_google'`). Drives trust ordering during claim resolution.
54
+ */
55
+ source: string;
56
+ /** Attribute key (e.g. `'email'`). */
57
+ attrKey: string;
58
+ /** Attribute value. */
59
+ attrValue: string;
60
+ /** Whether the provider verified this attribute. */
61
+ verified: boolean;
62
+ /** Optional expiry after which the attribute must be re-verified. */
63
+ expiresAt?: Date;
64
+ }
65
+ /**
66
+ * Strategy interface implemented by every authentication method (ADR-003).
67
+ *
68
+ * @experimental
69
+ */
70
+ export interface CredentialProvider {
71
+ /**
72
+ * Stable discriminator the server's provider registry registers and
73
+ * resolves this provider by (e.g. `'password'`, `'wallet'`).
74
+ */
75
+ readonly type: string;
76
+ /**
77
+ * Verify the provider-specific `input` and resolve a normalized
78
+ * {@link VerifiedIdentity}. Implementations reject on verification failure.
79
+ */
80
+ verify(input: unknown): Promise<VerifiedIdentity>;
81
+ /**
82
+ * Derive normalized {@link UserAttribute} rows from a successful verification
83
+ * result, ready to be upserted into `user_attributes`.
84
+ */
85
+ extractAttributes(result: VerifiedIdentity): UserAttribute[];
86
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The verification half of a sign-in method (ADR-003): what every provider
3
+ * QAuth registers implements, and what an AuthMethod extends.
4
+ *
5
+ * The auth engine depends only on this contract: it resolves a provider by
6
+ * `type`, calls `verify()`, and upserts the attributes from
7
+ * `extractAttributes()`. It contains no provider-specific logic, so new
8
+ * providers are added without changing the engine.
9
+ *
10
+ * EXPERIMENTAL as part of this package, the plugin API of ADR-018 §4: it may
11
+ * change in a minor release.
12
+ *
13
+ * @see https://docs.qauth.dev/reference/records/adr/003-credential-provider-interface/
14
+ */
15
+ export {};
@@ -0,0 +1,125 @@
1
+ import type { AuthMethod, AuthMethodContext, AuthMethodRoute, AuthMethodRouteRequest, UpstreamStart, UpstreamStartRequest } from './auth-method.js';
2
+ import { type UpstreamFailureReason } from './contract.js';
3
+ import type { AssuranceLevel, UserAttribute, VerifiedIdentity } from './credential-provider.js';
4
+ /**
5
+ * Why the upstream step ended without an identity: a callback's reason, or
6
+ * `verification_refused` when `verify()` rejected the callback's result.
7
+ *
8
+ * @experimental
9
+ */
10
+ export type UpstreamStepFailureReason = UpstreamFailureReason | 'verification_refused';
11
+ /**
12
+ * How the upstream step ended, as the sign-in flow acts on it:
13
+ *
14
+ * - `identity`: a checked `VerifiedIdentity` and its attributes, each with
15
+ * `source` equal to the method's `type`. The flow looks the credential up by
16
+ * `(realm, type, externalSub)`; it never matches an account by an attribute.
17
+ * - `declined`: the person cancelled or declined at the provider.
18
+ * - `failed`: anything else. `cause` is `verify()`'s rejection, for the log.
19
+ *
20
+ * @experimental
21
+ */
22
+ export type UpstreamStepCompletion = {
23
+ readonly kind: 'identity';
24
+ readonly identity: VerifiedIdentity;
25
+ readonly attributes: readonly UserAttribute[];
26
+ } | {
27
+ readonly kind: 'declined';
28
+ } | {
29
+ readonly kind: 'failed';
30
+ readonly reason: UpstreamStepFailureReason;
31
+ readonly cause?: unknown;
32
+ };
33
+ /**
34
+ * Start an upstream transaction: call `method.start()` and check its answer.
35
+ * The redirect must be an absolute `https` URL with no credentials and no
36
+ * fragment, the transaction JSON data within its cap, and the binding 22 to
37
+ * 512 base64url characters. The returned transaction is a frozen copy; the
38
+ * host stores it with the binding (a `PendingUpstreamTransaction`).
39
+ *
40
+ * A rejection of `start()` (the provider's discovery document cannot be read,
41
+ * for instance) propagates unchanged.
42
+ *
43
+ * @throws AuthMethodFaultError when the answer fails a check
44
+ * @experimental
45
+ */
46
+ export declare function startUpstreamStep<TConfig, TTransaction>(method: AuthMethod<TConfig, TTransaction>, request: UpstreamStartRequest, context: AuthMethodContext<TConfig>): Promise<UpstreamStart<TTransaction>>;
47
+ /**
48
+ * The binding a request to `route` carries: the single value of the route's
49
+ * binding parameter (`state` unless the route names another), from the
50
+ * query of a `GET` route and the form of a `POST` one. `undefined` when the
51
+ * parameter is missing or repeated, or `route` is not a callback route.
52
+ *
53
+ * @experimental
54
+ */
55
+ export declare function presentedUpstreamBinding(route: AuthMethodRoute, request: Pick<AuthMethodRouteRequest, 'query' | 'form'>): string | undefined;
56
+ /**
57
+ * Whether the binding a callback carried is the pending transaction's, in
58
+ * constant time (`timingSafeEqual` over the UTF-8 bytes). `false` unless both are strings and
59
+ * the expected one is a binding: a missing or repeated parameter never
60
+ * matches, and neither does an empty one.
61
+ *
62
+ * The host runs it before it takes the transaction, so a callback from
63
+ * another site cannot spend or cancel a pending sign-in.
64
+ *
65
+ * @experimental
66
+ */
67
+ export declare function upstreamBindingMatches(expected: unknown, presented: unknown): boolean;
68
+ /**
69
+ * The lowest assurance level, the ceiling of a method's identity when the
70
+ * caller names none.
71
+ *
72
+ * @experimental
73
+ */
74
+ export declare const DEFAULT_UPSTREAM_MAX_ASSURANCE: AssuranceLevel;
75
+ /**
76
+ * Options of {@link completeUpstreamStep}.
77
+ *
78
+ * @experimental
79
+ */
80
+ export interface UpstreamStepCompletionOptions {
81
+ /**
82
+ * The highest assurance level the identity may carry: a method that
83
+ * resolves a higher one is lowered to it. Default
84
+ * {@link DEFAULT_UPSTREAM_MAX_ASSURANCE} (`low`).
85
+ *
86
+ * The assurance level reaches `acr` and step-up, so a method does not get
87
+ * to set it alone. The owner's decision of 23 Oct 2026 ranks an upstream
88
+ * sign-in with a password, so by default it counts as `low` whatever the
89
+ * method claims; an operator raises the ceiling for a configured provider
90
+ * it trusts more (the plugin runtime and the upstream sign-in flow, #600
91
+ * and #639, pass it).
92
+ */
93
+ readonly maxAssurance?: AssuranceLevel;
94
+ }
95
+ /**
96
+ * Turn what a method's callback handed back into the end of the upstream
97
+ * step. A `result` goes to `method.verify()`; the identity it resolves is
98
+ * checked and its assurance level lowered to `options.maxAssurance`, then
99
+ * `method.extractAttributes()` gives its attributes, which are checked too.
100
+ *
101
+ * @param outcome - what the callback route's handler returned, unchecked, or
102
+ * the {@link AuthMethodFaultError} the route mount made of a handler that
103
+ * threw or a context that could not be built
104
+ * @param options - the assurance ceiling
105
+ * @throws AuthMethodFaultError when the outcome is a fault or outside the
106
+ * closed set, or the identity or an attribute fails a check
107
+ * @throws TypeError when `options.maxAssurance` is not an assurance level
108
+ * @experimental
109
+ */
110
+ export declare function completeUpstreamStep(method: AuthMethod, outcome: unknown, options?: UpstreamStepCompletionOptions): Promise<UpstreamStepCompletion>;
111
+ /**
112
+ * Why `value` cannot be where an upstream step sends the browser, or
113
+ * `undefined` when it can.
114
+ *
115
+ * @experimental
116
+ */
117
+ export declare function upstreamRedirectProblem(value: unknown): string | undefined;
118
+ /**
119
+ * A frozen copy of `value` when it is JSON data within the transaction cap:
120
+ * plain objects, arrays, strings, finite numbers, booleans and `null`, nested
121
+ * at most 16 deep. `undefined` otherwise.
122
+ *
123
+ * @experimental
124
+ */
125
+ export declare function storableTransaction(value: unknown): unknown;