@boopkit/cap-oauth-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/capability.json +120 -0
  2. package/dist/authorization-code-service.d.ts +105 -0
  3. package/dist/authorization-code-service.d.ts.map +1 -0
  4. package/dist/authorization-code-service.js +142 -0
  5. package/dist/authorization-code-service.js.map +1 -0
  6. package/dist/bearer.d.ts +146 -0
  7. package/dist/bearer.d.ts.map +1 -0
  8. package/dist/bearer.js +150 -0
  9. package/dist/bearer.js.map +1 -0
  10. package/dist/body-field.d.ts +24 -0
  11. package/dist/body-field.d.ts.map +1 -0
  12. package/dist/body-field.js +43 -0
  13. package/dist/body-field.js.map +1 -0
  14. package/dist/client-registry-service.d.ts +120 -0
  15. package/dist/client-registry-service.d.ts.map +1 -0
  16. package/dist/client-registry-service.js +450 -0
  17. package/dist/client-registry-service.js.map +1 -0
  18. package/dist/config.d.ts +82 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +157 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/connections-controller.d.ts +106 -0
  23. package/dist/connections-controller.d.ts.map +1 -0
  24. package/dist/connections-controller.js +161 -0
  25. package/dist/connections-controller.js.map +1 -0
  26. package/dist/consent-controller.d.ts +197 -0
  27. package/dist/consent-controller.d.ts.map +1 -0
  28. package/dist/consent-controller.js +532 -0
  29. package/dist/consent-controller.js.map +1 -0
  30. package/dist/discovery-controller.d.ts +16 -0
  31. package/dist/discovery-controller.d.ts.map +1 -0
  32. package/dist/discovery-controller.js +52 -0
  33. package/dist/discovery-controller.js.map +1 -0
  34. package/dist/discovery.d.ts +95 -0
  35. package/dist/discovery.d.ts.map +1 -0
  36. package/dist/discovery.js +101 -0
  37. package/dist/discovery.js.map +1 -0
  38. package/dist/errors.d.ts +26 -0
  39. package/dist/errors.d.ts.map +1 -0
  40. package/dist/errors.js +41 -0
  41. package/dist/errors.js.map +1 -0
  42. package/dist/express.d.ts +66 -0
  43. package/dist/express.d.ts.map +1 -0
  44. package/dist/express.js +3 -0
  45. package/dist/express.js.map +1 -0
  46. package/dist/grant-service.d.ts +102 -0
  47. package/dist/grant-service.d.ts.map +1 -0
  48. package/dist/grant-service.js +137 -0
  49. package/dist/grant-service.js.map +1 -0
  50. package/dist/index.d.ts +67 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +148 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/metadata-document.d.ts +60 -0
  55. package/dist/metadata-document.d.ts.map +1 -0
  56. package/dist/metadata-document.js +148 -0
  57. package/dist/metadata-document.js.map +1 -0
  58. package/dist/oauth-error.d.ts +45 -0
  59. package/dist/oauth-error.d.ts.map +1 -0
  60. package/dist/oauth-error.js +51 -0
  61. package/dist/oauth-error.js.map +1 -0
  62. package/dist/pkce.d.ts +29 -0
  63. package/dist/pkce.d.ts.map +1 -0
  64. package/dist/pkce.js +65 -0
  65. package/dist/pkce.js.map +1 -0
  66. package/dist/property-array.d.ts +60 -0
  67. package/dist/property-array.d.ts.map +1 -0
  68. package/dist/property-array.js +31 -0
  69. package/dist/property-array.js.map +1 -0
  70. package/dist/public-id.d.ts +35 -0
  71. package/dist/public-id.d.ts.map +1 -0
  72. package/dist/public-id.js +76 -0
  73. package/dist/public-id.js.map +1 -0
  74. package/dist/rate-limit.d.ts +56 -0
  75. package/dist/rate-limit.d.ts.map +1 -0
  76. package/dist/rate-limit.js +55 -0
  77. package/dist/rate-limit.js.map +1 -0
  78. package/dist/redirect-uri.d.ts +89 -0
  79. package/dist/redirect-uri.d.ts.map +1 -0
  80. package/dist/redirect-uri.js +249 -0
  81. package/dist/redirect-uri.js.map +1 -0
  82. package/dist/registration-controller.d.ts +62 -0
  83. package/dist/registration-controller.d.ts.map +1 -0
  84. package/dist/registration-controller.js +79 -0
  85. package/dist/registration-controller.js.map +1 -0
  86. package/dist/request-client.d.ts +19 -0
  87. package/dist/request-client.d.ts.map +1 -0
  88. package/dist/request-client.js +32 -0
  89. package/dist/request-client.js.map +1 -0
  90. package/dist/revocation-controller.d.ts +28 -0
  91. package/dist/revocation-controller.d.ts.map +1 -0
  92. package/dist/revocation-controller.js +176 -0
  93. package/dist/revocation-controller.js.map +1 -0
  94. package/dist/revocation-service.d.ts +113 -0
  95. package/dist/revocation-service.d.ts.map +1 -0
  96. package/dist/revocation-service.js +121 -0
  97. package/dist/revocation-service.js.map +1 -0
  98. package/dist/token-controller.d.ts +79 -0
  99. package/dist/token-controller.d.ts.map +1 -0
  100. package/dist/token-controller.js +207 -0
  101. package/dist/token-controller.js.map +1 -0
  102. package/dist/token-service.d.ts +175 -0
  103. package/dist/token-service.d.ts.map +1 -0
  104. package/dist/token-service.js +251 -0
  105. package/dist/token-service.js.map +1 -0
  106. package/dist/types.d.ts +130 -0
  107. package/dist/types.d.ts.map +1 -0
  108. package/dist/types.js +13 -0
  109. package/dist/types.js.map +1 -0
  110. package/dist/user-store.d.ts +110 -0
  111. package/dist/user-store.d.ts.map +1 -0
  112. package/dist/user-store.js +40 -0
  113. package/dist/user-store.js.map +1 -0
  114. package/package.json +37 -0
  115. package/scaffold/prisma/schema/oauth-clients.prisma +80 -0
  116. package/scaffold/reference/oauth-server.ref.md +478 -0
  117. package/scaffold/src/middleware/oauth-server.ts +227 -0
  118. package/scaffold/src/scripts/seed-oauth-clients.ts +72 -0
  119. package/scaffold/src/services/oauth-server.service.ts +101 -0
  120. package/scaffold/styles/oauth-server.css +345 -0
  121. package/scaffold/views/oauth-connected-clients.hbs +98 -0
  122. package/scaffold/views/oauth-consent-error.hbs +22 -0
  123. package/scaffold/views/oauth-consent.hbs +138 -0
  124. package/scaffold/views/oauth-disconnect-confirm.hbs +57 -0
package/dist/pkce.js ADDED
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CODE_CHALLENGE_METHOD = void 0;
4
+ exports.isValidCodeVerifier = isValidCodeVerifier;
5
+ exports.isValidCodeChallenge = isValidCodeChallenge;
6
+ exports.deriveCodeChallenge = deriveCodeChallenge;
7
+ exports.verifyCodeChallenge = verifyCodeChallenge;
8
+ const node_crypto_1 = require("node:crypto");
9
+ /**
10
+ * PKCE, RFC 7636 — and only `S256`.
11
+ *
12
+ * `plain` is not a weaker method, it is the absence of one: the "challenge" is
13
+ * the verifier, so an attacker who intercepted the authorization response has
14
+ * everything needed to redeem the code. The discovery document advertises
15
+ * `["S256"]` and this module is the other half of that promise — a request
16
+ * carrying `code_challenge_method=plain` is refused rather than downgraded to.
17
+ *
18
+ * OAuth 2.1 requires PKCE for public clients, and every client this server will
19
+ * ever see is public, so the challenge is **required** rather than optional.
20
+ */
21
+ /** The only method this server accepts. Matches the discovery document. */
22
+ exports.CODE_CHALLENGE_METHOD = 'S256';
23
+ /** RFC 7636 §4.1: 43–128 characters from the unreserved set. */
24
+ const VERIFIER_RE = /^[A-Za-z0-9\-._~]{43,128}$/;
25
+ /**
26
+ * RFC 7636 §4.2: base64url of a SHA-256 digest — 43 characters, no padding.
27
+ *
28
+ * Checked as a shape before it is compared, because a challenge that was stored
29
+ * from a malformed registration request should fail loudly rather than by never
30
+ * matching anything.
31
+ */
32
+ const CHALLENGE_RE = /^[A-Za-z0-9\-_]{43}$/;
33
+ function isValidCodeVerifier(value) {
34
+ return typeof value === 'string' && VERIFIER_RE.test(value);
35
+ }
36
+ function isValidCodeChallenge(value) {
37
+ return typeof value === 'string' && CHALLENGE_RE.test(value);
38
+ }
39
+ /** BASE64URL-ENCODE(SHA256(ASCII(verifier))), per RFC 7636 §4.6. */
40
+ function deriveCodeChallenge(verifier) {
41
+ return (0, node_crypto_1.createHash)('sha256').update(verifier, 'ascii').digest('base64url');
42
+ }
43
+ /**
44
+ * Does `verifier` satisfy the `challenge` the authorization request carried?
45
+ *
46
+ * Compared with `timingSafeEqual` rather than `===`. The challenge is not a
47
+ * secret — it travelled in a query string — but the comparison is on the
48
+ * redemption path next to comparisons that are, and a codebase where one string
49
+ * equality is constant-time and its neighbour is not teaches the wrong habit to
50
+ * whoever writes the third one.
51
+ */
52
+ function verifyCodeChallenge(verifier, challenge, method = exports.CODE_CHALLENGE_METHOD) {
53
+ if (method !== exports.CODE_CHALLENGE_METHOD)
54
+ return false;
55
+ if (!isValidCodeVerifier(verifier))
56
+ return false;
57
+ if (!isValidCodeChallenge(challenge))
58
+ return false;
59
+ const derived = Buffer.from(deriveCodeChallenge(verifier), 'utf8');
60
+ const expected = Buffer.from(challenge, 'utf8');
61
+ if (derived.length !== expected.length)
62
+ return false;
63
+ return (0, node_crypto_1.timingSafeEqual)(derived, expected);
64
+ }
65
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":";;;AA8BA,kDAEC;AAED,oDAEC;AAGD,kDAEC;AAWD,kDAaC;AAjED,6CAA0D;AAE1D;;;;;;;;;;;GAWG;AAEH,2EAA2E;AAC9D,QAAA,qBAAqB,GAAG,MAAM,CAAC;AAE5C,gEAAgE;AAChE,MAAM,WAAW,GAAG,4BAA4B,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAE5C,SAAgB,mBAAmB,CAAC,KAAc;IAChD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED,SAAgB,oBAAoB,CAAC,KAAc;IACjD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC/D,CAAC;AAED,oEAAoE;AACpE,SAAgB,mBAAmB,CAAC,QAAgB;IAClD,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAC5E,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,mBAAmB,CACjC,QAAiB,EACjB,SAAkB,EAClB,SAAkB,6BAAqB;IAEvC,IAAI,MAAM,KAAK,6BAAqB;QAAE,OAAO,KAAK,CAAC;IACnD,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IACjD,IAAI,CAAC,oBAAoB,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAEnD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACrD,OAAO,IAAA,6BAAe,EAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,60 @@
1
+ import type { PropertyStore, PropertyWriteOptions, StoredProperty } from './user-store.js';
2
+ /**
3
+ * The read-modify-write discipline both OAuth user properties are written
4
+ * under, in one place so the two cannot drift apart.
5
+ *
6
+ * ## Why the whole array, every time
7
+ *
8
+ * `oauth-grants` and `oauth-codes` are arrays inside a single `info` blob.
9
+ * Writing one means writing all of it, and that will feel wasteful next to
10
+ * cap-users' `addAuthenticationMethod`, which updates one entry. Reaching for
11
+ * that narrower shape is the bug in boop-zj8: it *replaces* an entry rather
12
+ * than merging it, so it silently destroys every field the write did not name.
13
+ * The pattern to follow is `setAuthorization`'s.
14
+ *
15
+ * The array rewrite is not merely the safe option, it is what makes the
16
+ * property worth being a property. A concurrent consent and revoke both read
17
+ * revision N and both write; the second write carries a stale
18
+ * `expectedRevision` and is **refused**. Without the revision check one of the
19
+ * two would simply vanish, and nothing anywhere would say so.
20
+ *
21
+ * ## No retry here, on purpose
22
+ *
23
+ * `write` surfaces the refusal rather than re-reading and trying again. A retry
24
+ * loop in this module would make the collision invisible to its caller, and the
25
+ * two callers want different things from it: the token endpoint should answer
26
+ * the client rather than race a human, and a revocation should be told that
27
+ * something else changed the grant underneath it.
28
+ */
29
+ /** A property's current contents plus the revision a write must quote back. */
30
+ export interface ArraySnapshot<T> {
31
+ entries: T[];
32
+ /**
33
+ * `undefined` when the property does not exist yet — which is what a first
34
+ * write must pass, since quoting a revision for a row that is about to be
35
+ * created would refuse every first write.
36
+ */
37
+ revision: number | undefined;
38
+ record: StoredProperty<{
39
+ entries?: unknown;
40
+ }> | null;
41
+ }
42
+ /**
43
+ * Read a property whose `info` is `{ entries: [...] }`.
44
+ *
45
+ * A missing property, a null `info`, or an `entries` that is not an array all
46
+ * read as empty rather than throwing. The blob accepts arbitrary JSON and the
47
+ * database cannot object, so the read path has to treat the shape as a claim.
48
+ */
49
+ export declare function readArray<T>(store: PropertyStore, userId: number, property: string): Promise<ArraySnapshot<T>>;
50
+ /**
51
+ * Write the whole array back, quoting the revision the caller read.
52
+ *
53
+ * Throws cap-services' `StaleEntityError` when the stored revision has moved
54
+ * on. Use `isStaleWriteError` to recognise it; see this module's header for why
55
+ * that is not caught here.
56
+ */
57
+ export declare function writeArray<T>(store: PropertyStore, userId: number, property: string, entries: T[], snapshot: Pick<ArraySnapshot<T>, 'revision'>, options?: Omit<PropertyWriteOptions, 'expectedRevision'>): Promise<StoredProperty<{
58
+ entries: T[];
59
+ }>>;
60
+ //# sourceMappingURL=property-array.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"property-array.d.ts","sourceRoot":"","sources":["../src/property-array.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE3F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,OAAO,EAAE,CAAC,EAAE,CAAC;IACb;;;;OAIG;IACH,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,MAAM,EAAE,cAAc,CAAC;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;CACtD;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAQ3B;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,CAAC,EAAE,EACZ,QAAQ,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,EAC5C,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,kBAAkB,CAAM,GAC3D,OAAO,CAAC,cAAc,CAAC;IAAE,OAAO,EAAE,CAAC,EAAE,CAAA;CAAE,CAAC,CAAC,CAO3C"}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readArray = readArray;
4
+ exports.writeArray = writeArray;
5
+ /**
6
+ * Read a property whose `info` is `{ entries: [...] }`.
7
+ *
8
+ * A missing property, a null `info`, or an `entries` that is not an array all
9
+ * read as empty rather than throwing. The blob accepts arbitrary JSON and the
10
+ * database cannot object, so the read path has to treat the shape as a claim.
11
+ */
12
+ async function readArray(store, userId, property) {
13
+ const record = await store.get(userId, property);
14
+ const raw = record?.info?.entries;
15
+ return {
16
+ entries: Array.isArray(raw) ? raw : [],
17
+ revision: record?.revision,
18
+ record: record ?? null,
19
+ };
20
+ }
21
+ /**
22
+ * Write the whole array back, quoting the revision the caller read.
23
+ *
24
+ * Throws cap-services' `StaleEntityError` when the stored revision has moved
25
+ * on. Use `isStaleWriteError` to recognise it; see this module's header for why
26
+ * that is not caught here.
27
+ */
28
+ async function writeArray(store, userId, property, entries, snapshot, options = {}) {
29
+ return store.put(userId, property, { entries }, { ...options, expectedRevision: snapshot.revision });
30
+ }
31
+ //# sourceMappingURL=property-array.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"property-array.js","sourceRoot":"","sources":["../src/property-array.ts"],"names":[],"mappings":";;AAiDA,8BAYC;AASD,gCAcC;AA1CD;;;;;;GAMG;AACI,KAAK,UAAU,SAAS,CAC7B,KAAoB,EACpB,MAAc,EACd,QAAgB;IAEhB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAwB,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxE,MAAM,GAAG,GAAG,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;IAClC,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,GAAW,CAAC,CAAC,CAAC,EAAE;QAC/C,QAAQ,EAAE,MAAM,EAAE,QAAQ;QAC1B,MAAM,EAAE,MAAM,IAAI,IAAI;KACvB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,UAAU,CAC9B,KAAoB,EACpB,MAAc,EACd,QAAgB,EAChB,OAAY,EACZ,QAA4C,EAC5C,UAA0D,EAAE;IAE5D,OAAO,KAAK,CAAC,GAAG,CACd,MAAM,EACN,QAAQ,EACR,EAAE,OAAO,EAAE,EACX,EAAE,GAAG,OAAO,EAAE,gBAAgB,EAAE,QAAQ,CAAC,QAAQ,EAAE,CACpD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The client's `publicId` — the token prefix, in every registration mode.
3
+ *
4
+ * Plan, Decision 3: twelve lowercase base36 characters that *we* assign at
5
+ * registration. It is never the raw `client_id`, because a Client ID Metadata
6
+ * Document client's `client_id` is a URL containing `/`, `:` and `.`, and all
7
+ * three of those break something:
8
+ *
9
+ * - `<publicId>.<generateToken()>` is the token wire format, split on the
10
+ * first `.`;
11
+ * - `<publicId>|<sha256>` is the `user_identities.key`, split on the `|` and
12
+ * bounded by that column's `varchar(255)`;
13
+ * - `key LIKE '<publicId>|%'` is the prefix scan client-wide revocation
14
+ * depends on, which an unbounded URL would make meaningless.
15
+ *
16
+ * So the alphabet is the load-bearing part, not the length. Twelve base36
17
+ * characters is about 62 bits, which is ample for an identifier that is not
18
+ * itself a secret; the token half carries the entropy that matters.
19
+ *
20
+ * This is deliberately *not* `generatePublicId()` from `@boopkit/cap-database`.
21
+ * The house format (Crockford base32, `xxxxxx-xxxxxx`) would in fact survive
22
+ * both splits, but it is a 13-character value with a hyphen, and Decision 3
23
+ * fixed a 12-character `varchar(16)` column against the base36 alphabet. Taking
24
+ * the dependency to get a different-shaped value than the schema declares would
25
+ * trade one small module for a mismatch the schema cannot express.
26
+ */
27
+ /** Lowercase base36. No `.`, no `|`, no `/`, no `:` — see the header. */
28
+ export declare const CLIENT_PUBLIC_ID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
29
+ /** Twelve characters, per Decision 3. The `varchar(16)` column allows headroom. */
30
+ export declare const CLIENT_PUBLIC_ID_LENGTH = 12;
31
+ /** A fresh client `publicId`. Uniqueness is enforced by the column, not here. */
32
+ export declare function generateClientPublicId(): string;
33
+ /** Is this a well-formed client `publicId`? */
34
+ export declare function isValidClientPublicId(value: string): boolean;
35
+ //# sourceMappingURL=public-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-id.d.ts","sourceRoot":"","sources":["../src/public-id.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,yCAAyC,CAAC;AAEhF,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAiC1C,iFAAiF;AACjF,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,+CAA+C;AAC/C,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK5D"}
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLIENT_PUBLIC_ID_LENGTH = exports.CLIENT_PUBLIC_ID_ALPHABET = void 0;
4
+ exports.generateClientPublicId = generateClientPublicId;
5
+ exports.isValidClientPublicId = isValidClientPublicId;
6
+ const node_crypto_1 = require("node:crypto");
7
+ /**
8
+ * The client's `publicId` — the token prefix, in every registration mode.
9
+ *
10
+ * Plan, Decision 3: twelve lowercase base36 characters that *we* assign at
11
+ * registration. It is never the raw `client_id`, because a Client ID Metadata
12
+ * Document client's `client_id` is a URL containing `/`, `:` and `.`, and all
13
+ * three of those break something:
14
+ *
15
+ * - `<publicId>.<generateToken()>` is the token wire format, split on the
16
+ * first `.`;
17
+ * - `<publicId>|<sha256>` is the `user_identities.key`, split on the `|` and
18
+ * bounded by that column's `varchar(255)`;
19
+ * - `key LIKE '<publicId>|%'` is the prefix scan client-wide revocation
20
+ * depends on, which an unbounded URL would make meaningless.
21
+ *
22
+ * So the alphabet is the load-bearing part, not the length. Twelve base36
23
+ * characters is about 62 bits, which is ample for an identifier that is not
24
+ * itself a secret; the token half carries the entropy that matters.
25
+ *
26
+ * This is deliberately *not* `generatePublicId()` from `@boopkit/cap-database`.
27
+ * The house format (Crockford base32, `xxxxxx-xxxxxx`) would in fact survive
28
+ * both splits, but it is a 13-character value with a hyphen, and Decision 3
29
+ * fixed a 12-character `varchar(16)` column against the base36 alphabet. Taking
30
+ * the dependency to get a different-shaped value than the schema declares would
31
+ * trade one small module for a mismatch the schema cannot express.
32
+ */
33
+ /** Lowercase base36. No `.`, no `|`, no `/`, no `:` — see the header. */
34
+ exports.CLIENT_PUBLIC_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
35
+ /** Twelve characters, per Decision 3. The `varchar(16)` column allows headroom. */
36
+ exports.CLIENT_PUBLIC_ID_LENGTH = 12;
37
+ /**
38
+ * Draw `length` characters uniformly from the alphabet.
39
+ *
40
+ * Rejection sampling rather than `% alphabet.length`: 256 is not a multiple of
41
+ * 36, so the naive modulo would make the first four symbols about 14% more
42
+ * likely than the rest. That is not a break here — a `publicId` is an
43
+ * identifier rather than a secret — but the same generator shape gets copied
44
+ * onto things that are, and a biased draw is invisible in every test that does
45
+ * not go looking for it.
46
+ *
47
+ * `Math.random()` is not used anywhere in this package; it is on the umbrella
48
+ * spec's donor-defect list for exactly this family of function.
49
+ */
50
+ function draw(length) {
51
+ const alphabet = exports.CLIENT_PUBLIC_ID_ALPHABET;
52
+ const limit = Math.floor(256 / alphabet.length) * alphabet.length;
53
+ const out = [];
54
+ while (out.length < length) {
55
+ // Over-read so the common case needs one syscall, not `length` of them.
56
+ const bytes = (0, node_crypto_1.randomBytes)((length - out.length) * 2);
57
+ for (const byte of bytes) {
58
+ if (byte >= limit)
59
+ continue;
60
+ out.push(alphabet[byte % alphabet.length]);
61
+ if (out.length === length)
62
+ break;
63
+ }
64
+ }
65
+ return out.join('');
66
+ }
67
+ /** A fresh client `publicId`. Uniqueness is enforced by the column, not here. */
68
+ function generateClientPublicId() {
69
+ return draw(exports.CLIENT_PUBLIC_ID_LENGTH);
70
+ }
71
+ /** Is this a well-formed client `publicId`? */
72
+ function isValidClientPublicId(value) {
73
+ return (value.length === exports.CLIENT_PUBLIC_ID_LENGTH &&
74
+ [...value].every((ch) => exports.CLIENT_PUBLIC_ID_ALPHABET.includes(ch)));
75
+ }
76
+ //# sourceMappingURL=public-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-id.js","sourceRoot":"","sources":["../src/public-id.ts"],"names":[],"mappings":";;;AAmEA,wDAEC;AAGD,sDAKC;AA7ED,6CAA0C;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,yEAAyE;AAC5D,QAAA,yBAAyB,GAAG,sCAAsC,CAAC;AAEhF,mFAAmF;AACtE,QAAA,uBAAuB,GAAG,EAAE,CAAC;AAE1C;;;;;;;;;;;;GAYG;AACH,SAAS,IAAI,CAAC,MAAc;IAC1B,MAAM,QAAQ,GAAG,iCAAyB,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC;IAClE,MAAM,GAAG,GAAa,EAAE,CAAC;IAEzB,OAAO,GAAG,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,wEAAwE;QACxE,MAAM,KAAK,GAAG,IAAA,yBAAW,EAAC,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACrD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,IAAI,KAAK;gBAAE,SAAS;YAC5B,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;YAC3C,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;gBAAE,MAAM;QACnC,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACtB,CAAC;AAED,iFAAiF;AACjF,SAAgB,sBAAsB;IACpC,OAAO,IAAI,CAAC,+BAAuB,CAAC,CAAC;AACvC,CAAC;AAED,+CAA+C;AAC/C,SAAgB,qBAAqB,CAAC,KAAa;IACjD,OAAO,CACL,KAAK,CAAC,MAAM,KAAK,+BAAuB;QACxC,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,iCAAyB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CACjE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,56 @@
1
+ import type { RequestHandler } from 'express';
2
+ /**
3
+ * Per-IP rate limiting for the one endpoint that lets a stranger write a row.
4
+ *
5
+ * `POST /oauth/register` is unauthenticated by design — RFC 7591 open
6
+ * registration has no caller to authenticate — so there is no account to hang a
7
+ * quota on and the source address is the only thing a limiter has to go on.
8
+ * Neither donor has anything like this: paywalls' registration endpoint
9
+ * required an authenticated `service-user`, which is a gate this endpoint
10
+ * cannot have.
11
+ *
12
+ * ## What this is, and what it is not
13
+ *
14
+ * A fixed-window counter in process memory. That means:
15
+ *
16
+ * - it resets when the process restarts, and
17
+ * - each instance of a horizontally-scaled app counts separately.
18
+ *
19
+ * Both are acceptable for what it defends. The row ceiling in the registry
20
+ * (`maxRegisteredClients`) is the hard bound on damage; this limiter exists to
21
+ * make reaching that ceiling slow and noisy rather than instant. An app that
22
+ * wants a shared limiter across instances should mount its own in front of the
23
+ * handler — the router factory takes one — rather than have this one grow a
24
+ * Redis dependency the capability would then require of everybody.
25
+ *
26
+ * A fixed window also lets a caller send `2 × limit` requests across a window
27
+ * boundary. Named here rather than fixed, because the mitigation (a sliding
28
+ * window) buys precision this does not need against a ceiling that is already
29
+ * the real bound.
30
+ */
31
+ export interface RateLimiterOptions {
32
+ /** Requests allowed per window, per address. */
33
+ limit?: number;
34
+ /** Window length in milliseconds. */
35
+ windowMs?: number;
36
+ /** Injectable clock, so the window can be tested without waiting for it. */
37
+ now?: () => number;
38
+ /**
39
+ * How many distinct addresses to track before the oldest window is dropped.
40
+ * Bounds the memory an attacker can make this hold.
41
+ */
42
+ maxTrackedAddresses?: number;
43
+ }
44
+ export declare const DEFAULT_REGISTRATION_RATE_LIMIT = 10;
45
+ export declare const DEFAULT_REGISTRATION_RATE_WINDOW_MS: number;
46
+ export interface RegistrationRateLimiter {
47
+ middleware: RequestHandler;
48
+ /** Exposed for tests and for an operator script; not part of the route. */
49
+ check(address: string): {
50
+ allowed: boolean;
51
+ retryAfterSeconds: number;
52
+ };
53
+ reset(): void;
54
+ }
55
+ export declare function createRegistrationRateLimiter(options?: RateLimiterOptions): RegistrationRateLimiter;
56
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAClD,eAAO,MAAM,mCAAmC,QAAiB,CAAC;AAQlE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,cAAc,CAAC;IAC3B,2EAA2E;IAC3E,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE,kBAAuB,GAC/B,uBAAuB,CAmDzB"}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_REGISTRATION_RATE_WINDOW_MS = exports.DEFAULT_REGISTRATION_RATE_LIMIT = void 0;
4
+ exports.createRegistrationRateLimiter = createRegistrationRateLimiter;
5
+ exports.DEFAULT_REGISTRATION_RATE_LIMIT = 10;
6
+ exports.DEFAULT_REGISTRATION_RATE_WINDOW_MS = 60 * 60 * 1000;
7
+ const DEFAULT_MAX_TRACKED_ADDRESSES = 10000;
8
+ function createRegistrationRateLimiter(options = {}) {
9
+ const limit = options.limit ?? exports.DEFAULT_REGISTRATION_RATE_LIMIT;
10
+ const windowMs = options.windowMs ?? exports.DEFAULT_REGISTRATION_RATE_WINDOW_MS;
11
+ const now = options.now ?? (() => Date.now());
12
+ const maxTracked = options.maxTrackedAddresses ?? DEFAULT_MAX_TRACKED_ADDRESSES;
13
+ const windows = new Map();
14
+ function check(address) {
15
+ const at = now();
16
+ const existing = windows.get(address);
17
+ if (!existing || existing.resetAt <= at) {
18
+ // Map insertion order is insertion order, so the first key is the least
19
+ // recently *started* window — good enough for an eviction whose only job
20
+ // is to bound memory.
21
+ if (windows.size >= maxTracked) {
22
+ const oldest = windows.keys().next();
23
+ if (!oldest.done)
24
+ windows.delete(oldest.value);
25
+ }
26
+ windows.set(address, { count: 1, resetAt: at + windowMs });
27
+ return { allowed: true, retryAfterSeconds: 0 };
28
+ }
29
+ existing.count += 1;
30
+ if (existing.count > limit) {
31
+ return {
32
+ allowed: false,
33
+ retryAfterSeconds: Math.max(1, Math.ceil((existing.resetAt - at) / 1000)),
34
+ };
35
+ }
36
+ return { allowed: true, retryAfterSeconds: 0 };
37
+ }
38
+ const middleware = (req, res, next) => {
39
+ const address = req.ip ?? req.socket?.remoteAddress ?? 'unknown';
40
+ const verdict = check(address);
41
+ if (verdict.allowed)
42
+ return next();
43
+ res.setHeader('Retry-After', String(verdict.retryAfterSeconds));
44
+ res.status(429).json({
45
+ error: 'temporarily_unavailable',
46
+ error_description: 'too many registration requests from this address; try again later',
47
+ });
48
+ };
49
+ return {
50
+ middleware,
51
+ check,
52
+ reset: () => windows.clear(),
53
+ };
54
+ }
55
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":";;;AA8DA,sEAqDC;AArEY,QAAA,+BAA+B,GAAG,EAAE,CAAC;AACrC,QAAA,mCAAmC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAClE,MAAM,6BAA6B,GAAG,KAAM,CAAC;AAc7C,SAAgB,6BAA6B,CAC3C,UAA8B,EAAE;IAEhC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,uCAA+B,CAAC;IAC/D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,2CAAmC,CAAC;IACzE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC9C,MAAM,UAAU,GAAG,OAAO,CAAC,mBAAmB,IAAI,6BAA6B,CAAC;IAEhF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE1C,SAAS,KAAK,CAAC,OAAe;QAC5B,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC;QACjB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEtC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;YACxC,wEAAwE;YACxE,yEAAyE;YACzE,sBAAsB;YACtB,IAAI,OAAO,CAAC,IAAI,IAAI,UAAU,EAAE,CAAC;gBAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;gBACrC,IAAI,CAAC,MAAM,CAAC,IAAI;oBAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACjD,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC;YAC3D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACjD,CAAC;QAED,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;QACpB,IAAI,QAAQ,CAAC,KAAK,GAAG,KAAK,EAAE,CAAC;YAC3B,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,OAAO,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;aAC1E,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;IACjD,CAAC;IAED,MAAM,UAAU,GAAmB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACrF,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,EAAE,aAAa,IAAI,SAAS,CAAC;QACjE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,OAAO,CAAC,OAAO;YAAE,OAAO,IAAI,EAAE,CAAC;QAEnC,GAAG,CAAC,SAAS,CAAC,aAAa,EAAE,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;QAChE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE,yBAAyB;YAChC,iBAAiB,EAAE,mEAAmE;SACvF,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO;QACL,UAAU;QACV,KAAK;QACL,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;KAC7B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Redirect-URI policy: what may be registered, and what a registered entry
3
+ * accepts at the authorization endpoint.
4
+ *
5
+ * Both questions are answered from one classifier, `classifyRedirectUri`,
6
+ * because the two halves only work as a pair. A shape registration stores but
7
+ * the matcher will not parse is worse than a shape neither accepts: the client
8
+ * registers, believes it is connected, and then fails at the authorization
9
+ * endpoint with `redirect_uri is not registered`. Keeping the rule in one
10
+ * function is what makes that disagreement impossible rather than unlikely.
11
+ *
12
+ * ## The three shapes
13
+ *
14
+ * RFC 8252 (OAuth 2.0 for Native Apps) names exactly three ways a client may be
15
+ * called back, and this server accepts those three and nothing else:
16
+ *
17
+ * `https` an absolute https URL — §7.2, the claimed-https redirect
18
+ * a hosted connector such as Claude's web app uses.
19
+ * `loopback-http` `http` on `localhost` or `127.0.0.1` — §7.3, because a
20
+ * native app cannot terminate TLS on the loopback interface.
21
+ * `private-use` a hierarchical URI in an application's own scheme, such as
22
+ * `com.example.app:/oauth2redirect/cb` or
23
+ * `claude://oauth/callback` — §7.1, how a desktop or mobile
24
+ * app is handed the response by its operating system.
25
+ *
26
+ * ## The matching rule
27
+ *
28
+ * **Exact string match**, with exactly one exception: a registered
29
+ * `loopback-http` entry matches a presented URI that differs only in port. That
30
+ * is RFC 8252 §7.3, and it is what Claude Code's published Client ID Metadata
31
+ * Document requires — it binds an ephemeral loopback port at connect time, so
32
+ * the port it will present is not knowable at registration.
33
+ *
34
+ * The exception is scoped to `loopback-http` and not merely to a loopback
35
+ * *host*. A private-use scheme has no TCP port to be ephemeral: the operating
36
+ * system routes it by scheme name, so `myapp://localhost/cb` gets the plain
37
+ * exact rule even though its host happens to read as loopback.
38
+ *
39
+ * The exception lives here, in one function with its own test table, rather
40
+ * than being threaded through the authorization controller as a special case.
41
+ * A redirect-URI check that is spread across a controller is how the umbrella
42
+ * spec's "declared-but-unenforced redirect URI lists" defect happens: the list
43
+ * is stored, the comparison drifts, and nothing says so.
44
+ *
45
+ * Nothing here reads a database or a request. It compares two strings.
46
+ */
47
+ /** Which of the three accepted shapes a redirect URI is. */
48
+ export type RedirectUriShape = 'https' | 'loopback-http' | 'private-use';
49
+ /**
50
+ * The verdict on one redirect URI.
51
+ *
52
+ * `reason` is a sentence fragment completing "this entry …", so a caller can
53
+ * prefix it with whatever names the offending value — the client registry says
54
+ * `redirect_uris entry ${reason}: ${entry}`.
55
+ */
56
+ export type RedirectUriClassification = {
57
+ accepted: true;
58
+ shape: RedirectUriShape;
59
+ url: URL;
60
+ } | {
61
+ accepted: false;
62
+ reason: string;
63
+ };
64
+ /**
65
+ * Classify one redirect URI, or say why it is refused.
66
+ *
67
+ * The single source of the policy. `validateRedirectUris` in the client
68
+ * registry calls it to decide whether a URI may be **stored**; `matchRedirectUri`
69
+ * below calls it to decide whether a stored or presented URI may be
70
+ * **compared**.
71
+ */
72
+ export declare function classifyRedirectUri(value: string): RedirectUriClassification;
73
+ /**
74
+ * Does `presented` satisfy the registered entry `registered`?
75
+ *
76
+ * @param registered one entry from the client's `redirect_uris`
77
+ * @param presented the `redirect_uri` the authorization request carried
78
+ */
79
+ export declare function matchRedirectUri(registered: string, presented: string): boolean;
80
+ /**
81
+ * Does any registered entry accept the presented URI?
82
+ *
83
+ * The authorization controller's actual question, kept next to the rule it is
84
+ * built from so a caller never has to write the loop itself.
85
+ */
86
+ export declare function matchesAnyRedirectUri(registered: readonly string[], presented: string): boolean;
87
+ /** Is this host one the loopback exception applies to? */
88
+ export declare function isLoopbackHost(host: string): boolean;
89
+ //# sourceMappingURL=redirect-uri.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect-uri.d.ts","sourceRoot":"","sources":["../src/redirect-uri.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAsDH,4DAA4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,eAAe,GAAG,aAAa,CAAC;AAEzE;;;;;;GAMG;AACH,MAAM,MAAM,yBAAyB,GACjC;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,GACrD;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AA6BxC;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,yBAAyB,CAsC5E;AAiBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CA+C/E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAE/F;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD"}