@proveanything/smartlinks 1.7.3 → 1.7.5

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.
@@ -106,7 +106,7 @@ export declare namespace comms {
106
106
  * No broadcast record is created. The send is logged to the contact's
107
107
  * communication history with sourceType: 'transactional'.
108
108
  *
109
- * POST /admin/collection/:collectionId/comm/send
109
+ * POST /admin/collection/:collectionId/comm.send
110
110
  *
111
111
  * @example
112
112
  * ```typescript
package/dist/api/comms.js CHANGED
@@ -201,7 +201,7 @@ export var comms;
201
201
  * No broadcast record is created. The send is logged to the contact's
202
202
  * communication history with sourceType: 'transactional'.
203
203
  *
204
- * POST /admin/collection/:collectionId/comm/send
204
+ * POST /admin/collection/:collectionId/comm.send
205
205
  *
206
206
  * @example
207
207
  * ```typescript
@@ -222,7 +222,7 @@ export var comms;
222
222
  * ```
223
223
  */
224
224
  async function sendTransactional(collectionId, body) {
225
- const path = `/admin/collection/${encodeURIComponent(collectionId)}/comm/send`;
225
+ const path = `/admin/collection/${encodeURIComponent(collectionId)}/comm.send`;
226
226
  return post(path, body);
227
227
  }
228
228
  comms.sendTransactional = sendTransactional;
@@ -66,4 +66,42 @@ export declare namespace proof {
66
66
  * GET /admin/collection/:collectionId/product/:productId/batch/:batchId/proof
67
67
  */
68
68
  function getByBatch(collectionId: string, productId: string, batchId: string): Promise<ProofResponse[]>;
69
+ /**
70
+ * Migrate a proof to a different product within the same collection (admin only).
71
+ *
72
+ * Because the Firestore ledger document ID is `{productId}-{proofId}`, a proof
73
+ * cannot simply be re-assigned to another product by updating a field — the
74
+ * document must be re-keyed. This endpoint handles that atomically:
75
+ *
76
+ * 1. Reads the source ledger document (`{sourceProductId}-{proofId}`).
77
+ * 2. Writes a new document (`{targetProductId}-{proofId}`) with `productId`
78
+ * and `proofGroup` updated. The short `proofId` (nanoid) is unchanged.
79
+ * 3. Writes a migration history entry to the new document's `history`
80
+ * subcollection (snapshot of the original proof + migration metadata).
81
+ * 4. Copies all subcollections — `assets`, `attestations`, `history` — from
82
+ * the old document to the new one.
83
+ * 5. Deletes the old subcollections and then the old document.
84
+ *
85
+ * Repeated migrations are safe — each one appends a history record; no
86
+ * migration metadata is stored on the proof document itself.
87
+ *
88
+ * @param collectionId - Identifier of the parent collection
89
+ * @param productId - Current (source) product ID that owns the proof
90
+ * @param proofId - Identifier of the proof to migrate
91
+ * @param data - `{ targetProductId }` — the destination product
92
+ * @returns The migrated proof object (now owned by `targetProductId`)
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const migrated = await proof.migrate('coll_123', 'prod_old', 'proof_abc', {
97
+ * targetProductId: 'prod_new',
98
+ * })
99
+ * console.log(migrated.productId) // 'prod_new'
100
+ * ```
101
+ */
102
+ function migrate(collectionId: string, productId: string, proofId: string,
103
+ /** The destination product ID */
104
+ data: {
105
+ targetProductId: string;
106
+ }): Promise<ProofResponse>;
69
107
  }
package/dist/api/proof.js CHANGED
@@ -116,4 +116,44 @@ export var proof;
116
116
  return request(path);
117
117
  }
118
118
  proof.getByBatch = getByBatch;
119
+ /**
120
+ * Migrate a proof to a different product within the same collection (admin only).
121
+ *
122
+ * Because the Firestore ledger document ID is `{productId}-{proofId}`, a proof
123
+ * cannot simply be re-assigned to another product by updating a field — the
124
+ * document must be re-keyed. This endpoint handles that atomically:
125
+ *
126
+ * 1. Reads the source ledger document (`{sourceProductId}-{proofId}`).
127
+ * 2. Writes a new document (`{targetProductId}-{proofId}`) with `productId`
128
+ * and `proofGroup` updated. The short `proofId` (nanoid) is unchanged.
129
+ * 3. Writes a migration history entry to the new document's `history`
130
+ * subcollection (snapshot of the original proof + migration metadata).
131
+ * 4. Copies all subcollections — `assets`, `attestations`, `history` — from
132
+ * the old document to the new one.
133
+ * 5. Deletes the old subcollections and then the old document.
134
+ *
135
+ * Repeated migrations are safe — each one appends a history record; no
136
+ * migration metadata is stored on the proof document itself.
137
+ *
138
+ * @param collectionId - Identifier of the parent collection
139
+ * @param productId - Current (source) product ID that owns the proof
140
+ * @param proofId - Identifier of the proof to migrate
141
+ * @param data - `{ targetProductId }` — the destination product
142
+ * @returns The migrated proof object (now owned by `targetProductId`)
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const migrated = await proof.migrate('coll_123', 'prod_old', 'proof_abc', {
147
+ * targetProductId: 'prod_new',
148
+ * })
149
+ * console.log(migrated.productId) // 'prod_new'
150
+ * ```
151
+ */
152
+ async function migrate(collectionId, productId, proofId,
153
+ /** The destination product ID */
154
+ data) {
155
+ const path = `/admin/collection/${encodeURIComponent(collectionId)}/products/${encodeURIComponent(productId)}/proofs/${encodeURIComponent(proofId)}/migrate`;
156
+ return post(path, data);
157
+ }
158
+ proof.migrate = migrate;
119
159
  })(proof || (proof = {}));
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.7.3 | Generated: 2026-03-04T13:56:12.543Z
3
+ Version: 1.7.5 | Generated: 2026-03-10T14:23:18.789Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -3006,7 +3006,7 @@ export interface SubscriptionsResolveResponse {
3006
3006
  * No broadcast record is created; the send is logged directly to the
3007
3007
  * contact's communication history with sourceType: 'transactional'.
3008
3008
  *
3009
- * POST /admin/collection/:collectionId/comm/send
3009
+ * POST /admin/collection/:collectionId/comm.send
3010
3010
  */
3011
3011
  export interface TransactionalSendRequest {
3012
3012
  /** CRM contact UUID */
@@ -5978,7 +5978,7 @@ Analytics: Recipients who performed an action, optionally with outcome. POST /ad
5978
5978
 
5979
5979
  **sendTransactional**(collectionId: string,
5980
5980
  body: TransactionalSendRequest) → `Promise<TransactionalSendResult>`
5981
- Send a single transactional message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with sourceType: 'transactional'. POST /admin/collection/:collectionId/comm/send ```typescript const result = await comms.sendTransactional(collectionId, { contactId: 'e4f2a1b0-...', templateId: 'warranty-update', channel: 'preferred', props: { claimRef: 'CLM-0042', decision: 'approved' }, include: { productId: 'prod-abc123', appCase: 'c9d1e2f3-...' }, ref: 'warranty-decision-notification', appId: 'warrantyApp', }) if (result.ok) { console.log(`Sent via ${result.channel}`, result.messageId) } else { console.error('Send failed:', result.error) } ```
5981
+ Send a single transactional message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with sourceType: 'transactional'. POST /admin/collection/:collectionId/comm.send ```typescript const result = await comms.sendTransactional(collectionId, { contactId: 'e4f2a1b0-...', templateId: 'warranty-update', channel: 'preferred', props: { claimRef: 'CLM-0042', decision: 'approved' }, include: { productId: 'prod-abc123', appCase: 'c9d1e2f3-...' }, ref: 'warranty-decision-notification', appId: 'warrantyApp', }) if (result.ok) { console.log(`Sent via ${result.channel}`, result.messageId) } else { console.error('Send failed:', result.error) } ```
5982
5982
 
5983
5983
  **logCommunicationEvent**(collectionId: string,
5984
5984
  body: LogCommunicationEventBody) → `Promise<AppendResult>`
@@ -6470,6 +6470,13 @@ Find proofs for a product (admin only). POST /admin/collection/:collectionId/pro
6470
6470
  batchId: string) → `Promise<ProofResponse[]>`
6471
6471
  Get proofs for a batch (admin only). GET /admin/collection/:collectionId/product/:productId/batch/:batchId/proof
6472
6472
 
6473
+ **migrate**(collectionId: string,
6474
+ productId: string,
6475
+ proofId: string,
6476
+ /** The destination product ID */
6477
+ data: { targetProductId: string }) → `Promise<ProofResponse>`
6478
+ Migrate a proof to a different product within the same collection (admin only). Because the Firestore ledger document ID is `{productId}-{proofId}`, a proof cannot simply be re-assigned to another product by updating a field — the document must be re-keyed. This endpoint handles that atomically: 1. Reads the source ledger document (`{sourceProductId}-{proofId}`). 2. Writes a new document (`{targetProductId}-{proofId}`) with `productId` and `proofGroup` updated. The short `proofId` (nanoid) is unchanged. 3. Writes a migration history entry to the new document's `history` subcollection (snapshot of the original proof + migration metadata). 4. Copies all subcollections — `assets`, `attestations`, `history` — from the old document to the new one. 5. Deletes the old subcollections and then the old document. Repeated migrations are safe — each one appends a history record; no migration metadata is stored on the proof document itself. ```typescript const migrated = await proof.migrate('coll_123', 'prod_old', 'proof_abc', { targetProductId: 'prod_new', }) console.log(migrated.productId) // 'prod_new' ```
6479
+
6473
6480
  ### qr
6474
6481
 
6475
6482
  **lookupShortCode**(shortId: string, code: string) → `Promise<QrShortCodeLookupResponse>`
package/dist/docs/ai.md CHANGED
@@ -19,6 +19,7 @@ Complete guide to using AI capabilities in the SmartLinks SDK, including chat co
19
19
  - [Error Handling](#error-handling)
20
20
  - [Rate Limiting](#rate-limiting)
21
21
  - [Best Practices](#best-practices)
22
+ - [Providing Content to the AI Assistant](#providing-content-to-the-ai-assistant)
22
23
 
23
24
  ---
24
25
 
@@ -1547,6 +1548,294 @@ console.log('Estimated cost: $', cost.toFixed(4));
1547
1548
 
1548
1549
  ---
1549
1550
 
1551
+ ## Providing Content to the AI Assistant
1552
+
1553
+ The portal's built-in AI assistant can discuss the content currently visible to the user. To make this work, your app must supply contextual content when the assistant requests it. There are three extraction methods, tried in priority order:
1554
+
1555
+ | Priority | Method | When Used |
1556
+ |----------|--------|-----------|
1557
+ | 1 | **Direct prop callback** | Container/widget rendered in the parent React context |
1558
+ | 2 | **PostMessage protocol** | App rendered in an iframe |
1559
+ | 3 | **DOM text fallback** | Neither of the above responded |
1560
+
1561
+ This section covers **methods 1 and 2** — the ones you implement in your app.
1562
+
1563
+ ### Method 1: Direct Prop (`onRequestAIContent`)
1564
+
1565
+ When your app is rendered as a **container** (a direct component in the parent's React tree), the framework passes an `onRequestAIContent` prop to your exported component. You do **not** call this prop yourself — the framework calls it when the AI assistant needs context.
1566
+
1567
+ Structure your component to make current state accessible when the callback fires:
1568
+
1569
+ ```tsx
1570
+ import { useEffect, useRef } from 'react';
1571
+
1572
+ export function PublicContainer(props) {
1573
+ const { onRequestAIContent, appId, ...rest } = props;
1574
+
1575
+ // Keep a ref to the latest content so the callback always returns fresh data
1576
+ const currentContentRef = useRef(null);
1577
+
1578
+ useEffect(() => {
1579
+ currentContentRef.current = buildCurrentContent();
1580
+ }, [relevantState]);
1581
+
1582
+ return <div data-app-container={appId}>{/* your UI */}</div>;
1583
+ }
1584
+ ```
1585
+
1586
+ The framework registers the content provider internally via `useAIContentExtraction.registerContentProvider()`. Your component just needs to respond when the callback is invoked.
1587
+
1588
+ ### Method 2: PostMessage Protocol (Iframes)
1589
+
1590
+ For iframe-embedded apps the framework sends a `postMessage` request and expects a response within **500 ms**. If your app doesn't reply in time the framework falls back to DOM text extraction.
1591
+
1592
+ **Request sent by the framework to your iframe:**
1593
+
1594
+ ```typescript
1595
+ {
1596
+ type: 'smartlinks:request-ai-content',
1597
+ requestId: 'ai-content-1709834567890-abc123' // unique per request
1598
+ }
1599
+ ```
1600
+
1601
+ **Response your app must send back:**
1602
+
1603
+ ```typescript
1604
+ {
1605
+ type: 'smartlinks:ai-content-response',
1606
+ requestId: 'ai-content-1709834567890-abc123', // echo back the requestId
1607
+ content: AIContentResponse
1608
+ }
1609
+ ```
1610
+
1611
+ **Minimal implementation:**
1612
+
1613
+ ```typescript
1614
+ window.addEventListener('message', async (event) => {
1615
+ if (event.data?.type === 'smartlinks:request-ai-content') {
1616
+ const content = await gatherAIContent();
1617
+
1618
+ window.parent.postMessage({
1619
+ type: 'smartlinks:ai-content-response',
1620
+ requestId: event.data.requestId,
1621
+ content,
1622
+ }, '*');
1623
+ }
1624
+ });
1625
+
1626
+ async function gatherAIContent(): Promise<AIContentResponse> {
1627
+ return {
1628
+ text: 'The user is viewing the warranty registration form...',
1629
+ contentLabel: 'Warranty Registration',
1630
+ };
1631
+ }
1632
+ ```
1633
+
1634
+ ### The `AIContentResponse` Interface
1635
+
1636
+ ```typescript
1637
+ interface AIContentResponse {
1638
+ /**
1639
+ * Plain text or markdown for the AI to use as context.
1640
+ * Injected into the system prompt. Max recommended: ~4000 characters.
1641
+ */
1642
+ text: string;
1643
+
1644
+ /**
1645
+ * Optional structured metadata (key-value pairs).
1646
+ * Not used directly in prompts but available for custom providers.
1647
+ */
1648
+ metadata?: Record<string, unknown>;
1649
+
1650
+ /**
1651
+ * Pre-built RAG configuration. When provided, the assistant can use
1652
+ * SL.ai.public.chat() to ground answers in indexed product documents.
1653
+ */
1654
+ ragHint?: {
1655
+ /** The product ID whose indexed documents should be queried */
1656
+ productId: string;
1657
+ /** Optional session ID for multi-turn RAG conversations */
1658
+ sessionId?: string;
1659
+ /** Optional context hint to help scope the RAG query */
1660
+ context?: string;
1661
+ };
1662
+
1663
+ /**
1664
+ * Human-readable label shown in context-update messages
1665
+ * (e.g. "Product Manual", "FAQ").
1666
+ */
1667
+ contentLabel?: string;
1668
+
1669
+ /**
1670
+ * How the assistant should use this content:
1671
+ * - 'context' (default): inject text into the system prompt
1672
+ * - 'rag': use ragHint to query indexed docs via SL.ai.public.chat()
1673
+ * - 'hybrid': inject text as context AND ground answers via RAG
1674
+ */
1675
+ strategy?: 'context' | 'rag' | 'hybrid';
1676
+ }
1677
+ ```
1678
+
1679
+ ### Response Strategies
1680
+
1681
+ #### `context` (Default)
1682
+
1683
+ Return readable text. The assistant injects it into the system prompt as background knowledge. Good for descriptions, summaries, structured data, and FAQs.
1684
+
1685
+ ```typescript
1686
+ return {
1687
+ text: `
1688
+ ## Wine Details
1689
+ - **Name**: 2023 Château Margaux
1690
+ - **Region**: Bordeaux, France
1691
+ - **Tasting Notes**: Dark fruit, cedar, tobacco
1692
+ - **Food Pairing**: Lamb, aged cheese
1693
+ `,
1694
+ contentLabel: 'Wine Information',
1695
+ strategy: 'context',
1696
+ };
1697
+ ```
1698
+
1699
+ #### `rag`
1700
+
1701
+ Tell the assistant to query pre-indexed documents via SmartLinks RAG. The `text` field is minimal — the real knowledge comes from the indexed docs. Good for product manuals, large document sets, and technical specs.
1702
+
1703
+ ```typescript
1704
+ return {
1705
+ text: 'The user is viewing the espresso machine product page.',
1706
+ contentLabel: 'Product Assistant',
1707
+ strategy: 'rag',
1708
+ ragHint: {
1709
+ productId: 'espresso-machine-pro',
1710
+ sessionId: `rag-session-${userId}`,
1711
+ context: 'User is on the troubleshooting section',
1712
+ },
1713
+ };
1714
+ ```
1715
+
1716
+ When the assistant receives a `rag` strategy it routes the question through `SL.ai.public.chat()`:
1717
+
1718
+ ```typescript
1719
+ const response = await SL.ai.public.chat(collectionId, {
1720
+ productId: ragHint.productId,
1721
+ userId: currentUserId,
1722
+ message: userQuestion,
1723
+ sessionId: ragHint.sessionId,
1724
+ });
1725
+ ```
1726
+
1727
+ #### `hybrid`
1728
+
1729
+ Combines both — the `text` is injected as additional context **and** the user's questions are also grounded via RAG. Good for museum exhibits, guided experiences, or any scenario with rich metadata alongside large document sets.
1730
+
1731
+ ```typescript
1732
+ return {
1733
+ text: `
1734
+ ## Exhibit: The Starry Night
1735
+ - **Artist**: Vincent van Gogh
1736
+ - **Year**: 1889
1737
+ - **Current Location**: Gallery 3, East Wing
1738
+ - **Audio Guide**: Available in 12 languages
1739
+ `,
1740
+ contentLabel: 'Museum Exhibit',
1741
+ strategy: 'hybrid',
1742
+ ragHint: {
1743
+ productId: 'starry-night-exhibit',
1744
+ context: 'Art history and technique questions',
1745
+ },
1746
+ };
1747
+ ```
1748
+
1749
+ ### Content Extraction Examples
1750
+
1751
+ #### Museum Guide App
1752
+
1753
+ ```typescript
1754
+ async function gatherAIContent(): Promise<AIContentResponse> {
1755
+ const exhibit = getCurrentExhibit();
1756
+
1757
+ return {
1758
+ text: `
1759
+ Exhibit: "${exhibit.title}" by ${exhibit.artist}
1760
+ Period: ${exhibit.period}
1761
+ Medium: ${exhibit.medium}
1762
+ Description: ${exhibit.curatorNotes}
1763
+ Related works in this gallery: ${exhibit.relatedWorks.join(', ')}
1764
+ `.trim(),
1765
+ contentLabel: `Exhibit: ${exhibit.title}`,
1766
+ strategy: 'hybrid',
1767
+ ragHint: {
1768
+ productId: exhibit.smartlinksProductId,
1769
+ context: `Art history, technique, and visitor information for ${exhibit.title}`,
1770
+ },
1771
+ metadata: {
1772
+ exhibitId: exhibit.id,
1773
+ gallery: exhibit.gallery,
1774
+ audioGuideAvailable: exhibit.hasAudioGuide,
1775
+ },
1776
+ };
1777
+ }
1778
+ ```
1779
+
1780
+ #### Wine Product App
1781
+
1782
+ ```typescript
1783
+ async function gatherAIContent(): Promise<AIContentResponse> {
1784
+ const wine = getCurrentWine();
1785
+ const reviews = await fetchRecentReviews(wine.id, 5);
1786
+
1787
+ return {
1788
+ text: `
1789
+ Wine: ${wine.name} (${wine.vintage})
1790
+ Winery: ${wine.winery}
1791
+ Region: ${wine.region}, ${wine.country}
1792
+ Grape: ${wine.grape}
1793
+ ABV: ${wine.abv}%
1794
+ Price: ${wine.price}
1795
+ Tasting Notes: ${wine.tastingNotes}
1796
+
1797
+ Recent Reviews:
1798
+ ${reviews.map(r => `- "${r.text}" (${r.rating}/5)`).join('\n')}
1799
+ `.trim(),
1800
+ contentLabel: 'Wine Details',
1801
+ strategy: 'context',
1802
+ };
1803
+ }
1804
+ ```
1805
+
1806
+ #### Equipment Manual App (RAG-only)
1807
+
1808
+ ```typescript
1809
+ async function gatherAIContent(): Promise<AIContentResponse> {
1810
+ const equipment = getCurrentEquipment();
1811
+
1812
+ return {
1813
+ text: `User is viewing: ${equipment.name} (Model: ${equipment.modelNumber})`,
1814
+ contentLabel: `${equipment.name} Assistant`,
1815
+ strategy: 'rag',
1816
+ ragHint: {
1817
+ productId: equipment.smartlinksProductId,
1818
+ sessionId: `manual-${equipment.id}-${Date.now()}`,
1819
+ context: 'Technical manual, troubleshooting, and maintenance',
1820
+ },
1821
+ };
1822
+ }
1823
+ ```
1824
+
1825
+ ### Timing & Lifecycle
1826
+
1827
+ - **On navigation**: When the user navigates to a new product/proof/app, the assistant automatically requests fresh content and injects a context-update system message.
1828
+ - **On first message**: If no content has been gathered yet, the assistant requests it before the first AI call.
1829
+ - **On manual refresh**: The assistant can re-request content at any time (e.g. if the user's view within the app has changed).
1830
+
1831
+ Content is requested **lazily** — your handler is only called when the AI assistant is active and needs context. If the user never opens the assistant, your handler is never called.
1832
+
1833
+ ### Method 3: DOM Fallback (Automatic)
1834
+
1835
+ If your app doesn't implement either of the above, the framework extracts `innerText` from the DOM element with `data-app-container="{appId}"`, truncated to ~4000 characters. This is a **last resort** — content quality is much lower than a structured response. Implementing Method 1 or 2 is strongly recommended.
1836
+
1837
+ ---
1838
+
1550
1839
  ## Related Documentation
1551
1840
 
1552
1841
  - [API Summary](./API_SUMMARY.md) - Complete API reference
@@ -19,7 +19,7 @@ This guide covers the full communications surface of the SDK: transactional send
19
19
 
20
20
  Sends a single message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with `sourceType: 'transactional'`.
21
21
 
22
- **Endpoint:** `POST /admin/collection/:collectionId/comm/send`
22
+ **Endpoint:** `POST /admin/collection/:collectionId/comm.send`
23
23
 
24
24
  ```typescript
25
25
  import { comms } from '@proveanything/smartlinks'
package/dist/http.js CHANGED
@@ -23,6 +23,16 @@ let ngrokSkipBrowserWarning = false;
23
23
  let extraHeadersGlobal = {};
24
24
  /** Whether initializeApi has been successfully called at least once. */
25
25
  let initialized = false;
26
+ /** Safely returns the current browser hostname, or an empty string in non-browser / Node environments. */
27
+ function getSourceDomain() {
28
+ var _a;
29
+ try {
30
+ return (typeof window !== 'undefined' && ((_a = window.location) === null || _a === void 0 ? void 0 : _a.hostname)) || '';
31
+ }
32
+ catch (_b) {
33
+ return '';
34
+ }
35
+ }
26
36
  const httpCache = new Map();
27
37
  let cacheEnabled = true;
28
38
  /** Default TTL used when no per-resource rule matches (milliseconds). */
@@ -758,6 +768,9 @@ export async function request(path) {
758
768
  headers["AUTHORIZATION"] = `Bearer ${bearerToken}`;
759
769
  if (ngrokSkipBrowserWarning)
760
770
  headers["ngrok-skip-browser-warning"] = "true";
771
+ const _getDomain = getSourceDomain();
772
+ if (_getDomain)
773
+ headers["X-Source-Domain"] = _getDomain;
761
774
  for (const [k, v] of Object.entries(extraHeadersGlobal))
762
775
  headers[k] = v;
763
776
  logDebug('[smartlinks] GET fetch', { url, headers: redactHeaders(headers) });
@@ -828,6 +841,9 @@ export async function post(path, body, extraHeaders) {
828
841
  headers["AUTHORIZATION"] = `Bearer ${bearerToken}`;
829
842
  if (ngrokSkipBrowserWarning)
830
843
  headers["ngrok-skip-browser-warning"] = "true";
844
+ const _postDomain = getSourceDomain();
845
+ if (_postDomain)
846
+ headers["X-Source-Domain"] = _postDomain;
831
847
  for (const [k, v] of Object.entries(extraHeadersGlobal))
832
848
  if (!(k in headers))
833
849
  headers[k] = v;
@@ -882,6 +898,9 @@ export async function put(path, body, extraHeaders) {
882
898
  headers["AUTHORIZATION"] = `Bearer ${bearerToken}`;
883
899
  if (ngrokSkipBrowserWarning)
884
900
  headers["ngrok-skip-browser-warning"] = "true";
901
+ const _putDomain = getSourceDomain();
902
+ if (_putDomain)
903
+ headers["X-Source-Domain"] = _putDomain;
885
904
  for (const [k, v] of Object.entries(extraHeadersGlobal))
886
905
  if (!(k in headers))
887
906
  headers[k] = v;
@@ -936,6 +955,9 @@ export async function patch(path, body, extraHeaders) {
936
955
  headers["AUTHORIZATION"] = `Bearer ${bearerToken}`;
937
956
  if (ngrokSkipBrowserWarning)
938
957
  headers["ngrok-skip-browser-warning"] = "true";
958
+ const _patchDomain = getSourceDomain();
959
+ if (_patchDomain)
960
+ headers["X-Source-Domain"] = _patchDomain;
939
961
  for (const [k, v] of Object.entries(extraHeadersGlobal))
940
962
  if (!(k in headers))
941
963
  headers[k] = v;
@@ -1034,7 +1056,8 @@ export async function requestWithOptions(path, options) {
1034
1056
  extraHeaders = Object.assign({}, options.headers);
1035
1057
  }
1036
1058
  }
1037
- const headers = Object.assign(Object.assign(Object.assign(Object.assign({ "Content-Type": "application/json" }, (apiKey ? { "X-API-Key": apiKey } : {})), (bearerToken ? { "AUTHORIZATION": `Bearer ${bearerToken}` } : {})), (ngrokSkipBrowserWarning ? { "ngrok-skip-browser-warning": "true" } : {})), extraHeaders);
1059
+ const _rwoDomain = getSourceDomain();
1060
+ const headers = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ "Content-Type": "application/json" }, (apiKey ? { "X-API-Key": apiKey } : {})), (bearerToken ? { "AUTHORIZATION": `Bearer ${bearerToken}` } : {})), (ngrokSkipBrowserWarning ? { "ngrok-skip-browser-warning": "true" } : {})), (_rwoDomain ? { "X-Source-Domain": _rwoDomain } : {})), extraHeaders);
1038
1061
  // Merge global custom headers (do not override existing keys from options.headers)
1039
1062
  for (const [k, v] of Object.entries(extraHeadersGlobal))
1040
1063
  if (!(k in headers))
@@ -1105,6 +1128,9 @@ export async function del(path, extraHeaders) {
1105
1128
  headers["AUTHORIZATION"] = `Bearer ${bearerToken}`;
1106
1129
  if (ngrokSkipBrowserWarning)
1107
1130
  headers["ngrok-skip-browser-warning"] = "true";
1131
+ const _delDomain = getSourceDomain();
1132
+ if (_delDomain)
1133
+ headers["X-Source-Domain"] = _delDomain;
1108
1134
  for (const [k, v] of Object.entries(extraHeadersGlobal))
1109
1135
  if (!(k in headers))
1110
1136
  headers[k] = v;
@@ -1142,6 +1168,9 @@ export function getApiHeaders() {
1142
1168
  headers["AUTHORIZATION"] = `Bearer ${bearerToken}`;
1143
1169
  if (ngrokSkipBrowserWarning)
1144
1170
  headers["ngrok-skip-browser-warning"] = "true";
1171
+ const sourceDomain = getSourceDomain();
1172
+ if (sourceDomain)
1173
+ headers["X-Source-Domain"] = sourceDomain;
1145
1174
  for (const [k, v] of Object.entries(extraHeadersGlobal))
1146
1175
  if (!(k in headers))
1147
1176
  headers[k] = v;
package/dist/openapi.yaml CHANGED
@@ -2138,6 +2138,39 @@ paths:
2138
2138
  description: Unauthorized
2139
2139
  404:
2140
2140
  description: Not found
2141
+ /admin/collection/{collectionId}/comm.send:
2142
+ post:
2143
+ tags:
2144
+ - comms
2145
+ summary: Send a single transactional message to one contact using a template.
2146
+ operationId: comms_sendTransactional
2147
+ security:
2148
+ - bearerAuth: []
2149
+ parameters:
2150
+ - name: collectionId
2151
+ in: path
2152
+ required: true
2153
+ schema:
2154
+ type: string
2155
+ responses:
2156
+ 200:
2157
+ description: Success
2158
+ content:
2159
+ application/json:
2160
+ schema:
2161
+ $ref: "#/components/schemas/TransactionalSendResult"
2162
+ 400:
2163
+ description: Bad request
2164
+ 401:
2165
+ description: Unauthorized
2166
+ 404:
2167
+ description: Not found
2168
+ requestBody:
2169
+ required: true
2170
+ content:
2171
+ application/json:
2172
+ schema:
2173
+ $ref: "#/components/schemas/TransactionalSendRequest"
2141
2174
  /admin/collection/{collectionId}/comm.settings:
2142
2175
  patch:
2143
2176
  tags:
@@ -2421,39 +2454,6 @@ paths:
2421
2454
  application/json:
2422
2455
  schema:
2423
2456
  $ref: "#/components/schemas/CommsRecipientsWithoutActionQuery"
2424
- /admin/collection/{collectionId}/comm/send:
2425
- post:
2426
- tags:
2427
- - comms
2428
- summary: Send a single transactional message to one contact using a template.
2429
- operationId: comms_sendTransactional
2430
- security:
2431
- - bearerAuth: []
2432
- parameters:
2433
- - name: collectionId
2434
- in: path
2435
- required: true
2436
- schema:
2437
- type: string
2438
- responses:
2439
- 200:
2440
- description: Success
2441
- content:
2442
- application/json:
2443
- schema:
2444
- $ref: "#/components/schemas/TransactionalSendResult"
2445
- 400:
2446
- description: Bad request
2447
- 401:
2448
- description: Unauthorized
2449
- 404:
2450
- description: Not found
2451
- requestBody:
2452
- required: true
2453
- content:
2454
- application/json:
2455
- schema:
2456
- $ref: "#/components/schemas/TransactionalSendRequest"
2457
2457
  /admin/collection/{collectionId}/contacts:
2458
2458
  get:
2459
2459
  tags:
@@ -5203,6 +5203,50 @@ paths:
5203
5203
  description: Unauthorized
5204
5204
  404:
5205
5205
  description: Not found
5206
+ /admin/collection/{collectionId}/products/{productId}/proofs/{proofId}/migrate:
5207
+ post:
5208
+ tags:
5209
+ - proof
5210
+ summary: proof.migrate
5211
+ operationId: proof_migrate
5212
+ security:
5213
+ - bearerAuth: []
5214
+ parameters:
5215
+ - name: collectionId
5216
+ in: path
5217
+ required: true
5218
+ schema:
5219
+ type: string
5220
+ - name: productId
5221
+ in: path
5222
+ required: true
5223
+ schema:
5224
+ type: string
5225
+ - name: proofId
5226
+ in: path
5227
+ required: true
5228
+ schema:
5229
+ type: string
5230
+ responses:
5231
+ 200:
5232
+ description: Success
5233
+ content:
5234
+ application/json:
5235
+ schema:
5236
+ $ref: "#/components/schemas/ProofResponse"
5237
+ 400:
5238
+ description: Bad request
5239
+ 401:
5240
+ description: Unauthorized
5241
+ 404:
5242
+ description: Not found
5243
+ requestBody:
5244
+ required: true
5245
+ content:
5246
+ application/json:
5247
+ schema:
5248
+ type: object
5249
+ additionalProperties: true
5206
5250
  /admin/collection/{collectionId}/proof/findByUser/{userId}:
5207
5251
  get:
5208
5252
  tags:
@@ -280,7 +280,7 @@ export interface SubscriptionsResolveResponse {
280
280
  * No broadcast record is created; the send is logged directly to the
281
281
  * contact's communication history with sourceType: 'transactional'.
282
282
  *
283
- * POST /admin/collection/:collectionId/comm/send
283
+ * POST /admin/collection/:collectionId/comm.send
284
284
  */
285
285
  export interface TransactionalSendRequest {
286
286
  /** CRM contact UUID */
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.7.3 | Generated: 2026-03-04T13:56:12.543Z
3
+ Version: 1.7.5 | Generated: 2026-03-10T14:23:18.789Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -3006,7 +3006,7 @@ export interface SubscriptionsResolveResponse {
3006
3006
  * No broadcast record is created; the send is logged directly to the
3007
3007
  * contact's communication history with sourceType: 'transactional'.
3008
3008
  *
3009
- * POST /admin/collection/:collectionId/comm/send
3009
+ * POST /admin/collection/:collectionId/comm.send
3010
3010
  */
3011
3011
  export interface TransactionalSendRequest {
3012
3012
  /** CRM contact UUID */
@@ -5978,7 +5978,7 @@ Analytics: Recipients who performed an action, optionally with outcome. POST /ad
5978
5978
 
5979
5979
  **sendTransactional**(collectionId: string,
5980
5980
  body: TransactionalSendRequest) → `Promise<TransactionalSendResult>`
5981
- Send a single transactional message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with sourceType: 'transactional'. POST /admin/collection/:collectionId/comm/send ```typescript const result = await comms.sendTransactional(collectionId, { contactId: 'e4f2a1b0-...', templateId: 'warranty-update', channel: 'preferred', props: { claimRef: 'CLM-0042', decision: 'approved' }, include: { productId: 'prod-abc123', appCase: 'c9d1e2f3-...' }, ref: 'warranty-decision-notification', appId: 'warrantyApp', }) if (result.ok) { console.log(`Sent via ${result.channel}`, result.messageId) } else { console.error('Send failed:', result.error) } ```
5981
+ Send a single transactional message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with sourceType: 'transactional'. POST /admin/collection/:collectionId/comm.send ```typescript const result = await comms.sendTransactional(collectionId, { contactId: 'e4f2a1b0-...', templateId: 'warranty-update', channel: 'preferred', props: { claimRef: 'CLM-0042', decision: 'approved' }, include: { productId: 'prod-abc123', appCase: 'c9d1e2f3-...' }, ref: 'warranty-decision-notification', appId: 'warrantyApp', }) if (result.ok) { console.log(`Sent via ${result.channel}`, result.messageId) } else { console.error('Send failed:', result.error) } ```
5982
5982
 
5983
5983
  **logCommunicationEvent**(collectionId: string,
5984
5984
  body: LogCommunicationEventBody) → `Promise<AppendResult>`
@@ -6470,6 +6470,13 @@ Find proofs for a product (admin only). POST /admin/collection/:collectionId/pro
6470
6470
  batchId: string) → `Promise<ProofResponse[]>`
6471
6471
  Get proofs for a batch (admin only). GET /admin/collection/:collectionId/product/:productId/batch/:batchId/proof
6472
6472
 
6473
+ **migrate**(collectionId: string,
6474
+ productId: string,
6475
+ proofId: string,
6476
+ /** The destination product ID */
6477
+ data: { targetProductId: string }) → `Promise<ProofResponse>`
6478
+ Migrate a proof to a different product within the same collection (admin only). Because the Firestore ledger document ID is `{productId}-{proofId}`, a proof cannot simply be re-assigned to another product by updating a field — the document must be re-keyed. This endpoint handles that atomically: 1. Reads the source ledger document (`{sourceProductId}-{proofId}`). 2. Writes a new document (`{targetProductId}-{proofId}`) with `productId` and `proofGroup` updated. The short `proofId` (nanoid) is unchanged. 3. Writes a migration history entry to the new document's `history` subcollection (snapshot of the original proof + migration metadata). 4. Copies all subcollections — `assets`, `attestations`, `history` — from the old document to the new one. 5. Deletes the old subcollections and then the old document. Repeated migrations are safe — each one appends a history record; no migration metadata is stored on the proof document itself. ```typescript const migrated = await proof.migrate('coll_123', 'prod_old', 'proof_abc', { targetProductId: 'prod_new', }) console.log(migrated.productId) // 'prod_new' ```
6479
+
6473
6480
  ### qr
6474
6481
 
6475
6482
  **lookupShortCode**(shortId: string, code: string) → `Promise<QrShortCodeLookupResponse>`
package/docs/ai.md CHANGED
@@ -19,6 +19,7 @@ Complete guide to using AI capabilities in the SmartLinks SDK, including chat co
19
19
  - [Error Handling](#error-handling)
20
20
  - [Rate Limiting](#rate-limiting)
21
21
  - [Best Practices](#best-practices)
22
+ - [Providing Content to the AI Assistant](#providing-content-to-the-ai-assistant)
22
23
 
23
24
  ---
24
25
 
@@ -1547,6 +1548,294 @@ console.log('Estimated cost: $', cost.toFixed(4));
1547
1548
 
1548
1549
  ---
1549
1550
 
1551
+ ## Providing Content to the AI Assistant
1552
+
1553
+ The portal's built-in AI assistant can discuss the content currently visible to the user. To make this work, your app must supply contextual content when the assistant requests it. There are three extraction methods, tried in priority order:
1554
+
1555
+ | Priority | Method | When Used |
1556
+ |----------|--------|-----------|
1557
+ | 1 | **Direct prop callback** | Container/widget rendered in the parent React context |
1558
+ | 2 | **PostMessage protocol** | App rendered in an iframe |
1559
+ | 3 | **DOM text fallback** | Neither of the above responded |
1560
+
1561
+ This section covers **methods 1 and 2** — the ones you implement in your app.
1562
+
1563
+ ### Method 1: Direct Prop (`onRequestAIContent`)
1564
+
1565
+ When your app is rendered as a **container** (a direct component in the parent's React tree), the framework passes an `onRequestAIContent` prop to your exported component. You do **not** call this prop yourself — the framework calls it when the AI assistant needs context.
1566
+
1567
+ Structure your component to make current state accessible when the callback fires:
1568
+
1569
+ ```tsx
1570
+ import { useEffect, useRef } from 'react';
1571
+
1572
+ export function PublicContainer(props) {
1573
+ const { onRequestAIContent, appId, ...rest } = props;
1574
+
1575
+ // Keep a ref to the latest content so the callback always returns fresh data
1576
+ const currentContentRef = useRef(null);
1577
+
1578
+ useEffect(() => {
1579
+ currentContentRef.current = buildCurrentContent();
1580
+ }, [relevantState]);
1581
+
1582
+ return <div data-app-container={appId}>{/* your UI */}</div>;
1583
+ }
1584
+ ```
1585
+
1586
+ The framework registers the content provider internally via `useAIContentExtraction.registerContentProvider()`. Your component just needs to respond when the callback is invoked.
1587
+
1588
+ ### Method 2: PostMessage Protocol (Iframes)
1589
+
1590
+ For iframe-embedded apps the framework sends a `postMessage` request and expects a response within **500 ms**. If your app doesn't reply in time the framework falls back to DOM text extraction.
1591
+
1592
+ **Request sent by the framework to your iframe:**
1593
+
1594
+ ```typescript
1595
+ {
1596
+ type: 'smartlinks:request-ai-content',
1597
+ requestId: 'ai-content-1709834567890-abc123' // unique per request
1598
+ }
1599
+ ```
1600
+
1601
+ **Response your app must send back:**
1602
+
1603
+ ```typescript
1604
+ {
1605
+ type: 'smartlinks:ai-content-response',
1606
+ requestId: 'ai-content-1709834567890-abc123', // echo back the requestId
1607
+ content: AIContentResponse
1608
+ }
1609
+ ```
1610
+
1611
+ **Minimal implementation:**
1612
+
1613
+ ```typescript
1614
+ window.addEventListener('message', async (event) => {
1615
+ if (event.data?.type === 'smartlinks:request-ai-content') {
1616
+ const content = await gatherAIContent();
1617
+
1618
+ window.parent.postMessage({
1619
+ type: 'smartlinks:ai-content-response',
1620
+ requestId: event.data.requestId,
1621
+ content,
1622
+ }, '*');
1623
+ }
1624
+ });
1625
+
1626
+ async function gatherAIContent(): Promise<AIContentResponse> {
1627
+ return {
1628
+ text: 'The user is viewing the warranty registration form...',
1629
+ contentLabel: 'Warranty Registration',
1630
+ };
1631
+ }
1632
+ ```
1633
+
1634
+ ### The `AIContentResponse` Interface
1635
+
1636
+ ```typescript
1637
+ interface AIContentResponse {
1638
+ /**
1639
+ * Plain text or markdown for the AI to use as context.
1640
+ * Injected into the system prompt. Max recommended: ~4000 characters.
1641
+ */
1642
+ text: string;
1643
+
1644
+ /**
1645
+ * Optional structured metadata (key-value pairs).
1646
+ * Not used directly in prompts but available for custom providers.
1647
+ */
1648
+ metadata?: Record<string, unknown>;
1649
+
1650
+ /**
1651
+ * Pre-built RAG configuration. When provided, the assistant can use
1652
+ * SL.ai.public.chat() to ground answers in indexed product documents.
1653
+ */
1654
+ ragHint?: {
1655
+ /** The product ID whose indexed documents should be queried */
1656
+ productId: string;
1657
+ /** Optional session ID for multi-turn RAG conversations */
1658
+ sessionId?: string;
1659
+ /** Optional context hint to help scope the RAG query */
1660
+ context?: string;
1661
+ };
1662
+
1663
+ /**
1664
+ * Human-readable label shown in context-update messages
1665
+ * (e.g. "Product Manual", "FAQ").
1666
+ */
1667
+ contentLabel?: string;
1668
+
1669
+ /**
1670
+ * How the assistant should use this content:
1671
+ * - 'context' (default): inject text into the system prompt
1672
+ * - 'rag': use ragHint to query indexed docs via SL.ai.public.chat()
1673
+ * - 'hybrid': inject text as context AND ground answers via RAG
1674
+ */
1675
+ strategy?: 'context' | 'rag' | 'hybrid';
1676
+ }
1677
+ ```
1678
+
1679
+ ### Response Strategies
1680
+
1681
+ #### `context` (Default)
1682
+
1683
+ Return readable text. The assistant injects it into the system prompt as background knowledge. Good for descriptions, summaries, structured data, and FAQs.
1684
+
1685
+ ```typescript
1686
+ return {
1687
+ text: `
1688
+ ## Wine Details
1689
+ - **Name**: 2023 Château Margaux
1690
+ - **Region**: Bordeaux, France
1691
+ - **Tasting Notes**: Dark fruit, cedar, tobacco
1692
+ - **Food Pairing**: Lamb, aged cheese
1693
+ `,
1694
+ contentLabel: 'Wine Information',
1695
+ strategy: 'context',
1696
+ };
1697
+ ```
1698
+
1699
+ #### `rag`
1700
+
1701
+ Tell the assistant to query pre-indexed documents via SmartLinks RAG. The `text` field is minimal — the real knowledge comes from the indexed docs. Good for product manuals, large document sets, and technical specs.
1702
+
1703
+ ```typescript
1704
+ return {
1705
+ text: 'The user is viewing the espresso machine product page.',
1706
+ contentLabel: 'Product Assistant',
1707
+ strategy: 'rag',
1708
+ ragHint: {
1709
+ productId: 'espresso-machine-pro',
1710
+ sessionId: `rag-session-${userId}`,
1711
+ context: 'User is on the troubleshooting section',
1712
+ },
1713
+ };
1714
+ ```
1715
+
1716
+ When the assistant receives a `rag` strategy it routes the question through `SL.ai.public.chat()`:
1717
+
1718
+ ```typescript
1719
+ const response = await SL.ai.public.chat(collectionId, {
1720
+ productId: ragHint.productId,
1721
+ userId: currentUserId,
1722
+ message: userQuestion,
1723
+ sessionId: ragHint.sessionId,
1724
+ });
1725
+ ```
1726
+
1727
+ #### `hybrid`
1728
+
1729
+ Combines both — the `text` is injected as additional context **and** the user's questions are also grounded via RAG. Good for museum exhibits, guided experiences, or any scenario with rich metadata alongside large document sets.
1730
+
1731
+ ```typescript
1732
+ return {
1733
+ text: `
1734
+ ## Exhibit: The Starry Night
1735
+ - **Artist**: Vincent van Gogh
1736
+ - **Year**: 1889
1737
+ - **Current Location**: Gallery 3, East Wing
1738
+ - **Audio Guide**: Available in 12 languages
1739
+ `,
1740
+ contentLabel: 'Museum Exhibit',
1741
+ strategy: 'hybrid',
1742
+ ragHint: {
1743
+ productId: 'starry-night-exhibit',
1744
+ context: 'Art history and technique questions',
1745
+ },
1746
+ };
1747
+ ```
1748
+
1749
+ ### Content Extraction Examples
1750
+
1751
+ #### Museum Guide App
1752
+
1753
+ ```typescript
1754
+ async function gatherAIContent(): Promise<AIContentResponse> {
1755
+ const exhibit = getCurrentExhibit();
1756
+
1757
+ return {
1758
+ text: `
1759
+ Exhibit: "${exhibit.title}" by ${exhibit.artist}
1760
+ Period: ${exhibit.period}
1761
+ Medium: ${exhibit.medium}
1762
+ Description: ${exhibit.curatorNotes}
1763
+ Related works in this gallery: ${exhibit.relatedWorks.join(', ')}
1764
+ `.trim(),
1765
+ contentLabel: `Exhibit: ${exhibit.title}`,
1766
+ strategy: 'hybrid',
1767
+ ragHint: {
1768
+ productId: exhibit.smartlinksProductId,
1769
+ context: `Art history, technique, and visitor information for ${exhibit.title}`,
1770
+ },
1771
+ metadata: {
1772
+ exhibitId: exhibit.id,
1773
+ gallery: exhibit.gallery,
1774
+ audioGuideAvailable: exhibit.hasAudioGuide,
1775
+ },
1776
+ };
1777
+ }
1778
+ ```
1779
+
1780
+ #### Wine Product App
1781
+
1782
+ ```typescript
1783
+ async function gatherAIContent(): Promise<AIContentResponse> {
1784
+ const wine = getCurrentWine();
1785
+ const reviews = await fetchRecentReviews(wine.id, 5);
1786
+
1787
+ return {
1788
+ text: `
1789
+ Wine: ${wine.name} (${wine.vintage})
1790
+ Winery: ${wine.winery}
1791
+ Region: ${wine.region}, ${wine.country}
1792
+ Grape: ${wine.grape}
1793
+ ABV: ${wine.abv}%
1794
+ Price: ${wine.price}
1795
+ Tasting Notes: ${wine.tastingNotes}
1796
+
1797
+ Recent Reviews:
1798
+ ${reviews.map(r => `- "${r.text}" (${r.rating}/5)`).join('\n')}
1799
+ `.trim(),
1800
+ contentLabel: 'Wine Details',
1801
+ strategy: 'context',
1802
+ };
1803
+ }
1804
+ ```
1805
+
1806
+ #### Equipment Manual App (RAG-only)
1807
+
1808
+ ```typescript
1809
+ async function gatherAIContent(): Promise<AIContentResponse> {
1810
+ const equipment = getCurrentEquipment();
1811
+
1812
+ return {
1813
+ text: `User is viewing: ${equipment.name} (Model: ${equipment.modelNumber})`,
1814
+ contentLabel: `${equipment.name} Assistant`,
1815
+ strategy: 'rag',
1816
+ ragHint: {
1817
+ productId: equipment.smartlinksProductId,
1818
+ sessionId: `manual-${equipment.id}-${Date.now()}`,
1819
+ context: 'Technical manual, troubleshooting, and maintenance',
1820
+ },
1821
+ };
1822
+ }
1823
+ ```
1824
+
1825
+ ### Timing & Lifecycle
1826
+
1827
+ - **On navigation**: When the user navigates to a new product/proof/app, the assistant automatically requests fresh content and injects a context-update system message.
1828
+ - **On first message**: If no content has been gathered yet, the assistant requests it before the first AI call.
1829
+ - **On manual refresh**: The assistant can re-request content at any time (e.g. if the user's view within the app has changed).
1830
+
1831
+ Content is requested **lazily** — your handler is only called when the AI assistant is active and needs context. If the user never opens the assistant, your handler is never called.
1832
+
1833
+ ### Method 3: DOM Fallback (Automatic)
1834
+
1835
+ If your app doesn't implement either of the above, the framework extracts `innerText` from the DOM element with `data-app-container="{appId}"`, truncated to ~4000 characters. This is a **last resort** — content quality is much lower than a structured response. Implementing Method 1 or 2 is strongly recommended.
1836
+
1837
+ ---
1838
+
1550
1839
  ## Related Documentation
1551
1840
 
1552
1841
  - [API Summary](./API_SUMMARY.md) - Complete API reference
package/docs/comms.md CHANGED
@@ -19,7 +19,7 @@ This guide covers the full communications surface of the SDK: transactional send
19
19
 
20
20
  Sends a single message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with `sourceType: 'transactional'`.
21
21
 
22
- **Endpoint:** `POST /admin/collection/:collectionId/comm/send`
22
+ **Endpoint:** `POST /admin/collection/:collectionId/comm.send`
23
23
 
24
24
  ```typescript
25
25
  import { comms } from '@proveanything/smartlinks'
package/openapi.yaml CHANGED
@@ -2138,6 +2138,39 @@ paths:
2138
2138
  description: Unauthorized
2139
2139
  404:
2140
2140
  description: Not found
2141
+ /admin/collection/{collectionId}/comm.send:
2142
+ post:
2143
+ tags:
2144
+ - comms
2145
+ summary: Send a single transactional message to one contact using a template.
2146
+ operationId: comms_sendTransactional
2147
+ security:
2148
+ - bearerAuth: []
2149
+ parameters:
2150
+ - name: collectionId
2151
+ in: path
2152
+ required: true
2153
+ schema:
2154
+ type: string
2155
+ responses:
2156
+ 200:
2157
+ description: Success
2158
+ content:
2159
+ application/json:
2160
+ schema:
2161
+ $ref: "#/components/schemas/TransactionalSendResult"
2162
+ 400:
2163
+ description: Bad request
2164
+ 401:
2165
+ description: Unauthorized
2166
+ 404:
2167
+ description: Not found
2168
+ requestBody:
2169
+ required: true
2170
+ content:
2171
+ application/json:
2172
+ schema:
2173
+ $ref: "#/components/schemas/TransactionalSendRequest"
2141
2174
  /admin/collection/{collectionId}/comm.settings:
2142
2175
  patch:
2143
2176
  tags:
@@ -2421,39 +2454,6 @@ paths:
2421
2454
  application/json:
2422
2455
  schema:
2423
2456
  $ref: "#/components/schemas/CommsRecipientsWithoutActionQuery"
2424
- /admin/collection/{collectionId}/comm/send:
2425
- post:
2426
- tags:
2427
- - comms
2428
- summary: Send a single transactional message to one contact using a template.
2429
- operationId: comms_sendTransactional
2430
- security:
2431
- - bearerAuth: []
2432
- parameters:
2433
- - name: collectionId
2434
- in: path
2435
- required: true
2436
- schema:
2437
- type: string
2438
- responses:
2439
- 200:
2440
- description: Success
2441
- content:
2442
- application/json:
2443
- schema:
2444
- $ref: "#/components/schemas/TransactionalSendResult"
2445
- 400:
2446
- description: Bad request
2447
- 401:
2448
- description: Unauthorized
2449
- 404:
2450
- description: Not found
2451
- requestBody:
2452
- required: true
2453
- content:
2454
- application/json:
2455
- schema:
2456
- $ref: "#/components/schemas/TransactionalSendRequest"
2457
2457
  /admin/collection/{collectionId}/contacts:
2458
2458
  get:
2459
2459
  tags:
@@ -5203,6 +5203,50 @@ paths:
5203
5203
  description: Unauthorized
5204
5204
  404:
5205
5205
  description: Not found
5206
+ /admin/collection/{collectionId}/products/{productId}/proofs/{proofId}/migrate:
5207
+ post:
5208
+ tags:
5209
+ - proof
5210
+ summary: proof.migrate
5211
+ operationId: proof_migrate
5212
+ security:
5213
+ - bearerAuth: []
5214
+ parameters:
5215
+ - name: collectionId
5216
+ in: path
5217
+ required: true
5218
+ schema:
5219
+ type: string
5220
+ - name: productId
5221
+ in: path
5222
+ required: true
5223
+ schema:
5224
+ type: string
5225
+ - name: proofId
5226
+ in: path
5227
+ required: true
5228
+ schema:
5229
+ type: string
5230
+ responses:
5231
+ 200:
5232
+ description: Success
5233
+ content:
5234
+ application/json:
5235
+ schema:
5236
+ $ref: "#/components/schemas/ProofResponse"
5237
+ 400:
5238
+ description: Bad request
5239
+ 401:
5240
+ description: Unauthorized
5241
+ 404:
5242
+ description: Not found
5243
+ requestBody:
5244
+ required: true
5245
+ content:
5246
+ application/json:
5247
+ schema:
5248
+ type: object
5249
+ additionalProperties: true
5206
5250
  /admin/collection/{collectionId}/proof/findByUser/{userId}:
5207
5251
  get:
5208
5252
  tags:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",