@stackable-labs/mcp-app-extension 1.30.0 → 2.0.1
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.
- package/dist/index.js +112 -106
- package/dist/server.js +112 -106
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
-
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP,
|
|
4
|
+
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, EVENT_HOOK_PERMISSION_MAP, MESSAGING_SEND_EXAMPLES } from '@stackable-labs/sdk-extension-contracts';
|
|
5
5
|
import { validatePatternFormat } from '@stackable-labs/lib-contracts';
|
|
6
6
|
import fs4, { readFile, mkdir, writeFile, constants } from 'fs/promises';
|
|
7
7
|
import path, { join } from 'path';
|
|
@@ -348,6 +348,11 @@ var CORE_CONTENT = `# Extension SDK \u2014 Editorial Notes
|
|
|
348
348
|
- Always check the \`loading\` flag before using context values
|
|
349
349
|
- Context is only available when the host provides it (e.g., when viewing a ticket)
|
|
350
350
|
|
|
351
|
+
### \`messaging.send\` resolves to null without throwing
|
|
352
|
+
- The host silently handles \`no_conversation\` / \`reauth_required\` / \`forbidden\` \u2014 \`send()\` returns null and \`error\` stays unset on these codes
|
|
353
|
+
- These are operational/admin concerns (no active conversation, instance disconnected from messaging, permission revoked) \u2014 surfaced to admins via dashboard remediation UI, not user-actionable from the extension
|
|
354
|
+
- Actionable codes (\`invalid_message\`, \`rate_limited\`, \`upstream_error\`) DO populate \`error\` and throw \u2014 handle in UI
|
|
355
|
+
|
|
351
356
|
## Best Practices
|
|
352
357
|
|
|
353
358
|
### Performance
|
|
@@ -506,7 +511,12 @@ useExtendIdentity((claims) => ({
|
|
|
506
511
|
external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
|
|
507
512
|
loyalty_tier: 'bronze', // custom \u2014 sync, known at login
|
|
508
513
|
verified: false, // custom \u2014 default; updated async post-verification
|
|
509
|
-
}))
|
|
514
|
+
}))`,
|
|
515
|
+
"messaging.send": `import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
516
|
+
|
|
517
|
+
// manifest.json: { "permissions": ["messaging:send"] }
|
|
518
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
519
|
+
await send({ kind: 'text', body: 'Hello from the extension' })`
|
|
510
520
|
};
|
|
511
521
|
var HOOK_SNIPPETS_MEMOIZED = {
|
|
512
522
|
"events:messaging": `import { useCallback } from 'react'
|
|
@@ -836,7 +846,7 @@ The publisher-side bundle scan validates your declaration at submission time:
|
|
|
836
846
|
|
|
837
847
|
Post a message into the **active conversation** bound to the current Instance. The host attributes each message to the extension via the per-Instance author label set by the admin (see "Author label" below). Two complementary APIs:
|
|
838
848
|
|
|
839
|
-
1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { loading, error, data }]\` \u2014 lets callers rename \`send\` per-instance when used multiple times in one component. Preferred for UI components; narrows the error surface to actionable codes only.
|
|
849
|
+
1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { enabled, loading, error, data }]\` \u2014 lets callers rename \`send\` per-instance when used multiple times in one component. Preferred for UI components; narrows the error surface to actionable codes only. Use \`enabled\` to pre-empt the silent \`no_conversation\` case **without** declaring \`context:read\`.
|
|
840
850
|
2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
|
|
841
851
|
|
|
842
852
|
### Manifest contract
|
|
@@ -879,14 +889,9 @@ Four kinds:
|
|
|
879
889
|
### Hook usage
|
|
880
890
|
|
|
881
891
|
\`\`\`tsx
|
|
882
|
-
import { useMessaging
|
|
892
|
+
import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
883
893
|
|
|
884
|
-
const {
|
|
885
|
-
const [send, { loading, error }] = useMessaging()
|
|
886
|
-
|
|
887
|
-
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
888
|
-
// host-handled no_conversation log + null return.
|
|
889
|
-
const canSend = !!messaging?.conversationId
|
|
894
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
890
895
|
|
|
891
896
|
const onApprove = async () => {
|
|
892
897
|
try {
|
|
@@ -900,11 +905,15 @@ const onApprove = async () => {
|
|
|
900
905
|
}
|
|
901
906
|
}
|
|
902
907
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
908
|
+
return (
|
|
909
|
+
<button disabled={!enabled || loading} onClick={onApprove}>
|
|
910
|
+
{error === 'rate_limited' ? 'Slow down\u2026' : 'Approve'}
|
|
911
|
+
</button>
|
|
912
|
+
)
|
|
906
913
|
\`\`\`
|
|
907
914
|
|
|
915
|
+
The \`enabled\` flag is a permission-free, host-pushed signal \u2014 \`true\` whenever an active conversation exists. Wiring buttons to \`disabled={!enabled || loading}\` pre-empts the silent \`no_conversation\` case without forcing the extension to declare \`context:read\`. If the extension already has \`context:read\` for other reasons, \`useContextData().messaging?.conversationId\` is an equivalent gate.
|
|
916
|
+
|
|
908
917
|
### Imperative usage
|
|
909
918
|
|
|
910
919
|
\`\`\`tsx
|
|
@@ -929,7 +938,7 @@ The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two
|
|
|
929
938
|
|
|
930
939
|
| Code | What the framework does |
|
|
931
940
|
| --- | --- |
|
|
932
|
-
| \`no_conversation\` | \`console.info\` \u2014 pre-empt via \`
|
|
941
|
+
| \`no_conversation\` | \`console.info\` \u2014 pre-empt via the \`enabled\` flag from \`useMessaging()\` (no permission needed) |
|
|
933
942
|
| \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
|
|
934
943
|
| \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
|
|
935
944
|
|
|
@@ -947,7 +956,7 @@ Three gates fire in series, surfacing the same \`forbidden\` error if any blocks
|
|
|
947
956
|
|
|
948
957
|
\`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
|
|
949
958
|
|
|
950
|
-
> **Note:** the postback \`payload\` field is currently not surfaced by the Web Widget (only the button text \`actionName\`). Until that gap is closed, design action labels to be self-describing or pair sends with a follow-up \`data.
|
|
959
|
+
> **Note:** the postback \`payload\` field is currently not surfaced by the Web Widget (only the button text \`actionName\`). Until that gap is closed, design action labels to be self-describing or pair sends with a follow-up \`data.fetch\` lookup.
|
|
951
960
|
`;
|
|
952
961
|
};
|
|
953
962
|
|
|
@@ -1310,16 +1319,12 @@ export function Content(): React.ReactElement {
|
|
|
1310
1319
|
</Surface>
|
|
1311
1320
|
)
|
|
1312
1321
|
}`,
|
|
1313
|
-
"messaging.send": `import { useMessaging,
|
|
1322
|
+
"messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
1314
1323
|
|
|
1315
1324
|
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
1316
1325
|
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
1317
1326
|
export function Content(): React.ReactElement {
|
|
1318
|
-
const {
|
|
1319
|
-
const [send, { loading, error }] = useMessaging()
|
|
1320
|
-
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
1321
|
-
// host-handled no_conversation log + null return.
|
|
1322
|
-
const canSend = !!messaging?.conversationId
|
|
1327
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
1323
1328
|
|
|
1324
1329
|
const onSayHello = async () => {
|
|
1325
1330
|
try {
|
|
@@ -1339,7 +1344,7 @@ export function Content(): React.ReactElement {
|
|
|
1339
1344
|
return (
|
|
1340
1345
|
<Surface id="slot.content">
|
|
1341
1346
|
<ui.Stack direction="column" gap="2" className="p-3">
|
|
1342
|
-
<ui.Button onClick={onSayHello} disabled={
|
|
1347
|
+
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
|
|
1343
1348
|
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
1344
1349
|
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
1345
1350
|
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
@@ -1375,10 +1380,6 @@ Bootstrap the extension runtime. Call once in \`src/index.tsx\`.
|
|
|
1375
1380
|
${EXAMPLE_SNIPPETS.bootstrap}
|
|
1376
1381
|
\`\`\`
|
|
1377
1382
|
|
|
1378
|
-
## Surface component
|
|
1379
|
-
Wraps content for a target slot. The \`id\` must match a target in \`manifest.json\`.
|
|
1380
|
-
- \`<Surface id="slot.content">...</Surface>\`
|
|
1381
|
-
|
|
1382
1383
|
## useCapabilities()
|
|
1383
1384
|
Returns the capabilities object for calling host-mediated APIs.
|
|
1384
1385
|
\`\`\`tsx
|
|
@@ -1389,7 +1390,7 @@ const capabilities = useCapabilities()
|
|
|
1389
1390
|
// capabilities.actions.toast(payload)
|
|
1390
1391
|
// capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
|
|
1391
1392
|
// capabilities.identity.extend(patch) \u2014 push enrichment claims to user.metadata + JWT custom_claims (imperative; for handler-style at login, use useExtendIdentity hook). Each patch key MUST be declared in manifest.identityClaims or the host filter drops it.
|
|
1392
|
-
// capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback.
|
|
1393
|
+
// capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Prefer the useMessaging hook instead. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback.
|
|
1393
1394
|
\`\`\`
|
|
1394
1395
|
|
|
1395
1396
|
## useStore(store, selector?)
|
|
@@ -1398,6 +1399,17 @@ Subscribe to a shared store. Re-renders when the selected state changes.
|
|
|
1398
1399
|
const viewState = useStore(appStore, (s) => s.viewState)
|
|
1399
1400
|
\`\`\`
|
|
1400
1401
|
|
|
1402
|
+
## createStore(initialState)
|
|
1403
|
+
Create a shared store for cross-surface state coordination.
|
|
1404
|
+
\`\`\`tsx
|
|
1405
|
+
const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
|
|
1406
|
+
\`\`\`
|
|
1407
|
+
|
|
1408
|
+
**Store\\<T\\> interface**
|
|
1409
|
+
- \`get(): T\` \u2014 read current state
|
|
1410
|
+
- \`set(partial: Partial<T>): void\` \u2014 merge partial state update
|
|
1411
|
+
- \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
|
|
1412
|
+
|
|
1401
1413
|
## useContextData()
|
|
1402
1414
|
Reads host-provided context including extension settings. Returns \`{ loading, customerId, customerEmail, messaging, settings, ... }\`. \`messaging.conversationId\` is the active Messaging conversation ID (or \`null\` until one exists).
|
|
1403
1415
|
\`\`\`tsx
|
|
@@ -1424,26 +1436,48 @@ Returns extension-level context.
|
|
|
1424
1436
|
const { extensionId } = useExtension()
|
|
1425
1437
|
\`\`\`
|
|
1426
1438
|
|
|
1427
|
-
##
|
|
1428
|
-
|
|
1439
|
+
## useEvent(eventType, handler)
|
|
1440
|
+
Generic cross-domain event hook (should not be used unless absolutely required). Subscribe to any event using fully-qualified event types.
|
|
1441
|
+
- \`eventType: EventType\` \u2014 fully-qualified (e.g., \`'activity:product_view'\`, \`'identity:login'\`, \`'messaging:postback'\`)
|
|
1442
|
+
- Domain wildcard (e.g., \`'activity'\`) receives all events in that domain
|
|
1443
|
+
- \`handler: (event: BaseEvent) => void\`
|
|
1444
|
+
|
|
1429
1445
|
\`\`\`tsx
|
|
1430
|
-
|
|
1446
|
+
useEvent('activity', (event) => {
|
|
1447
|
+
console.log('Activity:', event.data)
|
|
1448
|
+
})
|
|
1431
1449
|
\`\`\`
|
|
1432
1450
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
- \`set(partial: Partial<T>): void\` \u2014 merge partial state update
|
|
1436
|
-
- \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
|
|
1451
|
+
## useMessaging()
|
|
1452
|
+
Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`enabled\` / \`loading\` / \`error\` / \`data\`. Returns a tuple \`[send, state]\` so callers can rename \`send\` when the hook is used multiple times in one component. Requires \`messaging:send\` permission. The author label rendered above outbound messages is set per-Instance by the admin (Instance settings \`messagingDisplayName\`); falls back to \`extension.manifest.name\` when blank.
|
|
1437
1453
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
- \`
|
|
1441
|
-
-
|
|
1442
|
-
-
|
|
1443
|
-
-
|
|
1454
|
+
- **Returns:** \`readonly [send, { enabled, loading, error, data }]\`
|
|
1455
|
+
- **\`send(payload: SendMessagePayload): Promise<SendMessageResponse | null>\`** \u2014 returns the response on success; throws on **actionable** errors; resolves to \`null\` on host-handled errors (SDK logs a breadcrumb, host surfaces remediation)
|
|
1456
|
+
- **\`enabled: boolean\`** \u2014 advisory; \`true\` when an active conversation exists. Permission-free (no \`context:read\` needed). Wire to button \`disabled\` to pre-empt the silent \`no_conversation\` case. Starts \`false\`; flips reactively via host-pushed availability events.
|
|
1457
|
+
- **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
|
|
1458
|
+
- **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
|
|
1459
|
+
- **\`error: SendMessageActionableErrorCode | null\`** \u2014 one of \`invalid_message\` / \`rate_limited\` / \`upstream_error\` after a failed send. Host-handled codes (\`no_conversation\` / \`reauth_required\` / \`forbidden\`) never surface here.
|
|
1460
|
+
|
|
1461
|
+
Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
|
|
1444
1462
|
|
|
1445
1463
|
\`\`\`tsx
|
|
1446
|
-
|
|
1464
|
+
import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
1465
|
+
|
|
1466
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
1467
|
+
|
|
1468
|
+
const onApprove = async () => {
|
|
1469
|
+
try {
|
|
1470
|
+
await send({ kind: 'text', body: 'Approved \u2713' })
|
|
1471
|
+
} catch {
|
|
1472
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return (
|
|
1477
|
+
<button disabled={!enabled || loading} onClick={onApprove}>
|
|
1478
|
+
Approve
|
|
1479
|
+
</button>
|
|
1480
|
+
)
|
|
1447
1481
|
\`\`\`
|
|
1448
1482
|
|
|
1449
1483
|
## useMessagingEvent(eventType, handler)
|
|
@@ -1474,16 +1508,15 @@ Subscribe to activity events pushed from the host via the framework. Requires \`
|
|
|
1474
1508
|
${stripImports(HOOK_SNIPPETS["events:activity"])}
|
|
1475
1509
|
\`\`\`
|
|
1476
1510
|
|
|
1477
|
-
##
|
|
1478
|
-
|
|
1479
|
-
- \`eventType:
|
|
1480
|
-
-
|
|
1481
|
-
- \`
|
|
1511
|
+
## useIdentityEvent(eventType, handler)
|
|
1512
|
+
Subscribe to identity events pushed from the host via the framework. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
|
|
1513
|
+
- \`eventType: ${identityEventTypes}\`
|
|
1514
|
+
- \`handler: (event: IdentityEvent) => void\`
|
|
1515
|
+
- \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
|
|
1516
|
+
- \`IdentityState: { authenticated: boolean, user: UserIdentity | null, expiresAt?: string }\`
|
|
1482
1517
|
|
|
1483
1518
|
\`\`\`tsx
|
|
1484
|
-
|
|
1485
|
-
console.log('Activity:', event.data)
|
|
1486
|
-
})
|
|
1519
|
+
${stripImports(HOOK_SNIPPETS["events:identity"])}
|
|
1487
1520
|
\`\`\`
|
|
1488
1521
|
|
|
1489
1522
|
## useExtendIdentity(handler)
|
|
@@ -1500,41 +1533,12 @@ With \`useCallback\` (for memoized handlers):
|
|
|
1500
1533
|
${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}
|
|
1501
1534
|
\`\`\`
|
|
1502
1535
|
|
|
1503
|
-
|
|
1536
|
+
**Identity via context.read()**
|
|
1504
1537
|
Identity state is available in the \`context.read()\` response as an \`identity\` field. Requires \`context:read\` permission (no separate identity permission needed).
|
|
1505
1538
|
\`\`\`tsx
|
|
1506
1539
|
const context = await capabilities.context.read()
|
|
1507
1540
|
// context.identity \u2014 { authenticated, user, expiresAt? }
|
|
1508
1541
|
\`\`\`
|
|
1509
|
-
|
|
1510
|
-
## useMessaging()
|
|
1511
|
-
Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`loading\` / \`error\` / \`data\`. Returns a tuple \`[send, state]\` so callers can rename \`send\` when the hook is used multiple times in one component. Requires \`messaging:send\` permission. The author label rendered above outbound messages is set per-Instance by the admin (Instance settings \`messagingDisplayName\`); falls back to \`extension.manifest.name\` when blank.
|
|
1512
|
-
|
|
1513
|
-
- **Returns:** \`readonly [send, { loading, error, data }]\`
|
|
1514
|
-
- **\`send(payload: SendMessagePayload): Promise<SendMessageResponse | null>\`** \u2014 returns the response on success; throws on **actionable** errors; resolves to \`null\` on host-handled errors (SDK logs a breadcrumb, host surfaces remediation)
|
|
1515
|
-
- **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
|
|
1516
|
-
- **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
|
|
1517
|
-
- **\`error: SendMessageActionableErrorCode | null\`** \u2014 one of \`invalid_message\` / \`rate_limited\` / \`upstream_error\` after a failed send. Host-handled codes (\`no_conversation\` / \`reauth_required\` / \`forbidden\`) never surface here.
|
|
1518
|
-
|
|
1519
|
-
Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
|
|
1520
|
-
|
|
1521
|
-
\`\`\`tsx
|
|
1522
|
-
import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
|
|
1523
|
-
|
|
1524
|
-
const { messaging } = useContextData()
|
|
1525
|
-
const [send, { loading, error }] = useMessaging()
|
|
1526
|
-
|
|
1527
|
-
// Proactive gate: skip the call when there's no conversation
|
|
1528
|
-
const canSend = !!messaging?.conversationId
|
|
1529
|
-
|
|
1530
|
-
const onApprove = async () => {
|
|
1531
|
-
try {
|
|
1532
|
-
await send({ kind: 'text', body: 'Approved \u2713' })
|
|
1533
|
-
} catch {
|
|
1534
|
-
// error holds the typed SendMessageActionableErrorCode
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
\`\`\`
|
|
1538
1542
|
`;
|
|
1539
1543
|
};
|
|
1540
1544
|
|
|
@@ -4160,7 +4164,7 @@ alongside existing surfaces.
|
|
|
4160
4164
|
Add the target to the \`targets\` array in \`packages/extension/public/manifest.json\`.
|
|
4161
4165
|
Also add any required permissions based on the target-permission mapping:
|
|
4162
4166
|
- \`slot.header\` \u2192 \`context:read\`
|
|
4163
|
-
- \`slot.content\` \u2192 \`context:read\`, \`data:query\`, \`actions:toast\`, \`actions:invoke\`
|
|
4167
|
+
- \`slot.content\` \u2192 \`context:read\`, \`data:query\`, \`actions:toast\`, \`actions:invoke\`, \`messaging:send\`
|
|
4164
4168
|
- \`slot.footer\` \u2192 (none)
|
|
4165
4169
|
- \`slot.footer-links\` \u2192 (none)
|
|
4166
4170
|
|
|
@@ -4237,13 +4241,7 @@ useIdentityEvent('refresh', (event) => {
|
|
|
4237
4241
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
4238
4242
|
})
|
|
4239
4243
|
`,
|
|
4240
|
-
"messaging.send":
|
|
4241
|
-
// Send a message into the active conversation bound to this Instance.
|
|
4242
|
-
// Requires the 'messaging:send' permission. The author label rendered above
|
|
4243
|
-
// outbound messages is set per-Instance by the admin (Instance settings
|
|
4244
|
-
// messagingDisplayName); falls back to extension.manifest.name when blank.
|
|
4245
|
-
const [send, { loading, error }] = useMessaging()
|
|
4246
|
-
const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
4244
|
+
"messaging.send": `${HOOK_SNIPPETS["messaging.send"]}
|
|
4247
4245
|
|
|
4248
4246
|
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
4249
4247
|
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
@@ -4251,7 +4249,8 @@ const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
|
4251
4249
|
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
4252
4250
|
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
4253
4251
|
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
4254
|
-
// can pre-empt 'no_conversation' via
|
|
4252
|
+
// can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
|
|
4253
|
+
// (permission-free) \u2014 wire to button disabled={!enabled || loading}.
|
|
4255
4254
|
if (error === 'rate_limited') { /* show "slow down" UI */ }
|
|
4256
4255
|
`
|
|
4257
4256
|
};
|
|
@@ -4330,7 +4329,7 @@ export function Content(): React.ReactElement {
|
|
|
4330
4329
|
// ../../sdk/extension/ai-docs/src/commands/add-capability.ts
|
|
4331
4330
|
var generateAddCapabilityCommand = () => {
|
|
4332
4331
|
const fm = frontmatter({
|
|
4333
|
-
description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity) in this extension",
|
|
4332
|
+
description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, messaging.send, identity.extend, events:identity, events:messaging, events:activity) in this extension",
|
|
4334
4333
|
targets: ["*"]
|
|
4335
4334
|
});
|
|
4336
4335
|
return `${fm}
|
|
@@ -4346,6 +4345,7 @@ Ask which capability to add. Valid capabilities:
|
|
|
4346
4345
|
- \`context.read\` \u2014 read host-provided context (customerId, customerEmail, messaging.conversationId, etc.)
|
|
4347
4346
|
- \`actions.toast\` \u2014 show toast notifications (success, error, info, warning)
|
|
4348
4347
|
- \`actions.invoke\` \u2014 invoke host actions (e.g., open new conversation)
|
|
4348
|
+
- \`messaging.send\` \u2014 send messages into the active conversation bound to this Instance (text/image/file/carousel + reply/link/postback actions)
|
|
4349
4349
|
- \`identity.extend\` \u2014 enrich identity JWT claims before signing
|
|
4350
4350
|
- \`events:identity\` \u2014 subscribe to identity events (login, logout, refresh, expired)
|
|
4351
4351
|
- \`events:messaging\` \u2014 subscribe to messaging events (postback button clicks)
|
|
@@ -4358,6 +4358,7 @@ Add the corresponding permission to \`packages/extension/public/manifest.json\`:
|
|
|
4358
4358
|
- \`context.read\` \u2192 \`"context:read"\`
|
|
4359
4359
|
- \`actions.toast\` \u2192 \`"actions:toast"\`
|
|
4360
4360
|
- \`actions.invoke\` \u2192 \`"actions:invoke"\`
|
|
4361
|
+
- \`messaging.send\` \u2192 \`"messaging:send"\`
|
|
4361
4362
|
- \`identity.extend\` \u2192 \`"identity:extend"\`
|
|
4362
4363
|
- \`events:identity\` \u2192 \`"events:identity"\` (also add entries to \`events\` array, e.g. \`["identity:login", "identity:logout"]\`)
|
|
4363
4364
|
- \`events:messaging\` \u2192 \`"events:messaging"\` (also add entries to \`events\` array, e.g. \`["messaging:postback:Buy Now"]\`)
|
|
@@ -4400,12 +4401,15 @@ const capabilities = useCapabilities()
|
|
|
4400
4401
|
// actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
|
|
4401
4402
|
\`\`\`
|
|
4402
4403
|
|
|
4403
|
-
### Special handling (events + identity.extend):
|
|
4404
|
+
### Special handling (events + identity.extend + messaging.send):
|
|
4404
4405
|
|
|
4405
4406
|
#### For events \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
|
|
4406
4407
|
Events are subscribed via \`useIdentityEvent\` / \`useMessagingEvent\` / \`useActivityEvent\` \u2014 never use \`capabilities.events.*\` directly (events are not part of the \`capabilities\` object).
|
|
4407
4408
|
|
|
4408
|
-
#### For
|
|
4409
|
+
#### For messaging.send \u2014 ALWAYS use the \`useMessaging\` hook:
|
|
4410
|
+
\`useMessaging\` returns a \`[send, { enabled, loading, error }]\` tuple. The \`send\` function is the imperative call wrapped with React state + typed errors. ALWAYS use the hook \u2014 do NOT call \`capabilities.messaging.send(...)\` directly from \`useCapabilities()\`. The hook is the canonical pattern: state management (\`enabled\`/\`loading\`) + typed errors (\`error\`) + correct payload shape (\`{ kind, body }\` with the \`kind\` discriminator). Wire send buttons to \`disabled={!enabled || loading}\` \u2014 the \`enabled\` flag pre-empts the silent \`no_conversation\` case without declaring \`context:read\`. See the snippet below for the full error-handling surface (actionable codes vs. host-handled codes).
|
|
4411
|
+
|
|
4412
|
+
#### For identity.extend \u2014 CHOOSE the correct option:
|
|
4409
4413
|
- **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires only once at initial login. ALWAYS use for known-at-login enrichment.
|
|
4410
4414
|
- **\`capabilities.identity.extend(patch)\`** \u2014 imperative call via \`useCapabilities()\`, fires post-login (after async verification, webhook callbacks, user-triggered flows). Re-signs the JWT and broadcasts \`identity:refresh\`.
|
|
4411
4415
|
|
|
@@ -4424,6 +4428,11 @@ ${HOOK_SNIPPETS["events:messaging"]}
|
|
|
4424
4428
|
${HOOK_SNIPPETS["events:activity"]}
|
|
4425
4429
|
\`\`\`
|
|
4426
4430
|
|
|
4431
|
+
\`\`\`tsx
|
|
4432
|
+
// messaging.send \u2014 use useMessaging hook (returns [send, { enabled, loading, error }] tuple)
|
|
4433
|
+
${HOOK_SNIPPETS["messaging.send"]}
|
|
4434
|
+
\`\`\`
|
|
4435
|
+
|
|
4427
4436
|
\`\`\`tsx
|
|
4428
4437
|
// identity.extend \u2014 login-time useExtendIdentity hook OR post-login imperative capabilities.identity.extend
|
|
4429
4438
|
${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
@@ -4435,6 +4444,7 @@ ${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
|
4435
4444
|
- For data/context/actions: confirm accessed via \`useCapabilities()\` hook
|
|
4436
4445
|
- For events: confirm using \`useIdentityEvent\`, \`useMessagingEvent\`, or \`useActivityEvent\` hooks
|
|
4437
4446
|
- For identity.extend: confirm using \`useExtendIdentity\` hook (login-time) and/or \`capabilities.identity.extend(patch)\` (imperative post-login)
|
|
4447
|
+
- For messaging.send: confirm using \`useMessaging\` hook
|
|
4438
4448
|
`;
|
|
4439
4449
|
};
|
|
4440
4450
|
|
|
@@ -5383,16 +5393,12 @@ export function Content() {
|
|
|
5383
5393
|
</Surface>
|
|
5384
5394
|
)
|
|
5385
5395
|
}`,
|
|
5386
|
-
"messaging.send": `import { useMessaging,
|
|
5396
|
+
"messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
5387
5397
|
|
|
5388
5398
|
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
5389
5399
|
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
5390
5400
|
export function Content() {
|
|
5391
|
-
const {
|
|
5392
|
-
const [send, { loading, error }] = useMessaging()
|
|
5393
|
-
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
5394
|
-
// host-handled no_conversation log + null return.
|
|
5395
|
-
const canSend = !!messaging?.conversationId
|
|
5401
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
5396
5402
|
|
|
5397
5403
|
const onSayHello = async () => {
|
|
5398
5404
|
try {
|
|
@@ -5412,7 +5418,7 @@ export function Content() {
|
|
|
5412
5418
|
return (
|
|
5413
5419
|
<Surface id="slot.content">
|
|
5414
5420
|
<ui.Stack direction="column" gap="2" className="p-3">
|
|
5415
|
-
<ui.Button onClick={onSayHello} disabled={
|
|
5421
|
+
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
|
|
5416
5422
|
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
5417
5423
|
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
5418
5424
|
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
@@ -5511,12 +5517,11 @@ await capabilities.identity.extend({ verified: true })
|
|
|
5511
5517
|
useIdentityEvent('refresh', (event) => {
|
|
5512
5518
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
5513
5519
|
})`,
|
|
5514
|
-
"messaging.send":
|
|
5515
|
-
|
|
5516
|
-
//
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
5520
|
+
"messaging.send": `import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
5521
|
+
|
|
5522
|
+
// manifest.json: { "permissions": ["messaging:send"] }
|
|
5523
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
5524
|
+
await send({ kind: 'text', body: 'Hello from the extension' })
|
|
5520
5525
|
|
|
5521
5526
|
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
5522
5527
|
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
@@ -5524,7 +5529,8 @@ const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
|
5524
5529
|
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
5525
5530
|
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
5526
5531
|
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
5527
|
-
// can pre-empt 'no_conversation' via
|
|
5532
|
+
// can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
|
|
5533
|
+
// (permission-free) \u2014 wire to button disabled={!enabled || loading}.
|
|
5528
5534
|
if (error === 'rate_limited') { /* show "slow down" UI */ }`
|
|
5529
5535
|
};
|
|
5530
5536
|
var EVENT_SNIPPETS2 = {
|
|
@@ -5901,7 +5907,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
5901
5907
|
usedPermissions.add(permission);
|
|
5902
5908
|
}
|
|
5903
5909
|
}
|
|
5904
|
-
for (const [hook, permission] of Object.entries(
|
|
5910
|
+
for (const [hook, permission] of Object.entries(HOOK_PERMISSION_MAP)) {
|
|
5905
5911
|
if (allSource.includes(hook)) {
|
|
5906
5912
|
usedPermissions.add(permission);
|
|
5907
5913
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP,
|
|
2
|
+
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, EVENT_HOOK_PERMISSION_MAP, MESSAGING_SEND_EXAMPLES } from '@stackable-labs/sdk-extension-contracts';
|
|
3
3
|
import { validatePatternFormat } from '@stackable-labs/lib-contracts';
|
|
4
4
|
import { readFile } from 'fs/promises';
|
|
5
5
|
import { join } from 'path';
|
|
@@ -341,6 +341,11 @@ var CORE_CONTENT = `# Extension SDK \u2014 Editorial Notes
|
|
|
341
341
|
- Always check the \`loading\` flag before using context values
|
|
342
342
|
- Context is only available when the host provides it (e.g., when viewing a ticket)
|
|
343
343
|
|
|
344
|
+
### \`messaging.send\` resolves to null without throwing
|
|
345
|
+
- The host silently handles \`no_conversation\` / \`reauth_required\` / \`forbidden\` \u2014 \`send()\` returns null and \`error\` stays unset on these codes
|
|
346
|
+
- These are operational/admin concerns (no active conversation, instance disconnected from messaging, permission revoked) \u2014 surfaced to admins via dashboard remediation UI, not user-actionable from the extension
|
|
347
|
+
- Actionable codes (\`invalid_message\`, \`rate_limited\`, \`upstream_error\`) DO populate \`error\` and throw \u2014 handle in UI
|
|
348
|
+
|
|
344
349
|
## Best Practices
|
|
345
350
|
|
|
346
351
|
### Performance
|
|
@@ -499,7 +504,12 @@ useExtendIdentity((claims) => ({
|
|
|
499
504
|
external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
|
|
500
505
|
loyalty_tier: 'bronze', // custom \u2014 sync, known at login
|
|
501
506
|
verified: false, // custom \u2014 default; updated async post-verification
|
|
502
|
-
}))
|
|
507
|
+
}))`,
|
|
508
|
+
"messaging.send": `import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
509
|
+
|
|
510
|
+
// manifest.json: { "permissions": ["messaging:send"] }
|
|
511
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
512
|
+
await send({ kind: 'text', body: 'Hello from the extension' })`
|
|
503
513
|
};
|
|
504
514
|
var HOOK_SNIPPETS_MEMOIZED = {
|
|
505
515
|
"events:messaging": `import { useCallback } from 'react'
|
|
@@ -829,7 +839,7 @@ The publisher-side bundle scan validates your declaration at submission time:
|
|
|
829
839
|
|
|
830
840
|
Post a message into the **active conversation** bound to the current Instance. The host attributes each message to the extension via the per-Instance author label set by the admin (see "Author label" below). Two complementary APIs:
|
|
831
841
|
|
|
832
|
-
1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { loading, error, data }]\` \u2014 lets callers rename \`send\` per-instance when used multiple times in one component. Preferred for UI components; narrows the error surface to actionable codes only.
|
|
842
|
+
1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { enabled, loading, error, data }]\` \u2014 lets callers rename \`send\` per-instance when used multiple times in one component. Preferred for UI components; narrows the error surface to actionable codes only. Use \`enabled\` to pre-empt the silent \`no_conversation\` case **without** declaring \`context:read\`.
|
|
833
843
|
2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
|
|
834
844
|
|
|
835
845
|
### Manifest contract
|
|
@@ -872,14 +882,9 @@ Four kinds:
|
|
|
872
882
|
### Hook usage
|
|
873
883
|
|
|
874
884
|
\`\`\`tsx
|
|
875
|
-
import { useMessaging
|
|
885
|
+
import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
876
886
|
|
|
877
|
-
const {
|
|
878
|
-
const [send, { loading, error }] = useMessaging()
|
|
879
|
-
|
|
880
|
-
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
881
|
-
// host-handled no_conversation log + null return.
|
|
882
|
-
const canSend = !!messaging?.conversationId
|
|
887
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
883
888
|
|
|
884
889
|
const onApprove = async () => {
|
|
885
890
|
try {
|
|
@@ -893,11 +898,15 @@ const onApprove = async () => {
|
|
|
893
898
|
}
|
|
894
899
|
}
|
|
895
900
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
901
|
+
return (
|
|
902
|
+
<button disabled={!enabled || loading} onClick={onApprove}>
|
|
903
|
+
{error === 'rate_limited' ? 'Slow down\u2026' : 'Approve'}
|
|
904
|
+
</button>
|
|
905
|
+
)
|
|
899
906
|
\`\`\`
|
|
900
907
|
|
|
908
|
+
The \`enabled\` flag is a permission-free, host-pushed signal \u2014 \`true\` whenever an active conversation exists. Wiring buttons to \`disabled={!enabled || loading}\` pre-empts the silent \`no_conversation\` case without forcing the extension to declare \`context:read\`. If the extension already has \`context:read\` for other reasons, \`useContextData().messaging?.conversationId\` is an equivalent gate.
|
|
909
|
+
|
|
901
910
|
### Imperative usage
|
|
902
911
|
|
|
903
912
|
\`\`\`tsx
|
|
@@ -922,7 +931,7 @@ The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two
|
|
|
922
931
|
|
|
923
932
|
| Code | What the framework does |
|
|
924
933
|
| --- | --- |
|
|
925
|
-
| \`no_conversation\` | \`console.info\` \u2014 pre-empt via \`
|
|
934
|
+
| \`no_conversation\` | \`console.info\` \u2014 pre-empt via the \`enabled\` flag from \`useMessaging()\` (no permission needed) |
|
|
926
935
|
| \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
|
|
927
936
|
| \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
|
|
928
937
|
|
|
@@ -940,7 +949,7 @@ Three gates fire in series, surfacing the same \`forbidden\` error if any blocks
|
|
|
940
949
|
|
|
941
950
|
\`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
|
|
942
951
|
|
|
943
|
-
> **Note:** the postback \`payload\` field is currently not surfaced by the Web Widget (only the button text \`actionName\`). Until that gap is closed, design action labels to be self-describing or pair sends with a follow-up \`data.
|
|
952
|
+
> **Note:** the postback \`payload\` field is currently not surfaced by the Web Widget (only the button text \`actionName\`). Until that gap is closed, design action labels to be self-describing or pair sends with a follow-up \`data.fetch\` lookup.
|
|
944
953
|
`;
|
|
945
954
|
};
|
|
946
955
|
|
|
@@ -1303,16 +1312,12 @@ export function Content(): React.ReactElement {
|
|
|
1303
1312
|
</Surface>
|
|
1304
1313
|
)
|
|
1305
1314
|
}`,
|
|
1306
|
-
"messaging.send": `import { useMessaging,
|
|
1315
|
+
"messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
1307
1316
|
|
|
1308
1317
|
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
1309
1318
|
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
1310
1319
|
export function Content(): React.ReactElement {
|
|
1311
|
-
const {
|
|
1312
|
-
const [send, { loading, error }] = useMessaging()
|
|
1313
|
-
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
1314
|
-
// host-handled no_conversation log + null return.
|
|
1315
|
-
const canSend = !!messaging?.conversationId
|
|
1320
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
1316
1321
|
|
|
1317
1322
|
const onSayHello = async () => {
|
|
1318
1323
|
try {
|
|
@@ -1332,7 +1337,7 @@ export function Content(): React.ReactElement {
|
|
|
1332
1337
|
return (
|
|
1333
1338
|
<Surface id="slot.content">
|
|
1334
1339
|
<ui.Stack direction="column" gap="2" className="p-3">
|
|
1335
|
-
<ui.Button onClick={onSayHello} disabled={
|
|
1340
|
+
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
|
|
1336
1341
|
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
1337
1342
|
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
1338
1343
|
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
@@ -1368,10 +1373,6 @@ Bootstrap the extension runtime. Call once in \`src/index.tsx\`.
|
|
|
1368
1373
|
${EXAMPLE_SNIPPETS.bootstrap}
|
|
1369
1374
|
\`\`\`
|
|
1370
1375
|
|
|
1371
|
-
## Surface component
|
|
1372
|
-
Wraps content for a target slot. The \`id\` must match a target in \`manifest.json\`.
|
|
1373
|
-
- \`<Surface id="slot.content">...</Surface>\`
|
|
1374
|
-
|
|
1375
1376
|
## useCapabilities()
|
|
1376
1377
|
Returns the capabilities object for calling host-mediated APIs.
|
|
1377
1378
|
\`\`\`tsx
|
|
@@ -1382,7 +1383,7 @@ const capabilities = useCapabilities()
|
|
|
1382
1383
|
// capabilities.actions.toast(payload)
|
|
1383
1384
|
// capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
|
|
1384
1385
|
// capabilities.identity.extend(patch) \u2014 push enrichment claims to user.metadata + JWT custom_claims (imperative; for handler-style at login, use useExtendIdentity hook). Each patch key MUST be declared in manifest.identityClaims or the host filter drops it.
|
|
1385
|
-
// capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback.
|
|
1386
|
+
// capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Prefer the useMessaging hook instead. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback.
|
|
1386
1387
|
\`\`\`
|
|
1387
1388
|
|
|
1388
1389
|
## useStore(store, selector?)
|
|
@@ -1391,6 +1392,17 @@ Subscribe to a shared store. Re-renders when the selected state changes.
|
|
|
1391
1392
|
const viewState = useStore(appStore, (s) => s.viewState)
|
|
1392
1393
|
\`\`\`
|
|
1393
1394
|
|
|
1395
|
+
## createStore(initialState)
|
|
1396
|
+
Create a shared store for cross-surface state coordination.
|
|
1397
|
+
\`\`\`tsx
|
|
1398
|
+
const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
|
|
1399
|
+
\`\`\`
|
|
1400
|
+
|
|
1401
|
+
**Store\\<T\\> interface**
|
|
1402
|
+
- \`get(): T\` \u2014 read current state
|
|
1403
|
+
- \`set(partial: Partial<T>): void\` \u2014 merge partial state update
|
|
1404
|
+
- \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
|
|
1405
|
+
|
|
1394
1406
|
## useContextData()
|
|
1395
1407
|
Reads host-provided context including extension settings. Returns \`{ loading, customerId, customerEmail, messaging, settings, ... }\`. \`messaging.conversationId\` is the active Messaging conversation ID (or \`null\` until one exists).
|
|
1396
1408
|
\`\`\`tsx
|
|
@@ -1417,26 +1429,48 @@ Returns extension-level context.
|
|
|
1417
1429
|
const { extensionId } = useExtension()
|
|
1418
1430
|
\`\`\`
|
|
1419
1431
|
|
|
1420
|
-
##
|
|
1421
|
-
|
|
1432
|
+
## useEvent(eventType, handler)
|
|
1433
|
+
Generic cross-domain event hook (should not be used unless absolutely required). Subscribe to any event using fully-qualified event types.
|
|
1434
|
+
- \`eventType: EventType\` \u2014 fully-qualified (e.g., \`'activity:product_view'\`, \`'identity:login'\`, \`'messaging:postback'\`)
|
|
1435
|
+
- Domain wildcard (e.g., \`'activity'\`) receives all events in that domain
|
|
1436
|
+
- \`handler: (event: BaseEvent) => void\`
|
|
1437
|
+
|
|
1422
1438
|
\`\`\`tsx
|
|
1423
|
-
|
|
1439
|
+
useEvent('activity', (event) => {
|
|
1440
|
+
console.log('Activity:', event.data)
|
|
1441
|
+
})
|
|
1424
1442
|
\`\`\`
|
|
1425
1443
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
- \`set(partial: Partial<T>): void\` \u2014 merge partial state update
|
|
1429
|
-
- \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
|
|
1444
|
+
## useMessaging()
|
|
1445
|
+
Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`enabled\` / \`loading\` / \`error\` / \`data\`. Returns a tuple \`[send, state]\` so callers can rename \`send\` when the hook is used multiple times in one component. Requires \`messaging:send\` permission. The author label rendered above outbound messages is set per-Instance by the admin (Instance settings \`messagingDisplayName\`); falls back to \`extension.manifest.name\` when blank.
|
|
1430
1446
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
- \`
|
|
1434
|
-
-
|
|
1435
|
-
-
|
|
1436
|
-
-
|
|
1447
|
+
- **Returns:** \`readonly [send, { enabled, loading, error, data }]\`
|
|
1448
|
+
- **\`send(payload: SendMessagePayload): Promise<SendMessageResponse | null>\`** \u2014 returns the response on success; throws on **actionable** errors; resolves to \`null\` on host-handled errors (SDK logs a breadcrumb, host surfaces remediation)
|
|
1449
|
+
- **\`enabled: boolean\`** \u2014 advisory; \`true\` when an active conversation exists. Permission-free (no \`context:read\` needed). Wire to button \`disabled\` to pre-empt the silent \`no_conversation\` case. Starts \`false\`; flips reactively via host-pushed availability events.
|
|
1450
|
+
- **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
|
|
1451
|
+
- **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
|
|
1452
|
+
- **\`error: SendMessageActionableErrorCode | null\`** \u2014 one of \`invalid_message\` / \`rate_limited\` / \`upstream_error\` after a failed send. Host-handled codes (\`no_conversation\` / \`reauth_required\` / \`forbidden\`) never surface here.
|
|
1453
|
+
|
|
1454
|
+
Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
|
|
1437
1455
|
|
|
1438
1456
|
\`\`\`tsx
|
|
1439
|
-
|
|
1457
|
+
import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
1458
|
+
|
|
1459
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
1460
|
+
|
|
1461
|
+
const onApprove = async () => {
|
|
1462
|
+
try {
|
|
1463
|
+
await send({ kind: 'text', body: 'Approved \u2713' })
|
|
1464
|
+
} catch {
|
|
1465
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
return (
|
|
1470
|
+
<button disabled={!enabled || loading} onClick={onApprove}>
|
|
1471
|
+
Approve
|
|
1472
|
+
</button>
|
|
1473
|
+
)
|
|
1440
1474
|
\`\`\`
|
|
1441
1475
|
|
|
1442
1476
|
## useMessagingEvent(eventType, handler)
|
|
@@ -1467,16 +1501,15 @@ Subscribe to activity events pushed from the host via the framework. Requires \`
|
|
|
1467
1501
|
${stripImports(HOOK_SNIPPETS["events:activity"])}
|
|
1468
1502
|
\`\`\`
|
|
1469
1503
|
|
|
1470
|
-
##
|
|
1471
|
-
|
|
1472
|
-
- \`eventType:
|
|
1473
|
-
-
|
|
1474
|
-
- \`
|
|
1504
|
+
## useIdentityEvent(eventType, handler)
|
|
1505
|
+
Subscribe to identity events pushed from the host via the framework. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
|
|
1506
|
+
- \`eventType: ${identityEventTypes}\`
|
|
1507
|
+
- \`handler: (event: IdentityEvent) => void\`
|
|
1508
|
+
- \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
|
|
1509
|
+
- \`IdentityState: { authenticated: boolean, user: UserIdentity | null, expiresAt?: string }\`
|
|
1475
1510
|
|
|
1476
1511
|
\`\`\`tsx
|
|
1477
|
-
|
|
1478
|
-
console.log('Activity:', event.data)
|
|
1479
|
-
})
|
|
1512
|
+
${stripImports(HOOK_SNIPPETS["events:identity"])}
|
|
1480
1513
|
\`\`\`
|
|
1481
1514
|
|
|
1482
1515
|
## useExtendIdentity(handler)
|
|
@@ -1493,41 +1526,12 @@ With \`useCallback\` (for memoized handlers):
|
|
|
1493
1526
|
${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}
|
|
1494
1527
|
\`\`\`
|
|
1495
1528
|
|
|
1496
|
-
|
|
1529
|
+
**Identity via context.read()**
|
|
1497
1530
|
Identity state is available in the \`context.read()\` response as an \`identity\` field. Requires \`context:read\` permission (no separate identity permission needed).
|
|
1498
1531
|
\`\`\`tsx
|
|
1499
1532
|
const context = await capabilities.context.read()
|
|
1500
1533
|
// context.identity \u2014 { authenticated, user, expiresAt? }
|
|
1501
1534
|
\`\`\`
|
|
1502
|
-
|
|
1503
|
-
## useMessaging()
|
|
1504
|
-
Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`loading\` / \`error\` / \`data\`. Returns a tuple \`[send, state]\` so callers can rename \`send\` when the hook is used multiple times in one component. Requires \`messaging:send\` permission. The author label rendered above outbound messages is set per-Instance by the admin (Instance settings \`messagingDisplayName\`); falls back to \`extension.manifest.name\` when blank.
|
|
1505
|
-
|
|
1506
|
-
- **Returns:** \`readonly [send, { loading, error, data }]\`
|
|
1507
|
-
- **\`send(payload: SendMessagePayload): Promise<SendMessageResponse | null>\`** \u2014 returns the response on success; throws on **actionable** errors; resolves to \`null\` on host-handled errors (SDK logs a breadcrumb, host surfaces remediation)
|
|
1508
|
-
- **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
|
|
1509
|
-
- **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
|
|
1510
|
-
- **\`error: SendMessageActionableErrorCode | null\`** \u2014 one of \`invalid_message\` / \`rate_limited\` / \`upstream_error\` after a failed send. Host-handled codes (\`no_conversation\` / \`reauth_required\` / \`forbidden\`) never surface here.
|
|
1511
|
-
|
|
1512
|
-
Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
|
|
1513
|
-
|
|
1514
|
-
\`\`\`tsx
|
|
1515
|
-
import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
|
|
1516
|
-
|
|
1517
|
-
const { messaging } = useContextData()
|
|
1518
|
-
const [send, { loading, error }] = useMessaging()
|
|
1519
|
-
|
|
1520
|
-
// Proactive gate: skip the call when there's no conversation
|
|
1521
|
-
const canSend = !!messaging?.conversationId
|
|
1522
|
-
|
|
1523
|
-
const onApprove = async () => {
|
|
1524
|
-
try {
|
|
1525
|
-
await send({ kind: 'text', body: 'Approved \u2713' })
|
|
1526
|
-
} catch {
|
|
1527
|
-
// error holds the typed SendMessageActionableErrorCode
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
\`\`\`
|
|
1531
1535
|
`;
|
|
1532
1536
|
};
|
|
1533
1537
|
|
|
@@ -4153,7 +4157,7 @@ alongside existing surfaces.
|
|
|
4153
4157
|
Add the target to the \`targets\` array in \`packages/extension/public/manifest.json\`.
|
|
4154
4158
|
Also add any required permissions based on the target-permission mapping:
|
|
4155
4159
|
- \`slot.header\` \u2192 \`context:read\`
|
|
4156
|
-
- \`slot.content\` \u2192 \`context:read\`, \`data:query\`, \`actions:toast\`, \`actions:invoke\`
|
|
4160
|
+
- \`slot.content\` \u2192 \`context:read\`, \`data:query\`, \`actions:toast\`, \`actions:invoke\`, \`messaging:send\`
|
|
4157
4161
|
- \`slot.footer\` \u2192 (none)
|
|
4158
4162
|
- \`slot.footer-links\` \u2192 (none)
|
|
4159
4163
|
|
|
@@ -4230,13 +4234,7 @@ useIdentityEvent('refresh', (event) => {
|
|
|
4230
4234
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
4231
4235
|
})
|
|
4232
4236
|
`,
|
|
4233
|
-
"messaging.send":
|
|
4234
|
-
// Send a message into the active conversation bound to this Instance.
|
|
4235
|
-
// Requires the 'messaging:send' permission. The author label rendered above
|
|
4236
|
-
// outbound messages is set per-Instance by the admin (Instance settings
|
|
4237
|
-
// messagingDisplayName); falls back to extension.manifest.name when blank.
|
|
4238
|
-
const [send, { loading, error }] = useMessaging()
|
|
4239
|
-
const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
4237
|
+
"messaging.send": `${HOOK_SNIPPETS["messaging.send"]}
|
|
4240
4238
|
|
|
4241
4239
|
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
4242
4240
|
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
@@ -4244,7 +4242,8 @@ const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
|
4244
4242
|
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
4245
4243
|
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
4246
4244
|
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
4247
|
-
// can pre-empt 'no_conversation' via
|
|
4245
|
+
// can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
|
|
4246
|
+
// (permission-free) \u2014 wire to button disabled={!enabled || loading}.
|
|
4248
4247
|
if (error === 'rate_limited') { /* show "slow down" UI */ }
|
|
4249
4248
|
`
|
|
4250
4249
|
};
|
|
@@ -4323,7 +4322,7 @@ export function Content(): React.ReactElement {
|
|
|
4323
4322
|
// ../../sdk/extension/ai-docs/src/commands/add-capability.ts
|
|
4324
4323
|
var generateAddCapabilityCommand = () => {
|
|
4325
4324
|
const fm = frontmatter({
|
|
4326
|
-
description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity) in this extension",
|
|
4325
|
+
description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, messaging.send, identity.extend, events:identity, events:messaging, events:activity) in this extension",
|
|
4327
4326
|
targets: ["*"]
|
|
4328
4327
|
});
|
|
4329
4328
|
return `${fm}
|
|
@@ -4339,6 +4338,7 @@ Ask which capability to add. Valid capabilities:
|
|
|
4339
4338
|
- \`context.read\` \u2014 read host-provided context (customerId, customerEmail, messaging.conversationId, etc.)
|
|
4340
4339
|
- \`actions.toast\` \u2014 show toast notifications (success, error, info, warning)
|
|
4341
4340
|
- \`actions.invoke\` \u2014 invoke host actions (e.g., open new conversation)
|
|
4341
|
+
- \`messaging.send\` \u2014 send messages into the active conversation bound to this Instance (text/image/file/carousel + reply/link/postback actions)
|
|
4342
4342
|
- \`identity.extend\` \u2014 enrich identity JWT claims before signing
|
|
4343
4343
|
- \`events:identity\` \u2014 subscribe to identity events (login, logout, refresh, expired)
|
|
4344
4344
|
- \`events:messaging\` \u2014 subscribe to messaging events (postback button clicks)
|
|
@@ -4351,6 +4351,7 @@ Add the corresponding permission to \`packages/extension/public/manifest.json\`:
|
|
|
4351
4351
|
- \`context.read\` \u2192 \`"context:read"\`
|
|
4352
4352
|
- \`actions.toast\` \u2192 \`"actions:toast"\`
|
|
4353
4353
|
- \`actions.invoke\` \u2192 \`"actions:invoke"\`
|
|
4354
|
+
- \`messaging.send\` \u2192 \`"messaging:send"\`
|
|
4354
4355
|
- \`identity.extend\` \u2192 \`"identity:extend"\`
|
|
4355
4356
|
- \`events:identity\` \u2192 \`"events:identity"\` (also add entries to \`events\` array, e.g. \`["identity:login", "identity:logout"]\`)
|
|
4356
4357
|
- \`events:messaging\` \u2192 \`"events:messaging"\` (also add entries to \`events\` array, e.g. \`["messaging:postback:Buy Now"]\`)
|
|
@@ -4393,12 +4394,15 @@ const capabilities = useCapabilities()
|
|
|
4393
4394
|
// actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
|
|
4394
4395
|
\`\`\`
|
|
4395
4396
|
|
|
4396
|
-
### Special handling (events + identity.extend):
|
|
4397
|
+
### Special handling (events + identity.extend + messaging.send):
|
|
4397
4398
|
|
|
4398
4399
|
#### For events \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
|
|
4399
4400
|
Events are subscribed via \`useIdentityEvent\` / \`useMessagingEvent\` / \`useActivityEvent\` \u2014 never use \`capabilities.events.*\` directly (events are not part of the \`capabilities\` object).
|
|
4400
4401
|
|
|
4401
|
-
#### For
|
|
4402
|
+
#### For messaging.send \u2014 ALWAYS use the \`useMessaging\` hook:
|
|
4403
|
+
\`useMessaging\` returns a \`[send, { enabled, loading, error }]\` tuple. The \`send\` function is the imperative call wrapped with React state + typed errors. ALWAYS use the hook \u2014 do NOT call \`capabilities.messaging.send(...)\` directly from \`useCapabilities()\`. The hook is the canonical pattern: state management (\`enabled\`/\`loading\`) + typed errors (\`error\`) + correct payload shape (\`{ kind, body }\` with the \`kind\` discriminator). Wire send buttons to \`disabled={!enabled || loading}\` \u2014 the \`enabled\` flag pre-empts the silent \`no_conversation\` case without declaring \`context:read\`. See the snippet below for the full error-handling surface (actionable codes vs. host-handled codes).
|
|
4404
|
+
|
|
4405
|
+
#### For identity.extend \u2014 CHOOSE the correct option:
|
|
4402
4406
|
- **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires only once at initial login. ALWAYS use for known-at-login enrichment.
|
|
4403
4407
|
- **\`capabilities.identity.extend(patch)\`** \u2014 imperative call via \`useCapabilities()\`, fires post-login (after async verification, webhook callbacks, user-triggered flows). Re-signs the JWT and broadcasts \`identity:refresh\`.
|
|
4404
4408
|
|
|
@@ -4417,6 +4421,11 @@ ${HOOK_SNIPPETS["events:messaging"]}
|
|
|
4417
4421
|
${HOOK_SNIPPETS["events:activity"]}
|
|
4418
4422
|
\`\`\`
|
|
4419
4423
|
|
|
4424
|
+
\`\`\`tsx
|
|
4425
|
+
// messaging.send \u2014 use useMessaging hook (returns [send, { enabled, loading, error }] tuple)
|
|
4426
|
+
${HOOK_SNIPPETS["messaging.send"]}
|
|
4427
|
+
\`\`\`
|
|
4428
|
+
|
|
4420
4429
|
\`\`\`tsx
|
|
4421
4430
|
// identity.extend \u2014 login-time useExtendIdentity hook OR post-login imperative capabilities.identity.extend
|
|
4422
4431
|
${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
@@ -4428,6 +4437,7 @@ ${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
|
4428
4437
|
- For data/context/actions: confirm accessed via \`useCapabilities()\` hook
|
|
4429
4438
|
- For events: confirm using \`useIdentityEvent\`, \`useMessagingEvent\`, or \`useActivityEvent\` hooks
|
|
4430
4439
|
- For identity.extend: confirm using \`useExtendIdentity\` hook (login-time) and/or \`capabilities.identity.extend(patch)\` (imperative post-login)
|
|
4440
|
+
- For messaging.send: confirm using \`useMessaging\` hook
|
|
4431
4441
|
`;
|
|
4432
4442
|
};
|
|
4433
4443
|
|
|
@@ -5376,16 +5386,12 @@ export function Content() {
|
|
|
5376
5386
|
</Surface>
|
|
5377
5387
|
)
|
|
5378
5388
|
}`,
|
|
5379
|
-
"messaging.send": `import { useMessaging,
|
|
5389
|
+
"messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
5380
5390
|
|
|
5381
5391
|
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
5382
5392
|
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
5383
5393
|
export function Content() {
|
|
5384
|
-
const {
|
|
5385
|
-
const [send, { loading, error }] = useMessaging()
|
|
5386
|
-
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
5387
|
-
// host-handled no_conversation log + null return.
|
|
5388
|
-
const canSend = !!messaging?.conversationId
|
|
5394
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
5389
5395
|
|
|
5390
5396
|
const onSayHello = async () => {
|
|
5391
5397
|
try {
|
|
@@ -5405,7 +5411,7 @@ export function Content() {
|
|
|
5405
5411
|
return (
|
|
5406
5412
|
<Surface id="slot.content">
|
|
5407
5413
|
<ui.Stack direction="column" gap="2" className="p-3">
|
|
5408
|
-
<ui.Button onClick={onSayHello} disabled={
|
|
5414
|
+
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
|
|
5409
5415
|
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
5410
5416
|
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
5411
5417
|
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
@@ -5504,12 +5510,11 @@ await capabilities.identity.extend({ verified: true })
|
|
|
5504
5510
|
useIdentityEvent('refresh', (event) => {
|
|
5505
5511
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
5506
5512
|
})`,
|
|
5507
|
-
"messaging.send":
|
|
5508
|
-
|
|
5509
|
-
//
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
5513
|
+
"messaging.send": `import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
5514
|
+
|
|
5515
|
+
// manifest.json: { "permissions": ["messaging:send"] }
|
|
5516
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
5517
|
+
await send({ kind: 'text', body: 'Hello from the extension' })
|
|
5513
5518
|
|
|
5514
5519
|
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
5515
5520
|
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
@@ -5517,7 +5522,8 @@ const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
|
5517
5522
|
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
5518
5523
|
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
5519
5524
|
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
5520
|
-
// can pre-empt 'no_conversation' via
|
|
5525
|
+
// can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
|
|
5526
|
+
// (permission-free) \u2014 wire to button disabled={!enabled || loading}.
|
|
5521
5527
|
if (error === 'rate_limited') { /* show "slow down" UI */ }`
|
|
5522
5528
|
};
|
|
5523
5529
|
var EVENT_SNIPPETS2 = {
|
|
@@ -5850,7 +5856,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
5850
5856
|
usedPermissions.add(permission);
|
|
5851
5857
|
}
|
|
5852
5858
|
}
|
|
5853
|
-
for (const [hook, permission] of Object.entries(
|
|
5859
|
+
for (const [hook, permission] of Object.entries(HOOK_PERMISSION_MAP)) {
|
|
5854
5860
|
if (allSource.includes(hook)) {
|
|
5855
5861
|
usedPermissions.add(permission);
|
|
5856
5862
|
}
|