@dwk/solid-pod 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.
Files changed (68) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +108 -0
  3. package/dist/auth.d.ts +33 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +160 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/config.d.ts +181 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +74 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/encoding.d.ts +13 -0
  12. package/dist/encoding.d.ts.map +1 -0
  13. package/dist/encoding.js +31 -0
  14. package/dist/encoding.js.map +1 -0
  15. package/dist/gc.d.ts +22 -0
  16. package/dist/gc.d.ts.map +1 -0
  17. package/dist/gc.js +33 -0
  18. package/dist/gc.js.map +1 -0
  19. package/dist/handler.d.ts +20 -0
  20. package/dist/handler.d.ts.map +1 -0
  21. package/dist/handler.js +155 -0
  22. package/dist/handler.js.map +1 -0
  23. package/dist/index.d.ts +24 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +23 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/jwt.d.ts +36 -0
  28. package/dist/jwt.d.ts.map +1 -0
  29. package/dist/jwt.js +120 -0
  30. package/dist/jwt.js.map +1 -0
  31. package/dist/ldp.d.ts +37 -0
  32. package/dist/ldp.d.ts.map +1 -0
  33. package/dist/ldp.js +85 -0
  34. package/dist/ldp.js.map +1 -0
  35. package/dist/log.d.ts +55 -0
  36. package/dist/log.d.ts.map +1 -0
  37. package/dist/log.js +51 -0
  38. package/dist/log.js.map +1 -0
  39. package/dist/negotiation.d.ts +23 -0
  40. package/dist/negotiation.d.ts.map +1 -0
  41. package/dist/negotiation.js +80 -0
  42. package/dist/negotiation.js.map +1 -0
  43. package/dist/patch.d.ts +80 -0
  44. package/dist/patch.d.ts.map +1 -0
  45. package/dist/patch.js +425 -0
  46. package/dist/patch.js.map +1 -0
  47. package/dist/pod.d.ts +20 -0
  48. package/dist/pod.d.ts.map +1 -0
  49. package/dist/pod.js +860 -0
  50. package/dist/pod.js.map +1 -0
  51. package/dist/wac.d.ts +33 -0
  52. package/dist/wac.d.ts.map +1 -0
  53. package/dist/wac.js +84 -0
  54. package/dist/wac.js.map +1 -0
  55. package/package.json +55 -0
  56. package/src/auth.ts +203 -0
  57. package/src/config.ts +254 -0
  58. package/src/encoding.ts +32 -0
  59. package/src/gc.ts +47 -0
  60. package/src/handler.ts +199 -0
  61. package/src/index.ts +32 -0
  62. package/src/jwt.ts +166 -0
  63. package/src/ldp.ts +99 -0
  64. package/src/log.ts +59 -0
  65. package/src/negotiation.ts +97 -0
  66. package/src/patch.ts +539 -0
  67. package/src/pod.ts +1195 -0
  68. package/src/wac.ts +119 -0
package/dist/gc.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The R2 garbage-collection cron handler.
3
+ *
4
+ * Each pod's Durable Object forwards its orphaned blob keys (copy-on-write
5
+ * displacements and deletes) into a shared D1 table as it writes. This handler
6
+ * — wired to a Cloudflare cron trigger — reclaims those R2 objects once they
7
+ * are older than a safety window ≥ the maximum write duration, touching only D1
8
+ * and R2 and never waking a Durable Object. The reclamation logic lives in
9
+ * `@dwk/store`; this is the thin endpoint wiring.
10
+ */
11
+ import { type SolidPodConfig, type SolidPodGcEnv } from "./config";
12
+ /** A `scheduled`-compatible cron handler for R2 blob garbage collection. */
13
+ export type SolidPodGcHandler = (event: ScheduledController, env: SolidPodGcEnv, ctx: ExecutionContext) => Promise<void>;
14
+ /**
15
+ * Create the cron handler that reclaims orphaned R2 blobs. Bind it to a
16
+ * `scheduled` trigger alongside the pod Worker; it shares the same `BLOBS`
17
+ * bucket and the `GC_DB` D1 database the DO forwards orphans into.
18
+ *
19
+ * Fails loudly when the required `BLOBS` / `GC_DB` bindings are missing.
20
+ */
21
+ export declare function createSolidPodGc(config: SolidPodConfig): SolidPodGcHandler;
22
+ //# sourceMappingURL=gc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gc.d.ts","sourceRoot":"","sources":["../src/gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,UAAU,CAAC;AAElB,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG,CAC9B,KAAK,EAAE,mBAAmB,EAC1B,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,iBAAiB,CAa1E"}
package/dist/gc.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The R2 garbage-collection cron handler.
3
+ *
4
+ * Each pod's Durable Object forwards its orphaned blob keys (copy-on-write
5
+ * displacements and deletes) into a shared D1 table as it writes. This handler
6
+ * — wired to a Cloudflare cron trigger — reclaims those R2 objects once they
7
+ * are older than a safety window ≥ the maximum write duration, touching only D1
8
+ * and R2 and never waking a Durable Object. The reclamation logic lives in
9
+ * `@dwk/store`; this is the thin endpoint wiring.
10
+ */
11
+ import { collectGarbage, ensureGcSchema } from "@dwk/store";
12
+ import { resolveConfig, } from "./config";
13
+ /**
14
+ * Create the cron handler that reclaims orphaned R2 blobs. Bind it to a
15
+ * `scheduled` trigger alongside the pod Worker; it shares the same `BLOBS`
16
+ * bucket and the `GC_DB` D1 database the DO forwards orphans into.
17
+ *
18
+ * Fails loudly when the required `BLOBS` / `GC_DB` bindings are missing.
19
+ */
20
+ export function createSolidPodGc(config) {
21
+ const resolved = resolveConfig(config);
22
+ return async (_event, env, _ctx) => {
23
+ if (!env.BLOBS) {
24
+ throw new Error("@dwk/solid-pod: GC requires the `BLOBS` R2 binding");
25
+ }
26
+ if (!env.GC_DB) {
27
+ throw new Error("@dwk/solid-pod: GC requires the `GC_DB` D1 binding");
28
+ }
29
+ await ensureGcSchema(env.GC_DB);
30
+ await collectGarbage(env, { safetyWindowMs: resolved.gcSafetyWindowMs });
31
+ };
32
+ }
33
+ //# sourceMappingURL=gc.js.map
package/dist/gc.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gc.js","sourceRoot":"","sources":["../src/gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EACL,aAAa,GAGd,MAAM,UAAU,CAAC;AASlB;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAsB;IACrD,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACjC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,cAAc,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC3E,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The stateless Solid Pod front door.
3
+ *
4
+ * It authenticates DPoP-bound bearer tokens at the edge (`auth.ts`), then hands
5
+ * the request — augmented with the verified agent facts via internal headers —
6
+ * to the per-pod Durable Object, which owns all consistency, authorization, and
7
+ * notification logic. v1 runs a single Durable Object per `baseUrl` (no
8
+ * sharding). The handler is mountable under any path prefix because it routes
9
+ * purely on the request URL.
10
+ */
11
+ import { type SolidPodConfig, type SolidPodEnv } from "./config";
12
+ /** A `fetch`-compatible Worker handler. */
13
+ export type SolidPodHandler = (request: Request, env: SolidPodEnv, ctx: ExecutionContext) => Promise<Response>;
14
+ /**
15
+ * Create the stateless Solid Pod front-door handler. Writes funnel through the
16
+ * per-pod {@link SolidPodObject}; reads and notifications likewise route through
17
+ * it so the DO remains the single consistency authority.
18
+ */
19
+ export declare function createSolidPod(config: SolidPodConfig): SolidPodHandler;
20
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,WAAW,EACjB,MAAM,UAAU,CAAC;AAIlB,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,QAAQ,CAAC,CAAC;AA0IvB;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,eAAe,CA2BtE"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The stateless Solid Pod front door.
3
+ *
4
+ * It authenticates DPoP-bound bearer tokens at the edge (`auth.ts`), then hands
5
+ * the request — augmented with the verified agent facts via internal headers —
6
+ * to the per-pod Durable Object, which owns all consistency, authorization, and
7
+ * notification logic. v1 runs a single Durable Object per `baseUrl` (no
8
+ * sharding). The handler is mountable under any path prefix because it routes
9
+ * purely on the request URL.
10
+ */
11
+ import { hostFromUrl } from "@dwk/log";
12
+ import { INTERNAL_HEADERS, resolveConfig, } from "./config";
13
+ import { authenticate } from "./auth";
14
+ import { PodOutcome, SolidPodLogEvent } from "./log";
15
+ /** Client headers that are safe to forward verbatim to the Durable Object. */
16
+ const FORWARDED_HEADERS = [
17
+ "accept",
18
+ "content-type",
19
+ "content-length",
20
+ "if-match",
21
+ "if-none-match",
22
+ "slug",
23
+ "link",
24
+ "origin",
25
+ "upgrade",
26
+ "sec-websocket-key",
27
+ "sec-websocket-version",
28
+ "sec-websocket-protocol",
29
+ "sec-websocket-extensions",
30
+ ];
31
+ /** Fail loudly if a required Cloudflare binding is missing (no silent degradation). */
32
+ function assertBindings(env) {
33
+ if (!env.POD) {
34
+ throw new Error("@dwk/solid-pod: missing required Durable Object binding `POD`");
35
+ }
36
+ if (!env.BLOBS) {
37
+ throw new Error("@dwk/solid-pod: missing required R2 binding `BLOBS`");
38
+ }
39
+ }
40
+ /** Build the internal DO request, stripping any client-supplied auth headers. */
41
+ function internalRequest(request, config, auth) {
42
+ const headers = new Headers();
43
+ for (const name of FORWARDED_HEADERS) {
44
+ const value = request.headers.get(name);
45
+ if (value !== null)
46
+ headers.set(name, value);
47
+ }
48
+ // The DO trusts these headers, so they are always set by us — never passed
49
+ // through from the client.
50
+ if (auth) {
51
+ headers.set(INTERNAL_HEADERS.webid, auth.webid);
52
+ headers.set(INTERNAL_HEADERS.jti, auth.jti);
53
+ headers.set(INTERNAL_HEADERS.jkt, auth.jkt);
54
+ }
55
+ headers.set(INTERNAL_HEADERS.config, JSON.stringify({
56
+ owners: config.owners,
57
+ allowAnonymousWrites: config.allowAnonymousWrites,
58
+ ...(config.maxInlineBytes !== undefined
59
+ ? { maxInlineBytes: config.maxInlineBytes }
60
+ : {}),
61
+ }));
62
+ const method = request.method.toUpperCase();
63
+ const hasBody = method !== "GET" && method !== "HEAD";
64
+ return new Request(request.url, {
65
+ method: request.method,
66
+ headers,
67
+ ...(hasBody ? { body: request.body } : {}),
68
+ });
69
+ }
70
+ /** A `401` with the DPoP challenge and a machine-readable failure reason. */
71
+ function unauthorized(reason) {
72
+ return new Response("Unauthorized", {
73
+ status: 401,
74
+ headers: {
75
+ "content-type": "text/plain; charset=utf-8",
76
+ "www-authenticate": `DPoP realm="solid", error="invalid_token", error_description="${reason}"`,
77
+ },
78
+ });
79
+ }
80
+ /**
81
+ * Emit a structured event on both the logger and the metrics seam, which share
82
+ * one event vocabulary (see `@dwk/log`): `warn` for handled-but-notable
83
+ * rejections, `info` for normal outcomes. Honors the redaction policy — callers
84
+ * pass only reason codes, HTTP method/status, and sanitized hosts.
85
+ */
86
+ function emit(config, level, event, fields) {
87
+ config.logger[level](event, fields);
88
+ config.metrics.count(event, fields);
89
+ }
90
+ /**
91
+ * Translate the Durable Object's internal authorization-outcome header into the
92
+ * matching {@link SolidPodLogEvent} on the injected seams, then strip the header
93
+ * before the response reaches the client. The DO cannot hold the injected
94
+ * logger/metrics (they do not cross the isolate boundary), so it signals its WAC
95
+ * denial / anonymous-write refusal / replay rejection here, at the composition
96
+ * boundary, where the seams are wired. Responses without the header (including
97
+ * WebSocket upgrades) pass through untouched.
98
+ */
99
+ function logPodOutcome(config, request, response) {
100
+ const outcome = response.headers.get(INTERNAL_HEADERS.outcome);
101
+ if (!outcome)
102
+ return response;
103
+ const method = request.method;
104
+ switch (outcome) {
105
+ case PodOutcome.WacDenied:
106
+ emit(config, "warn", SolidPodLogEvent.AccessDenied, {
107
+ method,
108
+ status: response.status,
109
+ });
110
+ break;
111
+ case PodOutcome.AnonymousWriteRefused:
112
+ emit(config, "warn", SolidPodLogEvent.AnonymousWriteRefused, { method });
113
+ break;
114
+ case PodOutcome.Replay:
115
+ emit(config, "warn", SolidPodLogEvent.ReplayRejected, { method });
116
+ break;
117
+ }
118
+ const headers = new Headers(response.headers);
119
+ headers.delete(INTERNAL_HEADERS.outcome);
120
+ return new Response(response.body, {
121
+ status: response.status,
122
+ statusText: response.statusText,
123
+ headers,
124
+ });
125
+ }
126
+ /**
127
+ * Create the stateless Solid Pod front-door handler. Writes funnel through the
128
+ * per-pod {@link SolidPodObject}; reads and notifications likewise route through
129
+ * it so the DO remains the single consistency authority.
130
+ */
131
+ export function createSolidPod(config) {
132
+ const resolved = resolveConfig(config);
133
+ return async (request, env, _ctx) => {
134
+ assertBindings(env);
135
+ const result = await authenticate(request, resolved);
136
+ if (result.kind === "rejected") {
137
+ emit(resolved, "warn", SolidPodLogEvent.AuthRejected, {
138
+ reason: result.reason,
139
+ });
140
+ return unauthorized(result.reason);
141
+ }
142
+ const auth = result.kind === "authenticated" ? result.context : null;
143
+ if (auth) {
144
+ emit(resolved, "info", SolidPodLogEvent.AuthAccepted, {
145
+ agentHost: hostFromUrl(auth.webid),
146
+ });
147
+ }
148
+ const forwarded = internalRequest(request, resolved, auth);
149
+ // One Durable Object per pod, keyed by the identity root (no sharding).
150
+ const id = env.POD.idFromName(resolved.baseUrl);
151
+ const response = await env.POD.get(id).fetch(forwarded);
152
+ return logPodOutcome(resolved, request, response);
153
+ };
154
+ }
155
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@dwk/solid-pod` — edge-native Solid Pod.
3
+ *
4
+ * A stateless Worker front door over a per-pod Durable Object that is the
5
+ * consistency, authz, and notification authority, with R2 for blob bodies. This
6
+ * is the only `@dwk` package that ships a Durable Object.
7
+ *
8
+ * The package is **not** protocol-agnostic: it is the Solid Protocol endpoint,
9
+ * composing the reusable libs `@dwk/dpop` (edge DPoP validation), `@dwk/rdf`
10
+ * (Turtle/JSON-LD content negotiation), `@dwk/wac` (access control), and
11
+ * `@dwk/store` (DO-SQLite quads + R2 copy-on-write blobs). v1 is a **Resource
12
+ * Server only** (no OIDC OP) and runs **one Durable Object per pod** (no
13
+ * sharding).
14
+ *
15
+ * @see spec/packages/solid-pod.md
16
+ * @packageDocumentation
17
+ */
18
+ export { createSolidPod, type SolidPodHandler } from "./handler";
19
+ export { createSolidPodGc, type SolidPodGcHandler } from "./gc";
20
+ export { SolidPodObject } from "./pod";
21
+ export { type SolidPodConfig, type SolidPodEnv, type SolidPodGcEnv, type AuthContext, type Jwks, } from "./config";
22
+ export { SolidPodLogEvent } from "./log";
23
+ export type { Logger, Metrics } from "@dwk/log";
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,IAAI,GACV,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACzC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `@dwk/solid-pod` — edge-native Solid Pod.
3
+ *
4
+ * A stateless Worker front door over a per-pod Durable Object that is the
5
+ * consistency, authz, and notification authority, with R2 for blob bodies. This
6
+ * is the only `@dwk` package that ships a Durable Object.
7
+ *
8
+ * The package is **not** protocol-agnostic: it is the Solid Protocol endpoint,
9
+ * composing the reusable libs `@dwk/dpop` (edge DPoP validation), `@dwk/rdf`
10
+ * (Turtle/JSON-LD content negotiation), `@dwk/wac` (access control), and
11
+ * `@dwk/store` (DO-SQLite quads + R2 copy-on-write blobs). v1 is a **Resource
12
+ * Server only** (no OIDC OP) and runs **one Durable Object per pod** (no
13
+ * sharding).
14
+ *
15
+ * @see spec/packages/solid-pod.md
16
+ * @packageDocumentation
17
+ */
18
+ export { createSolidPod } from "./handler";
19
+ export { createSolidPodGc } from "./gc";
20
+ export { SolidPodObject } from "./pod";
21
+ export {} from "./config";
22
+ export { SolidPodLogEvent } from "./log";
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,cAAc,EAAwB,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAA0B,MAAM,MAAM,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAMN,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC"}
package/dist/jwt.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Minimal JWT decode and JWKS-based signature verification for the Resource
3
+ * Server edge.
4
+ *
5
+ * Solid-OIDC access tokens are asymmetrically signed by the issuer (the OP), so
6
+ * — unlike `@dwk/indieauth`'s HS256 self-issued tokens — verification needs the
7
+ * issuer's public JWKS rather than a shared secret. This module decodes the
8
+ * compact JWS, selects the key by `kid`/`kty`, and verifies the signature with
9
+ * Web Crypto. It performs no claim policy (issuer/audience/expiry) itself; that
10
+ * lives in `auth.ts`.
11
+ *
12
+ * Only asymmetric algorithms are accepted. `HS*` and `none` are refused: an
13
+ * access token must be signed by the issuer's private key, never a symmetric
14
+ * secret an RS could not safely hold.
15
+ */
16
+ /** Asymmetric JOSE algorithms accepted for issuer-signed access tokens. */
17
+ export type JwtAlgorithm = "ES256" | "ES384" | "RS256" | "PS256";
18
+ /** A decoded JWT: its header, claims, and the raw segments for verification. */
19
+ export interface DecodedJwt {
20
+ readonly header: Record<string, unknown>;
21
+ readonly payload: Record<string, unknown>;
22
+ readonly signingInput: string;
23
+ readonly signature: Uint8Array;
24
+ }
25
+ /**
26
+ * Decode a compact JWS into its header, payload, and signature. Returns `null`
27
+ * for anything that is not three base64url segments with JSON header/payload.
28
+ */
29
+ export declare function decodeJwt(token: string): DecodedJwt | null;
30
+ /**
31
+ * Verify `decoded`'s signature against a JWKS. Selects the verification key by
32
+ * `kid` when the header carries one, otherwise tries every key compatible with
33
+ * the header `alg`. Returns `true` on the first key that verifies.
34
+ */
35
+ export declare function verifyJwtSignature(decoded: DecodedJwt, jwks: readonly JsonWebKey[]): Promise<boolean>;
36
+ //# sourceMappingURL=jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,2EAA2E;AAC3E,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AA+CjE,gFAAgF;AAChF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;CAChC;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAsB1D;AASD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,SAAS,UAAU,EAAE,GAC1B,OAAO,CAAC,OAAO,CAAC,CA4ClB"}
package/dist/jwt.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Minimal JWT decode and JWKS-based signature verification for the Resource
3
+ * Server edge.
4
+ *
5
+ * Solid-OIDC access tokens are asymmetrically signed by the issuer (the OP), so
6
+ * — unlike `@dwk/indieauth`'s HS256 self-issued tokens — verification needs the
7
+ * issuer's public JWKS rather than a shared secret. This module decodes the
8
+ * compact JWS, selects the key by `kid`/`kty`, and verifies the signature with
9
+ * Web Crypto. It performs no claim policy (issuer/audience/expiry) itself; that
10
+ * lives in `auth.ts`.
11
+ *
12
+ * Only asymmetric algorithms are accepted. `HS*` and `none` are refused: an
13
+ * access token must be signed by the issuer's private key, never a symmetric
14
+ * secret an RS could not safely hold.
15
+ */
16
+ import { base64urlToBytes, base64urlToText } from "./encoding";
17
+ // We avoid the DOM lib's algorithm-parameter type names (not declared by
18
+ // @cloudflare/workers-types); these objects are accepted structurally by
19
+ // crypto.subtle.importKey / verify, mirroring the approach in `@dwk/dpop`.
20
+ const ALGS = {
21
+ ES256: {
22
+ kty: "EC",
23
+ crv: "P-256",
24
+ importParams: { name: "ECDSA", namedCurve: "P-256" },
25
+ verifyParams: { name: "ECDSA", hash: "SHA-256" },
26
+ },
27
+ ES384: {
28
+ kty: "EC",
29
+ crv: "P-384",
30
+ importParams: { name: "ECDSA", namedCurve: "P-384" },
31
+ verifyParams: { name: "ECDSA", hash: "SHA-384" },
32
+ },
33
+ RS256: {
34
+ kty: "RSA",
35
+ importParams: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
36
+ verifyParams: { name: "RSASSA-PKCS1-v1_5" },
37
+ },
38
+ PS256: {
39
+ kty: "RSA",
40
+ importParams: { name: "RSA-PSS", hash: "SHA-256" },
41
+ verifyParams: { name: "RSA-PSS", saltLength: 32 },
42
+ },
43
+ };
44
+ function isObject(value) {
45
+ return typeof value === "object" && value !== null && !Array.isArray(value);
46
+ }
47
+ /**
48
+ * Decode a compact JWS into its header, payload, and signature. Returns `null`
49
+ * for anything that is not three base64url segments with JSON header/payload.
50
+ */
51
+ export function decodeJwt(token) {
52
+ if (typeof token !== "string")
53
+ return null;
54
+ const segments = token.split(".");
55
+ if (segments.length !== 3)
56
+ return null;
57
+ const [headerSeg, payloadSeg, signatureSeg] = segments;
58
+ try {
59
+ const header = JSON.parse(base64urlToText(headerSeg));
60
+ const payload = JSON.parse(base64urlToText(payloadSeg));
61
+ if (!isObject(header) || !isObject(payload))
62
+ return null;
63
+ return {
64
+ header,
65
+ payload,
66
+ signingInput: `${headerSeg}.${payloadSeg}`,
67
+ signature: base64urlToBytes(signatureSeg),
68
+ };
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ /** Whether a JWK could verify a signature under the given algorithm. */
75
+ function keyMatchesAlg(jwk, spec) {
76
+ if (jwk.kty !== spec.kty)
77
+ return false;
78
+ if (spec.crv !== undefined && jwk.crv !== spec.crv)
79
+ return false;
80
+ return true;
81
+ }
82
+ /**
83
+ * Verify `decoded`'s signature against a JWKS. Selects the verification key by
84
+ * `kid` when the header carries one, otherwise tries every key compatible with
85
+ * the header `alg`. Returns `true` on the first key that verifies.
86
+ */
87
+ export async function verifyJwtSignature(decoded, jwks) {
88
+ const alg = decoded.header.alg;
89
+ const spec = typeof alg === "string" ? ALGS[alg] : undefined;
90
+ if (!spec)
91
+ return false;
92
+ const kid = decoded.header.kid;
93
+ const compatible = jwks.filter((jwk) => keyMatchesAlg(jwk, spec));
94
+ // When the token names a `kid`, pin verification to the key(s) carrying it.
95
+ // A token that names a `kid` matching no JWKS key is rejected rather than
96
+ // verified against unrelated keys: trying other keys would silently weaken
97
+ // `kid` pinning (a token claiming an unknown key still passing). Only when
98
+ // the header omits `kid` do we try every alg-compatible key.
99
+ let candidates = compatible;
100
+ if (typeof kid === "string") {
101
+ // `kid` is not in the standard JsonWebKey type but is carried by JWKS keys.
102
+ candidates = compatible.filter((jwk) => jwk.kid === kid);
103
+ if (candidates.length === 0)
104
+ return false;
105
+ }
106
+ for (const jwk of candidates) {
107
+ try {
108
+ const key = await crypto.subtle.importKey("jwk", jwk, spec.importParams, false, ["verify"]);
109
+ const ok = await crypto.subtle.verify(spec.verifyParams, key, decoded.signature, new TextEncoder().encode(decoded.signingInput));
110
+ if (ok)
111
+ return true;
112
+ }
113
+ catch {
114
+ // A key that fails to import (wrong shape) simply does not match; try
115
+ // the next candidate rather than aborting the whole verification.
116
+ }
117
+ }
118
+ return false;
119
+ }
120
+ //# sourceMappingURL=jwt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAsB/D,yEAAyE;AACzE,yEAAyE;AACzE,2EAA2E;AAC3E,MAAM,IAAI,GAAkC;IAC1C,KAAK,EAAE;QACL,GAAG,EAAE,IAAI;QACT,GAAG,EAAE,OAAO;QACZ,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE;QACpD,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;KACjD;IACD,KAAK,EAAE;QACL,GAAG,EAAE,IAAI;QACT,GAAG,EAAE,OAAO;QACZ,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE;QACpD,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;KACjD;IACD,KAAK,EAAE;QACL,GAAG,EAAE,KAAK;QACV,YAAY,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,SAAS,EAAE;QAC5D,YAAY,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE;KAC5C;IACD,KAAK,EAAE;QACL,GAAG,EAAE,KAAK;QACV,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;QAClD,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,EAAE;KAClD;CACF,CAAC;AAUF,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,YAAY,CAAC,GAAG,QAI7C,CAAC;IACF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,CAAY,CAAC;QACjE,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,UAAU,CAAC,CAAY,CAAC;QACnE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QACzD,OAAO;YACL,MAAM;YACN,OAAO;YACP,YAAY,EAAE,GAAG,SAAS,IAAI,UAAU,EAAE;YAC1C,SAAS,EAAE,gBAAgB,CAAC,YAAY,CAAC;SAC1C,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,wEAAwE;AACxE,SAAS,aAAa,CAAC,GAAe,EAAE,IAAa;IACnD,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAC;IACjE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAmB,EACnB,IAA2B;IAE3B,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;IAC/B,MAAM,IAAI,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAmB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7E,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IAExB,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;IAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;IAElE,4EAA4E;IAC5E,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,6DAA6D;IAC7D,IAAI,UAAU,GAAG,UAAU,CAAC;IAC5B,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,4EAA4E;QAC5E,UAAU,GAAG,UAAU,CAAC,MAAM,CAC5B,CAAC,GAAG,EAAE,EAAE,CAAE,GAAyB,CAAC,GAAG,KAAK,GAAG,CAChD,CAAC;QACF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;IAC5C,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EACL,GAAG,EACH,IAAI,CAAC,YAAY,EACjB,KAAK,EACL,CAAC,QAAQ,CAAC,CACX,CAAC;YACF,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CACnC,IAAI,CAAC,YAAY,EACjB,GAAG,EACH,OAAO,CAAC,SAAS,EACjB,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAC/C,CAAC;YACF,IAAI,EAAE;gBAAE,OAAO,IAAI,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;YACtE,kEAAkE;QACpE,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/dist/ldp.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Path and IRI helpers for the LDP resource/container model.
3
+ *
4
+ * Resources are keyed in the store by their URL pathname. Containers are the
5
+ * keys that end in `/`; everything else is a plain resource. ACL documents
6
+ * follow the Solid convention `<resource>.acl` (and `<container>.acl`). These
7
+ * helpers are pure string math so they unit-test without a Workers runtime.
8
+ */
9
+ /** Whether a store key denotes an LDP container (trailing slash). */
10
+ export declare function isContainer(path: string): boolean;
11
+ /** Whether a path is an ACL document (`*.acl`). */
12
+ export declare function isAclPath(path: string): boolean;
13
+ /** Whether a name/path ends in a reserved auxiliary suffix (`.acl`/`.meta`). */
14
+ export declare function hasReservedAuxiliarySuffix(value: string): boolean;
15
+ /** The ACL document path that governs `path` (`<path>.acl`). */
16
+ export declare function aclPath(path: string): string;
17
+ /** The resource an ACL document governs (`<resource>.acl` → `<resource>`). */
18
+ export declare function resourceForAcl(aclPathValue: string): string;
19
+ /**
20
+ * The immediate parent container of `path`, or `null` for the root container.
21
+ * `"/a/b"` → `"/a/"`, `"/a/b/"` → `"/a/"`, `"/"` → `null`.
22
+ */
23
+ export declare function parentContainer(path: string): string | null;
24
+ /**
25
+ * The ancestor containers of `path`, nearest first, up to and including the
26
+ * root container `"/"`.
27
+ */
28
+ export declare function ancestorContainers(path: string): string[];
29
+ /** Resolve a store key to its absolute resource IRI under `origin`. */
30
+ export declare function toIri(origin: string, path: string): string;
31
+ /**
32
+ * Pick a child key for a `POST` to `container`, honoring a client `Slug` when
33
+ * present and safe, and falling back to a random name. A trailing slash is
34
+ * appended when the new resource is itself a container.
35
+ */
36
+ export declare function childKey(container: string, slug: string | null, asContainer: boolean): string;
37
+ //# sourceMappingURL=ldp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ldp.d.ts","sourceRoot":"","sources":["../src/ldp.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C;AAUD,gFAAgF;AAChF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAQzD;AAED,uEAAuE;AACvE,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAID;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,WAAW,EAAE,OAAO,GACnB,MAAM,CAeR"}
package/dist/ldp.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Path and IRI helpers for the LDP resource/container model.
3
+ *
4
+ * Resources are keyed in the store by their URL pathname. Containers are the
5
+ * keys that end in `/`; everything else is a plain resource. ACL documents
6
+ * follow the Solid convention `<resource>.acl` (and `<container>.acl`). These
7
+ * helpers are pure string math so they unit-test without a Workers runtime.
8
+ */
9
+ /** Whether a store key denotes an LDP container (trailing slash). */
10
+ export function isContainer(path) {
11
+ return path.endsWith("/");
12
+ }
13
+ /** Whether a path is an ACL document (`*.acl`). */
14
+ export function isAclPath(path) {
15
+ return path.endsWith(".acl");
16
+ }
17
+ /**
18
+ * Suffixes of server-governed auxiliary resources. Writing one requires
19
+ * `acl:Control` on the resource it governs (WAC), so a client `Slug` must
20
+ * never be allowed to mint one through a container `POST` (which is only
21
+ * authorized for `Append`/`Write` on the parent).
22
+ */
23
+ const RESERVED_AUXILIARY_SUFFIXES = [".acl", ".meta"];
24
+ /** Whether a name/path ends in a reserved auxiliary suffix (`.acl`/`.meta`). */
25
+ export function hasReservedAuxiliarySuffix(value) {
26
+ return RESERVED_AUXILIARY_SUFFIXES.some((suffix) => value.endsWith(suffix));
27
+ }
28
+ /** The ACL document path that governs `path` (`<path>.acl`). */
29
+ export function aclPath(path) {
30
+ return `${path}.acl`;
31
+ }
32
+ /** The resource an ACL document governs (`<resource>.acl` → `<resource>`). */
33
+ export function resourceForAcl(aclPathValue) {
34
+ return aclPathValue.slice(0, -".acl".length);
35
+ }
36
+ /**
37
+ * The immediate parent container of `path`, or `null` for the root container.
38
+ * `"/a/b"` → `"/a/"`, `"/a/b/"` → `"/a/"`, `"/"` → `null`.
39
+ */
40
+ export function parentContainer(path) {
41
+ if (path === "/")
42
+ return null;
43
+ const trimmed = isContainer(path) ? path.slice(0, -1) : path;
44
+ const slash = trimmed.lastIndexOf("/");
45
+ return slash < 0 ? "/" : trimmed.slice(0, slash + 1);
46
+ }
47
+ /**
48
+ * The ancestor containers of `path`, nearest first, up to and including the
49
+ * root container `"/"`.
50
+ */
51
+ export function ancestorContainers(path) {
52
+ const ancestors = [];
53
+ let current = parentContainer(path);
54
+ while (current !== null) {
55
+ ancestors.push(current);
56
+ current = parentContainer(current);
57
+ }
58
+ return ancestors;
59
+ }
60
+ /** Resolve a store key to its absolute resource IRI under `origin`. */
61
+ export function toIri(origin, path) {
62
+ return `${origin}${path}`;
63
+ }
64
+ const SLUG_UNSAFE = /[^A-Za-z0-9._~-]+/g;
65
+ /**
66
+ * Pick a child key for a `POST` to `container`, honoring a client `Slug` when
67
+ * present and safe, and falling back to a random name. A trailing slash is
68
+ * appended when the new resource is itself a container.
69
+ */
70
+ export function childKey(container, slug, asContainer) {
71
+ const cleaned = slug
72
+ ?.trim()
73
+ .replace(SLUG_UNSAFE, "-")
74
+ .replace(/^-+|-+$/g, "");
75
+ // A Slug must never mint a reserved auxiliary resource (`.acl`/`.meta`):
76
+ // those govern a sibling's access/metadata and require `acl:Control` to
77
+ // write, which a container POST (authorized only for Append/Write on the
78
+ // parent) does not confer. Treat such a Slug as unusable and fall back to a
79
+ // random name, closing a privilege-escalation path (issue #28).
80
+ const name = cleaned && cleaned.length > 0 && !hasReservedAuxiliarySuffix(cleaned)
81
+ ? cleaned
82
+ : crypto.randomUUID();
83
+ return `${container}${name}${asContainer ? "/" : ""}`;
84
+ }
85
+ //# sourceMappingURL=ldp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ldp.js","sourceRoot":"","sources":["../src/ldp.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,qEAAqE;AACrE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;GAKG;AACH,MAAM,2BAA2B,GAAG,CAAC,MAAM,EAAE,OAAO,CAAU,CAAC;AAE/D,gFAAgF;AAChF,MAAM,UAAU,0BAA0B,CAAC,KAAa;IACtD,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,GAAG,IAAI,MAAM,CAAC;AACvB,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,YAAoB;IACjD,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACvC,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO,OAAO,KAAK,IAAI,EAAE,CAAC;QACxB,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxB,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,KAAK,CAAC,MAAc,EAAE,IAAY;IAChD,OAAO,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED,MAAM,WAAW,GAAG,oBAAoB,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CACtB,SAAiB,EACjB,IAAmB,EACnB,WAAoB;IAEpB,MAAM,OAAO,GAAG,IAAI;QAClB,EAAE,IAAI,EAAE;SACP,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC;SACzB,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC3B,yEAAyE;IACzE,wEAAwE;IACxE,yEAAyE;IACzE,4EAA4E;IAC5E,gEAAgE;IAChE,MAAM,IAAI,GACR,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,0BAA0B,CAAC,OAAO,CAAC;QACnE,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IAC1B,OAAO,GAAG,SAAS,GAAG,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AACxD,CAAC"}