@proveanything/smartlinks 2.0.6 → 2.0.10

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 (179) hide show
  1. package/dist/api/ai.d.ts +1 -1
  2. package/dist/api/ai.js +1 -1
  3. package/dist/api/analytics.d.ts +1 -1
  4. package/dist/api/analytics.js +1 -1
  5. package/dist/api/appConfiguration.d.ts +3 -3
  6. package/dist/api/appConfiguration.js +3 -3
  7. package/dist/api/appObjects.d.ts +1 -1
  8. package/dist/api/appObjects.js +1 -1
  9. package/dist/api/asset.d.ts +1 -1
  10. package/dist/api/asset.js +2 -2
  11. package/dist/api/async.d.ts +1 -1
  12. package/dist/api/async.js +1 -1
  13. package/dist/api/attestation.d.ts +1 -1
  14. package/dist/api/attestation.js +1 -1
  15. package/dist/api/attestations.d.ts +1 -1
  16. package/dist/api/attestations.js +1 -1
  17. package/dist/api/auth.d.ts +2 -2
  18. package/dist/api/auth.js +2 -2
  19. package/dist/api/authKit.d.ts +1 -1
  20. package/dist/api/authKit.js +1 -1
  21. package/dist/api/batch.d.ts +1 -1
  22. package/dist/api/batch.js +1 -1
  23. package/dist/api/broadcasts.d.ts +2 -2
  24. package/dist/api/broadcasts.js +1 -1
  25. package/dist/api/claimSet.d.ts +1 -1
  26. package/dist/api/claimSet.js +1 -1
  27. package/dist/api/collection.d.ts +1 -1
  28. package/dist/api/collection.js +1 -1
  29. package/dist/api/comms.d.ts +15 -15
  30. package/dist/api/comms.js +1 -1
  31. package/dist/api/config.d.ts +1 -1
  32. package/dist/api/config.js +1 -1
  33. package/dist/api/contact.d.ts +1 -1
  34. package/dist/api/contact.js +1 -1
  35. package/dist/api/containers.d.ts +1 -1
  36. package/dist/api/containers.js +1 -1
  37. package/dist/api/crate.d.ts +1 -1
  38. package/dist/api/crate.js +1 -1
  39. package/dist/api/facets.d.ts +1 -1
  40. package/dist/api/facets.js +1 -1
  41. package/dist/api/form.js +1 -1
  42. package/dist/api/http.js +1 -1
  43. package/dist/api/index.d.ts +46 -46
  44. package/dist/api/index.js +46 -46
  45. package/dist/api/integrations.d.ts +1 -1
  46. package/dist/api/integrations.js +1 -1
  47. package/dist/api/interactions.d.ts +1 -1
  48. package/dist/api/interactions.js +1 -1
  49. package/dist/api/jobs.d.ts +1 -1
  50. package/dist/api/jobs.js +1 -1
  51. package/dist/api/journeys.d.ts +1 -1
  52. package/dist/api/journeys.js +1 -1
  53. package/dist/api/journeysAnalytics.d.ts +1 -1
  54. package/dist/api/journeysAnalytics.js +1 -1
  55. package/dist/api/location.d.ts +1 -1
  56. package/dist/api/location.js +1 -1
  57. package/dist/api/lots.d.ts +1 -1
  58. package/dist/api/lots.js +1 -1
  59. package/dist/api/loyalty.d.ts +1 -1
  60. package/dist/api/loyalty.js +1 -1
  61. package/dist/api/navigation.d.ts +1 -1
  62. package/dist/api/navigation.js +1 -1
  63. package/dist/api/nfc.d.ts +1 -1
  64. package/dist/api/nfc.js +1 -1
  65. package/dist/api/order.d.ts +1 -1
  66. package/dist/api/order.js +1 -1
  67. package/dist/api/product.d.ts +1 -1
  68. package/dist/api/product.js +1 -1
  69. package/dist/api/products.d.ts +1 -1
  70. package/dist/api/products.js +1 -1
  71. package/dist/api/proof.d.ts +1 -1
  72. package/dist/api/proof.js +1 -1
  73. package/dist/api/qr.d.ts +1 -1
  74. package/dist/api/qr.js +1 -1
  75. package/dist/api/realtime.d.ts +1 -1
  76. package/dist/api/realtime.js +1 -1
  77. package/dist/api/research.d.ts +1 -1
  78. package/dist/api/research.js +1 -1
  79. package/dist/api/secrets.d.ts +1 -1
  80. package/dist/api/secrets.js +1 -1
  81. package/dist/api/segments.d.ts +1 -1
  82. package/dist/api/segments.js +1 -1
  83. package/dist/api/sequence.js +1 -1
  84. package/dist/api/tags.d.ts +1 -1
  85. package/dist/api/tags.js +1 -1
  86. package/dist/api/template.d.ts +1 -1
  87. package/dist/api/template.js +1 -1
  88. package/dist/api/translations.d.ts +1 -1
  89. package/dist/api/translations.js +2 -2
  90. package/dist/api/variant.d.ts +1 -1
  91. package/dist/api/variant.js +1 -1
  92. package/dist/containers/types.d.ts +1 -1
  93. package/dist/docs/API_SUMMARY.md +7 -7
  94. package/dist/docs/agent-tools.md +26 -14
  95. package/dist/docs/ai.md +14 -520
  96. package/dist/docs/analytics.md +41 -2
  97. package/dist/docs/app-data-storage.md +0 -38
  98. package/dist/docs/app-manifest.md +106 -7
  99. package/dist/docs/app-objects.md +0 -148
  100. package/dist/docs/app-records-pattern.md +2 -2
  101. package/dist/docs/building-react-components.md +6 -14
  102. package/dist/docs/caching.md +20 -21
  103. package/dist/docs/container-tracking.md +2 -0
  104. package/dist/docs/containers.md +14 -66
  105. package/dist/docs/executor.md +4 -4
  106. package/dist/docs/host-dependency-contract.md +27 -0
  107. package/dist/docs/iframe-responder.md +308 -0
  108. package/dist/docs/item-context.md +0 -2
  109. package/dist/docs/manifests.md +3 -3
  110. package/dist/docs/mobile-admin-container.md +4 -4
  111. package/dist/docs/mpa.md +5 -5
  112. package/dist/docs/native-facade.md +1 -1
  113. package/dist/docs/overview.md +35 -16
  114. package/dist/docs/portal-back-button.md +2 -3
  115. package/dist/docs/widgets.md +11 -69
  116. package/dist/http.d.ts +24 -8
  117. package/dist/http.js +32 -14
  118. package/dist/iframe.d.ts +2 -2
  119. package/dist/iframe.js +1 -1
  120. package/dist/iframeResponder.d.ts +7 -1
  121. package/dist/iframeResponder.js +45 -4
  122. package/dist/index.d.ts +30 -27
  123. package/dist/index.js +10 -8
  124. package/dist/mobile-admin/errors.d.ts +1 -1
  125. package/dist/mobile-admin/types.d.ts +2 -2
  126. package/dist/openapi.yaml +12 -0
  127. package/dist/shared-dependencies.d.ts +37 -0
  128. package/dist/shared-dependencies.js +79 -0
  129. package/dist/testing/index.d.ts +1 -1
  130. package/dist/translationCache.d.ts +1 -1
  131. package/dist/types/appManifest.d.ts +23 -0
  132. package/dist/types/broadcasts.d.ts +1 -1
  133. package/dist/types/collection.d.ts +2 -2
  134. package/dist/types/comms.d.ts +5 -5
  135. package/dist/types/contact.d.ts +1 -1
  136. package/dist/types/facets.d.ts +1 -1
  137. package/dist/types/iframeResponder.d.ts +3 -3
  138. package/dist/types/index.d.ts +44 -44
  139. package/dist/types/index.js +44 -44
  140. package/dist/types/interaction.d.ts +1 -1
  141. package/dist/types/itemContext.d.ts +1 -1
  142. package/dist/types/journeysAnalytics.d.ts +1 -1
  143. package/dist/types/navigation.d.ts +1 -1
  144. package/dist/types/product.d.ts +1 -1
  145. package/dist/types/proof.d.ts +1 -1
  146. package/dist/types/segments.d.ts +1 -1
  147. package/dist/types/widgets.d.ts +2 -2
  148. package/dist/utils/conditions.d.ts +1 -1
  149. package/dist/utils/index.d.ts +3 -3
  150. package/dist/utils/index.js +3 -3
  151. package/dist/utils/paths.d.ts +4 -4
  152. package/docs/API_SUMMARY.md +7 -7
  153. package/docs/agent-tools.md +26 -14
  154. package/docs/ai.md +14 -520
  155. package/docs/analytics.md +41 -2
  156. package/docs/app-data-storage.md +0 -38
  157. package/docs/app-manifest.md +106 -7
  158. package/docs/app-objects.md +0 -148
  159. package/docs/app-records-pattern.md +2 -2
  160. package/docs/building-react-components.md +6 -14
  161. package/docs/caching.md +20 -21
  162. package/docs/container-tracking.md +2 -0
  163. package/docs/containers.md +14 -66
  164. package/docs/executor.md +4 -4
  165. package/docs/host-dependency-contract.md +27 -0
  166. package/docs/iframe-responder.md +308 -0
  167. package/docs/item-context.md +0 -2
  168. package/docs/mobile-admin-container.md +4 -4
  169. package/docs/mpa.md +5 -5
  170. package/docs/native-facade.md +1 -1
  171. package/docs/overview.md +35 -16
  172. package/docs/portal-back-button.md +2 -3
  173. package/docs/widgets.md +11 -69
  174. package/openapi.yaml +12 -0
  175. package/package.json +17 -6
  176. package/scripts/doctor.mjs +171 -0
  177. package/docs/analytics-metadata-conventions.md +0 -88
  178. package/docs/iframe-streaming-parent-changes.md +0 -308
  179. package/docs/manifests.md +0 -204
package/package.json CHANGED
@@ -1,37 +1,48 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.6",
3
+ "version": "2.0.10",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
+ "type": "module",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "exports": {
8
9
  ".": {
9
10
  "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
10
12
  "default": "./dist/index.js"
11
13
  },
12
14
  "./testing": {
13
15
  "types": "./dist/testing/index.d.ts",
16
+ "import": "./dist/testing/index.js",
14
17
  "default": "./dist/testing/index.js"
15
- }
18
+ },
19
+ "./*": {
20
+ "types": "./dist/*.d.ts",
21
+ "import": "./dist/*.js",
22
+ "default": "./dist/*.js"
23
+ },
24
+ "./package.json": "./package.json"
16
25
  },
17
26
  "bin": {
18
27
  "smartlinks-register-release": "scripts/register-release.mjs",
19
- "smartlinks-publish": "scripts/publish.mjs"
28
+ "smartlinks-publish": "scripts/publish.mjs",
29
+ "smartlinks-doctor": "scripts/doctor.mjs"
20
30
  },
21
31
  "files": [
22
32
  "dist/",
23
33
  "docs/",
24
34
  "scripts/register-release.mjs",
25
35
  "scripts/publish.mjs",
36
+ "scripts/doctor.mjs",
26
37
  "openapi.yaml",
27
38
  "README.md"
28
39
  ],
29
40
  "scripts": {
30
41
  "test": "node test/run.cjs",
31
- "build": "tsc && node generate-api-summary.js && node generate-openapi.js && node scripts/copy-docs-to-dist.js",
42
+ "build": "tsc && node scripts/fix-esm-extensions.mjs && node generate-api-summary.cjs && node generate-openapi.cjs && node scripts/copy-docs-to-dist.cjs",
32
43
  "docs": "typedoc",
33
- "docs:summary": "node generate-api-summary.js",
34
- "docs:openapi": "node generate-openapi.js",
44
+ "docs:summary": "node generate-api-summary.cjs",
45
+ "docs:openapi": "node generate-openapi.cjs",
35
46
  "build:docs": "tsc build-docs.ts --outDir dist && node dist/build-docs.js",
36
47
  "prepublishOnly": "npm run build"
37
48
  },
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // smartlinks doctor — verify an app's ESM bundles against the shared-dependency
4
+ // contract, so a modern (esm/dual) app can't ship a bare import the host won't
5
+ // resolve.
6
+ //
7
+ // A correctly-built externalized ESM bundle inlines everything EXCEPT the host's
8
+ // shared singletons. So every *bare* import left in the output must be a contract
9
+ // entry — anything else will either fail to resolve through the import map at
10
+ // runtime, or silently pull in a second copy (the duplicate-React class of bug).
11
+ //
12
+ // Usage:
13
+ // smartlinks-doctor [appDir] # defaults to cwd
14
+ // npx @proveanything/smartlinks doctor (once a unified `smartlinks` bin exists)
15
+ //
16
+ // Exit code: 0 = clean, 1 = violations (CI-friendly).
17
+ // =============================================================================
18
+
19
+ import { readFileSync, existsSync } from 'node:fs';
20
+ import { resolve, dirname, join } from 'node:path';
21
+ import { fileURLToPath, pathToFileURL } from 'node:url';
22
+
23
+ const here = dirname(fileURLToPath(import.meta.url));
24
+
25
+ // Read the contract from THIS SDK build — one source of truth with the runtime.
26
+ const contractUrl = pathToFileURL(resolve(here, '../dist/shared-dependencies.js')).href;
27
+ const { SHARED_DEPENDENCY_SPECIFIERS, SHARED_DEPENDENCY_CONTRACT_VERSION } = await import(contractUrl);
28
+ const CONTRACT = new Set(SHARED_DEPENDENCY_SPECIFIERS);
29
+
30
+ const RED = '\x1b[31m';
31
+ const GREEN = '\x1b[32m';
32
+ const YELLOW = '\x1b[33m';
33
+ const DIM = '\x1b[2m';
34
+ const BOLD = '\x1b[1m';
35
+ const RESET = '\x1b[0m';
36
+
37
+ function die(msg) {
38
+ console.error(`${RED}smartlinks doctor: ${msg}${RESET}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ const appDir = resolve(process.argv[2] || process.cwd());
43
+ if (!existsSync(appDir)) die(`app directory not found: ${appDir}`);
44
+
45
+ // Locate the manifest.
46
+ const manifestPath = ['public/app.manifest.json', 'app.manifest.json', 'dist/app.manifest.json']
47
+ .map((p) => join(appDir, p))
48
+ .find(existsSync);
49
+ if (!manifestPath) die(`no app.manifest.json found under ${appDir} (looked in public/, ., dist/)`);
50
+
51
+ let manifest;
52
+ try {
53
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
54
+ } catch (e) {
55
+ die(`could not parse ${manifestPath}: ${e.message}`);
56
+ }
57
+ const manifestDir = dirname(manifestPath);
58
+ const meta = manifest.meta || {};
59
+
60
+ console.log(`${BOLD}SmartLinks doctor${RESET} ${DIM}— ${appDir}${RESET}`);
61
+ console.log(`${DIM}manifest:${RESET} ${manifestPath.replace(appDir + '\\', '').replace(appDir + '/', '')}`);
62
+ console.log(`${DIM}contract:${RESET} ${SHARED_DEPENDENCY_CONTRACT_VERSION} (${CONTRACT.size} shared deps)`);
63
+ console.log(`${DIM}moduleFormat:${RESET} ${meta.moduleFormat || '(absent → umd)'} · ${DIM}sharedDependencies:${RESET} ${meta.sharedDependencies || '(none)'}\n`);
64
+
65
+ const problems = [];
66
+ const warnings = [];
67
+
68
+ // Manifest-level sanity.
69
+ const format = meta.moduleFormat || 'umd';
70
+ const checkEsm = format === 'esm' || format === 'dual';
71
+ if (checkEsm && !meta.sharedDependencies) {
72
+ warnings.push(`meta.moduleFormat is "${format}" but meta.sharedDependencies is not declared — set it to "${SHARED_DEPENDENCY_CONTRACT_VERSION}".`);
73
+ }
74
+ if (meta.sharedDependencies && meta.sharedDependencies !== SHARED_DEPENDENCY_CONTRACT_VERSION) {
75
+ warnings.push(`built against contract ${meta.sharedDependencies}, but this SDK ships ${SHARED_DEPENDENCY_CONTRACT_VERSION} — host may not serve a matching import map.`);
76
+ }
77
+
78
+ // Pull the bare imports out of a bundle (handles minified `from"x"` too).
79
+ function bareImportsOf(code) {
80
+ const specs = new Set();
81
+ const patterns = [
82
+ /\bfrom\s*["']([^"']+)["']/g, // static import/export ... from "x"
83
+ /\bimport\s*["']([^"']+)["']/g, // side-effect import "x"
84
+ /\bimport\(\s*["']([^"']+)["']\s*\)/g, // dynamic import("x")
85
+ ];
86
+ for (const re of patterns) {
87
+ let m;
88
+ while ((m = re.exec(code))) {
89
+ const spec = m[1];
90
+ if (spec.startsWith('.') || spec.startsWith('/')) continue; // internal/relative
91
+ if (/^https?:/.test(spec)) continue; // absolute URL import
92
+ specs.add(spec);
93
+ }
94
+ }
95
+ return [...specs];
96
+ }
97
+
98
+ // Resolve a manifest-relative bundle path against the likely roots (dist paths
99
+ // are sometimes relative to the app root, sometimes to the manifest dir).
100
+ function resolveBundle(rel) {
101
+ const bases = [manifestDir, appDir, join(appDir, 'public'), join(appDir, 'dist')];
102
+ for (const b of bases) {
103
+ const p = resolve(b, rel);
104
+ if (existsSync(p)) return p;
105
+ }
106
+ return null;
107
+ }
108
+
109
+ // Check each ESM surface declared in the manifest.
110
+ const surfaces = [
111
+ ['widgets', manifest.widgets],
112
+ ['containers', manifest.containers],
113
+ ['mobileAdmin', manifest.mobileAdmin],
114
+ ];
115
+
116
+ let checkedAny = false;
117
+ for (const [name, block] of surfaces) {
118
+ const esm = block?.files?.js?.esm;
119
+ if (!esm) continue;
120
+
121
+ // UMD-format app that still declares an ESM bundle: the host won't load it, so
122
+ // it's dead weight and a common scaffold-confusion source — warn, don't fail.
123
+ if (!checkEsm) {
124
+ warnings.push(`${name}: ESM bundle "${esm}" is declared but meta.moduleFormat is "${format}", so the host never loads it. Set moduleFormat to "dual" to use it, or drop the esm entry.`);
125
+ continue;
126
+ }
127
+
128
+ checkedAny = true;
129
+ const bundlePath = resolveBundle(esm);
130
+ if (!bundlePath) {
131
+ console.log(`${RED}✗${RESET} ${name} ${DIM}(${esm})${RESET} — declared ESM bundle not found on disk`);
132
+ problems.push({ surface: name, spec: null, msg: `declared ESM bundle not found: ${esm}` });
133
+ continue;
134
+ }
135
+ const code = readFileSync(bundlePath, 'utf8');
136
+ const imports = bareImportsOf(code);
137
+ const offenders = imports.filter((s) => !CONTRACT.has(s));
138
+ const ok = imports.length - offenders.length;
139
+
140
+ if (offenders.length === 0) {
141
+ console.log(`${GREEN}✓${RESET} ${name} ${DIM}(${esm})${RESET} — ${ok} bare import${ok === 1 ? '' : 's'}, all in contract`);
142
+ } else {
143
+ console.log(`${RED}✗${RESET} ${name} ${DIM}(${esm})${RESET} — ${offenders.length} outside the contract:`);
144
+ for (const spec of offenders) {
145
+ const hint = spec.startsWith('node:')
146
+ ? 'node built-in — must not appear in a browser bundle'
147
+ : 'not host-provided — bundle it (don\'t externalize) or it will fail to resolve / double-load';
148
+ console.log(` ${RED}${spec}${RESET} ${DIM}— ${hint}${RESET}`);
149
+ problems.push({ surface: name, spec, msg: hint });
150
+ }
151
+ }
152
+ }
153
+
154
+ if (checkEsm && !checkedAny) {
155
+ warnings.push(`moduleFormat is "${format}" but no surface declares files.js.esm — nothing to check.`);
156
+ }
157
+ if (!checkEsm && problems.length === 0) {
158
+ console.log(`${DIM}moduleFormat "${format}" — ESM path not in use; UMD bundles resolve shared deps from window globals.${RESET}`);
159
+ }
160
+
161
+ console.log('');
162
+ for (const w of warnings) console.log(`${YELLOW}⚠ ${w}${RESET}`);
163
+
164
+ if (problems.length) {
165
+ console.log(`\n${RED}${BOLD}FAIL${RESET} — ${problems.length} problem${problems.length === 1 ? '' : 's'} outside the shared-dependency contract.`);
166
+ console.log(`${DIM}Externalize only the ${CONTRACT.size} contract specifiers (import { SHARED_DEPENDENCY_SPECIFIERS } from '@proveanything/smartlinks'); bundle everything else.${RESET}`);
167
+ process.exit(1);
168
+ }
169
+
170
+ console.log(`${GREEN}${BOLD}OK${RESET} — bundles conform to shared-dependency contract ${SHARED_DEPENDENCY_CONTRACT_VERSION}.`);
171
+ process.exit(0);
@@ -1,88 +0,0 @@
1
- # Analytics Metadata Conventions
2
-
3
- Use these as the recommended standard analytics keys.
4
-
5
- Some of these are now promoted top-level analytics fields. Others remain good metadata keys for custom dimensions.
6
-
7
- ---
8
-
9
- ## Recommended Keys
10
-
11
- ### Promoted top-level fields
12
-
13
- - `visitorId`
14
- - `referrerHost`
15
- - `entryType`
16
- - `pageId`
17
- - `scanMethod`
18
- - `source` - collection/web-events only. Free-form client app identifier, e.g. `'portal'`, `'hub'`. No enum/whitelist. Not available on tag events - see the `analytics.tag.track(event)` section in [docs/analytics.md](analytics.md) for why.
19
- - `redirectMode` - tag-events only. Usually server-written; only set this yourself if you're logging a redirect-style event.
20
-
21
- These should be sent as top-level analytics fields, not inside `metadata`.
22
-
23
- ### Metadata-friendly keys
24
-
25
- - `referrer`
26
- - `utmSource`
27
- - `utmMedium`
28
- - `utmCampaign`
29
- - `utmContent`
30
- - `utmTerm`
31
- - `group`
32
- - `tag`
33
- - `campaign`
34
- - `placement`
35
- - `linkGroup`
36
- - `linkPlacement`
37
- - `linkPosition`
38
- - `linkTitle`
39
- - `destinationDomain`
40
- - `pagePath`
41
- - `qrCodeId`
42
-
43
- ---
44
-
45
- ## Why These Matter
46
-
47
- These keys give teams a shared vocabulary for:
48
-
49
- - inbound traffic attribution
50
- - outbound link analysis
51
- - link placement and link-tree performance
52
- - QR and page-level traffic grouping
53
- - physical scan source analysis
54
-
55
- ---
56
-
57
- ## Recommendation
58
-
59
- - Treat these as reserved standard keys.
60
- - Prefer these names before inventing custom alternatives.
61
- - Send promoted fields at top level.
62
- - Keep values flat and scalar where possible so they are easier to filter and break down later.
63
- - Promote a field to a first-class backend column only when it becomes a hot platform-wide dimension.
64
- - Note: `source` (the event column) and the query-time `source` parameter (`'events' | 'tag'`, which table to query) are unrelated fields that happen to share a name. When filtering by the `source` column, use the plural `sources` array - there is no singular `source` filter, precisely to avoid colliding with the table selector.
65
-
66
- ---
67
-
68
- ## Example
69
-
70
- ```typescript
71
- analytics.collection.track({
72
- sessionId: 1234567890,
73
- eventType: 'click_link',
74
- collectionId: 'demo-collection',
75
- visitorId: 'visitor_123',
76
- linkId: 'hero-cta',
77
- href: 'https://example.com/buy',
78
- referrerHost: 'instagram.com',
79
- placement: 'hero',
80
- campaign: 'summer-launch',
81
- utmSource: 'email',
82
- pageId: 'QR123',
83
- source: 'portal',
84
- metadata: {
85
- pagePath: '/c/demo-collection',
86
- },
87
- })
88
- ```
@@ -1,308 +0,0 @@
1
- # Iframe Streaming Parent Changes
2
-
3
- This note describes the parent-side changes needed to support AI streaming when an embedded SmartLinks app is running in iframe proxy mode.
4
-
5
- If you are using the SDK `IframeResponder` directly, this is already implemented in the SDK changes. You only need this document if your parent application has its own iframe proxy handler and does not rely on `IframeResponder`.
6
-
7
- ## Goal
8
-
9
- Keep the existing architecture:
10
-
11
- - local mode: child calls API directly
12
- - iframe proxy mode: child never owns auth state and streams through the parent
13
-
14
- This keeps user/session authority in the parent while making AI streaming behave like the rest of the SDK transport.
15
-
16
- ## What changed
17
-
18
- Previously, proxy mode only supported one-shot request/response messages:
19
-
20
- - `_smartlinksProxyRequest`
21
- - `_smartlinksProxyResponse`
22
-
23
- Streaming now adds a second protocol for long-lived responses:
24
-
25
- - `_smartlinksProxyStreamRequest`
26
- - `_smartlinksProxyStream`
27
- - `_smartlinksProxyStreamAbort`
28
-
29
- ## New parent message handling
30
-
31
- ### 1. Listen for stream requests
32
-
33
- The iframe child may now send this message:
34
-
35
- ```ts
36
- {
37
- _smartlinksProxyStreamRequest: true,
38
- id: string,
39
- method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
40
- path: string,
41
- body?: any,
42
- headers?: Record<string, string>
43
- }
44
- ```
45
-
46
- Parent behavior:
47
-
48
- - treat this like a proxied API request
49
- - build the real API URL from your configured base URL plus `path`
50
- - send the request using the parent's current auth/session context
51
- - expect an SSE / streaming response body
52
- - keep the request open until the stream ends or is aborted
53
-
54
- ### 2. Forward stream lifecycle messages back to the child
55
-
56
- The parent should send messages back to the iframe using this envelope:
57
-
58
- ```ts
59
- {
60
- _smartlinksProxyStream: true,
61
- id: string,
62
- phase: 'open' | 'event' | 'end' | 'error',
63
- data?: any,
64
- error?: string,
65
- status?: number
66
- }
67
- ```
68
-
69
- Phases:
70
-
71
- - `open`
72
- - optional but recommended
73
- - indicates the upstream streaming request was accepted and a body exists
74
- - `event`
75
- - contains one parsed JSON event from an SSE `data:` frame
76
- - send one message per logical event payload
77
- - `end`
78
- - sent once when the stream finishes normally
79
- - `error`
80
- - sent if the upstream request fails before or during streaming
81
-
82
- ### 3. Support abort from the child
83
-
84
- The child may stop reading early and send:
85
-
86
- ```ts
87
- {
88
- _smartlinksProxyStreamAbort: true,
89
- id: string
90
- }
91
- ```
92
-
93
- Parent behavior:
94
-
95
- - look up the active stream by `id`
96
- - abort the underlying fetch / reader
97
- - clean up any local state for that stream
98
- - do not keep streaming after abort
99
-
100
- ## SSE forwarding rules
101
-
102
- The upstream AI endpoints return SSE-like frames. The parent should:
103
-
104
- - read the response body as a stream
105
- - buffer text until line boundaries
106
- - collect `data:` lines for a single event
107
- - join multi-line `data:` payloads with `\n`
108
- - ignore blank events
109
- - stop on `data: [DONE]`
110
- - JSON-parse each event payload
111
- - forward parsed payloads to the iframe as `_smartlinksProxyStream` with `phase: 'event'`
112
-
113
- Minimal parsing behavior:
114
-
115
- 1. accumulate bytes into text
116
- 2. split on `\r?\n`
117
- 3. collect each `data:` line
118
- 4. on blank line, finalize the event
119
- 5. if payload is `[DONE]`, finish
120
- 6. otherwise `JSON.parse(payload)` and forward
121
-
122
- ## Auth and session expectations
123
-
124
- The parent remains the source of truth for auth.
125
-
126
- That means the parent stream handler should:
127
-
128
- - use the same auth headers/token source as normal proxied requests
129
- - not require the iframe to know the bearer token or API key
130
- - naturally pick up the current logged-in user when the stream starts
131
- - cancel active streams if your app invalidates session state on logout or account switch
132
-
133
- In practice, the stream request should use the same header-building logic as your normal parent proxy transport.
134
-
135
- ## Error handling expectations
136
-
137
- If the upstream fetch returns a non-2xx status:
138
-
139
- - try to read the JSON error body
140
- - derive a useful message
141
- - send one `_smartlinksProxyStream` message with `phase: 'error'`
142
- - include `status` when available
143
- - do not send `end` afterward
144
-
145
- If the stream body is missing unexpectedly:
146
-
147
- - send `phase: 'error'`
148
-
149
- If JSON parsing fails for a single event chunk:
150
-
151
- - safest behavior is to ignore that malformed chunk and continue
152
-
153
- ## State the parent should keep
154
-
155
- Track active streams in a map keyed by `id`:
156
-
157
- ```ts
158
- Map<string, AbortController>
159
- ```
160
-
161
- Recommended cleanup points:
162
-
163
- - on normal stream end
164
- - on error
165
- - on child abort
166
- - on iframe detach/unmount
167
- - on parent auth reset/logout if you want all in-flight streams cancelled immediately
168
-
169
- ## Parent implementation outline
170
-
171
- ```ts
172
- const activeStreams = new Map<string, AbortController>()
173
-
174
- window.addEventListener('message', async (event) => {
175
- const msg = event.data
176
-
177
- if (msg?._smartlinksProxyStreamAbort && msg.id) {
178
- activeStreams.get(msg.id)?.abort()
179
- activeStreams.delete(msg.id)
180
- return
181
- }
182
-
183
- if (msg?._smartlinksProxyStreamRequest && msg.id) {
184
- const controller = new AbortController()
185
- activeStreams.set(msg.id, controller)
186
-
187
- try {
188
- const response = await fetch(buildUrl(msg.path), {
189
- method: msg.method,
190
- headers: msg.headers,
191
- body: msg.body ? JSON.stringify(msg.body) : undefined,
192
- signal: controller.signal,
193
- })
194
-
195
- if (!response.ok || !response.body) {
196
- postError(...)
197
- return
198
- }
199
-
200
- postOpen(...)
201
- await forwardSse(response.body, parsed => postEvent(...parsed))
202
- postEnd(...)
203
- } catch (err) {
204
- if (err?.name !== 'AbortError') postError(...)
205
- } finally {
206
- activeStreams.delete(msg.id)
207
- }
208
- }
209
- })
210
- ```
211
-
212
- ## Exact protocol summary
213
-
214
- ### Child → parent
215
-
216
- Standard stream request:
217
-
218
- ```ts
219
- {
220
- _smartlinksProxyStreamRequest: true,
221
- id,
222
- method,
223
- path,
224
- body,
225
- headers
226
- }
227
- ```
228
-
229
- Abort request:
230
-
231
- ```ts
232
- {
233
- _smartlinksProxyStreamAbort: true,
234
- id
235
- }
236
- ```
237
-
238
- ### Parent → child
239
-
240
- Open:
241
-
242
- ```ts
243
- {
244
- _smartlinksProxyStream: true,
245
- id,
246
- phase: 'open'
247
- }
248
- ```
249
-
250
- Event:
251
-
252
- ```ts
253
- {
254
- _smartlinksProxyStream: true,
255
- id,
256
- phase: 'event',
257
- data: parsedJsonEvent
258
- }
259
- ```
260
-
261
- End:
262
-
263
- ```ts
264
- {
265
- _smartlinksProxyStream: true,
266
- id,
267
- phase: 'end'
268
- }
269
- ```
270
-
271
- Error:
272
-
273
- ```ts
274
- {
275
- _smartlinksProxyStream: true,
276
- id,
277
- phase: 'error',
278
- error: 'message',
279
- status?: number
280
- }
281
- ```
282
-
283
- ## What does not change
284
-
285
- These parts of the parent iframe integration stay the same:
286
-
287
- - normal `_smartlinksProxyRequest` request/response flow
288
- - upload proxy flow
289
- - auth login/logout postMessage handling
290
- - route/deep-link handling
291
- - resize handling
292
-
293
- This is an additive protocol, not a replacement.
294
-
295
- ## Current SDK reference
296
-
297
- The SDK implementation lives in:
298
-
299
- - [src/http.ts](src/http.ts)
300
- - [src/iframeResponder.ts](src/iframeResponder.ts)
301
- - [src/types/iframeResponder.ts](src/types/iframeResponder.ts)
302
- - [src/api/ai.ts](src/api/ai.ts)
303
-
304
- ## Practical recommendation
305
-
306
- If your parent already uses `IframeResponder`, prefer upgrading to the SDK version with these changes instead of re-implementing the protocol manually.
307
-
308
- If your parent has a custom iframe bridge, implement exactly the three new message types above and reuse your existing auth/header logic from normal proxied requests.