@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
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The publishable key: `st_pk_<env>_<base64url(JSON { organizationId, apiBase })>`.
3
+ *
4
+ * It is NOT a credential — it is an ADDRESS, and the docs must say so in the
5
+ * same paragraph that introduces it. It is decoded locally, with zero network,
6
+ * and collapses the two required provider props into one dashboard
7
+ * copy-paste. The read path is unauthenticated BY DESIGN; security lives in
8
+ * the origin registry (minting requires a verified origin registered to the
9
+ * paying org). We ship the key because one string is better onboarding than
10
+ * two, not because it protects anything. `organizationId` and `apiBase`
11
+ * remain as explicit props for self-hosted bases and stay the documented
12
+ * escape hatch — when both are given, the explicit props win.
13
+ *
14
+ * Because the key is unsigned, an apiBase that arrives VIA a key is an
15
+ * unproven address: anyone can hand-encode a key pointing at their own host.
16
+ * Content fetches may follow it (they carry identity hashes, not secrets),
17
+ * but the signed mint token — a real spend credential — may not: the mint
18
+ * POST fires only for an explicit, first-party (*.sparkletree.io), or
19
+ * loopback apiBase (security review S-2; see isFirstPartyApiBase/maybeMint).
20
+ */
21
+ export interface PublishableKeyPayload {
22
+ organizationId: string;
23
+ apiBase: string;
24
+ }
25
+ export type PublishableKeyEnv = "live" | "test";
26
+ /**
27
+ * Decode a publishable key. Throws TypeError on anything malformed — a bad
28
+ * key is a configuration error at integration time, not a runtime condition
29
+ * to degrade around.
30
+ */
31
+ export declare function decodePublishableKey(key: string): PublishableKeyPayload;
32
+ /** Encode a publishable key. Dashboard-side counterpart of decode. */
33
+ export declare function encodePublishableKey(options: {
34
+ organizationId: string;
35
+ apiBase: string;
36
+ env: PublishableKeyEnv;
37
+ }): string;
38
+ //# sourceMappingURL=publishableKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publishableKey.d.ts","sourceRoot":"","sources":["../src/publishableKey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAC;AAkBhD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,qBAAqB,CAqBvE;AAED,sEAAsE;AACtE,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,iBAAiB,CAAC;CACxB,GAAG,MAAM,CAMT"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The publishable key: `st_pk_<env>_<base64url(JSON { organizationId, apiBase })>`.
3
+ *
4
+ * It is NOT a credential — it is an ADDRESS, and the docs must say so in the
5
+ * same paragraph that introduces it. It is decoded locally, with zero network,
6
+ * and collapses the two required provider props into one dashboard
7
+ * copy-paste. The read path is unauthenticated BY DESIGN; security lives in
8
+ * the origin registry (minting requires a verified origin registered to the
9
+ * paying org). We ship the key because one string is better onboarding than
10
+ * two, not because it protects anything. `organizationId` and `apiBase`
11
+ * remain as explicit props for self-hosted bases and stay the documented
12
+ * escape hatch — when both are given, the explicit props win.
13
+ *
14
+ * Because the key is unsigned, an apiBase that arrives VIA a key is an
15
+ * unproven address: anyone can hand-encode a key pointing at their own host.
16
+ * Content fetches may follow it (they carry identity hashes, not secrets),
17
+ * but the signed mint token — a real spend credential — may not: the mint
18
+ * POST fires only for an explicit, first-party (*.sparkletree.io), or
19
+ * loopback apiBase (security review S-2; see isFirstPartyApiBase/maybeMint).
20
+ */
21
+ const KEY_PATTERN = /^st_pk_(live|test)_([A-Za-z0-9_-]+)$/;
22
+ function toBase64Url(text) {
23
+ const bytes = new TextEncoder().encode(text);
24
+ let binary = "";
25
+ for (const byte of bytes)
26
+ binary += String.fromCharCode(byte);
27
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
28
+ }
29
+ function fromBase64Url(encoded) {
30
+ const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
31
+ const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
32
+ const binary = atob(padded);
33
+ return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
34
+ }
35
+ /**
36
+ * Decode a publishable key. Throws TypeError on anything malformed — a bad
37
+ * key is a configuration error at integration time, not a runtime condition
38
+ * to degrade around.
39
+ */
40
+ export function decodePublishableKey(key) {
41
+ const match = typeof key === "string" ? KEY_PATTERN.exec(key) : null;
42
+ if (!match) {
43
+ throw new TypeError('SparkleTree: malformed publishable key — expected "st_pk_live_…" or "st_pk_test_…" copied from the dashboard.');
44
+ }
45
+ let payload;
46
+ try {
47
+ payload = JSON.parse(fromBase64Url(match[2]));
48
+ }
49
+ catch {
50
+ throw new TypeError("SparkleTree: publishable key payload is not decodable JSON.");
51
+ }
52
+ const { organizationId, apiBase } = (payload ?? {});
53
+ if (typeof organizationId !== "string" || organizationId === "") {
54
+ throw new TypeError("SparkleTree: publishable key is missing organizationId.");
55
+ }
56
+ if (typeof apiBase !== "string" || apiBase === "") {
57
+ throw new TypeError("SparkleTree: publishable key is missing apiBase.");
58
+ }
59
+ return { organizationId, apiBase };
60
+ }
61
+ /** Encode a publishable key. Dashboard-side counterpart of decode. */
62
+ export function encodePublishableKey(options) {
63
+ const payload = JSON.stringify({
64
+ organizationId: options.organizationId,
65
+ apiBase: options.apiBase,
66
+ });
67
+ return `st_pk_${options.env}_${toBase64Url(payload)}`;
68
+ }
69
+ //# sourceMappingURL=publishableKey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publishableKey.js","sourceRoot":"","sources":["../src/publishableKey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,MAAM,WAAW,GAAG,sCAAsC,CAAC;AAE3D,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACjF,CAAC;AAED,SAAS,aAAa,CAAC,OAAe;IACpC,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACzF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACrE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,SAAS,CACjB,+GAA+G,CAChH,CAAC;IACJ,CAAC;IACD,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,SAAS,CAAC,6DAA6D,CAAC,CAAC;IACrF,CAAC;IACD,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAmC,CAAC;IACtF,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,KAAK,EAAE,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CAAC,yDAAyD,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QAClD,MAAM,IAAI,SAAS,CAAC,kDAAkD,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC;AACrC,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,oBAAoB,CAAC,OAIpC;IACC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;QAC7B,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC;IACH,OAAO,SAAS,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;AACxD,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * URL hygiene for values that cross the wire into a customer's page.
3
+ *
4
+ * Both helpers exist because the SDK runs INSIDE the customer's origin:
5
+ * anything the server sends is rendered with the customer's privileges, so a
6
+ * value we would never type ourselves must be refused here rather than
7
+ * escaped downstream. Refusal is silent — never a throw, never a console
8
+ * line — because the failure mode this package optimises for is "the
9
+ * creative quietly stops adapting", not "the customer's console fills up".
10
+ */
11
+ /**
12
+ * Vet a server-supplied action URL before it reaches an `<a href>`.
13
+ *
14
+ * Allowed through unchanged:
15
+ * - absolute `https:`, `http:`, `mailto:`, `tel:` URLs;
16
+ * - relative references — `/…`, `./…`, `#…`, `?…` — which resolve against
17
+ * the customer's own origin; plus protocol-relative `//…`, which keeps
18
+ * the page's scheme and is no worse than the absolute `https:` URLs we
19
+ * already allow (both can point at any host).
20
+ *
21
+ * Anything else returns `undefined`: `javascript:`/`data:`/`vbscript:`
22
+ * outright, but also bare strings with no scheme and no relative prefix —
23
+ * we cannot prove where they point, and a CTA with no destination degrades
24
+ * to a plain button, which is the honest outcome.
25
+ *
26
+ * The input is trimmed first because browsers strip leading ASCII whitespace
27
+ * from `href` before parsing — without that, `" javascript:…"` would sneak a
28
+ * scheme past a prefix check that the browser would then honour.
29
+ */
30
+ export declare function safeActionUrl(url: string | undefined): string | undefined;
31
+ /**
32
+ * Is this apiBase allowed to carry SDK traffic?
33
+ *
34
+ * Production traffic is `https:` only — an `http:` apiBase sends campaign
35
+ * configuration and context over a channel any middlebox can rewrite, which
36
+ * is S-4 with extra steps. The one exception is loopback
37
+ * (`http://localhost`, `http://127.0.0.1`, any port), because local
38
+ * development against a dev edge is a real and legitimate setup.
39
+ */
40
+ export declare function isSecureApiBase(apiBase: string): boolean;
41
+ /**
42
+ * Is this apiBase first-party — the hosted SparkleTree edge, or loopback?
43
+ *
44
+ * The publishable key is an unsigned address ANYONE can hand-encode (see
45
+ * publishableKey.ts), so an apiBase that arrived via a key proves nothing
46
+ * about who chose it. The one value that must never follow an unproven
47
+ * address is the signed mint token: it is a real spend credential, and a
48
+ * doctored key pointing at an attacker host would receive it on the mint
49
+ * POST and replay it against the real API for its TTL (security review
50
+ * S-2). The mint therefore fires only when the apiBase is explicit (the
51
+ * developer's own prop — their choice to make), first-party, or loopback.
52
+ * Self-hosted bases are exactly what the explicit `apiBase` escape hatch is
53
+ * for; a self-made key carrying a third-party host gets content but no mint.
54
+ */
55
+ export declare function isFirstPartyApiBase(apiBase: string): boolean;
56
+ //# sourceMappingURL=safeUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safeUrl.d.ts","sourceRoot":"","sources":["../src/safeUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAmBH;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAyBzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CASxD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAY5D"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * URL hygiene for values that cross the wire into a customer's page.
3
+ *
4
+ * Both helpers exist because the SDK runs INSIDE the customer's origin:
5
+ * anything the server sends is rendered with the customer's privileges, so a
6
+ * value we would never type ourselves must be refused here rather than
7
+ * escaped downstream. Refusal is silent — never a throw, never a console
8
+ * line — because the failure mode this package optimises for is "the
9
+ * creative quietly stops adapting", not "the customer's console fills up".
10
+ */
11
+ /**
12
+ * Schemes a CTA href may carry. `https`/`http` are links; `mailto`/`tel` are
13
+ * the other two things a button legitimately does. Everything else —
14
+ * `javascript:`, `data:`, `vbscript:` — is click-XSS in the customer's origin
15
+ * the moment a consumer renders `ctaAction` as `<a href>`.
16
+ */
17
+ const ALLOWED_ACTION_SCHEMES = new Set(["https:", "http:", "mailto:", "tel:"]);
18
+ /** A scheme per RFC 3986: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ":". */
19
+ const SCHEME_PREFIX = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
20
+ /**
21
+ * Sniffing base for `new URL`. Only the SCHEME is read off the result — never
22
+ * the resolved href — so the dummy origin can never leak into returned values.
23
+ */
24
+ const SNIFF_BASE = "https://url.sparkletree.invalid";
25
+ /**
26
+ * Vet a server-supplied action URL before it reaches an `<a href>`.
27
+ *
28
+ * Allowed through unchanged:
29
+ * - absolute `https:`, `http:`, `mailto:`, `tel:` URLs;
30
+ * - relative references — `/…`, `./…`, `#…`, `?…` — which resolve against
31
+ * the customer's own origin; plus protocol-relative `//…`, which keeps
32
+ * the page's scheme and is no worse than the absolute `https:` URLs we
33
+ * already allow (both can point at any host).
34
+ *
35
+ * Anything else returns `undefined`: `javascript:`/`data:`/`vbscript:`
36
+ * outright, but also bare strings with no scheme and no relative prefix —
37
+ * we cannot prove where they point, and a CTA with no destination degrades
38
+ * to a plain button, which is the honest outcome.
39
+ *
40
+ * The input is trimmed first because browsers strip leading ASCII whitespace
41
+ * from `href` before parsing — without that, `" javascript:…"` would sneak a
42
+ * scheme past a prefix check that the browser would then honour.
43
+ */
44
+ export function safeActionUrl(url) {
45
+ if (!url)
46
+ return undefined;
47
+ const trimmed = url.trim();
48
+ if (!trimmed)
49
+ return undefined;
50
+ if (trimmed.startsWith("/") ||
51
+ trimmed.startsWith("./") ||
52
+ trimmed.startsWith("#") ||
53
+ trimmed.startsWith("?")) {
54
+ return trimmed;
55
+ }
56
+ // No scheme and no relative prefix: unprovable, so refused. (This also
57
+ // catches whitespace-obfuscated schemes like "java\tscript:" — the tab is
58
+ // not a scheme character, so the string simply has no scheme here.)
59
+ if (!SCHEME_PREFIX.test(trimmed))
60
+ return undefined;
61
+ try {
62
+ const scheme = new URL(trimmed, SNIFF_BASE).protocol;
63
+ return ALLOWED_ACTION_SCHEMES.has(scheme) ? trimmed : undefined;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ /**
70
+ * Is this apiBase allowed to carry SDK traffic?
71
+ *
72
+ * Production traffic is `https:` only — an `http:` apiBase sends campaign
73
+ * configuration and context over a channel any middlebox can rewrite, which
74
+ * is S-4 with extra steps. The one exception is loopback
75
+ * (`http://localhost`, `http://127.0.0.1`, any port), because local
76
+ * development against a dev edge is a real and legitimate setup.
77
+ */
78
+ export function isSecureApiBase(apiBase) {
79
+ try {
80
+ const url = new URL(apiBase);
81
+ if (url.protocol === "https:")
82
+ return true;
83
+ if (url.protocol !== "http:")
84
+ return false;
85
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1";
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ /**
92
+ * Is this apiBase first-party — the hosted SparkleTree edge, or loopback?
93
+ *
94
+ * The publishable key is an unsigned address ANYONE can hand-encode (see
95
+ * publishableKey.ts), so an apiBase that arrived via a key proves nothing
96
+ * about who chose it. The one value that must never follow an unproven
97
+ * address is the signed mint token: it is a real spend credential, and a
98
+ * doctored key pointing at an attacker host would receive it on the mint
99
+ * POST and replay it against the real API for its TTL (security review
100
+ * S-2). The mint therefore fires only when the apiBase is explicit (the
101
+ * developer's own prop — their choice to make), first-party, or loopback.
102
+ * Self-hosted bases are exactly what the explicit `apiBase` escape hatch is
103
+ * for; a self-made key carrying a third-party host gets content but no mint.
104
+ */
105
+ export function isFirstPartyApiBase(apiBase) {
106
+ try {
107
+ const { hostname } = new URL(apiBase);
108
+ return (hostname === "sparkletree.io" ||
109
+ hostname.endsWith(".sparkletree.io") ||
110
+ hostname === "localhost" ||
111
+ hostname === "127.0.0.1");
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ //# sourceMappingURL=safeUrl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safeUrl.js","sourceRoot":"","sources":["../src/safeUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;GAKG;AACH,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;AAE/E,6EAA6E;AAC7E,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAElD;;;GAGG;AACH,MAAM,UAAU,GAAG,iCAAiC,CAAC;AAErD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,GAAuB;IACnD,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAE/B,IACE,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QACvB,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;QACxB,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QACvB,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EACvB,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,uEAAuE;IACvE,0EAA0E;IAC1E,oEAAoE;IACpE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IAEnD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC;QACrD,OAAO,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3C,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3C,OAAO,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QACtC,OAAO,CACL,QAAQ,KAAK,gBAAgB;YAC7B,QAAQ,CAAC,QAAQ,CAAC,iBAAiB,CAAC;YACpC,QAAQ,KAAK,WAAW;YACxB,QAAQ,KAAK,WAAW,CACzB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * SSE parsing.
3
+ *
4
+ * Deliberately NOT `EventSource`, for three reasons that each cost real
5
+ * behaviour:
6
+ *
7
+ * 1. EventSource cannot set request headers, so it cannot send
8
+ * `st-protocol` — negotiation would be query-param-only forever.
9
+ * 2. EventSource reconnects automatically and silently. On a stream that
10
+ * ends in `error` that means retrying a request the server already
11
+ * answered definitively, repeatedly, from inside a customer's page.
12
+ * 3. EventSource cannot read response headers, so the client could never
13
+ * see which protocol version it actually got.
14
+ *
15
+ * fetch + a manual parser gives all three back. The cost is ~80 lines, which
16
+ * is a good trade for not having a background reconnect loop we do not
17
+ * control.
18
+ */
19
+ export interface SseMessage {
20
+ /** `event:` name. Unnamed messages arrive as "message", per the spec. */
21
+ event: string;
22
+ /** `data:` payload, lines joined with "\n". */
23
+ data: string;
24
+ }
25
+ /**
26
+ * Incremental SSE frame parser. Feed it decoded text; it yields complete
27
+ * messages. Holds partial frames across chunk boundaries — a `data:` line
28
+ * split mid-UTF-8-sequence or mid-JSON is the normal case on a real network,
29
+ * not an edge case.
30
+ */
31
+ export declare class SseParser {
32
+ private buffer;
33
+ /** Feed a chunk of decoded text; returns every message it completed. */
34
+ push(chunk: string): SseMessage[];
35
+ /** Flush a trailing frame that never got its blank line (server closed). */
36
+ flush(): SseMessage[];
37
+ }
38
+ export interface ReadSseOptions {
39
+ signal?: AbortSignal;
40
+ /** Called for every complete message, in order. */
41
+ onMessage: (message: SseMessage) => void;
42
+ }
43
+ /**
44
+ * Drain a fetch Response body as SSE.
45
+ *
46
+ * Returns when the server closes the stream. Aborting via `signal` resolves
47
+ * rather than throwing: a component unmounting mid-stream is the normal way
48
+ * these end, not an error worth surfacing in a customer's console.
49
+ */
50
+ export declare function readSseStream(response: Response, options: ReadSseOptions): Promise<void>;
51
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAM;IAEpB,wEAAwE;IACxE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE;IAmBjC,4EAA4E;IAC5E,KAAK,IAAI,UAAU,EAAE;CAOtB;AAwBD,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mDAAmD;IACnD,SAAS,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAgDf"}
package/dist/sse.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * SSE parsing.
3
+ *
4
+ * Deliberately NOT `EventSource`, for three reasons that each cost real
5
+ * behaviour:
6
+ *
7
+ * 1. EventSource cannot set request headers, so it cannot send
8
+ * `st-protocol` — negotiation would be query-param-only forever.
9
+ * 2. EventSource reconnects automatically and silently. On a stream that
10
+ * ends in `error` that means retrying a request the server already
11
+ * answered definitively, repeatedly, from inside a customer's page.
12
+ * 3. EventSource cannot read response headers, so the client could never
13
+ * see which protocol version it actually got.
14
+ *
15
+ * fetch + a manual parser gives all three back. The cost is ~80 lines, which
16
+ * is a good trade for not having a background reconnect loop we do not
17
+ * control.
18
+ */
19
+ /**
20
+ * Incremental SSE frame parser. Feed it decoded text; it yields complete
21
+ * messages. Holds partial frames across chunk boundaries — a `data:` line
22
+ * split mid-UTF-8-sequence or mid-JSON is the normal case on a real network,
23
+ * not an edge case.
24
+ */
25
+ export class SseParser {
26
+ buffer = "";
27
+ /** Feed a chunk of decoded text; returns every message it completed. */
28
+ push(chunk) {
29
+ this.buffer += chunk;
30
+ const messages = [];
31
+ // Frames are separated by a blank line. Normalise CRLF first so a proxy
32
+ // that rewrites line endings does not silently stop the stream.
33
+ const normalised = this.buffer.replace(/\r\n/g, "\n");
34
+ const frames = normalised.split("\n\n");
35
+ // The last element is either "" (buffer ended on a separator) or a
36
+ // partial frame. Either way it stays buffered.
37
+ this.buffer = frames.pop() ?? "";
38
+ for (const frame of frames) {
39
+ const message = parseFrame(frame);
40
+ if (message)
41
+ messages.push(message);
42
+ }
43
+ return messages;
44
+ }
45
+ /** Flush a trailing frame that never got its blank line (server closed). */
46
+ flush() {
47
+ const remaining = this.buffer.trim();
48
+ this.buffer = "";
49
+ if (!remaining)
50
+ return [];
51
+ const message = parseFrame(remaining);
52
+ return message ? [message] : [];
53
+ }
54
+ }
55
+ function parseFrame(frame) {
56
+ let event = "message";
57
+ const data = [];
58
+ for (const line of frame.split("\n")) {
59
+ if (!line || line.startsWith(":"))
60
+ continue; // comment / keep-alive
61
+ const colon = line.indexOf(":");
62
+ const field = colon === -1 ? line : line.slice(0, colon);
63
+ // Exactly one optional leading space is stripped, per the SSE spec.
64
+ let value = colon === -1 ? "" : line.slice(colon + 1);
65
+ if (value.startsWith(" "))
66
+ value = value.slice(1);
67
+ if (field === "event")
68
+ event = value;
69
+ else if (field === "data")
70
+ data.push(value);
71
+ // `id` and `retry` are meaningful only to EventSource's reconnect logic,
72
+ // which we deliberately do not have.
73
+ }
74
+ if (!data.length && event === "message")
75
+ return null;
76
+ return { event, data: data.join("\n") };
77
+ }
78
+ /**
79
+ * Drain a fetch Response body as SSE.
80
+ *
81
+ * Returns when the server closes the stream. Aborting via `signal` resolves
82
+ * rather than throwing: a component unmounting mid-stream is the normal way
83
+ * these end, not an error worth surfacing in a customer's console.
84
+ */
85
+ export async function readSseStream(response, options) {
86
+ if (!response.body) {
87
+ throw new Error("SparkleTree: response has no body — SSE requires a streaming response");
88
+ }
89
+ const reader = response.body.getReader();
90
+ const decoder = new TextDecoder();
91
+ const parser = new SseParser();
92
+ const onAbort = () => {
93
+ void reader.cancel().catch(() => { });
94
+ };
95
+ options.signal?.addEventListener("abort", onAbort, { once: true });
96
+ try {
97
+ // A signal aborted BEFORE we started reading never fires "abort" again —
98
+ // without this check the drain below would run a stream its owner already
99
+ // cancelled.
100
+ if (options.signal?.aborted) {
101
+ await reader.cancel().catch(() => { });
102
+ return;
103
+ }
104
+ for (;;) {
105
+ const { done, value } = await reader.read();
106
+ if (done)
107
+ break;
108
+ // `stream: true` keeps multi-byte characters intact across chunk
109
+ // boundaries — without it a headline can acquire a permanent U+FFFD.
110
+ for (const message of parser.push(decoder.decode(value, { stream: true }))) {
111
+ options.onMessage(message);
112
+ }
113
+ }
114
+ for (const message of parser.flush())
115
+ options.onMessage(message);
116
+ }
117
+ catch (error) {
118
+ if (options.signal?.aborted)
119
+ return;
120
+ throw error;
121
+ }
122
+ finally {
123
+ options.signal?.removeEventListener("abort", onAbort);
124
+ // Hand the body back: a locked reader pins the response's resources, and
125
+ // cancel is idempotent so an already-finished stream costs nothing.
126
+ // Both are guarded — a stream torn down mid-read can reject either call,
127
+ // and this is cleanup, never a reason to throw into a customer's page.
128
+ await reader.cancel().catch(() => { });
129
+ try {
130
+ reader.releaseLock();
131
+ }
132
+ catch {
133
+ /* reads still pending — the lock releases with them */
134
+ }
135
+ }
136
+ }
137
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"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"}