@synap-core/sdk 1.3.0 → 1.5.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 (51) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +212 -12
  3. package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
  4. package/dist/chunk-UK2U6SMA.js.map +1 -0
  5. package/dist/{client-CVUT1Mcf.d.cts → client-B50KC2aJ.d.cts} +14 -9
  6. package/dist/{client-CVUT1Mcf.d.ts → client-B50KC2aJ.d.ts} +14 -9
  7. package/dist/connect.cjs +790 -161
  8. package/dist/connect.cjs.map +1 -1
  9. package/dist/connect.d.cts +610 -37
  10. package/dist/connect.d.ts +610 -37
  11. package/dist/connect.js +777 -162
  12. package/dist/connect.js.map +1 -1
  13. package/dist/public.cjs +9 -2
  14. package/dist/public.cjs.map +1 -1
  15. package/dist/public.d.cts +19 -4
  16. package/dist/public.d.ts +19 -4
  17. package/dist/public.js +8 -2
  18. package/dist/public.js.map +1 -1
  19. package/dist/testing.cjs +366 -17
  20. package/dist/testing.cjs.map +1 -1
  21. package/dist/testing.d.cts +190 -7
  22. package/dist/testing.d.ts +190 -7
  23. package/dist/testing.js +358 -17
  24. package/dist/testing.js.map +1 -1
  25. package/dist/{types-DpqPNaaa.d.cts → types-DZYNBCq7.d.cts} +47 -5
  26. package/dist/{types-DpqPNaaa.d.ts → types-DZYNBCq7.d.ts} +47 -5
  27. package/package.json +36 -12
  28. package/src/connect/auth.ts +314 -55
  29. package/src/connect/client.ts +472 -26
  30. package/src/connect/cloud.ts +210 -0
  31. package/src/connect/discovery.ts +79 -44
  32. package/src/connect/encoding.ts +1 -1
  33. package/src/connect/errors.ts +8 -1
  34. package/src/connect/flow.ts +80 -12
  35. package/src/connect/http.ts +42 -0
  36. package/src/connect/hub-types.ts +254 -0
  37. package/src/connect/index.ts +69 -23
  38. package/src/connect/rules.ts +18 -0
  39. package/src/connect/seal.ts +1 -1
  40. package/src/connect/store.ts +50 -8
  41. package/src/connect/types.ts +48 -4
  42. package/src/public/client.ts +14 -2
  43. package/src/public/safe-markdown.ts +13 -0
  44. package/src/public/share-model.ts +6 -2
  45. package/src/public/site-model.ts +5 -3
  46. package/src/testing/auth-server.ts +377 -20
  47. package/src/testing/cloud.ts +83 -0
  48. package/src/testing/fixtures.ts +26 -0
  49. package/src/testing/index.ts +10 -6
  50. package/src/testing/seed.ts +120 -0
  51. package/dist/chunk-J7XHAAFJ.js.map +0 -1
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A mock Synap Cloud pod chooser (Control Plane `GET /connect/pod`)
3
+ * for "Continue with Synap Cloud" tests and a kit's mock mode. No network:
4
+ * `choose()` stands in for the person signing in to the CP and picking a pod.
5
+ *
6
+ * Wire behaviour mirrors the CP route: it accepts only
7
+ * `client_id` (an https CIMD URL; http on loopback only with
8
+ * `allowInsecureLoopback`, for a kit's mock mode on http://localhost) and
9
+ * `state` (`^[A-Za-z0-9._~-]{16,256}$`),
10
+ * takes the return URL from the app's client metadata (`initiate_login_uri`),
11
+ * never from the query, and answers `<initiate_login_uri>?iss=<pod issuer>&state=<state>`.
12
+ * It never sees a code or a token.
13
+ *
14
+ * const cloud = createMockCloud();
15
+ * createPodAppAuth({ ...config, cloudUrl: cloud.url });
16
+ * cloud.choose(location, { pod: mockPod.issuer, metadata: auth.metadata }) → the app's return URL
17
+ */
18
+ import { CLOUD_STATE_RE as STATE_RE, LOOPBACK_HOSTS } from "../connect/rules";
19
+ import type { ClientMetadataDocument } from "../connect/types";
20
+
21
+ /** A placeholder Control Plane origin; never contacted. */
22
+ export const MOCK_CLOUD_URL = "https://cp.mock-synap.test";
23
+
24
+
25
+ export interface MockCloud {
26
+ url: string;
27
+ /** The chooser URLs the app sent browsers to. */
28
+ requests: string[];
29
+ /**
30
+ * The person picks `pod` on the chooser: validates the request like the CP,
31
+ * returns where the CP redirects the browser. Throws on a request the CP
32
+ * refuses (shown as a page there, never redirected).
33
+ */
34
+ choose(location: string, opts: { pod: string; metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri"> }): string;
35
+ /** The person cancels on the chooser: `<initiate_login_uri>?state=…&error=access_denied`, no `iss`. */
36
+ cancel(location: string, opts: { metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri"> }): string;
37
+ }
38
+
39
+ export function createMockCloud(options: { url?: string; allowInsecureLoopback?: boolean } = {}): MockCloud {
40
+ const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
41
+ const requests: string[] = [];
42
+ return {
43
+ url: base,
44
+ requests,
45
+ choose(location, { pod, metadata }) {
46
+ const back = check(location, metadata);
47
+ back.searchParams.set("iss", pod);
48
+ back.searchParams.set("state", new URL(location).searchParams.get("state")!);
49
+ return back.href;
50
+ },
51
+ cancel(location, { metadata }) {
52
+ const back = check(location, metadata);
53
+ back.searchParams.set("state", new URL(location).searchParams.get("state")!);
54
+ back.searchParams.set("error", "access_denied");
55
+ return back.href;
56
+ },
57
+ };
58
+
59
+ /** Validates like the CP (an HTML error there, never a redirect); returns the app's initiate_login_uri. */
60
+ function check(location: string, metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">): URL {
61
+ requests.push(location);
62
+ const url = new URL(location);
63
+ if (`${url.origin}${url.pathname}` !== `${base}/connect/pod`) throw new Error(`Not this CP's /connect/pod: ${location}`);
64
+ const clientId = url.searchParams.get("client_id");
65
+ const state = url.searchParams.get("state");
66
+ if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
67
+ if (!state || !STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
68
+ if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
69
+ return new URL(metadata.initiate_login_uri);
70
+ }
71
+
72
+ /** https; http only on a loopback host, and only with `allowInsecureLoopback`. */
73
+ function cimdUrlAccepted(clientId: string): boolean {
74
+ let url: URL;
75
+ try {
76
+ url = new URL(clientId);
77
+ } catch {
78
+ return false;
79
+ }
80
+ if (url.protocol === "https:") return true;
81
+ return url.protocol === "http:" && options.allowInsecureLoopback === true && LOOPBACK_HOSTS.has(url.hostname);
82
+ }
83
+ }
@@ -44,6 +44,32 @@ export const MOCK_SHARE: PublicShare = {
44
44
  publishedOn: "2026-09-27",
45
45
  };
46
46
 
47
+ /**
48
+ * A published HTML document (an estimate), as the pod serves an `html` body: a
49
+ * page renders it only inside `<iframe sandbox="" srcdoc>`, so the script below
50
+ * never runs.
51
+ */
52
+ export const MOCK_HTML_SHARE: PublicShare = {
53
+ resourceType: "entity",
54
+ title: "Estimate for Northwind",
55
+ properties: { total: "4 800 €", validUntil: "2026-11-30" },
56
+ body: {
57
+ format: "html",
58
+ content: [
59
+ "<!doctype html>",
60
+ '<html lang="en"><head><meta charset="utf-8"><title>Estimate</title>',
61
+ "<style>body{font-family:Georgia,serif;max-width:40rem;margin:2rem auto;line-height:1.5}td{padding:.25rem .75rem}</style>",
62
+ "</head><body>",
63
+ "<h1>Estimate for Northwind</h1>",
64
+ "<table><tr><td>Discovery workshop</td><td>1 200 €</td></tr><tr><td>Build</td><td>3 600 €</td></tr></table>",
65
+ "<p><strong>Total: 4 800 €</strong></p>",
66
+ "<script>document.body.textContent = 'scripts never run in a sandboxed frame'</script>",
67
+ "</body></html>",
68
+ ].join(""),
69
+ },
70
+ publishedOn: "2026-10-22",
71
+ };
72
+
47
73
  /** One collection of a mock site: its door config and its live records. */
48
74
  export interface MockCollection {
49
75
  /** Default sort keys (`-` = descending); also the sort keys the door accepts. */
@@ -12,12 +12,12 @@
12
12
  * `credentials: "include"` request is refused (it rejects, as a CORS failure
13
13
  * does); with `{ browser: true }` a non-safelisted request header is too.
14
14
  *
15
- * tokens MOCK_TOKENS.form / .share / .site the demo form, share and site
15
+ * tokens MOCK_TOKENS.form / .share / .htmlShare / .site the demo form, shares (markdown, html) and site
16
16
  * tokens MOCK_FAILURE_TOKENS.* one failure on every door
17
17
  * anything else the uniform 404
18
18
  *
19
19
  * Pod-app sign-in (`@synap-core/sdk/connect`): `createMockAuthServer()`, see
20
- * ./auth-server.ts.
20
+ * ./auth-server.ts, seeded from one JSON shape (`parseMockPodSeed`, ./seed.ts); Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
21
21
  */
22
22
  import type {
23
23
  PublicCollectionPage,
@@ -27,17 +27,21 @@ import type {
27
27
  PublicShare,
28
28
  PublicSite,
29
29
  } from "../public/client";
30
- import { MOCK_FORM, MOCK_SHARE, MOCK_SITE, type MockSite } from "./fixtures";
30
+ import { MOCK_FORM, MOCK_HTML_SHARE, MOCK_SHARE, MOCK_SITE, type MockSite } from "./fixtures";
31
31
 
32
- export { MOCK_FORM, MOCK_SHARE, MOCK_SITE };
32
+ export { MOCK_FORM, MOCK_HTML_SHARE, MOCK_SHARE, MOCK_SITE };
33
33
  export type { MockCollection, MockSite } from "./fixtures";
34
34
  // Pod-app sign-in: a mock authorization server + Hub REST doors.
35
35
  export * from "./auth-server";
36
+ // The mock pod's data as one JSON shape (1.5).
37
+ export * from "./seed";
38
+ // Synap Cloud: a mock Control Plane pod chooser.
39
+ export * from "./cloud";
36
40
 
37
41
  /** A placeholder origin; never contacted. */
38
42
  export const MOCK_POD_URL = "https://mock.synap.invalid";
39
43
 
40
- export const MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" } as const;
44
+ export const MOCK_TOKENS = { form: "demo-form", share: "demo", htmlShare: "demo-html", site: "demo-site" } as const;
41
45
 
42
46
  /** The `Retry-After` the `rateLimitedRetryAfter` token sends. */
43
47
  export const MOCK_RETRY_AFTER_SEC = 30;
@@ -287,7 +291,7 @@ function invalidKeys(form: PublicForm, body: unknown): string[] {
287
291
 
288
292
  export function createMockPod(options: MockPodOptions = {}): MockPod {
289
293
  const forms = options.forms ?? { [MOCK_TOKENS.form]: MOCK_FORM };
290
- const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE };
294
+ const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE, [MOCK_TOKENS.htmlShare]: MOCK_HTML_SHARE };
291
295
  const sites = options.sites ?? { [MOCK_TOKENS.site]: MOCK_SITE };
292
296
  const requests: MockRequest[] = [];
293
297
  const submissions: MockSubmission[] = [];
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The mock pod's data as ONE JSON-serialisable shape, so an app's mock mode,
3
+ * smoke and screenshots replay a recorded snapshot (one file) instead of
4
+ * fixtures built in app code:
5
+ *
6
+ * import { readFileSync } from "node:fs";
7
+ * const seed = parseMockPodSeed(readFileSync("mock/pod-seed.json", "utf8"));
8
+ * const pod = createMockAuthServer({ seed });
9
+ *
10
+ * Rows are the pod's wire shapes (see `@synap-core/sdk/connect` hub-types):
11
+ * `entities` as `GET /entities` answers them, `sessions` as `GET
12
+ * /focus-sessions` rows (missing columns get the pod's defaults), `documents`
13
+ * as `GET /documents/:id` (`sections` and `diagnostics` answered empty),
14
+ * `relations` as graph edges for `GET /entities/:id/connections`, `proposals`
15
+ * as `GET /proposals?view=basic` rows, `spaces` name each granted space's
16
+ * template (`synap_grant[].templateSlug` on the token response).
17
+ */
18
+ import type { MockProposal } from "./auth-server";
19
+
20
+ export interface MockSeedEntity {
21
+ id: string;
22
+ profileSlug?: string;
23
+ workspaceId?: string | null;
24
+ title?: string | null;
25
+ properties?: Record<string, unknown>;
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ export interface MockSeedSession {
30
+ id: string;
31
+ workspaceId?: string | null;
32
+ projectId?: string | null;
33
+ goal?: string;
34
+ status?: string;
35
+ /** Slots; `owner: "human"` ones are what waits on the person. */
36
+ expectedOutputs?: Array<Record<string, unknown>>;
37
+ /** The entity it is about: listed among that entity's connections. */
38
+ subjectEntityId?: string | null;
39
+ /** ISO 8601; the list is newest first by it. */
40
+ startedAt?: string;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export interface MockSeedDocument {
45
+ id: string;
46
+ title: string;
47
+ content: string;
48
+ type?: string;
49
+ language?: string | null;
50
+ workspaceId?: string | null;
51
+ revision?: number;
52
+ version?: number;
53
+ createdAt?: string;
54
+ updatedAt?: string;
55
+ }
56
+
57
+ export interface MockSeedRelation {
58
+ id: string;
59
+ sourceEntityId: string;
60
+ targetEntityId: string;
61
+ type: string;
62
+ createdAt?: string;
63
+ }
64
+
65
+ export interface MockSeedSpace {
66
+ id: string;
67
+ name: string;
68
+ /** The template it came from (e.g. `content-os`); absent = made by hand. */
69
+ templateSlug?: string;
70
+ }
71
+
72
+ export interface MockPodSeed {
73
+ entities?: MockSeedEntity[];
74
+ sessions?: MockSeedSession[];
75
+ documents?: MockSeedDocument[];
76
+ relations?: MockSeedRelation[];
77
+ /** Missing fields default to a pending `create` of an `entity`, with null ids and the seed's load time. */
78
+ proposals?: Array<Partial<MockProposal> & { id: string }>;
79
+ spaces?: MockSeedSpace[];
80
+ }
81
+
82
+ const KEYS = ["entities", "sessions", "documents", "relations", "proposals", "spaces"] as const;
83
+
84
+ /** Required string fields per collection (the rest is free-form, as the pod's rows). */
85
+ const REQUIRED: Record<(typeof KEYS)[number], readonly string[]> = {
86
+ entities: ["id"],
87
+ sessions: ["id"],
88
+ documents: ["id", "title", "content"],
89
+ relations: ["id", "sourceEntityId", "targetEntityId", "type"],
90
+ proposals: ["id"],
91
+ spaces: ["id", "name"],
92
+ };
93
+
94
+ /**
95
+ * Read a seed (JSON text or an already-parsed value) and check its shape:
96
+ * every collection optional, each an array of objects carrying its required
97
+ * string fields; unknown top-level keys are refused (a typo would otherwise
98
+ * seed nothing). Throws `TypeError` naming the first bad path.
99
+ */
100
+ export function parseMockPodSeed(input: string | unknown): MockPodSeed {
101
+ const value: unknown = typeof input === "string" ? JSON.parse(input) : input;
102
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("seed: expected an object");
103
+ const obj = value as Record<string, unknown>;
104
+ for (const key of Object.keys(obj)) {
105
+ if (!(KEYS as readonly string[]).includes(key)) throw new TypeError(`seed.${key}: unknown collection (expected ${KEYS.join(", ")})`);
106
+ }
107
+ for (const key of KEYS) {
108
+ const rows = obj[key];
109
+ if (rows === undefined) continue;
110
+ if (!Array.isArray(rows)) throw new TypeError(`seed.${key}: expected an array`);
111
+ rows.forEach((row, i) => {
112
+ if (!row || typeof row !== "object" || Array.isArray(row)) throw new TypeError(`seed.${key}[${i}]: expected an object`);
113
+ for (const field of REQUIRED[key]) {
114
+ if (typeof (row as Record<string, unknown>)[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
115
+ }
116
+ });
117
+ }
118
+ // A JSON round trip: what the mock serves is what a file could hold.
119
+ return JSON.parse(JSON.stringify(obj)) as MockPodSeed;
120
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/connect/encoding.ts"],"sourcesContent":["/**\n * Web-standard primitives (WebCrypto, TextEncoder, btoa/atob): the same code\n * runs on Node 18+, edge runtimes and workers. No Node import.\n */\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/** `Uint8Array<ArrayBuffer>`: what WebCrypto's `BufferSource` takes under TS 5.7+ lib types. */\nexport type Bytes = Uint8Array<ArrayBuffer>;\n\nexport function utf8(text: string): Bytes {\n return encoder.encode(text);\n}\n\nexport function fromUtf8(bytes: Uint8Array): string {\n return decoder.decode(bytes);\n}\n\nexport function base64url(bytes: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** Null when `text` is not base64url. */\nexport function fromBase64url(text: string): Bytes | null {\n if (!/^[A-Za-z0-9_-]*$/.test(text)) return null;\n const padded = text.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((text.length + 3) % 4);\n let binary: string;\n try {\n binary = atob(padded);\n } catch {\n return null;\n }\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n\nexport function randomBytes(length: number): Bytes {\n const out = new Uint8Array(length);\n crypto.getRandomValues(out);\n return out;\n}\n\n/** 32 random bytes, base64url (43 chars): a state value or a PKCE verifier. */\nexport function randomToken(): string {\n return base64url(randomBytes(32));\n}\n\n/** RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(code_verifier)))`. */\nexport async function pkceChallenge(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", utf8(verifier));\n return base64url(new Uint8Array(digest));\n}\n\n/** Compare two strings without an early exit on the first differing byte. */\nexport function constantTimeEqual(a: string, b: string): boolean {\n const x = utf8(a);\n const y = utf8(b);\n let diff = x.length ^ y.length;\n for (let i = 0; i < Math.max(x.length, y.length); i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0);\n return diff === 0;\n}\n\n/** The request's cookies by name (the first value wins, as browsers order the most specific first). */\nexport function parseCookies(header: string | null): Map<string, string> {\n const out = new Map<string, string>();\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n const name = part.slice(0, eq).trim();\n if (!name || out.has(name)) continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"')) value = value.slice(1, -1);\n out.set(name, value);\n }\n return out;\n}\n\nexport interface CookieAttributes {\n maxAgeSec: number;\n secure: boolean;\n}\n\n/** `HttpOnly; SameSite=Lax; Path=/` always; `Secure` on https. Never a `Domain`. */\nexport function serializeCookie(name: string, value: string, attrs: CookieAttributes): string {\n const parts = [`${name}=${value}`, \"Path=/\", \"HttpOnly\", \"SameSite=Lax\", `Max-Age=${Math.max(0, Math.floor(attrs.maxAgeSec))}`];\n if (attrs.secure) parts.push(\"Secure\");\n return parts.join(\"; \");\n}\n"],"mappings":";AAKA,IAAM,UAAU,IAAI,YAAY;AAChC,IAAM,UAAU,IAAI,YAAY;AAKzB,SAAS,KAAK,MAAqB;AACxC,SAAO,QAAQ,OAAO,IAAI;AAC5B;AAEO,SAAS,SAAS,OAA2B;AAClD,SAAO,QAAQ,OAAO,KAAK;AAC7B;AAEO,SAAS,UAAU,OAA2B;AACnD,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,WAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAC7E,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC/E;AAGO,SAAS,cAAc,MAA4B;AACxD,MAAI,CAAC,mBAAmB,KAAK,IAAI,EAAG,QAAO;AAC3C,QAAM,SAAS,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,IAAI,MAAM,OAAO,KAAK,SAAS,KAAK,CAAC;AAC7F,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,MAAM,IAAI,WAAW,OAAO,MAAM;AACxC,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,KAAI,CAAC,IAAI,OAAO,WAAW,CAAC;AACpE,SAAO;AACT;AAEO,SAAS,YAAY,QAAuB;AACjD,QAAM,MAAM,IAAI,WAAW,MAAM;AACjC,SAAO,gBAAgB,GAAG;AAC1B,SAAO;AACT;AAGO,SAAS,cAAsB;AACpC,SAAO,UAAU,YAAY,EAAE,CAAC;AAClC;AAGA,eAAsB,cAAc,UAAmC;AACrE,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK,QAAQ,CAAC;AACnE,SAAO,UAAU,IAAI,WAAW,MAAM,CAAC;AACzC;AAGO,SAAS,kBAAkB,GAAW,GAAoB;AAC/D,QAAM,IAAI,KAAK,CAAC;AAChB,QAAM,IAAI,KAAK,CAAC;AAChB,MAAI,OAAO,EAAE,SAAS,EAAE;AACxB,WAAS,IAAI,GAAG,IAAI,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAK,UAAS,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,KAAK;AACtF,SAAO,SAAS;AAClB;AAGO,SAAS,aAAa,QAA4C;AACvE,QAAM,MAAM,oBAAI,IAAoB;AACpC,MAAI,CAAC,OAAQ,QAAO;AACpB,aAAW,QAAQ,OAAO,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAI,KAAK,EAAG;AACZ,UAAM,OAAO,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;AACpC,QAAI,CAAC,QAAQ,IAAI,IAAI,IAAI,EAAG;AAC5B,QAAI,QAAQ,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK;AACpC,QAAI,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,EAAG,SAAQ,MAAM,MAAM,GAAG,EAAE;AAC3E,QAAI,IAAI,MAAM,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAQO,SAAS,gBAAgB,MAAc,OAAe,OAAiC;AAC5F,QAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,KAAK,IAAI,UAAU,YAAY,gBAAgB,WAAW,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE;AAC9H,MAAI,MAAM,OAAQ,OAAM,KAAK,QAAQ;AACrC,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}