@sparkletree/core 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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/analytics.d.ts +75 -0
  4. package/dist/analytics.d.ts.map +1 -0
  5. package/dist/analytics.js +170 -0
  6. package/dist/analytics.js.map +1 -0
  7. package/dist/audio.d.ts +85 -0
  8. package/dist/audio.d.ts.map +1 -0
  9. package/dist/audio.js +465 -0
  10. package/dist/audio.js.map +1 -0
  11. package/dist/client.d.ts +79 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +251 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/context.d.ts +104 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +277 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/fragments.d.ts +108 -0
  20. package/dist/fragments.d.ts.map +1 -0
  21. package/dist/fragments.js +223 -0
  22. package/dist/fragments.js.map +1 -0
  23. package/dist/index.d.ts +31 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +30 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/protocol.d.ts +70 -0
  28. package/dist/protocol.d.ts.map +1 -0
  29. package/dist/protocol.js +97 -0
  30. package/dist/protocol.js.map +1 -0
  31. package/dist/publishableKey.d.ts +38 -0
  32. package/dist/publishableKey.d.ts.map +1 -0
  33. package/dist/publishableKey.js +69 -0
  34. package/dist/publishableKey.js.map +1 -0
  35. package/dist/safeUrl.d.ts +56 -0
  36. package/dist/safeUrl.d.ts.map +1 -0
  37. package/dist/safeUrl.js +117 -0
  38. package/dist/safeUrl.js.map +1 -0
  39. package/dist/sse.d.ts +51 -0
  40. package/dist/sse.d.ts.map +1 -0
  41. package/dist/sse.js +137 -0
  42. package/dist/sse.js.map +1 -0
  43. package/dist/state.d.ts +321 -0
  44. package/dist/state.d.ts.map +1 -0
  45. package/dist/state.js +594 -0
  46. package/dist/state.js.map +1 -0
  47. package/dist/text.d.ts +10 -0
  48. package/dist/text.d.ts.map +1 -0
  49. package/dist/text.js +29 -0
  50. package/dist/text.js.map +1 -0
  51. package/dist/theme.d.ts +52 -0
  52. package/dist/theme.d.ts.map +1 -0
  53. package/dist/theme.js +88 -0
  54. package/dist/theme.js.map +1 -0
  55. package/dist/trust.d.ts +52 -0
  56. package/dist/trust.d.ts.map +1 -0
  57. package/dist/trust.js +95 -0
  58. package/dist/trust.js.map +1 -0
  59. package/dist/variant.d.ts +117 -0
  60. package/dist/variant.d.ts.map +1 -0
  61. package/dist/variant.js +167 -0
  62. package/dist/variant.js.map +1 -0
  63. package/dist/wire.d.ts +564 -0
  64. package/dist/wire.d.ts.map +1 -0
  65. package/dist/wire.js +133 -0
  66. package/dist/wire.js.map +1 -0
  67. package/package.json +55 -0
package/dist/client.js ADDED
@@ -0,0 +1,251 @@
1
+ /**
2
+ * The client: fetch `/content` for first paint, then open the stream and feed
3
+ * every event into the choreographer.
4
+ *
5
+ * The event dispatch below is the ONLY place in the SDK that knows event
6
+ * names, and every name it knows comes from the generated contract. A server
7
+ * that starts emitting something newer than this build is not an error — it is
8
+ * an older client, which the protocol explicitly supports — so unknown events
9
+ * are ignored rather than warned about.
10
+ */
11
+ import { CONTENT_SOURCES, COPY_MODES, EVENT_PROTOCOL_UNSUPPORTED, PROTOCOL_CURRENT, isWireEvent, } from "./wire.js";
12
+ import { isSecureApiBase, safeActionUrl } from "./safeUrl.js";
13
+ import { buildContentUrl, buildStreamUrl, negotiatedProtocol, } from "./protocol.js";
14
+ import { readSseStream } from "./sse.js";
15
+ import { CopyChoreographer, } from "./state.js";
16
+ /** Which copy field an event name addresses, if any. */
17
+ function copyFieldOf(event) {
18
+ const [field] = event.split("-");
19
+ if (field === "greeting" || field === "headline" || field === "body" || field === "cta") {
20
+ return field;
21
+ }
22
+ return null;
23
+ }
24
+ function parseData(raw) {
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ return parsed && typeof parsed === "object" ? parsed : {};
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ function str(value, fallback = "") {
34
+ return typeof value === "string" ? value : fallback;
35
+ }
36
+ /**
37
+ * Fetch the first-paint content.
38
+ *
39
+ * Content-first, rewrite-once: this is what the SDK paints BEFORE any stream
40
+ * exists, so there is never a skeleton on screen. If it fails, the consumer's
41
+ * `fallback` prop is what stands in — which is why that prop is required
42
+ * rather than optional.
43
+ */
44
+ export async function fetchContent(target, options = {}) {
45
+ const doFetch = options.fetchImpl ?? fetch;
46
+ // An apiBase that is neither https nor loopback http is a misconfiguration,
47
+ // not a request worth making: the content path degrades to the consumer's
48
+ // fallback, which is real copy by contract.
49
+ if (!isSecureApiBase(target.apiBase))
50
+ return null;
51
+ try {
52
+ // No custom headers: the protocol version already rides the URL
53
+ // (buildContentUrl), and a bare GET is a CORS "simple request" — no
54
+ // preflight to allowlist, no OPTIONS round-trip before first paint.
55
+ const response = await doFetch(buildContentUrl(target), {
56
+ signal: options.signal,
57
+ });
58
+ if (!response.ok)
59
+ return null;
60
+ // The body is untrusted: a proxy or a misconfigured edge can answer 200
61
+ // with HTML or a bare string, and casting that to ContentSnapshot would
62
+ // paint garbage instead of falling back.
63
+ const body = await response.json();
64
+ return body && typeof body === "object" ? body : null;
65
+ }
66
+ catch {
67
+ // A failed first-paint fetch is not an error state: the consumer's
68
+ // fallback is real copy and the stream may still arrive.
69
+ return null;
70
+ }
71
+ }
72
+ /**
73
+ * Open an island stream and choreograph it.
74
+ *
75
+ * Returns immediately with a handle; `finished` resolves with the terminal
76
+ * state. It never rejects — a broken stream leaves whatever was painted on
77
+ * screen and reports `phase: "failed"`, because throwing from inside a
78
+ * customer's render tree is a worse outcome than a creative that stopped
79
+ * adapting.
80
+ */
81
+ export function openStream(options) {
82
+ const { target, onState, onProtocolUnsupported } = options;
83
+ const doFetch = options.fetchImpl ?? fetch;
84
+ const choreographer = new CopyChoreographer(options);
85
+ if (onState)
86
+ choreographer.subscribe(onState);
87
+ // Refuse to open a stream to an apiBase that is neither https nor loopback
88
+ // http. Not a throw — a throw inside a customer's render tree is the one
89
+ // outcome this package exists to avoid — just the failed state the same
90
+ // consumer already handles for a dead transport.
91
+ if (!isSecureApiBase(target.apiBase)) {
92
+ choreographer.failed("apiBase must be https (http is allowed for localhost only)");
93
+ return {
94
+ choreographer,
95
+ finished: Promise.resolve(choreographer.getState()),
96
+ close: () => choreographer.dispose(),
97
+ };
98
+ }
99
+ const controller = new AbortController();
100
+ const abort = () => controller.abort();
101
+ // A signal that is ALREADY aborted never fires "abort" again, so wiring the
102
+ // listener alone would leak the stream open. Abort our controller up front.
103
+ if (options.signal?.aborted)
104
+ controller.abort();
105
+ else
106
+ options.signal?.addEventListener("abort", abort, { once: true });
107
+ const finished = (async () => {
108
+ try {
109
+ // `Accept` is a CORS-safelisted header and the protocol version rides
110
+ // the URL, so this stays a simple request: no preflight stands between
111
+ // a page and its stream. (The st-protocol REQUEST header is for
112
+ // consumer-supplied transports; ours never needs it.)
113
+ const response = await doFetch(buildStreamUrl(target), {
114
+ signal: controller.signal,
115
+ headers: { Accept: "text/event-stream" },
116
+ });
117
+ if (!response.ok) {
118
+ choreographer.failed(`stream request failed (${response.status})`);
119
+ return choreographer.getState();
120
+ }
121
+ const served = negotiatedProtocol(response);
122
+ choreographer.streamOpened();
123
+ await readSseStream(response, {
124
+ signal: controller.signal,
125
+ onMessage: (message) => {
126
+ const data = parseData(message.data);
127
+ if (message.event === EVENT_PROTOCOL_UNSUPPORTED) {
128
+ const supported = Array.isArray(data.supported)
129
+ ? data.supported.map(Number).filter(Number.isFinite)
130
+ : [];
131
+ onProtocolUnsupported?.({
132
+ requested: typeof data.requested === "number" ? data.requested : PROTOCOL_CURRENT,
133
+ supported,
134
+ });
135
+ // Not `failed`: nothing went wrong, this deployment is simply
136
+ // older than this build. First paint stands, and we do not retry
137
+ // — the same version would be refused again.
138
+ choreographer.done({ contentSource: "static", protocolUnsupported: true });
139
+ return;
140
+ }
141
+ // Unknown events are ignored by design: a newer server may emit
142
+ // things this build predates, and the contract guarantees they are
143
+ // additive.
144
+ if (!isWireEvent(message.event))
145
+ return;
146
+ dispatch(choreographer, message.event, data, served);
147
+ },
148
+ });
149
+ // A stream that ends without `done` ended badly — but quietly, and
150
+ // whatever is painted stays painted. Two exceptions: a stream that
151
+ // already reported WHY it failed keeps that reason ("Campaign not
152
+ // found" tells someone what to fix; overwriting it here would throw
153
+ // away the only useful thing the server said), and a stream ended by
154
+ // our OWN controller — a deliberate `close()` — is not a failure at
155
+ // all, just a component unmounting.
156
+ const phase = choreographer.getState().phase;
157
+ if (!controller.signal.aborted && phase !== "settled" && phase !== "failed") {
158
+ choreographer.failed("stream ended without a done event");
159
+ }
160
+ }
161
+ catch (error) {
162
+ if (!controller.signal.aborted) {
163
+ choreographer.failed(error instanceof Error ? error.message : String(error));
164
+ }
165
+ }
166
+ finally {
167
+ options.signal?.removeEventListener("abort", abort);
168
+ }
169
+ return choreographer.getState();
170
+ })();
171
+ return {
172
+ choreographer,
173
+ finished,
174
+ close: () => {
175
+ abort();
176
+ choreographer.dispose();
177
+ },
178
+ };
179
+ }
180
+ function dispatch(choreographer, event, data, _protocol) {
181
+ switch (event) {
182
+ case "theme-ready":
183
+ choreographer.themeReady(data.colors ?? {}, data.sources ?? {});
184
+ return;
185
+ case "experience-ready":
186
+ choreographer.layoutReady(str(data.layout), data.experienceManifest);
187
+ return;
188
+ case "layout-ready":
189
+ choreographer.layoutReady(str(data.layout));
190
+ return;
191
+ case "background-ready":
192
+ choreographer.backgroundReady(str(data.bgImage));
193
+ return;
194
+ case "sequence-ready":
195
+ choreographer.sequenceReady(Array.isArray(data.frames) ? data.frames : [], typeof data.heroIndex === "number" ? data.heroIndex : 0, typeof data.fps === "number" ? data.fps : 8);
196
+ return;
197
+ case "video-ready":
198
+ choreographer.videoReady(str(data.videoUrl));
199
+ return;
200
+ case "cta-style-ready":
201
+ choreographer.ctaStyleReady(data.style);
202
+ return;
203
+ case "cta-ready":
204
+ // The action is rendered as an <a href> in the customer's origin, so it
205
+ // is vetted before it ever reaches state: a server-supplied
206
+ // `javascript:` URL is click-XSS, and `undefined` degrades to a plain
207
+ // button rather than a poisoned link.
208
+ choreographer.ctaReady(str(data.text), data.style, safeActionUrl(str(data.action) || undefined));
209
+ return;
210
+ case "done":
211
+ choreographer.done(data.meta ?? {});
212
+ return;
213
+ case "error":
214
+ choreographer.failed(str(data.error, "stream error"));
215
+ return;
216
+ default:
217
+ break;
218
+ }
219
+ const field = copyFieldOf(event);
220
+ if (!field)
221
+ return;
222
+ if (event.endsWith("-start")) {
223
+ // An unlabelled start is an older edge revision mid-rollout: null mode
224
+ // takes the replay path, which is byte-identical to pre-negotiation
225
+ // behaviour. `static` is the honest default for undeclared provenance —
226
+ // silence must never be read as evidence of generation.
227
+ //
228
+ // Both enums are validated against the contract vocabularies rather than
229
+ // cast: the wire is untrusted, and an unknown string landing in the
230
+ // public typed state would drive trust chrome off a value the contract
231
+ // never defined. Unknown mode → null (unlabelled path); unknown source →
232
+ // "static" (the no-claim default).
233
+ const mode = typeof data.mode === "string" && COPY_MODES.includes(data.mode)
234
+ ? data.mode
235
+ : null;
236
+ const source = typeof data.contentSource === "string" &&
237
+ CONTENT_SOURCES.includes(data.contentSource)
238
+ ? data.contentSource
239
+ : "static";
240
+ choreographer.copyStart(field, mode, source);
241
+ return;
242
+ }
243
+ if (event.endsWith("-chunk")) {
244
+ choreographer.copyChunk(field, str(data.text));
245
+ return;
246
+ }
247
+ if (event.endsWith("-done")) {
248
+ choreographer.copyDone(field, str(data.text));
249
+ }
250
+ }
251
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Declared-context canonicalization — the TS arm of `ctxhash`.
3
+ *
4
+ * One algorithm, two implementations: this file and
5
+ * `mosaic-edge-serve/internal/ctxhash`. The shared vectors in
6
+ * `sdk/parity/fixtures/ctxhash/vectors.json` are the contract; a change here
7
+ * that is not mirrored in Go fails both parity suites.
8
+ *
9
+ * The vocabulary is CLOSED on purpose: it is simultaneously the cardinality
10
+ * fence (the cache-key space is the product of small enums) and the injection
11
+ * defense (attacker-reachable strings never key a shared cache entry).
12
+ * Unknown keys are dropped at both ends, by contract. An unknown value for a
13
+ * known enum is dropped from the hash — never a new bucket, never a
14
+ * generation.
15
+ *
16
+ * The client's hash is a cache hint. The server re-canonicalizes from raw
17
+ * dims and its own hash is the identity for generation, billing, and
18
+ * admission — nobody gets to pre-poison a bucket.
19
+ */
20
+ /** Platform intent vocabulary. */
21
+ export declare const CONTEXT_INTENTS: readonly ["signup", "signin", "retain", "upsell", "reassure", "explain", "browse", "convert"];
22
+ /** Dayparts — the timecontext classification, lowercased for the hash. */
23
+ export declare const CONTEXT_DAYPARTS: readonly ["earlymorning", "morning", "latemorning", "midday", "afternoon", "evening", "lateevening", "latenight"];
24
+ export declare const CONTEXT_DAY_TYPES: readonly ["weekday", "weekend"];
25
+ export declare const CONTEXT_WEATHER: readonly ["clear", "cloudy", "rainy", "snowy"];
26
+ export declare const CONTEXT_TEMP_BANDS: readonly ["cold", "mild", "warm", "hot"];
27
+ export declare const CONTEXT_DEVICES: readonly ["mobile", "tablet", "desktop", "signage"];
28
+ /** Existing platform surface-type vocabulary (mosaic/lib/surface-types.ts). */
29
+ export declare const CONTEXT_SURFACE_TYPES: readonly ["vending", "retail", "transit", "stadium", "mall", "outdoor_billboard", "indoor_display", "point_of_sale", "lobby", "speaker", "kiosk", "ar", "web", "other", "grocery", "convenience", "restaurant", "cafe", "bar"];
30
+ export declare const MAX_CUSTOM_DIMS = 4;
31
+ export declare const MAX_NOTES_LENGTH = 280;
32
+ /** Canonical dim map: short keys, normalized lowercase values. */
33
+ export type CanonicalDims = Record<string, string>;
34
+ /** Developer-declared placement, the public shape (long field names). */
35
+ export interface DeclaredContext {
36
+ /** Declared page class, `[a-z0-9-]{1,32}`. Never a URL or path. */
37
+ page?: string;
38
+ /** Platform intent enum. */
39
+ intent?: string;
40
+ /** Placement descriptor, never a user identifier. */
41
+ audience?: string;
42
+ /**
43
+ * Free text, ≤280 chars, describing the placement — never the visitor.
44
+ * Hashed in full: a distinct notes value is a distinct context.
45
+ */
46
+ notes?: string;
47
+ /** Org-declared custom dims: `{ tier: "pro" }` becomes dim `x.tier`. */
48
+ custom?: Record<string, string>;
49
+ }
50
+ /**
51
+ * Normalizes the free-text `notes` dim. Returns undefined when the field is
52
+ * empty after scrubbing or matches a rejected shape (emails, phone numbers,
53
+ * long token-like runs) — the field is dropped, never partially kept.
54
+ */
55
+ export declare function normalizeNotes(raw: string): string | undefined;
56
+ export interface NormalizeResult {
57
+ dims: CanonicalDims;
58
+ /** Keys (or `key=value` pairs) removed during normalization, for dev warnings. */
59
+ dropped: string[];
60
+ }
61
+ /**
62
+ * Normalizes a raw dim map into canonical dims: unknown keys dropped, enum
63
+ * values validated, tokens pattern-checked, custom dims capped at
64
+ * MAX_CUSTOM_DIMS (bytewise-first wins, deterministically), notes scrubbed.
65
+ */
66
+ export declare function normalizeDims(raw: Record<string, unknown>): NormalizeResult;
67
+ /** Maps the public DeclaredContext shape onto short-key dims. */
68
+ export declare function declaredToDims(declared: DeclaredContext): Record<string, unknown>;
69
+ /**
70
+ * Canonical form: drop absent/unknown keys → lowercase → sort keys bytewise →
71
+ * join "k:v|k:v". Already-normalized dims only; run normalizeDims first.
72
+ */
73
+ export declare function canonicalContextString(dims: CanonicalDims): string;
74
+ /** sha256(canonical) → hex, truncated to 16 chars (64 bits). */
75
+ export declare function computeContextHash(dims: CanonicalDims): Promise<string>;
76
+ /** Length-budget floors per kind — ceilings on the ceiling (spec §2.6). They
77
+ * cap the case where a developer's fallback is a single word. */
78
+ export declare const FRAGMENT_KIND_FLOORS: Record<string, number>;
79
+ /**
80
+ * ceil(max(len(fallback) × 1.3, floor(kind))). The developer's own fallback
81
+ * defines the layout envelope, which is what makes CLS zero by construction.
82
+ * Server twin: mosaic/services/fragments/fragmentService.ts computeFieldBudget
83
+ * — the server recomputes and never trusts the client's number.
84
+ */
85
+ export declare function computeFieldBudget(fallback: string, kind: string): number;
86
+ /**
87
+ * Schema hash: keys + kinds + budgets, never the fallback prose (prose in the
88
+ * GET URL would make the CDN object private and delete the cost story). MUST
89
+ * stay byte-identical to mosaic/services/fragments/fragmentService.ts
90
+ * computeSchemaHash; the shared vectors in
91
+ * sdk/parity/fixtures/ctxhash/schema-vectors.json are the contract.
92
+ */
93
+ export declare function computeSchemaHash(fields: readonly {
94
+ key: string;
95
+ kind: string;
96
+ budget: number;
97
+ }[]): Promise<string>;
98
+ /** Convenience: declared shape → normalized dims → hash. */
99
+ export declare function hashDeclaredContext(declared: DeclaredContext, environment?: Record<string, unknown>): Promise<{
100
+ dims: CanonicalDims;
101
+ hash: string;
102
+ dropped: string[];
103
+ }>;
104
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,kCAAkC;AAClC,eAAO,MAAM,eAAe,+FASlB,CAAC;AAEX,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,mHASnB,CAAC;AAEX,eAAO,MAAM,iBAAiB,iCAAkC,CAAC;AACjE,eAAO,MAAM,eAAe,gDAAiD,CAAC;AAC9E,eAAO,MAAM,kBAAkB,0CAA2C,CAAC;AAC3E,eAAO,MAAM,eAAe,qDAAsD,CAAC;AAEnF,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,gOAoBxB,CAAC;AAUX,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,kEAAkE;AAClE,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEnD,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAmBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAe9D;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,CAAC;IACpB,kFAAkF;IAClF,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,CAiE3E;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUjF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAOlE;AAaD,gEAAgE;AAChE,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAE7E;AAID;iEACiE;AACjE,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMvD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGzE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,GAC/D,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,4DAA4D;AAC5D,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,eAAe,EACzB,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACxC,OAAO,CAAC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAGnE"}
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Declared-context canonicalization — the TS arm of `ctxhash`.
3
+ *
4
+ * One algorithm, two implementations: this file and
5
+ * `mosaic-edge-serve/internal/ctxhash`. The shared vectors in
6
+ * `sdk/parity/fixtures/ctxhash/vectors.json` are the contract; a change here
7
+ * that is not mirrored in Go fails both parity suites.
8
+ *
9
+ * The vocabulary is CLOSED on purpose: it is simultaneously the cardinality
10
+ * fence (the cache-key space is the product of small enums) and the injection
11
+ * defense (attacker-reachable strings never key a shared cache entry).
12
+ * Unknown keys are dropped at both ends, by contract. An unknown value for a
13
+ * known enum is dropped from the hash — never a new bucket, never a
14
+ * generation.
15
+ *
16
+ * The client's hash is a cache hint. The server re-canonicalizes from raw
17
+ * dims and its own hash is the identity for generation, billing, and
18
+ * admission — nobody gets to pre-poison a bucket.
19
+ */
20
+ /** Platform intent vocabulary. */
21
+ export const CONTEXT_INTENTS = [
22
+ "signup",
23
+ "signin",
24
+ "retain",
25
+ "upsell",
26
+ "reassure",
27
+ "explain",
28
+ "browse",
29
+ "convert",
30
+ ];
31
+ /** Dayparts — the timecontext classification, lowercased for the hash. */
32
+ export const CONTEXT_DAYPARTS = [
33
+ "earlymorning",
34
+ "morning",
35
+ "latemorning",
36
+ "midday",
37
+ "afternoon",
38
+ "evening",
39
+ "lateevening",
40
+ "latenight",
41
+ ];
42
+ export const CONTEXT_DAY_TYPES = ["weekday", "weekend"];
43
+ export const CONTEXT_WEATHER = ["clear", "cloudy", "rainy", "snowy"];
44
+ export const CONTEXT_TEMP_BANDS = ["cold", "mild", "warm", "hot"];
45
+ export const CONTEXT_DEVICES = ["mobile", "tablet", "desktop", "signage"];
46
+ /** Existing platform surface-type vocabulary (mosaic/lib/surface-types.ts). */
47
+ export const CONTEXT_SURFACE_TYPES = [
48
+ "vending",
49
+ "retail",
50
+ "transit",
51
+ "stadium",
52
+ "mall",
53
+ "outdoor_billboard",
54
+ "indoor_display",
55
+ "point_of_sale",
56
+ "lobby",
57
+ "speaker",
58
+ "kiosk",
59
+ "ar",
60
+ "web",
61
+ "other",
62
+ "grocery",
63
+ "convenience",
64
+ "restaurant",
65
+ "cafe",
66
+ "bar",
67
+ ];
68
+ /** Org-declared token dims (`p`, `a`) and custom-dim values. */
69
+ const TOKEN_PATTERN = /^[a-z0-9-]{1,32}$/;
70
+ /** Custom dim keys: `x.` + a short token. */
71
+ const CUSTOM_KEY_PATTERN = /^x\.[a-z0-9-]{1,24}$/;
72
+ /** Loose BCP-47 shape, lowercased. */
73
+ const LOCALE_PATTERN = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/;
74
+ const COUNTRY_PATTERN = /^[a-z]{2}$/;
75
+ export const MAX_CUSTOM_DIMS = 4;
76
+ export const MAX_NOTES_LENGTH = 280;
77
+ const ENUM_DIMS = {
78
+ i: CONTEXT_INTENTS,
79
+ t: CONTEXT_DAYPARTS,
80
+ y: CONTEXT_DAY_TYPES,
81
+ w: CONTEXT_WEATHER,
82
+ b: CONTEXT_TEMP_BANDS,
83
+ d: CONTEXT_DEVICES,
84
+ s: CONTEXT_SURFACE_TYPES,
85
+ };
86
+ const MARKUP_PATTERN = /<[^>]*>/g;
87
+ const URL_PATTERN = /(?:https?:\/\/|www\.)\S+/gi;
88
+ /** Shapes that indicate visitor data or secrets: reject the whole field. */
89
+ const EMAIL_PATTERN = /\S+@\S+/;
90
+ const PHONE_PATTERN = /(?:\d[\s().+-]*){7,}/;
91
+ const TOKEN_SHAPE_PATTERN = /[a-z0-9_-]{24,}/;
92
+ /**
93
+ * Normalizes the free-text `notes` dim. Returns undefined when the field is
94
+ * empty after scrubbing or matches a rejected shape (emails, phone numbers,
95
+ * long token-like runs) — the field is dropped, never partially kept.
96
+ */
97
+ export function normalizeNotes(raw) {
98
+ let value = raw
99
+ .replace(MARKUP_PATTERN, " ")
100
+ .replace(URL_PATTERN, " ")
101
+ .replace(/\s+/g, " ")
102
+ .trim()
103
+ .toLowerCase();
104
+ // Cap by code points, matching the Go arm's rune slicing.
105
+ const points = Array.from(value);
106
+ if (points.length > MAX_NOTES_LENGTH)
107
+ value = points.slice(0, MAX_NOTES_LENGTH).join("").trim();
108
+ if (value.length === 0)
109
+ return undefined;
110
+ if (EMAIL_PATTERN.test(value) || PHONE_PATTERN.test(value) || TOKEN_SHAPE_PATTERN.test(value)) {
111
+ return undefined;
112
+ }
113
+ return value;
114
+ }
115
+ /**
116
+ * Normalizes a raw dim map into canonical dims: unknown keys dropped, enum
117
+ * values validated, tokens pattern-checked, custom dims capped at
118
+ * MAX_CUSTOM_DIMS (bytewise-first wins, deterministically), notes scrubbed.
119
+ */
120
+ export function normalizeDims(raw) {
121
+ const dims = {};
122
+ const dropped = [];
123
+ const customKeys = [];
124
+ for (const [rawKey, rawValue] of Object.entries(raw)) {
125
+ if (rawValue === undefined || rawValue === null || rawValue === "")
126
+ continue;
127
+ const key = rawKey.toLowerCase();
128
+ const value = String(rawValue).toLowerCase().trim();
129
+ if (value === "")
130
+ continue;
131
+ if (key === "n") {
132
+ const notes = normalizeNotes(String(rawValue));
133
+ if (notes === undefined)
134
+ dropped.push(key);
135
+ else
136
+ dims.n = notes;
137
+ continue;
138
+ }
139
+ if (key === "p" || key === "a") {
140
+ if (TOKEN_PATTERN.test(value))
141
+ dims[key] = value;
142
+ else
143
+ dropped.push(`${key}=${value}`);
144
+ continue;
145
+ }
146
+ if (key === "l") {
147
+ if (LOCALE_PATTERN.test(value)) {
148
+ dims.l = value;
149
+ }
150
+ else {
151
+ // Fall back to the primary language subtag (en-US-x-foo → en).
152
+ const subtag = value.split("-")[0];
153
+ if (/^[a-z]{2,3}$/.test(subtag))
154
+ dims.l = subtag;
155
+ else
156
+ dropped.push(`l=${value}`);
157
+ }
158
+ continue;
159
+ }
160
+ if (key === "c") {
161
+ if (COUNTRY_PATTERN.test(value))
162
+ dims.c = value;
163
+ else
164
+ dropped.push(`c=${value}`);
165
+ continue;
166
+ }
167
+ const enumValues = ENUM_DIMS[key];
168
+ if (enumValues) {
169
+ if (enumValues.includes(value))
170
+ dims[key] = value;
171
+ else
172
+ dropped.push(`${key}=${value}`);
173
+ continue;
174
+ }
175
+ if (CUSTOM_KEY_PATTERN.test(key)) {
176
+ if (TOKEN_PATTERN.test(value)) {
177
+ customKeys.push(key);
178
+ dims[key] = value;
179
+ }
180
+ else {
181
+ dropped.push(`${key}=${value}`);
182
+ }
183
+ continue;
184
+ }
185
+ dropped.push(key);
186
+ }
187
+ if (customKeys.length > MAX_CUSTOM_DIMS) {
188
+ customKeys.sort();
189
+ for (const key of customKeys.slice(MAX_CUSTOM_DIMS)) {
190
+ delete dims[key];
191
+ dropped.push(key);
192
+ }
193
+ }
194
+ return { dims, dropped };
195
+ }
196
+ /** Maps the public DeclaredContext shape onto short-key dims. */
197
+ export function declaredToDims(declared) {
198
+ const raw = {};
199
+ if (declared.page !== undefined)
200
+ raw.p = declared.page;
201
+ if (declared.intent !== undefined)
202
+ raw.i = declared.intent;
203
+ if (declared.audience !== undefined)
204
+ raw.a = declared.audience;
205
+ if (declared.notes !== undefined)
206
+ raw.n = declared.notes;
207
+ for (const [name, value] of Object.entries(declared.custom ?? {})) {
208
+ raw[`x.${name.toLowerCase()}`] = value;
209
+ }
210
+ return raw;
211
+ }
212
+ /**
213
+ * Canonical form: drop absent/unknown keys → lowercase → sort keys bytewise →
214
+ * join "k:v|k:v". Already-normalized dims only; run normalizeDims first.
215
+ */
216
+ export function canonicalContextString(dims) {
217
+ return Object.entries(dims)
218
+ .filter(([, value]) => value !== undefined && value !== null && value !== "")
219
+ .map(([key, value]) => [key.toLowerCase(), value.toLowerCase()])
220
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
221
+ .map(([key, value]) => `${key}:${value}`)
222
+ .join("|");
223
+ }
224
+ /** sha256(input) → hex, truncated to 16 chars (64 bits). WebCrypto, so it
225
+ * works unchanged in browsers, workers, and Node 18+. */
226
+ async function sha256Hex16(input) {
227
+ const bytes = new TextEncoder().encode(input);
228
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
229
+ return Array.from(new Uint8Array(digest))
230
+ .map((byte) => byte.toString(16).padStart(2, "0"))
231
+ .join("")
232
+ .slice(0, 16);
233
+ }
234
+ /** sha256(canonical) → hex, truncated to 16 chars (64 bits). */
235
+ export async function computeContextHash(dims) {
236
+ return sha256Hex16(canonicalContextString(dims));
237
+ }
238
+ // --- Fragment schema hashing (Declarative Context rung 2) -------------------
239
+ /** Length-budget floors per kind — ceilings on the ceiling (spec §2.6). They
240
+ * cap the case where a developer's fallback is a single word. */
241
+ export const FRAGMENT_KIND_FLOORS = {
242
+ cta: 24,
243
+ label: 32,
244
+ headline: 64,
245
+ body: 160,
246
+ "empty-state": 160,
247
+ };
248
+ /**
249
+ * ceil(max(len(fallback) × 1.3, floor(kind))). The developer's own fallback
250
+ * defines the layout envelope, which is what makes CLS zero by construction.
251
+ * Server twin: mosaic/services/fragments/fragmentService.ts computeFieldBudget
252
+ * — the server recomputes and never trusts the client's number.
253
+ */
254
+ export function computeFieldBudget(fallback, kind) {
255
+ const floor = FRAGMENT_KIND_FLOORS[kind] ?? FRAGMENT_KIND_FLOORS.body;
256
+ return Math.ceil(Math.max(fallback.length * 1.3, floor));
257
+ }
258
+ /**
259
+ * Schema hash: keys + kinds + budgets, never the fallback prose (prose in the
260
+ * GET URL would make the CDN object private and delete the cost story). MUST
261
+ * stay byte-identical to mosaic/services/fragments/fragmentService.ts
262
+ * computeSchemaHash; the shared vectors in
263
+ * sdk/parity/fixtures/ctxhash/schema-vectors.json are the contract.
264
+ */
265
+ export async function computeSchemaHash(fields) {
266
+ const canonical = [...fields]
267
+ .map((field) => `${field.key.toLowerCase()}:${field.kind}:${field.budget}`)
268
+ .sort()
269
+ .join("|");
270
+ return sha256Hex16(canonical);
271
+ }
272
+ /** Convenience: declared shape → normalized dims → hash. */
273
+ export async function hashDeclaredContext(declared, environment = {}) {
274
+ const { dims, dropped } = normalizeDims({ ...environment, ...declaredToDims(declared) });
275
+ return { dims, hash: await computeContextHash(dims), dropped };
276
+ }
277
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,kCAAkC;AAClC,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,SAAS;IACT,QAAQ;IACR,SAAS;CACD,CAAC;AAEX,0EAA0E;AAC1E,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,cAAc;IACd,SAAS;IACT,aAAa;IACb,QAAQ;IACR,WAAW;IACX,SAAS;IACT,aAAa;IACb,WAAW;CACH,CAAC;AAEX,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,SAAS,CAAU,CAAC;AACjE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAU,CAAC;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAU,CAAC;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAU,CAAC;AAEnF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,SAAS;IACT,QAAQ;IACR,SAAS;IACT,SAAS;IACT,MAAM;IACN,mBAAmB;IACnB,gBAAgB;IAChB,eAAe;IACf,OAAO;IACP,SAAS;IACT,OAAO;IACP,IAAI;IACJ,KAAK;IACL,OAAO;IACP,SAAS;IACT,aAAa;IACb,YAAY;IACZ,MAAM;IACN,KAAK;CACG,CAAC;AAEX,gEAAgE;AAChE,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAC1C,6CAA6C;AAC7C,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAClD,sCAAsC;AACtC,MAAM,cAAc,GAAG,+BAA+B,CAAC;AACvD,MAAM,eAAe,GAAG,YAAY,CAAC;AAErC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AACjC,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAsBpC,MAAM,SAAS,GAAsC;IACnD,CAAC,EAAE,eAAe;IAClB,CAAC,EAAE,gBAAgB;IACnB,CAAC,EAAE,iBAAiB;IACpB,CAAC,EAAE,eAAe;IAClB,CAAC,EAAE,kBAAkB;IACrB,CAAC,EAAE,eAAe;IAClB,CAAC,EAAE,qBAAqB;CACzB,CAAC;AAEF,MAAM,cAAc,GAAG,UAAU,CAAC;AAClC,MAAM,WAAW,GAAG,4BAA4B,CAAC;AACjD,4EAA4E;AAC5E,MAAM,aAAa,GAAG,SAAS,CAAC;AAChC,MAAM,aAAa,GAAG,sBAAsB,CAAC;AAC7C,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAE9C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,KAAK,GAAG,GAAG;SACZ,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC;SAC5B,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC;SACzB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE;SACN,WAAW,EAAE,CAAC;IACjB,0DAA0D;IAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,MAAM,CAAC,MAAM,GAAG,gBAAgB;QAAE,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChG,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9F,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAA4B;IACxD,MAAM,IAAI,GAAkB,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,EAAE;YAAE,SAAS;QAC7E,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QACpD,IAAI,KAAK,KAAK,EAAE;YAAE,SAAS;QAE3B,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;YAChB,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC/C,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;;gBACtC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC;YACpB,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;YAC/B,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;;gBAC5C,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC;YACrC,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;YAChB,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/B,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC;YACjB,CAAC;iBAAM,CAAC;gBACN,+DAA+D;gBAC/D,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACnC,IAAI,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC;oBAAE,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC;;oBAC5C,OAAO,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;YAClC,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;YAChB,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC;;gBAC3C,OAAO,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;YAChC,SAAS;QACX,CAAC;QACD,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;;gBAC7C,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC;YACrC,SAAS;QACX,CAAC;QACD,IAAI,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9B,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACrB,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACpB,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC;YAClC,CAAC;YACD,SAAS;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACxC,UAAU,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC;YACpD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,QAAyB;IACtD,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS;QAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC;IACvD,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS;QAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC3D,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS;QAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC;IAC/D,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS;QAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;IACzD,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;QAClE,GAAG,CAAC,KAAK,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC;IACzC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAmB;IACxD,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;SAC5E,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,KAAK,CAAC,WAAW,EAAE,CAAU,CAAC;SACxE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAChD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC;SACxC,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED;yDACyD;AACzD,KAAK,UAAU,WAAW,CAAC,KAAa;IACtC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACvE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACjD,IAAI,CAAC,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAmB;IAC1D,OAAO,WAAW,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,+EAA+E;AAE/E;iEACiE;AACjE,MAAM,CAAC,MAAM,oBAAoB,GAA2B;IAC1D,GAAG,EAAE,EAAE;IACP,KAAK,EAAE,EAAE;IACT,QAAQ,EAAE,EAAE;IACZ,IAAI,EAAE,GAAG;IACT,aAAa,EAAE,GAAG;CACnB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,IAAY;IAC/D,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC;IACtE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAgE;IAEhE,MAAM,SAAS,GAAG,CAAC,GAAG,MAAM,CAAC;SAC1B,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;SAC1E,IAAI,EAAE;SACN,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,WAAW,CAAC,SAAS,CAAC,CAAC;AAChC,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAyB,EACzB,cAAuC,EAAE;IAEzC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,EAAE,GAAG,WAAW,EAAE,GAAG,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACzF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;AACjE,CAAC"}