@dwk/oauth 0.1.0-beta.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.
- package/LICENSE +15 -0
- package/README.md +175 -0
- package/dist/encoding.d.ts +16 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +26 -0
- package/dist/encoding.js.map +1 -0
- package/dist/errors.d.ts +54 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +66 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +50 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/introspection.d.ts +83 -0
- package/dist/introspection.d.ts.map +1 -0
- package/dist/introspection.js +118 -0
- package/dist/introspection.js.map +1 -0
- package/dist/log.d.ts +42 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +40 -0
- package/dist/log.js.map +1 -0
- package/dist/metadata.d.ts +79 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +67 -0
- package/dist/metadata.js.map +1 -0
- package/dist/observability.d.ts +37 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +25 -0
- package/dist/observability.js.map +1 -0
- package/dist/par.d.ts +67 -0
- package/dist/par.d.ts.map +1 -0
- package/dist/par.js +132 -0
- package/dist/par.js.map +1 -0
- package/dist/registration.d.ts +71 -0
- package/dist/registration.d.ts.map +1 -0
- package/dist/registration.js +258 -0
- package/dist/registration.js.map +1 -0
- package/dist/revocation.d.ts +35 -0
- package/dist/revocation.d.ts.map +1 -0
- package/dist/revocation.js +50 -0
- package/dist/revocation.js.map +1 -0
- package/dist/store.d.ts +90 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +13 -0
- package/dist/store.js.map +1 -0
- package/package.json +53 -0
- package/src/encoding.ts +26 -0
- package/src/errors.ts +80 -0
- package/src/http.ts +51 -0
- package/src/index.ts +75 -0
- package/src/introspection.ts +185 -0
- package/src/log.ts +43 -0
- package/src/metadata.ts +133 -0
- package/src/observability.ts +56 -0
- package/src/par.ts +205 -0
- package/src/registration.ts +336 -0
- package/src/revocation.ts +92 -0
- package/src/store.ts +93 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Token Introspection (RFC 7662).
|
|
3
|
+
*
|
|
4
|
+
* A protected POST endpoint a Resource Server queries to learn whether a token
|
|
5
|
+
* is currently active and, if so, its metadata. The endpoint **MUST** be
|
|
6
|
+
* protected (RFC 7662 §2.1) so it cannot be used as a token-scanning oracle —
|
|
7
|
+
* hence {@link IntrospectionConfig.authenticate} is required, not optional.
|
|
8
|
+
*
|
|
9
|
+
* The lib owns only the protocol mechanics: it asks the caller to look the token
|
|
10
|
+
* up (storage stays in the consuming package, see `./store`), derives the
|
|
11
|
+
* `active` flag, and maps the record to the snake_case response. A DPoP-bound
|
|
12
|
+
* token surfaces its key thumbprint as `cnf: { jkt }` so the RS can complete the
|
|
13
|
+
* proof-of-possession binding via `@dwk/dpop`.
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.rfc-editor.org/rfc/rfc7662
|
|
16
|
+
*/
|
|
17
|
+
import { hostFromUrl } from "@dwk/log";
|
|
18
|
+
import { OAuthError, oauthErrorResponse } from "./errors";
|
|
19
|
+
import { json, methodNotAllowed, readForm } from "./http";
|
|
20
|
+
import { OAuthLogEvent } from "./log";
|
|
21
|
+
import { emit, resolveObservability, } from "./observability";
|
|
22
|
+
/** The single inactive response RFC 7662 §2.2 mandates for any non-active token. */
|
|
23
|
+
const INACTIVE = { active: false };
|
|
24
|
+
/**
|
|
25
|
+
* Whether `record` represents a currently-active token at `now`. A record is
|
|
26
|
+
* active unless it is `null`, explicitly `active: false`, revoked, past its
|
|
27
|
+
* `expiresAt`, or before its `notBefore`. Pure and side-effect-free.
|
|
28
|
+
*/
|
|
29
|
+
export function isTokenActive(record, now) {
|
|
30
|
+
if (record === null)
|
|
31
|
+
return false;
|
|
32
|
+
if (record.active === false)
|
|
33
|
+
return false;
|
|
34
|
+
if (record.revoked === true)
|
|
35
|
+
return false;
|
|
36
|
+
if (record.expiresAt !== undefined && now >= record.expiresAt)
|
|
37
|
+
return false;
|
|
38
|
+
if (record.notBefore !== undefined && now < record.notBefore)
|
|
39
|
+
return false;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Map an active token record to its RFC 7662 response, emitting only members the
|
|
44
|
+
* record actually carries (so the response never advertises empty claims). The
|
|
45
|
+
* caller MUST have already established the token is active.
|
|
46
|
+
*/
|
|
47
|
+
export function buildIntrospectionResponse(record) {
|
|
48
|
+
const response = { active: true };
|
|
49
|
+
if (record.scope !== undefined)
|
|
50
|
+
response.scope = record.scope;
|
|
51
|
+
if (record.clientId !== undefined)
|
|
52
|
+
response.client_id = record.clientId;
|
|
53
|
+
if (record.username !== undefined)
|
|
54
|
+
response.username = record.username;
|
|
55
|
+
if (record.tokenType !== undefined)
|
|
56
|
+
response.token_type = record.tokenType;
|
|
57
|
+
if (record.expiresAt !== undefined)
|
|
58
|
+
response.exp = record.expiresAt;
|
|
59
|
+
if (record.issuedAt !== undefined)
|
|
60
|
+
response.iat = record.issuedAt;
|
|
61
|
+
if (record.notBefore !== undefined)
|
|
62
|
+
response.nbf = record.notBefore;
|
|
63
|
+
if (record.subject !== undefined)
|
|
64
|
+
response.sub = record.subject;
|
|
65
|
+
if (record.audience !== undefined)
|
|
66
|
+
response.aud = record.audience;
|
|
67
|
+
if (record.issuer !== undefined)
|
|
68
|
+
response.iss = record.issuer;
|
|
69
|
+
if (record.tokenId !== undefined)
|
|
70
|
+
response.jti = record.tokenId;
|
|
71
|
+
if (record.jkt !== undefined)
|
|
72
|
+
response.cnf = { jkt: record.jkt };
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Create the introspection endpoint handler. The returned handler accepts a
|
|
77
|
+
* `POST` request and returns the introspection JSON; route it at whatever path
|
|
78
|
+
* the deployer mounts (it is path-agnostic).
|
|
79
|
+
*/
|
|
80
|
+
export function createIntrospectionHandler(config) {
|
|
81
|
+
const obs = resolveObservability(config);
|
|
82
|
+
const clock = config.now ?? (() => Math.floor(Date.now() / 1000));
|
|
83
|
+
return async (request) => {
|
|
84
|
+
if (request.method.toUpperCase() !== "POST") {
|
|
85
|
+
return methodNotAllowed("POST");
|
|
86
|
+
}
|
|
87
|
+
// Clone before consuming the body so the authenticator can read it too.
|
|
88
|
+
const authRequest = request.clone();
|
|
89
|
+
const form = await readForm(request);
|
|
90
|
+
// Protect the endpoint first, so an unauthenticated caller learns nothing
|
|
91
|
+
// about any token (not even "unknown" vs "known") — the token lookup stays
|
|
92
|
+
// after this gate. Reading the (small) form body up front is not sensitive.
|
|
93
|
+
const clientId = form.get("client_id") ?? undefined;
|
|
94
|
+
if (!(await config.authenticate(authRequest, clientId))) {
|
|
95
|
+
emit(obs, "warn", OAuthLogEvent.IntrospectionRejected, {
|
|
96
|
+
reason: "unauthenticated",
|
|
97
|
+
});
|
|
98
|
+
return oauthErrorResponse(OAuthError.InvalidClient, "introspection requires client authentication", 401, { "WWW-Authenticate": "Bearer" });
|
|
99
|
+
}
|
|
100
|
+
const token = form.get("token") ?? "";
|
|
101
|
+
if (!token) {
|
|
102
|
+
return oauthErrorResponse(OAuthError.InvalidRequest, "`token` is required");
|
|
103
|
+
}
|
|
104
|
+
const hint = form.get("token_type_hint") ?? undefined;
|
|
105
|
+
const record = await config.lookupToken(token, hint);
|
|
106
|
+
if (!isTokenActive(record, clock())) {
|
|
107
|
+
emit(obs, "info", OAuthLogEvent.IntrospectionInactive);
|
|
108
|
+
return json(INACTIVE);
|
|
109
|
+
}
|
|
110
|
+
// `record` is non-null here: isTokenActive(null) is false.
|
|
111
|
+
const active = record;
|
|
112
|
+
emit(obs, "info", OAuthLogEvent.IntrospectionActive, {
|
|
113
|
+
clientHost: active.clientId ? hostFromUrl(active.clientId) : undefined,
|
|
114
|
+
});
|
|
115
|
+
return json(buildIntrospectionResponse(active));
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=introspection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection.js","sourceRoot":"","sources":["../src/introspection.ts"],"names":[],"mappings":"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"}
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/oauth` — structured observability event taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* These endpoints sit on the security-sensitive edge of an authorization server:
|
|
5
|
+
* a refused introspection (token scanning), a rejected registration, or a
|
|
6
|
+
* consumed/forged `request_uri` is exactly the kind of event an operator needs a
|
|
7
|
+
* signal for. Logging and metrics are opt-in via an injected {@link Logger} and
|
|
8
|
+
* {@link Metrics} (see `@dwk/log`) and **share this one vocabulary** — the same
|
|
9
|
+
* dotted event name is passed to the logger and the metrics sink so a log line
|
|
10
|
+
* and its counter line up.
|
|
11
|
+
*
|
|
12
|
+
* Fields follow the redaction policy: a `client_id` URL is reduced to its host
|
|
13
|
+
* via `hostFromUrl`, and tokens, secrets, codes, and `request_uri` reference
|
|
14
|
+
* values are **never** logged — only machine-readable reason codes, hosts, and
|
|
15
|
+
* scopes.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
/** Stable event names emitted by `@dwk/oauth`. */
|
|
20
|
+
export declare const OAuthLogEvent: {
|
|
21
|
+
/** A token was introspected and reported active. */
|
|
22
|
+
readonly IntrospectionActive: "oauth.introspection.active";
|
|
23
|
+
/** A token was introspected and reported inactive (unknown/expired/revoked). */
|
|
24
|
+
readonly IntrospectionInactive: "oauth.introspection.inactive";
|
|
25
|
+
/** An introspection request was refused (failed endpoint authentication). */
|
|
26
|
+
readonly IntrospectionRejected: "oauth.introspection.rejected";
|
|
27
|
+
/** A token was revoked (or the no-op revocation of an unknown token). */
|
|
28
|
+
readonly TokenRevoked: "oauth.revocation.revoked";
|
|
29
|
+
/** A revocation request was refused (failed endpoint authentication). */
|
|
30
|
+
readonly RevocationRejected: "oauth.revocation.rejected";
|
|
31
|
+
/** A pushed authorization request was stored and a `request_uri` issued. */
|
|
32
|
+
readonly PushedRequestStored: "oauth.par.stored";
|
|
33
|
+
/** A pushed authorization request was rejected before storage. Field: `reason`. */
|
|
34
|
+
readonly PushedRequestRejected: "oauth.par.rejected";
|
|
35
|
+
/** A dynamic client registration succeeded. Field: `clientHost`. */
|
|
36
|
+
readonly ClientRegistered: "oauth.registration.registered";
|
|
37
|
+
/** A dynamic client registration was rejected. Field: `reason`. */
|
|
38
|
+
readonly ClientRegistrationRejected: "oauth.registration.rejected";
|
|
39
|
+
};
|
|
40
|
+
/** Union of the event-name string literals in {@link OAuthLogEvent}. */
|
|
41
|
+
export type OAuthLogEvent = (typeof OAuthLogEvent)[keyof typeof OAuthLogEvent];
|
|
42
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,kDAAkD;AAClD,eAAO,MAAM,aAAa;IACxB,oDAAoD;;IAEpD,gFAAgF;;IAEhF,6EAA6E;;IAE7E,yEAAyE;;IAEzE,yEAAyE;;IAEzE,4EAA4E;;IAE5E,mFAAmF;;IAEnF,oEAAoE;;IAEpE,mEAAmE;;CAE3D,CAAC;AAEX,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC"}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/oauth` — structured observability event taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* These endpoints sit on the security-sensitive edge of an authorization server:
|
|
5
|
+
* a refused introspection (token scanning), a rejected registration, or a
|
|
6
|
+
* consumed/forged `request_uri` is exactly the kind of event an operator needs a
|
|
7
|
+
* signal for. Logging and metrics are opt-in via an injected {@link Logger} and
|
|
8
|
+
* {@link Metrics} (see `@dwk/log`) and **share this one vocabulary** — the same
|
|
9
|
+
* dotted event name is passed to the logger and the metrics sink so a log line
|
|
10
|
+
* and its counter line up.
|
|
11
|
+
*
|
|
12
|
+
* Fields follow the redaction policy: a `client_id` URL is reduced to its host
|
|
13
|
+
* via `hostFromUrl`, and tokens, secrets, codes, and `request_uri` reference
|
|
14
|
+
* values are **never** logged — only machine-readable reason codes, hosts, and
|
|
15
|
+
* scopes.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
/** Stable event names emitted by `@dwk/oauth`. */
|
|
20
|
+
export const OAuthLogEvent = {
|
|
21
|
+
/** A token was introspected and reported active. */
|
|
22
|
+
IntrospectionActive: "oauth.introspection.active",
|
|
23
|
+
/** A token was introspected and reported inactive (unknown/expired/revoked). */
|
|
24
|
+
IntrospectionInactive: "oauth.introspection.inactive",
|
|
25
|
+
/** An introspection request was refused (failed endpoint authentication). */
|
|
26
|
+
IntrospectionRejected: "oauth.introspection.rejected",
|
|
27
|
+
/** A token was revoked (or the no-op revocation of an unknown token). */
|
|
28
|
+
TokenRevoked: "oauth.revocation.revoked",
|
|
29
|
+
/** A revocation request was refused (failed endpoint authentication). */
|
|
30
|
+
RevocationRejected: "oauth.revocation.rejected",
|
|
31
|
+
/** A pushed authorization request was stored and a `request_uri` issued. */
|
|
32
|
+
PushedRequestStored: "oauth.par.stored",
|
|
33
|
+
/** A pushed authorization request was rejected before storage. Field: `reason`. */
|
|
34
|
+
PushedRequestRejected: "oauth.par.rejected",
|
|
35
|
+
/** A dynamic client registration succeeded. Field: `clientHost`. */
|
|
36
|
+
ClientRegistered: "oauth.registration.registered",
|
|
37
|
+
/** A dynamic client registration was rejected. Field: `reason`. */
|
|
38
|
+
ClientRegistrationRejected: "oauth.registration.rejected",
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=log.js.map
|
package/dist/log.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,kDAAkD;AAClD,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,oDAAoD;IACpD,mBAAmB,EAAE,4BAA4B;IACjD,gFAAgF;IAChF,qBAAqB,EAAE,8BAA8B;IACrD,6EAA6E;IAC7E,qBAAqB,EAAE,8BAA8B;IACrD,yEAAyE;IACzE,YAAY,EAAE,0BAA0B;IACxC,yEAAyE;IACzE,kBAAkB,EAAE,2BAA2B;IAC/C,4EAA4E;IAC5E,mBAAmB,EAAE,kBAAkB;IACvC,mFAAmF;IACnF,qBAAqB,EAAE,oBAAoB;IAC3C,oEAAoE;IACpE,gBAAgB,EAAE,+BAA+B;IACjD,mEAAmE;IACnE,0BAA0B,EAAE,6BAA6B;CACjD,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Server Metadata (RFC 8414).
|
|
3
|
+
*
|
|
4
|
+
* One source of truth drives two consumers: the **static** metadata document
|
|
5
|
+
* Anglesite publishes at `/.well-known/oauth-authorization-server` (derived from
|
|
6
|
+
* config at build time), and any **runtime** behaviour that must agree with what
|
|
7
|
+
* was advertised. The builder is pure config→JSON; it is protocol-agnostic, so
|
|
8
|
+
* IndieAuth, a Solid-OIDC OP, or a bare OAuth AS all feed it their own endpoints
|
|
9
|
+
* and supported-value lists.
|
|
10
|
+
*
|
|
11
|
+
* @see https://www.rfc-editor.org/rfc/rfc8414
|
|
12
|
+
*/
|
|
13
|
+
/** Configuration for {@link buildAuthorizationServerMetadata}. */
|
|
14
|
+
export interface AuthorizationServerMetadataConfig {
|
|
15
|
+
/** The authorization-server issuer identifier (a URL, no query/fragment). */
|
|
16
|
+
readonly issuer: string;
|
|
17
|
+
/** Absolute authorization endpoint URL. */
|
|
18
|
+
readonly authorizationEndpoint?: string;
|
|
19
|
+
/** Absolute token endpoint URL. */
|
|
20
|
+
readonly tokenEndpoint?: string;
|
|
21
|
+
/** Absolute token-introspection endpoint URL (RFC 7662). */
|
|
22
|
+
readonly introspectionEndpoint?: string;
|
|
23
|
+
/** Absolute token-revocation endpoint URL (RFC 7009). */
|
|
24
|
+
readonly revocationEndpoint?: string;
|
|
25
|
+
/** Absolute pushed-authorization-request endpoint URL (RFC 9126). */
|
|
26
|
+
readonly pushedAuthorizationRequestEndpoint?: string;
|
|
27
|
+
/** Absolute dynamic-client-registration endpoint URL (RFC 7591). */
|
|
28
|
+
readonly registrationEndpoint?: string;
|
|
29
|
+
/** Absolute JWK Set document URL. */
|
|
30
|
+
readonly jwksUri?: string;
|
|
31
|
+
/** OAuth scopes the server supports. Omitted from the document when empty. */
|
|
32
|
+
readonly scopesSupported?: readonly string[];
|
|
33
|
+
/** Supported `response_type` values. Defaults to `["code"]`. */
|
|
34
|
+
readonly responseTypesSupported?: readonly string[];
|
|
35
|
+
/** Supported `grant_type` values. Defaults to `["authorization_code"]`. */
|
|
36
|
+
readonly grantTypesSupported?: readonly string[];
|
|
37
|
+
/** Supported token-endpoint client authentication methods. Defaults to `["none"]`. */
|
|
38
|
+
readonly tokenEndpointAuthMethodsSupported?: readonly string[];
|
|
39
|
+
/** Supported PKCE `code_challenge` methods. Defaults to `["S256"]`. */
|
|
40
|
+
readonly codeChallengeMethodsSupported?: readonly string[];
|
|
41
|
+
/** Supported DPoP proof signing algorithms (RFC 9449), if DPoP is offered. */
|
|
42
|
+
readonly dpopSigningAlgValuesSupported?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Whether the AS *requires* PAR for every authorization request (RFC 9126
|
|
45
|
+
* §4). Only emitted (as `true`) when a PAR endpoint is configured.
|
|
46
|
+
*/
|
|
47
|
+
readonly requirePushedAuthorizationRequests?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The JSON shape of the authorization-server metadata document. Snake-cased per
|
|
51
|
+
* RFC 8414; optional members are omitted (not `null`) when not configured.
|
|
52
|
+
*/
|
|
53
|
+
export interface AuthorizationServerMetadata {
|
|
54
|
+
readonly issuer: string;
|
|
55
|
+
readonly authorization_endpoint?: string;
|
|
56
|
+
readonly token_endpoint?: string;
|
|
57
|
+
readonly introspection_endpoint?: string;
|
|
58
|
+
readonly revocation_endpoint?: string;
|
|
59
|
+
readonly pushed_authorization_request_endpoint?: string;
|
|
60
|
+
readonly require_pushed_authorization_requests?: boolean;
|
|
61
|
+
readonly registration_endpoint?: string;
|
|
62
|
+
readonly jwks_uri?: string;
|
|
63
|
+
readonly scopes_supported?: readonly string[];
|
|
64
|
+
readonly response_types_supported: readonly string[];
|
|
65
|
+
readonly grant_types_supported: readonly string[];
|
|
66
|
+
readonly token_endpoint_auth_methods_supported: readonly string[];
|
|
67
|
+
readonly code_challenge_methods_supported: readonly string[];
|
|
68
|
+
readonly dpop_signing_alg_values_supported?: readonly string[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the RFC 8414 metadata document from config. `issuer`,
|
|
72
|
+
* `response_types_supported`, `grant_types_supported`,
|
|
73
|
+
* `token_endpoint_auth_methods_supported`, and `code_challenge_methods_supported`
|
|
74
|
+
* are always present (with defaults); every endpoint URL and the optional value
|
|
75
|
+
* lists are emitted only when supplied, so the document never advertises an
|
|
76
|
+
* endpoint the deployer did not mount.
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildAuthorizationServerMetadata(config: AuthorizationServerMetadataConfig): AuthorizationServerMetadata;
|
|
79
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,kEAAkE;AAClE,MAAM,WAAW,iCAAiC;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,mCAAmC;IACnC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,yDAAyD;IACzD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,kCAAkC,CAAC,EAAE,MAAM,CAAC;IACrD,oEAAoE;IACpE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,qCAAqC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,gEAAgE;IAChE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpD,2EAA2E;IAC3E,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,sFAAsF;IACtF,QAAQ,CAAC,iCAAiC,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/D,uEAAuE;IACvE,QAAQ,CAAC,6BAA6B,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3D,8EAA8E;IAC9E,QAAQ,CAAC,6BAA6B,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3D;;;OAGG;IACH,QAAQ,CAAC,kCAAkC,CAAC,EAAE,OAAO,CAAC;CACvD;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,qCAAqC,CAAC,EAAE,MAAM,CAAC;IACxD,QAAQ,CAAC,qCAAqC,CAAC,EAAE,OAAO,CAAC;IACzD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrD,QAAQ,CAAC,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IAClD,QAAQ,CAAC,qCAAqC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClE,QAAQ,CAAC,gCAAgC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7D,QAAQ,CAAC,iCAAiC,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChE;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,iCAAiC,GACxC,2BAA2B,CAkD7B"}
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Server Metadata (RFC 8414).
|
|
3
|
+
*
|
|
4
|
+
* One source of truth drives two consumers: the **static** metadata document
|
|
5
|
+
* Anglesite publishes at `/.well-known/oauth-authorization-server` (derived from
|
|
6
|
+
* config at build time), and any **runtime** behaviour that must agree with what
|
|
7
|
+
* was advertised. The builder is pure config→JSON; it is protocol-agnostic, so
|
|
8
|
+
* IndieAuth, a Solid-OIDC OP, or a bare OAuth AS all feed it their own endpoints
|
|
9
|
+
* and supported-value lists.
|
|
10
|
+
*
|
|
11
|
+
* @see https://www.rfc-editor.org/rfc/rfc8414
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Build the RFC 8414 metadata document from config. `issuer`,
|
|
15
|
+
* `response_types_supported`, `grant_types_supported`,
|
|
16
|
+
* `token_endpoint_auth_methods_supported`, and `code_challenge_methods_supported`
|
|
17
|
+
* are always present (with defaults); every endpoint URL and the optional value
|
|
18
|
+
* lists are emitted only when supplied, so the document never advertises an
|
|
19
|
+
* endpoint the deployer did not mount.
|
|
20
|
+
*/
|
|
21
|
+
export function buildAuthorizationServerMetadata(config) {
|
|
22
|
+
const metadata = {
|
|
23
|
+
issuer: config.issuer,
|
|
24
|
+
response_types_supported: config.responseTypesSupported ?? ["code"],
|
|
25
|
+
grant_types_supported: config.grantTypesSupported ?? ["authorization_code"],
|
|
26
|
+
token_endpoint_auth_methods_supported: config.tokenEndpointAuthMethodsSupported ?? ["none"],
|
|
27
|
+
code_challenge_methods_supported: config.codeChallengeMethodsSupported ?? [
|
|
28
|
+
"S256",
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
if (config.authorizationEndpoint !== undefined) {
|
|
32
|
+
metadata.authorization_endpoint = config.authorizationEndpoint;
|
|
33
|
+
}
|
|
34
|
+
if (config.tokenEndpoint !== undefined) {
|
|
35
|
+
metadata.token_endpoint = config.tokenEndpoint;
|
|
36
|
+
}
|
|
37
|
+
if (config.introspectionEndpoint !== undefined) {
|
|
38
|
+
metadata.introspection_endpoint = config.introspectionEndpoint;
|
|
39
|
+
}
|
|
40
|
+
if (config.revocationEndpoint !== undefined) {
|
|
41
|
+
metadata.revocation_endpoint = config.revocationEndpoint;
|
|
42
|
+
}
|
|
43
|
+
if (config.pushedAuthorizationRequestEndpoint !== undefined) {
|
|
44
|
+
metadata.pushed_authorization_request_endpoint =
|
|
45
|
+
config.pushedAuthorizationRequestEndpoint;
|
|
46
|
+
// `require_*` is only meaningful alongside the endpoint that satisfies it.
|
|
47
|
+
if (config.requirePushedAuthorizationRequests) {
|
|
48
|
+
metadata.require_pushed_authorization_requests = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (config.registrationEndpoint !== undefined) {
|
|
52
|
+
metadata.registration_endpoint = config.registrationEndpoint;
|
|
53
|
+
}
|
|
54
|
+
if (config.jwksUri !== undefined) {
|
|
55
|
+
metadata.jwks_uri = config.jwksUri;
|
|
56
|
+
}
|
|
57
|
+
if (config.scopesSupported && config.scopesSupported.length > 0) {
|
|
58
|
+
metadata.scopes_supported = config.scopesSupported;
|
|
59
|
+
}
|
|
60
|
+
if (config.dpopSigningAlgValuesSupported &&
|
|
61
|
+
config.dpopSigningAlgValuesSupported.length > 0) {
|
|
62
|
+
metadata.dpop_signing_alg_values_supported =
|
|
63
|
+
config.dpopSigningAlgValuesSupported;
|
|
64
|
+
}
|
|
65
|
+
return metadata;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA6DH;;;;;;;GAOG;AACH,MAAM,UAAU,gCAAgC,CAC9C,MAAyC;IAEzC,MAAM,QAAQ,GAA4B;QACxC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,wBAAwB,EAAE,MAAM,CAAC,sBAAsB,IAAI,CAAC,MAAM,CAAC;QACnE,qBAAqB,EAAE,MAAM,CAAC,mBAAmB,IAAI,CAAC,oBAAoB,CAAC;QAC3E,qCAAqC,EACnC,MAAM,CAAC,iCAAiC,IAAI,CAAC,MAAM,CAAC;QACtD,gCAAgC,EAAE,MAAM,CAAC,6BAA6B,IAAI;YACxE,MAAM;SACP;KACF,CAAC;IAEF,IAAI,MAAM,CAAC,qBAAqB,KAAK,SAAS,EAAE,CAAC;QAC/C,QAAQ,CAAC,sBAAsB,GAAG,MAAM,CAAC,qBAAqB,CAAC;IACjE,CAAC;IACD,IAAI,MAAM,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACvC,QAAQ,CAAC,cAAc,GAAG,MAAM,CAAC,aAAa,CAAC;IACjD,CAAC;IACD,IAAI,MAAM,CAAC,qBAAqB,KAAK,SAAS,EAAE,CAAC;QAC/C,QAAQ,CAAC,sBAAsB,GAAG,MAAM,CAAC,qBAAqB,CAAC;IACjE,CAAC;IACD,IAAI,MAAM,CAAC,kBAAkB,KAAK,SAAS,EAAE,CAAC;QAC5C,QAAQ,CAAC,mBAAmB,GAAG,MAAM,CAAC,kBAAkB,CAAC;IAC3D,CAAC;IACD,IAAI,MAAM,CAAC,kCAAkC,KAAK,SAAS,EAAE,CAAC;QAC5D,QAAQ,CAAC,qCAAqC;YAC5C,MAAM,CAAC,kCAAkC,CAAC;QAC5C,2EAA2E;QAC3E,IAAI,MAAM,CAAC,kCAAkC,EAAE,CAAC;YAC9C,QAAQ,CAAC,qCAAqC,GAAG,IAAI,CAAC;QACxD,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,oBAAoB,KAAK,SAAS,EAAE,CAAC;QAC9C,QAAQ,CAAC,qBAAqB,GAAG,MAAM,CAAC,oBAAoB,CAAC;IAC/D,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC;IACrC,CAAC;IACD,IAAI,MAAM,CAAC,eAAe,IAAI,MAAM,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChE,QAAQ,CAAC,gBAAgB,GAAG,MAAM,CAAC,eAAe,CAAC;IACrD,CAAC;IACD,IACE,MAAM,CAAC,6BAA6B;QACpC,MAAM,CAAC,6BAA6B,CAAC,MAAM,GAAG,CAAC,EAC/C,CAAC;QACD,QAAQ,CAAC,iCAAiC;YACxC,MAAM,CAAC,6BAA6B,CAAC;IACzC,CAAC;IAED,OAAO,QAAkD,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared logging/metrics seam used by every endpoint. Logging and metrics
|
|
3
|
+
* are opt-in (default no-op) and share one event vocabulary (see `./log` and
|
|
4
|
+
* `@dwk/log`): the same dotted event name flows to both the logger and the
|
|
5
|
+
* metrics sink so a log line and its counter line up.
|
|
6
|
+
*/
|
|
7
|
+
import { type Logger, type Metrics } from "@dwk/log";
|
|
8
|
+
import type { LogFields } from "@dwk/log";
|
|
9
|
+
/** Observability config fragment mixed into each handler's config. */
|
|
10
|
+
export interface ObservabilityConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Logger for endpoint events; defaults to a no-op. Wire a real logger (see
|
|
13
|
+
* `@dwk/log`) to surface introspection refusals, registration rejections, and
|
|
14
|
+
* the like instead of swallowing them.
|
|
15
|
+
*/
|
|
16
|
+
readonly logger?: Logger;
|
|
17
|
+
/**
|
|
18
|
+
* Metrics sink for the same events; defaults to a no-op. Wire an adapter (e.g.
|
|
19
|
+
* `analyticsEngineMetrics` from `@dwk/log`) to chart what the logger names.
|
|
20
|
+
*/
|
|
21
|
+
readonly metrics?: Metrics;
|
|
22
|
+
}
|
|
23
|
+
/** A resolved logger/metrics pair, defaults applied. */
|
|
24
|
+
export interface Observability {
|
|
25
|
+
readonly logger: Logger;
|
|
26
|
+
readonly metrics: Metrics;
|
|
27
|
+
}
|
|
28
|
+
/** Resolve the optional observability config to concrete (no-op) sinks. */
|
|
29
|
+
export declare function resolveObservability(config: ObservabilityConfig): Observability;
|
|
30
|
+
/**
|
|
31
|
+
* Emit a structured event on both the logger and the metrics sink. `warn` for
|
|
32
|
+
* handled-but-notable rejections, `info` for normal outcomes. Honors the
|
|
33
|
+
* redaction policy — callers pass only reason codes, sanitized hosts, and
|
|
34
|
+
* scopes, never tokens, secrets, or `request_uri` references.
|
|
35
|
+
*/
|
|
36
|
+
export declare function emit(obs: Observability, level: "info" | "warn", event: string, fields?: LogFields): void;
|
|
37
|
+
//# sourceMappingURL=observability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAA2B,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,sEAAsE;AACtE,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,mBAAmB,GAC1B,aAAa,CAKf;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAClB,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,SAAS,GACjB,IAAI,CAGN"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared logging/metrics seam used by every endpoint. Logging and metrics
|
|
3
|
+
* are opt-in (default no-op) and share one event vocabulary (see `./log` and
|
|
4
|
+
* `@dwk/log`): the same dotted event name flows to both the logger and the
|
|
5
|
+
* metrics sink so a log line and its counter line up.
|
|
6
|
+
*/
|
|
7
|
+
import { noopLogger, noopMetrics } from "@dwk/log";
|
|
8
|
+
/** Resolve the optional observability config to concrete (no-op) sinks. */
|
|
9
|
+
export function resolveObservability(config) {
|
|
10
|
+
return {
|
|
11
|
+
logger: config.logger ?? noopLogger,
|
|
12
|
+
metrics: config.metrics ?? noopMetrics,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Emit a structured event on both the logger and the metrics sink. `warn` for
|
|
17
|
+
* handled-but-notable rejections, `info` for normal outcomes. Honors the
|
|
18
|
+
* redaction policy — callers pass only reason codes, sanitized hosts, and
|
|
19
|
+
* scopes, never tokens, secrets, or `request_uri` references.
|
|
20
|
+
*/
|
|
21
|
+
export function emit(obs, level, event, fields) {
|
|
22
|
+
obs.logger[level](event, fields);
|
|
23
|
+
obs.metrics.count(event, fields);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=observability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAA6B,MAAM,UAAU,CAAC;AAwB9E,2EAA2E;AAC3E,MAAM,UAAU,oBAAoB,CAClC,MAA2B;IAE3B,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,UAAU;QACnC,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,WAAW;KACvC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,IAAI,CAClB,GAAkB,EAClB,KAAsB,EACtB,KAAa,EACb,MAAkB;IAElB,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACnC,CAAC"}
|
package/dist/par.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pushed Authorization Requests (RFC 9126).
|
|
3
|
+
*
|
|
4
|
+
* The client POSTs its authorization-request parameters directly to this
|
|
5
|
+
* endpoint; the server validates and stores them and hands back a short-lived,
|
|
6
|
+
* single-use `request_uri`. The client then starts the normal authorization
|
|
7
|
+
* flow with just `client_id` + `request_uri`, so the request parameters never
|
|
8
|
+
* travel through the browser/redirect and cannot be tampered with.
|
|
9
|
+
*
|
|
10
|
+
* This lib owns the *push* side (validate → store → mint `request_uri`). The
|
|
11
|
+
* *consume* side lives at the authorization endpoint in the consuming package:
|
|
12
|
+
* use {@link parseRequestUri} to recover the reference and the store's
|
|
13
|
+
* single-use `consume` ({@link PushedAuthorizationStore}). When DPoP binding is
|
|
14
|
+
* enabled and the push carries a `DPoP` header, the proof is verified via
|
|
15
|
+
* `@dwk/dpop` and its `jkt` recorded so the eventual token is key-bound
|
|
16
|
+
* (RFC 9449 §10).
|
|
17
|
+
*
|
|
18
|
+
* @see https://www.rfc-editor.org/rfc/rfc9126
|
|
19
|
+
*/
|
|
20
|
+
import { type ObservabilityConfig } from "./observability";
|
|
21
|
+
import type { EndpointAuthenticator } from "./introspection";
|
|
22
|
+
import type { PushedRequestRecord } from "./store";
|
|
23
|
+
/** The URN prefix RFC 9126 §2.2 mandates for a PAR `request_uri`. */
|
|
24
|
+
export declare const PUSHED_REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
|
|
25
|
+
/** Build the `request_uri` URN for a stored reference. */
|
|
26
|
+
export declare function requestUriFor(reference: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Recover the opaque reference from a PAR `request_uri`, or `null` if `uri` is
|
|
29
|
+
* not a `urn:ietf:params:oauth:request_uri:` value. Use this at the
|
|
30
|
+
* authorization endpoint before calling the store's single-use `consume`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseRequestUri(uri: string): string | null;
|
|
33
|
+
/** Configuration for {@link createPushedAuthorizationRequestHandler}. */
|
|
34
|
+
export interface PushedAuthorizationRequestConfig extends ObservabilityConfig {
|
|
35
|
+
/** Persist a freshly pushed request (keyed by its `reference`). */
|
|
36
|
+
readonly saveRequest: (record: PushedRequestRecord) => Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Optionally authenticate the caller (RFC 9126 §2: clients authenticate as at
|
|
39
|
+
* the token endpoint). Omit for public clients. Return `false` to reject with
|
|
40
|
+
* `401 invalid_client`.
|
|
41
|
+
*/
|
|
42
|
+
readonly authenticate?: EndpointAuthenticator;
|
|
43
|
+
/**
|
|
44
|
+
* Optional extra validation of the pushed parameters (e.g. requiring PKCE or a
|
|
45
|
+
* known `response_type`). Return an error description string to reject with
|
|
46
|
+
* `400 invalid_request`, or `null` to accept.
|
|
47
|
+
*/
|
|
48
|
+
readonly validate?: (params: Readonly<Record<string, string>>) => string | null | Promise<string | null>;
|
|
49
|
+
/** `request_uri` lifetime in seconds. Defaults to 60 (RFC 9126 favors short). */
|
|
50
|
+
readonly lifetimeSeconds?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Enable RFC 9449 DPoP binding: when `true` and the push carries a `DPoP`
|
|
53
|
+
* header, the proof is verified and its `jkt` recorded on the request. The
|
|
54
|
+
* `htu` is bound to {@link endpoint} (falling back to the request URL).
|
|
55
|
+
*/
|
|
56
|
+
readonly dpopBinding?: boolean;
|
|
57
|
+
/** Absolute PAR endpoint URL, used as the DPoP `htu`. */
|
|
58
|
+
readonly endpoint?: string;
|
|
59
|
+
/** Current time (seconds since the epoch). Defaults to `Date.now()`. */
|
|
60
|
+
readonly now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create the pushed-authorization-request endpoint handler. On success it
|
|
64
|
+
* returns `201` with `{ request_uri, expires_in }` (RFC 9126 §2.2).
|
|
65
|
+
*/
|
|
66
|
+
export declare function createPushedAuthorizationRequestHandler(config: PushedAuthorizationRequestConfig): (request: Request) => Promise<Response>;
|
|
67
|
+
//# sourceMappingURL=par.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"par.d.ts","sourceRoot":"","sources":["../src/par.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,qEAAqE;AACrE,eAAO,MAAM,yBAAyB,uCAAuC,CAAC;AAM9E,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI1D;AAED,yEAAyE;AACzE,MAAM,WAAW,gCAAiC,SAAQ,mBAAmB;IAC3E,mEAAmE;IACnE,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAClB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KACrC,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5C,iFAAiF;IACjF,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAgB,uCAAuC,CACrD,MAAM,EAAE,gCAAgC,GACvC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA4GzC"}
|