@c9up/transit 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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/dist/Oauth1Driver.d.ts +68 -0
  4. package/dist/Oauth1Driver.d.ts.map +1 -0
  5. package/dist/Oauth1Driver.js +148 -0
  6. package/dist/Oauth1Driver.js.map +1 -0
  7. package/dist/Oauth2Driver.d.ts +89 -0
  8. package/dist/Oauth2Driver.d.ts.map +1 -0
  9. package/dist/Oauth2Driver.js +196 -0
  10. package/dist/Oauth2Driver.js.map +1 -0
  11. package/dist/TransitManager.d.ts +59 -0
  12. package/dist/TransitManager.d.ts.map +1 -0
  13. package/dist/TransitManager.js +100 -0
  14. package/dist/TransitManager.js.map +1 -0
  15. package/dist/TransitProvider.d.ts +26 -0
  16. package/dist/TransitProvider.d.ts.map +1 -0
  17. package/dist/TransitProvider.js +32 -0
  18. package/dist/TransitProvider.js.map +1 -0
  19. package/dist/apple.d.ts +45 -0
  20. package/dist/apple.d.ts.map +1 -0
  21. package/dist/apple.js +80 -0
  22. package/dist/apple.js.map +1 -0
  23. package/dist/ber.d.ts +50 -0
  24. package/dist/ber.d.ts.map +1 -0
  25. package/dist/ber.js +156 -0
  26. package/dist/ber.js.map +1 -0
  27. package/dist/c14n.d.ts +38 -0
  28. package/dist/c14n.d.ts.map +1 -0
  29. package/dist/c14n.js +129 -0
  30. package/dist/c14n.js.map +1 -0
  31. package/dist/config.d.ts +115 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +146 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/configure.d.ts +18 -0
  36. package/dist/configure.d.ts.map +1 -0
  37. package/dist/configure.js +33 -0
  38. package/dist/configure.js.map +1 -0
  39. package/dist/drivers/AppleDriver.d.ts +36 -0
  40. package/dist/drivers/AppleDriver.d.ts.map +1 -0
  41. package/dist/drivers/AppleDriver.js +58 -0
  42. package/dist/drivers/AppleDriver.js.map +1 -0
  43. package/dist/drivers/DiscordDriver.d.ts +15 -0
  44. package/dist/drivers/DiscordDriver.d.ts.map +1 -0
  45. package/dist/drivers/DiscordDriver.js +45 -0
  46. package/dist/drivers/DiscordDriver.js.map +1 -0
  47. package/dist/drivers/FacebookDriver.d.ts +15 -0
  48. package/dist/drivers/FacebookDriver.d.ts.map +1 -0
  49. package/dist/drivers/FacebookDriver.js +47 -0
  50. package/dist/drivers/FacebookDriver.js.map +1 -0
  51. package/dist/drivers/GitHubDriver.d.ts +33 -0
  52. package/dist/drivers/GitHubDriver.d.ts.map +1 -0
  53. package/dist/drivers/GitHubDriver.js +72 -0
  54. package/dist/drivers/GitHubDriver.js.map +1 -0
  55. package/dist/drivers/GoogleDriver.d.ts +16 -0
  56. package/dist/drivers/GoogleDriver.d.ts.map +1 -0
  57. package/dist/drivers/GoogleDriver.js +31 -0
  58. package/dist/drivers/GoogleDriver.js.map +1 -0
  59. package/dist/drivers/LdapDirectory.d.ts +51 -0
  60. package/dist/drivers/LdapDirectory.d.ts.map +1 -0
  61. package/dist/drivers/LdapDirectory.js +127 -0
  62. package/dist/drivers/LdapDirectory.js.map +1 -0
  63. package/dist/drivers/LinkedInDriver.d.ts +29 -0
  64. package/dist/drivers/LinkedInDriver.d.ts.map +1 -0
  65. package/dist/drivers/LinkedInDriver.js +97 -0
  66. package/dist/drivers/LinkedInDriver.js.map +1 -0
  67. package/dist/drivers/LinkedInOpenidConnectDriver.d.ts +17 -0
  68. package/dist/drivers/LinkedInOpenidConnectDriver.d.ts.map +1 -0
  69. package/dist/drivers/LinkedInOpenidConnectDriver.js +27 -0
  70. package/dist/drivers/LinkedInOpenidConnectDriver.js.map +1 -0
  71. package/dist/drivers/OidcDriver.d.ts +64 -0
  72. package/dist/drivers/OidcDriver.d.ts.map +1 -0
  73. package/dist/drivers/OidcDriver.js +259 -0
  74. package/dist/drivers/OidcDriver.js.map +1 -0
  75. package/dist/drivers/SamlDriver.d.ts +66 -0
  76. package/dist/drivers/SamlDriver.d.ts.map +1 -0
  77. package/dist/drivers/SamlDriver.js +228 -0
  78. package/dist/drivers/SamlDriver.js.map +1 -0
  79. package/dist/drivers/SpotifyDriver.d.ts +16 -0
  80. package/dist/drivers/SpotifyDriver.d.ts.map +1 -0
  81. package/dist/drivers/SpotifyDriver.js +34 -0
  82. package/dist/drivers/SpotifyDriver.js.map +1 -0
  83. package/dist/drivers/TwitterDriver.d.ts +21 -0
  84. package/dist/drivers/TwitterDriver.d.ts.map +1 -0
  85. package/dist/drivers/TwitterDriver.js +46 -0
  86. package/dist/drivers/TwitterDriver.js.map +1 -0
  87. package/dist/drivers/TwitterXDriver.d.ts +22 -0
  88. package/dist/drivers/TwitterXDriver.d.ts.map +1 -0
  89. package/dist/drivers/TwitterXDriver.js +47 -0
  90. package/dist/drivers/TwitterXDriver.js.map +1 -0
  91. package/dist/dsig.d.ts +51 -0
  92. package/dist/dsig.d.ts.map +1 -0
  93. package/dist/dsig.js +273 -0
  94. package/dist/dsig.js.map +1 -0
  95. package/dist/index.d.ts +38 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +33 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/jwt.d.ts +135 -0
  100. package/dist/jwt.d.ts.map +1 -0
  101. package/dist/jwt.js +167 -0
  102. package/dist/jwt.js.map +1 -0
  103. package/dist/ldap.d.ts +82 -0
  104. package/dist/ldap.d.ts.map +1 -0
  105. package/dist/ldap.js +271 -0
  106. package/dist/ldap.js.map +1 -0
  107. package/dist/nodeEnv.d.ts +17 -0
  108. package/dist/nodeEnv.d.ts.map +1 -0
  109. package/dist/nodeEnv.js +33 -0
  110. package/dist/nodeEnv.js.map +1 -0
  111. package/dist/oauth1.d.ts +68 -0
  112. package/dist/oauth1.d.ts.map +1 -0
  113. package/dist/oauth1.js +106 -0
  114. package/dist/oauth1.js.map +1 -0
  115. package/dist/oidc.d.ts +61 -0
  116. package/dist/oidc.d.ts.map +1 -0
  117. package/dist/oidc.js +151 -0
  118. package/dist/oidc.js.map +1 -0
  119. package/dist/quasar.d.ts +14 -0
  120. package/dist/quasar.d.ts.map +1 -0
  121. package/dist/quasar.js +42 -0
  122. package/dist/quasar.js.map +1 -0
  123. package/dist/replay.d.ts +70 -0
  124. package/dist/replay.d.ts.map +1 -0
  125. package/dist/replay.js +82 -0
  126. package/dist/replay.js.map +1 -0
  127. package/dist/saml.d.ts +68 -0
  128. package/dist/saml.d.ts.map +1 -0
  129. package/dist/saml.js +181 -0
  130. package/dist/saml.js.map +1 -0
  131. package/dist/services/main.d.ts +22 -0
  132. package/dist/services/main.d.ts.map +1 -0
  133. package/dist/services/main.js +39 -0
  134. package/dist/services/main.js.map +1 -0
  135. package/dist/testing/FakeTransit.d.ts +82 -0
  136. package/dist/testing/FakeTransit.d.ts.map +1 -0
  137. package/dist/testing/FakeTransit.js +160 -0
  138. package/dist/testing/FakeTransit.js.map +1 -0
  139. package/dist/types.d.ts +118 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +27 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/xml.d.ts +72 -0
  144. package/dist/xml.d.ts.map +1 -0
  145. package/dist/xml.js +353 -0
  146. package/dist/xml.js.map +1 -0
  147. package/package.json +73 -0
  148. package/src/Oauth1Driver.ts +216 -0
  149. package/src/Oauth2Driver.ts +271 -0
  150. package/src/TransitManager.ts +145 -0
  151. package/src/TransitProvider.ts +51 -0
  152. package/src/apple.ts +101 -0
  153. package/src/ber.ts +175 -0
  154. package/src/c14n.ts +187 -0
  155. package/src/config.ts +160 -0
  156. package/src/configure.ts +47 -0
  157. package/src/drivers/AppleDriver.ts +69 -0
  158. package/src/drivers/DiscordDriver.ts +50 -0
  159. package/src/drivers/FacebookDriver.ts +53 -0
  160. package/src/drivers/GitHubDriver.ts +91 -0
  161. package/src/drivers/GoogleDriver.ts +38 -0
  162. package/src/drivers/LdapDirectory.ts +185 -0
  163. package/src/drivers/LinkedInDriver.ts +105 -0
  164. package/src/drivers/LinkedInOpenidConnectDriver.ts +33 -0
  165. package/src/drivers/OidcDriver.ts +379 -0
  166. package/src/drivers/SamlDriver.ts +326 -0
  167. package/src/drivers/SpotifyDriver.ts +38 -0
  168. package/src/drivers/TwitterDriver.ts +55 -0
  169. package/src/drivers/TwitterXDriver.ts +53 -0
  170. package/src/dsig.ts +405 -0
  171. package/src/index.ts +85 -0
  172. package/src/jwt.ts +263 -0
  173. package/src/ldap.ts +374 -0
  174. package/src/nodeEnv.ts +31 -0
  175. package/src/oauth1.ts +144 -0
  176. package/src/oidc.ts +208 -0
  177. package/src/quasar.ts +64 -0
  178. package/src/replay.ts +136 -0
  179. package/src/saml.ts +309 -0
  180. package/src/services/main.ts +47 -0
  181. package/src/testing/FakeTransit.ts +233 -0
  182. package/src/types.ts +150 -0
  183. package/src/xml.ts +438 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * OAuth 1.0a request signing (RFC 5849), HMAC-SHA1.
3
+ *
4
+ * OAuth1 authenticates each request by signing it, rather than by carrying a
5
+ * bearer token, which is why it needs this at all and OAuth2 does not. The
6
+ * signature covers the method, the URL and every parameter — so getting the
7
+ * base string wrong is the failure mode, and it fails as an opaque 401 from
8
+ * the provider.
9
+ *
10
+ * Everything here is pure: no network, no clock, no randomness. The nonce and
11
+ * timestamp are passed in, which is also what makes the result testable
12
+ * against the vector in the specification.
13
+ */
14
+ /** One parameter. Duplicates of a key are allowed, as the spec permits. */
15
+ export type Param = readonly [key: string, value: string];
16
+ export interface SignatureInput {
17
+ method: string;
18
+ /** Full URL. Its query string is folded into the signed parameters. */
19
+ url: string;
20
+ /** Parameters beyond the URL query and the `oauth_*` set — a form body. */
21
+ params?: readonly Param[];
22
+ /** The `oauth_*` parameters, without `oauth_signature`. */
23
+ oauth: readonly Param[];
24
+ consumerSecret: string;
25
+ /** Absent on the very first call, when no token exists yet. */
26
+ tokenSecret?: string;
27
+ }
28
+ /**
29
+ * Percent-encoding as the specification defines it: unreserved characters
30
+ * stay, everything else is uppercase `%XX`.
31
+ *
32
+ * `encodeURIComponent` is close but leaves `!'()*` alone, and a signature
33
+ * computed with those unescaped does not match the one the provider computes.
34
+ */
35
+ export declare function percentEncode(value: string): string;
36
+ /** A fresh nonce. Only its uniqueness matters. */
37
+ export declare function nonce(): string;
38
+ /**
39
+ * The signature base string — the exact bytes both sides hash.
40
+ *
41
+ * Query and body parameters are decoded, then re-encoded, then sorted by
42
+ * encoded key and encoded value. That round trip is deliberate: two requests
43
+ * differing only in how they spelled an escape have to produce the same
44
+ * string, or the signature depends on the spelling.
45
+ */
46
+ export declare function signatureBaseString(input: SignatureInput): string;
47
+ /** The signature itself, base64 of HMAC-SHA1 over the base string. */
48
+ export declare function sign(input: SignatureInput): string;
49
+ /**
50
+ * The `Authorization: OAuth …` header value, signature included.
51
+ *
52
+ * Only the `oauth_*` parameters go in the header; the query and body ones are
53
+ * signed but stay where they were.
54
+ */
55
+ export declare function authorizationHeader(input: SignatureInput): string;
56
+ /** The `oauth_*` parameters every signed request carries. */
57
+ export declare function baseOauthParams(options: {
58
+ consumerKey: string;
59
+ token?: string;
60
+ timestamp: number;
61
+ nonce: string;
62
+ }): Param[];
63
+ /**
64
+ * Parse a `key=value&…` response body. The token endpoints answer in this
65
+ * form rather than JSON.
66
+ */
67
+ export declare function parseFormBody(body: string): Record<string, string>;
68
+ //# sourceMappingURL=oauth1.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth1.d.ts","sourceRoot":"","sources":["../src/oauth1.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,2EAA2E;AAC3E,MAAM,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,MAAM,CAAC,EAAE,SAAS,KAAK,EAAE,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,EAAE,SAAS,KAAK,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED,kDAAkD;AAClD,wBAAgB,KAAK,IAAI,MAAM,CAE9B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAsBjE;AAED,sEAAsE;AACtE,wBAAgB,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAOlD;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CASjE;AAED,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,KAAK,EAAE,CAUV;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMlE"}
package/dist/oauth1.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * OAuth 1.0a request signing (RFC 5849), HMAC-SHA1.
3
+ *
4
+ * OAuth1 authenticates each request by signing it, rather than by carrying a
5
+ * bearer token, which is why it needs this at all and OAuth2 does not. The
6
+ * signature covers the method, the URL and every parameter — so getting the
7
+ * base string wrong is the failure mode, and it fails as an opaque 401 from
8
+ * the provider.
9
+ *
10
+ * Everything here is pure: no network, no clock, no randomness. The nonce and
11
+ * timestamp are passed in, which is also what makes the result testable
12
+ * against the vector in the specification.
13
+ */
14
+ import { createHmac, randomBytes } from "node:crypto";
15
+ /**
16
+ * Percent-encoding as the specification defines it: unreserved characters
17
+ * stay, everything else is uppercase `%XX`.
18
+ *
19
+ * `encodeURIComponent` is close but leaves `!'()*` alone, and a signature
20
+ * computed with those unescaped does not match the one the provider computes.
21
+ */
22
+ export function percentEncode(value) {
23
+ return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
24
+ }
25
+ /** A fresh nonce. Only its uniqueness matters. */
26
+ export function nonce() {
27
+ return randomBytes(16).toString("hex");
28
+ }
29
+ /**
30
+ * The signature base string — the exact bytes both sides hash.
31
+ *
32
+ * Query and body parameters are decoded, then re-encoded, then sorted by
33
+ * encoded key and encoded value. That round trip is deliberate: two requests
34
+ * differing only in how they spelled an escape have to produce the same
35
+ * string, or the signature depends on the spelling.
36
+ */
37
+ export function signatureBaseString(input) {
38
+ const url = new URL(input.url);
39
+ const query = [...url.searchParams.entries()].map(([key, value]) => [key, value]);
40
+ const encoded = [...query, ...(input.params ?? []), ...input.oauth]
41
+ .map(([key, value]) => [percentEncode(key), percentEncode(value)])
42
+ .sort((a, b) => (a[0] === b[0] ? compare(a[1], b[1]) : compare(a[0], b[0])))
43
+ .map(([key, value]) => `${key}=${value}`)
44
+ .join("&");
45
+ // The base URL carries no query, no fragment, and no default port.
46
+ url.search = "";
47
+ url.hash = "";
48
+ const base = url.toString().replace(/\/$/, url.pathname === "/" ? "/" : "");
49
+ return [
50
+ input.method.toUpperCase(),
51
+ percentEncode(base),
52
+ percentEncode(encoded),
53
+ ].join("&");
54
+ }
55
+ /** The signature itself, base64 of HMAC-SHA1 over the base string. */
56
+ export function sign(input) {
57
+ const key = `${percentEncode(input.consumerSecret)}&${percentEncode(input.tokenSecret ?? "")}`;
58
+ return createHmac("sha1", key)
59
+ .update(signatureBaseString(input))
60
+ .digest("base64");
61
+ }
62
+ /**
63
+ * The `Authorization: OAuth …` header value, signature included.
64
+ *
65
+ * Only the `oauth_*` parameters go in the header; the query and body ones are
66
+ * signed but stay where they were.
67
+ */
68
+ export function authorizationHeader(input) {
69
+ const signed = [
70
+ ...input.oauth,
71
+ ["oauth_signature", sign(input)],
72
+ ];
73
+ const parts = signed
74
+ .map(([key, value]) => `${percentEncode(key)}="${percentEncode(value)}"`)
75
+ .join(", ");
76
+ return `OAuth ${parts}`;
77
+ }
78
+ /** The `oauth_*` parameters every signed request carries. */
79
+ export function baseOauthParams(options) {
80
+ const params = [
81
+ ["oauth_consumer_key", options.consumerKey],
82
+ ["oauth_nonce", options.nonce],
83
+ ["oauth_signature_method", "HMAC-SHA1"],
84
+ ["oauth_timestamp", String(options.timestamp)],
85
+ ["oauth_version", "1.0"],
86
+ ];
87
+ if (options.token)
88
+ params.push(["oauth_token", options.token]);
89
+ return params;
90
+ }
91
+ /**
92
+ * Parse a `key=value&…` response body. The token endpoints answer in this
93
+ * form rather than JSON.
94
+ */
95
+ export function parseFormBody(body) {
96
+ const out = {};
97
+ for (const [key, value] of new URLSearchParams(body).entries()) {
98
+ out[key] = value;
99
+ }
100
+ return out;
101
+ }
102
+ /** Byte-order comparison, which is what the specification asks for. */
103
+ function compare(a, b) {
104
+ return a < b ? -1 : a > b ? 1 : 0;
105
+ }
106
+ //# sourceMappingURL=oauth1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth1.js","sourceRoot":"","sources":["../src/oauth1.ts"],"names":[],"mappings":"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"}
package/dist/oidc.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The two documents an OpenID Connect provider publishes about itself: where
3
+ * its endpoints are, and which keys it signs with.
4
+ *
5
+ * Both are cached, and both are refetched on a rule rather than on demand.
6
+ * Keys rotate, so a cache that never refreshes locks an application out at
7
+ * the worst moment; a cache that refreshes whenever a token names an unknown
8
+ * key hands anyone a way to make this application hammer the provider. The
9
+ * middle is a rate limit.
10
+ */
11
+ import type { Jwk } from "./jwt.js";
12
+ /** The slice of the discovery document this reads. */
13
+ export interface OidcMetadata {
14
+ issuer: string;
15
+ authorization_endpoint: string;
16
+ token_endpoint: string;
17
+ jwks_uri: string;
18
+ userinfo_endpoint?: string;
19
+ id_token_signing_alg_values_supported?: string[];
20
+ code_challenge_methods_supported?: string[];
21
+ }
22
+ export interface RemoteOptions {
23
+ /** How long a fetched document is trusted, in milliseconds. Default 1 hour. */
24
+ ttlMs?: number;
25
+ /** Shortest interval between two key refetches. Default 5 minutes. */
26
+ minRefetchMs?: number;
27
+ /** Injected in tests. */
28
+ now?: () => number;
29
+ }
30
+ /**
31
+ * Reads and caches `/.well-known/openid-configuration`.
32
+ *
33
+ * The document's own `issuer` is checked against the configured one. They are
34
+ * allowed to differ in the specification's letter about as much as a passport
35
+ * is allowed to name someone else: a mismatch means the URL that was asked is
36
+ * not the authority that answered.
37
+ */
38
+ export declare class OidcDiscovery {
39
+ #private;
40
+ constructor(issuer: string, options?: RemoteOptions);
41
+ get issuer(): string;
42
+ get url(): string;
43
+ metadata(): Promise<OidcMetadata>;
44
+ }
45
+ /**
46
+ * Reads and caches a provider's signing keys, and knows when it is allowed to
47
+ * look again.
48
+ */
49
+ export declare class JwksCache {
50
+ #private;
51
+ constructor(options?: RemoteOptions);
52
+ /**
53
+ * The key a token names.
54
+ *
55
+ * A `kid` that is not held triggers at most one refetch per interval — that
56
+ * is how a rotated key is picked up, and how a stream of tokens naming
57
+ * invented keys stays cheap.
58
+ */
59
+ key(uri: string, kid: string | undefined): Promise<Jwk>;
60
+ }
61
+ //# sourceMappingURL=oidc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc.d.ts","sourceRoot":"","sources":["../src/oidc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,qCAAqC,CAAC,EAAE,MAAM,EAAE,CAAC;IACjD,gCAAgC,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,aAAa;IAC7B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yBAAyB;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAKD;;;;;;;GAOG;AACH,qBAAa,aAAa;;gBAOb,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB;IAQvD,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,GAAG,IAAI,MAAM,CAEhB;IAEK,QAAQ,IAAI,OAAO,CAAC,YAAY,CAAC;CA2BvC;AAED;;;GAGG;AACH,qBAAa,SAAS;;gBAOT,OAAO,GAAE,aAAkB;IAKvC;;;;;;OAMG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC;CAwD7D"}
package/dist/oidc.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The two documents an OpenID Connect provider publishes about itself: where
3
+ * its endpoints are, and which keys it signs with.
4
+ *
5
+ * Both are cached, and both are refetched on a rule rather than on demand.
6
+ * Keys rotate, so a cache that never refreshes locks an application out at
7
+ * the worst moment; a cache that refreshes whenever a token names an unknown
8
+ * key hands anyone a way to make this application hammer the provider. The
9
+ * middle is a rate limit.
10
+ */
11
+ const DEFAULT_TTL_MS = 3_600_000;
12
+ const DEFAULT_MIN_REFETCH_MS = 300_000;
13
+ /**
14
+ * Reads and caches `/.well-known/openid-configuration`.
15
+ *
16
+ * The document's own `issuer` is checked against the configured one. They are
17
+ * allowed to differ in the specification's letter about as much as a passport
18
+ * is allowed to name someone else: a mismatch means the URL that was asked is
19
+ * not the authority that answered.
20
+ */
21
+ export class OidcDiscovery {
22
+ #issuer;
23
+ #ttlMs;
24
+ #now;
25
+ #cached;
26
+ #inFlight;
27
+ constructor(issuer, options = {}) {
28
+ // A trailing slash changes the well-known URL and, on some providers,
29
+ // the issuer claim it must be compared against.
30
+ this.#issuer = issuer.replace(/\/+$/, "");
31
+ this.#ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
32
+ this.#now = options.now ?? Date.now;
33
+ }
34
+ get issuer() {
35
+ return this.#issuer;
36
+ }
37
+ get url() {
38
+ return `${this.#issuer}/.well-known/openid-configuration`;
39
+ }
40
+ async metadata() {
41
+ const cached = this.#cached;
42
+ if (cached && this.#now() - cached.fetchedAt < this.#ttlMs) {
43
+ return cached.metadata;
44
+ }
45
+ // One fetch at a time: a burst of sign-ins on a cold cache would
46
+ // otherwise open one request per sign-in.
47
+ this.#inFlight ??= this.#fetch().finally(() => {
48
+ this.#inFlight = undefined;
49
+ });
50
+ return this.#inFlight;
51
+ }
52
+ async #fetch() {
53
+ const response = await fetch(this.url, {
54
+ headers: { Accept: "application/json" },
55
+ });
56
+ if (!response.ok) {
57
+ throw new Error(`[transit] OpenID Connect discovery failed for '${this.#issuer}' (HTTP ${response.status})`);
58
+ }
59
+ const body = (await response.json());
60
+ const metadata = assertMetadata(body, this.#issuer);
61
+ this.#cached = { metadata, fetchedAt: this.#now() };
62
+ return metadata;
63
+ }
64
+ }
65
+ /**
66
+ * Reads and caches a provider's signing keys, and knows when it is allowed to
67
+ * look again.
68
+ */
69
+ export class JwksCache {
70
+ #minRefetchMs;
71
+ #now;
72
+ #keys = [];
73
+ #fetchedAt = 0;
74
+ #inFlight;
75
+ constructor(options = {}) {
76
+ this.#minRefetchMs = options.minRefetchMs ?? DEFAULT_MIN_REFETCH_MS;
77
+ this.#now = options.now ?? Date.now;
78
+ }
79
+ /**
80
+ * The key a token names.
81
+ *
82
+ * A `kid` that is not held triggers at most one refetch per interval — that
83
+ * is how a rotated key is picked up, and how a stream of tokens naming
84
+ * invented keys stays cheap.
85
+ */
86
+ async key(uri, kid) {
87
+ const found = this.#find(kid);
88
+ if (found)
89
+ return found;
90
+ if (this.#keys.length > 0 &&
91
+ this.#now() - this.#fetchedAt < this.#minRefetchMs) {
92
+ throw new Error(`[transit] id_token names a signing key ('${kid ?? "no kid"}') the provider did not publish`);
93
+ }
94
+ await this.#fetch(uri);
95
+ const refreshed = this.#find(kid);
96
+ if (!refreshed) {
97
+ throw new Error(`[transit] id_token names a signing key ('${kid ?? "no kid"}') the provider did not publish`);
98
+ }
99
+ return refreshed;
100
+ }
101
+ #find(kid) {
102
+ const usable = this.#keys.filter((jwk) => jwk.use === undefined || jwk.use === "sig");
103
+ if (kid !== undefined)
104
+ return usable.find((jwk) => jwk.kid === kid);
105
+ // No `kid` is only unambiguous when the provider publishes one key.
106
+ return usable.length === 1 ? usable[0] : undefined;
107
+ }
108
+ async #fetch(uri) {
109
+ this.#inFlight ??= this.#fetchOnce(uri).finally(() => {
110
+ this.#inFlight = undefined;
111
+ });
112
+ return this.#inFlight;
113
+ }
114
+ async #fetchOnce(uri) {
115
+ const response = await fetch(uri, {
116
+ headers: { Accept: "application/json" },
117
+ });
118
+ if (!response.ok) {
119
+ throw new Error(`[transit] fetching the provider's signing keys failed (HTTP ${response.status})`);
120
+ }
121
+ const body = (await response.json());
122
+ if (!Array.isArray(body.keys)) {
123
+ throw new Error("[transit] the provider's JWKS has no `keys` array");
124
+ }
125
+ this.#keys = body.keys.filter(isJwk);
126
+ this.#fetchedAt = this.#now();
127
+ return this.#keys;
128
+ }
129
+ }
130
+ function isJwk(value) {
131
+ return (typeof value === "object" &&
132
+ value !== null &&
133
+ typeof Reflect.get(value, "kty") === "string");
134
+ }
135
+ function assertMetadata(body, expectedIssuer) {
136
+ for (const field of [
137
+ "issuer",
138
+ "authorization_endpoint",
139
+ "token_endpoint",
140
+ "jwks_uri",
141
+ ]) {
142
+ if (typeof body[field] !== "string" || body[field] === "") {
143
+ throw new Error(`[transit] the discovery document of '${expectedIssuer}' has no ${field}`);
144
+ }
145
+ }
146
+ if (body.issuer !== expectedIssuer) {
147
+ throw new Error(`[transit] discovery answered for issuer '${body.issuer}', not '${expectedIssuer}' — the URL asked is not the authority that replied`);
148
+ }
149
+ return body;
150
+ }
151
+ //# sourceMappingURL=oidc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc.js","sourceRoot":"","sources":["../src/oidc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolving a Redis connection by name, from `@c9up/quasar`.
3
+ *
4
+ * Transit does not depend on quasar: it is an optional peer, and this module
5
+ * never imports it statically — the specifier is built at runtime so the
6
+ * TypeScript build stays free of it too.
7
+ */
8
+ /**
9
+ * The named connection, checked for the commands the caller needs before it is
10
+ * handed over — a connection missing one would fail on the first sign-in, far
11
+ * from the cause.
12
+ */
13
+ export declare function quasarConnection<T>(name: string | undefined, required: readonly string[], what: string): Promise<T>;
14
+ //# sourceMappingURL=quasar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quasar.d.ts","sourceRoot":"","sources":["../src/quasar.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAcH;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACvC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,IAAI,EAAE,MAAM,GACV,OAAO,CAAC,CAAC,CAAC,CAkCZ"}
package/dist/quasar.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Resolving a Redis connection by name, from `@c9up/quasar`.
3
+ *
4
+ * Transit does not depend on quasar: it is an optional peer, and this module
5
+ * never imports it statically — the specifier is built at runtime so the
6
+ * TypeScript build stays free of it too.
7
+ */
8
+ function isConnectionSource(value) {
9
+ return (typeof value === "object" &&
10
+ value !== null &&
11
+ typeof Reflect.get(value, "connection") === "function");
12
+ }
13
+ /**
14
+ * The named connection, checked for the commands the caller needs before it is
15
+ * handed over — a connection missing one would fail on the first sign-in, far
16
+ * from the cause.
17
+ */
18
+ export async function quasarConnection(name, required, what) {
19
+ const specifier = "@c9up/quasar/services/main";
20
+ let loaded;
21
+ try {
22
+ loaded = await import(/* @vite-ignore */ specifier);
23
+ }
24
+ catch (cause) {
25
+ throw new Error(`[transit] naming a Redis connection needs @c9up/quasar, which is not installed.\n pnpm add @c9up/quasar`, { cause });
26
+ }
27
+ const manager = isConnectionSource(loaded)
28
+ ? loaded
29
+ : Reflect.get(Object(loaded), "default");
30
+ if (!isConnectionSource(manager)) {
31
+ throw new Error("[transit] @c9up/quasar/services/main did not expose a connection() manager");
32
+ }
33
+ const connection = manager.connection(name);
34
+ const missing = required.filter((command) => typeof connection !== "object" ||
35
+ connection === null ||
36
+ typeof Reflect.get(connection, command) !== "function");
37
+ if (missing.length > 0) {
38
+ throw new Error(`[transit] the quasar connection${name ? ` '${name}'` : ""} is missing ${missing.join(", ")}, which ${what} issues`);
39
+ }
40
+ return connection;
41
+ }
42
+ //# sourceMappingURL=quasar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quasar.js","sourceRoot":"","sources":["../src/quasar.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,SAAS,kBAAkB,CAAC,KAAc;IACzC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,UAAU,CACtD,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,IAAwB,EACxB,QAA2B,EAC3B,IAAY;IAEZ,MAAM,SAAS,GAAG,4BAA4B,CAAC;IAC/C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACJ,MAAM,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACd,0GAA0G,EAC1G,EAAE,KAAK,EAAE,CACT,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC;QACzC,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC;IAC1C,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CACd,4EAA4E,CAC5E,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAC9B,CAAC,OAAO,EAAE,EAAE,CACX,OAAO,UAAU,KAAK,QAAQ;QAC9B,UAAU,KAAK,IAAI;QACnB,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,UAAU,CACvD,CAAC;IACF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACd,kCAAkC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,eAAe,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,SAAS,CACnH,CAAC;IACH,CAAC;IACD,OAAO,UAAe,CAAC;AACxB,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Refusing an assertion that has already been used.
3
+ *
4
+ * A SAML assertion is a bearer token: whoever holds it is who it says they
5
+ * are, until it expires. A signature does not change that, and neither does
6
+ * any condition on it — a response captured once and posted twice is valid
7
+ * both times. Remembering the id until the assertion expires is what makes the
8
+ * second time fail.
9
+ *
10
+ * The window is short, so what is stored stays small: an id is forgotten as
11
+ * soon as the assertion it names could no longer have been accepted anyway.
12
+ */
13
+ export interface AssertionReplayStore {
14
+ /**
15
+ * Remember `assertionId` until `expiresAt` (epoch seconds).
16
+ *
17
+ * Answers `true` when this is the first time, `false` when the assertion
18
+ * has already been used. Implementations must decide that atomically:
19
+ * checking and then storing lets two concurrent posts of the same response
20
+ * both win.
21
+ */
22
+ remember(assertionId: string, expiresAt: number): Promise<boolean>;
23
+ }
24
+ /**
25
+ * Remembers in this process's memory.
26
+ *
27
+ * Correct while the application runs in one process, and only then: two
28
+ * replicas each keep their own table, so the same assertion is accepted once
29
+ * per replica. Reach for the Redis store the moment a second one exists.
30
+ */
31
+ export declare class MemoryAssertionReplayStore implements AssertionReplayStore {
32
+ #private;
33
+ remember(assertionId: string, expiresAt: number): Promise<boolean>;
34
+ }
35
+ /** The commands the Redis store issues. */
36
+ export interface ReplayRedisClient {
37
+ /** `SET key value EX <ttl> NX` — answers null when the key already exists. */
38
+ set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
39
+ }
40
+ export type ReplayRedisResolver = ReplayRedisClient | (() => ReplayRedisClient | Promise<ReplayRedisClient>);
41
+ export interface RedisReplayOptions {
42
+ /** Key prefix. Default `"transit:saml:assertion:"`. */
43
+ prefix?: string;
44
+ }
45
+ /**
46
+ * Remembers in Redis, so every replica refuses the assertion the first one
47
+ * accepted.
48
+ *
49
+ * `SET … NX` decides in one round trip, atomically on the server, which is
50
+ * what stops two concurrent posts of the same response from both winning.
51
+ */
52
+ export declare class RedisAssertionReplayStore implements AssertionReplayStore {
53
+ #private;
54
+ constructor(client: ReplayRedisResolver, options?: RedisReplayOptions);
55
+ remember(assertionId: string, expiresAt: number): Promise<boolean>;
56
+ }
57
+ /** The replay stores a config names. */
58
+ export declare const replayStores: {
59
+ /** In this process's memory. Bounds nothing across replicas. */
60
+ memory(): () => AssertionReplayStore;
61
+ /**
62
+ * Redis. `connection` takes a client, a function answering one, or the NAME
63
+ * of a `@c9up/quasar` connection.
64
+ */
65
+ redis(options: {
66
+ connection: ReplayRedisResolver | string;
67
+ prefix?: string;
68
+ }): () => AssertionReplayStore;
69
+ };
70
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,oBAAoB;IACpC;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED;;;;;;GAMG;AACH,qBAAa,0BAA2B,YAAW,oBAAoB;;IAGhE,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAWxE;AAED,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IACjC,8EAA8E;IAC9E,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED,MAAM,MAAM,mBAAmB,GAC5B,iBAAiB,GACjB,CAAC,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IAClC,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,yBAA0B,YAAW,oBAAoB;;gBAKzD,MAAM,EAAE,mBAAmB,EAAE,OAAO,GAAE,kBAAuB;IAKnE,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAwBxE;AAED,wCAAwC;AACxC,eAAO,MAAM,YAAY;IACxB,gEAAgE;cACtD,MAAM,oBAAoB;IAIpC;;;OAGG;mBACY;QACd,UAAU,EAAE,mBAAmB,GAAG,MAAM,CAAC;QACzC,MAAM,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,MAAM,oBAAoB;CAa9B,CAAC"}
package/dist/replay.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Refusing an assertion that has already been used.
3
+ *
4
+ * A SAML assertion is a bearer token: whoever holds it is who it says they
5
+ * are, until it expires. A signature does not change that, and neither does
6
+ * any condition on it — a response captured once and posted twice is valid
7
+ * both times. Remembering the id until the assertion expires is what makes the
8
+ * second time fail.
9
+ *
10
+ * The window is short, so what is stored stays small: an id is forgotten as
11
+ * soon as the assertion it names could no longer have been accepted anyway.
12
+ */
13
+ import { quasarConnection } from "./quasar.js";
14
+ /**
15
+ * Remembers in this process's memory.
16
+ *
17
+ * Correct while the application runs in one process, and only then: two
18
+ * replicas each keep their own table, so the same assertion is accepted once
19
+ * per replica. Reach for the Redis store the moment a second one exists.
20
+ */
21
+ export class MemoryAssertionReplayStore {
22
+ #seen = new Map();
23
+ async remember(assertionId, expiresAt) {
24
+ const now = Math.floor(Date.now() / 1000);
25
+ // An opportunistic sweep, bounded by the map's own size: the window is
26
+ // minutes, so nothing accumulates.
27
+ for (const [id, expiry] of this.#seen) {
28
+ if (expiry <= now)
29
+ this.#seen.delete(id);
30
+ }
31
+ if (this.#seen.has(assertionId))
32
+ return false;
33
+ this.#seen.set(assertionId, expiresAt);
34
+ return true;
35
+ }
36
+ }
37
+ /**
38
+ * Remembers in Redis, so every replica refuses the assertion the first one
39
+ * accepted.
40
+ *
41
+ * `SET … NX` decides in one round trip, atomically on the server, which is
42
+ * what stops two concurrent posts of the same response from both winning.
43
+ */
44
+ export class RedisAssertionReplayStore {
45
+ #source;
46
+ #prefix;
47
+ #resolved;
48
+ constructor(client, options = {}) {
49
+ this.#source = client;
50
+ this.#prefix = options.prefix ?? "transit:saml:assertion:";
51
+ }
52
+ async remember(assertionId, expiresAt) {
53
+ const client = await this.#client();
54
+ // The key outlives the assertion by a minute, so an assertion accepted
55
+ // at the edge of its window cannot be replayed just after it.
56
+ const ttl = Math.max(1, Math.ceil(expiresAt - Math.floor(Date.now() / 1000)) + 60);
57
+ const stored = await client.set(`${this.#prefix}${assertionId}`, "1", "EX", ttl, "NX");
58
+ return stored !== null && stored !== undefined;
59
+ }
60
+ #client() {
61
+ this.#resolved ??= Promise.resolve(typeof this.#source === "function" ? this.#source() : this.#source);
62
+ return this.#resolved;
63
+ }
64
+ }
65
+ /** The replay stores a config names. */
66
+ export const replayStores = {
67
+ /** In this process's memory. Bounds nothing across replicas. */
68
+ memory() {
69
+ return () => new MemoryAssertionReplayStore();
70
+ },
71
+ /**
72
+ * Redis. `connection` takes a client, a function answering one, or the NAME
73
+ * of a `@c9up/quasar` connection.
74
+ */
75
+ redis(options) {
76
+ const client = typeof options.connection === "string"
77
+ ? () => quasarConnection(options.connection, ["set"], "the assertion replay store")
78
+ : options.connection;
79
+ return () => new RedisAssertionReplayStore(client, { prefix: options.prefix });
80
+ },
81
+ };
82
+ //# sourceMappingURL=replay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.js","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAc/C;;;;;;GAMG;AACH,MAAM,OAAO,0BAA0B;IAC7B,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE3C,KAAK,CAAC,QAAQ,CAAC,WAAmB,EAAE,SAAiB;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,uEAAuE;QACvE,mCAAmC;QACnC,KAAK,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACvC,IAAI,MAAM,IAAI,GAAG;gBAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QAC9C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IACb,CAAC;CACD;AAiBD;;;;;;GAMG;AACH,MAAM,OAAO,yBAAyB;IAC5B,OAAO,CAAsB;IAC7B,OAAO,CAAS;IACzB,SAAS,CAAyC;IAElD,YAAY,MAA2B,EAAE,UAA8B,EAAE;QACxE,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,yBAAyB,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,WAAmB,EAAE,SAAiB;QACpD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACpC,uEAAuE;QACvE,8DAA8D;QAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CACnB,CAAC,EACD,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,EAAE,CACzD,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAC9B,GAAG,IAAI,CAAC,OAAO,GAAG,WAAW,EAAE,EAC/B,GAAG,EACH,IAAI,EACJ,GAAG,EACH,IAAI,CACJ,CAAC;QACF,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,CAAC;IAChD,CAAC;IAED,OAAO;QACN,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,OAAO,CACjC,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAClE,CAAC;QACF,OAAO,IAAI,CAAC,SAAS,CAAC;IACvB,CAAC;CACD;AAED,wCAAwC;AACxC,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,gEAAgE;IAChE,MAAM;QACL,OAAO,GAAG,EAAE,CAAC,IAAI,0BAA0B,EAAE,CAAC;IAC/C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAGL;QACA,MAAM,MAAM,GACX,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ;YACrC,CAAC,CAAC,GAAG,EAAE,CACL,gBAAgB,CACf,OAAO,CAAC,UAAoB,EAC5B,CAAC,KAAK,CAAC,EACP,4BAA4B,CAC5B;YACH,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QACvB,OAAO,GAAG,EAAE,CACX,IAAI,yBAAyB,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpE,CAAC;CACD,CAAC"}