@stackable-labs/mcp-app-extension 1.29.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +427 -38
- package/dist/server.js +427 -38
- package/package.json +1 -1
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, 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'
|
|
@@ -534,7 +544,7 @@ var generateCapabilities = () => {
|
|
|
534
544
|
const fm = frontmatter({
|
|
535
545
|
root: false,
|
|
536
546
|
targets: ["*"],
|
|
537
|
-
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
|
|
547
|
+
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, messaging.send, events:identity, events:messaging, events:activity",
|
|
538
548
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
539
549
|
});
|
|
540
550
|
return `${fm}
|
|
@@ -791,7 +801,7 @@ await capabilities.identity.extend({
|
|
|
791
801
|
// \u2192 identity:refresh broadcast to all extensions with events:identity
|
|
792
802
|
\`\`\`
|
|
793
803
|
|
|
794
|
-
Per-extension calls are serialized to prevent concurrent
|
|
804
|
+
Per-extension calls are serialized to prevent concurrent \`loginUser\` callbacks from orphaning each other.
|
|
795
805
|
|
|
796
806
|
### Consuming enriched state from another extension
|
|
797
807
|
|
|
@@ -824,6 +834,122 @@ The publisher-side bundle scan validates your declaration at submission time:
|
|
|
824
834
|
| \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
|
|
825
835
|
| \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
|
|
826
836
|
| \`identityClaims_too_many\` | error | More than 20 entries |
|
|
837
|
+
|
|
838
|
+
## messaging.send \u2014 Send Messages to Conversations
|
|
839
|
+
|
|
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:
|
|
841
|
+
|
|
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\`.
|
|
843
|
+
2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
|
|
844
|
+
|
|
845
|
+
### Manifest contract
|
|
846
|
+
|
|
847
|
+
\`\`\`json
|
|
848
|
+
{
|
|
849
|
+
"permissions": ["messaging:send"]
|
|
850
|
+
}
|
|
851
|
+
\`\`\`
|
|
852
|
+
|
|
853
|
+
- **\`messaging:send\` permission** \u2014 without it the host gate rejects the call before any network request.
|
|
854
|
+
|
|
855
|
+
### Author label (host-resolved, per-Instance)
|
|
856
|
+
|
|
857
|
+
Extensions do **not** set the author label. The host resolves it from:
|
|
858
|
+
1. \`instance.config.settings.messagingDisplayName\` \u2014 admin sets via the dashboard Instance form (visible only after OAuth completes; gated on \`messagingAppId\` being populated)
|
|
859
|
+
2. **Fallback**: \`extension.manifest.name\` \u2014 used when admin hasn't set a label
|
|
860
|
+
|
|
861
|
+
This prevents impersonation (extension A can't author as extension B) and gives admins per-deployment branding control.
|
|
862
|
+
|
|
863
|
+
### Runtime requirements
|
|
864
|
+
|
|
865
|
+
1. **Instance connected to a messaging provider** \u2014 bearer token stored, ready to post
|
|
866
|
+
2. **An active conversation** when \`send()\` is called (else host-handled \`no_conversation\` \u2014 \`send()\` returns \`null\`)
|
|
867
|
+
3. **Instance not disconnected** from the messaging provider (else host-handled \`reauth_required\` \u2014 admin reconnect surfaced in dashboard)
|
|
868
|
+
|
|
869
|
+
### Payload \u2014 discriminated by \`kind\`
|
|
870
|
+
|
|
871
|
+
Four kinds:
|
|
872
|
+
|
|
873
|
+
| kind | Required fields | Optional |
|
|
874
|
+
|---|---|---|
|
|
875
|
+
| \`'text'\` | \`body: string\` | \`actions?: MessageItemAction[]\`, \`metadata?\`, \`htmlText?\`, \`markdownText?\`, \`disableUserInput?\` |
|
|
876
|
+
| \`'image'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`actions?\`, \`metadata?\` |
|
|
877
|
+
| \`'file'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`metadata?\` (no \`actions\` \u2014 file kind doesn't accept item-level actions) |
|
|
878
|
+
| \`'carousel'\` | \`items: [MessageItem, ...MessageItem[]]\` (1\u201310 items, each with \`title\`, \`description?\`, \`imageUrl?\`, \`actions?\`) | \`displaySettings?: { imageAspectRatio?: 'square' \\| 'horizontal' }\` |
|
|
879
|
+
|
|
880
|
+
**Action types** (per-item or per-message): \`reply\` (quick-reply button \u2014 emits postback to \`useMessagingEvent\`), \`link\` (opens URL), \`postback\` (custom payload to your handler), \`locationRequest\` (asks user for location).
|
|
881
|
+
|
|
882
|
+
### Hook usage
|
|
883
|
+
|
|
884
|
+
\`\`\`tsx
|
|
885
|
+
import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
886
|
+
|
|
887
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
888
|
+
|
|
889
|
+
const onApprove = async () => {
|
|
890
|
+
try {
|
|
891
|
+
await send({
|
|
892
|
+
kind: 'text',
|
|
893
|
+
body: 'Order approved \u2713',
|
|
894
|
+
actions: [{ type: 'reply', label: 'Got it', payload: 'ACK' }],
|
|
895
|
+
})
|
|
896
|
+
} catch {
|
|
897
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return (
|
|
902
|
+
<button disabled={!enabled || loading} onClick={onApprove}>
|
|
903
|
+
{error === 'rate_limited' ? 'Slow down\u2026' : 'Approve'}
|
|
904
|
+
</button>
|
|
905
|
+
)
|
|
906
|
+
\`\`\`
|
|
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
|
+
|
|
910
|
+
### Imperative usage
|
|
911
|
+
|
|
912
|
+
\`\`\`tsx
|
|
913
|
+
const capabilities = useCapabilities()
|
|
914
|
+
const result = await capabilities.messaging.send({ kind: 'text', body: 'Hello' })
|
|
915
|
+
// result is either { messageId, receivedAt } OR { error: SendMessageErrorCode }
|
|
916
|
+
\`\`\`
|
|
917
|
+
|
|
918
|
+
### Typed error codes \u2014 split by who acts
|
|
919
|
+
|
|
920
|
+
The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two groups:
|
|
921
|
+
|
|
922
|
+
**Actionable (extension catches + renders UI)** \u2014 \`send()\` throws and \`state.error\` is populated:
|
|
923
|
+
|
|
924
|
+
| Code | When |
|
|
925
|
+
| --- | --- |
|
|
926
|
+
| \`invalid_message\` | Payload failed validation (e.g. empty \`body\` for text, carousel >10 items, list kind sent) |
|
|
927
|
+
| \`rate_limited\` | Upstream rate limit hit \u2014 back off + retry |
|
|
928
|
+
| \`upstream_error\` | Provider returned 5xx \u2014 transient; retry with backoff |
|
|
929
|
+
|
|
930
|
+
**Host-handled (extension ignores)** \u2014 \`send()\` resolves to \`null\`, \`state.error\` stays \`null\`, SDK logs a breadcrumb:
|
|
931
|
+
|
|
932
|
+
| Code | What the framework does |
|
|
933
|
+
| --- | --- |
|
|
934
|
+
| \`no_conversation\` | \`console.info\` \u2014 pre-empt via the \`enabled\` flag from \`useMessaging()\` (no permission needed) |
|
|
935
|
+
| \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
|
|
936
|
+
| \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
|
|
937
|
+
|
|
938
|
+
The imperative \`messagingSendCapability\` path returns the full taxonomy without the actionable/host-handled split \u2014 useful when you need every code (e.g. dynamic dispatch).
|
|
939
|
+
|
|
940
|
+
### Defense-in-depth gates
|
|
941
|
+
|
|
942
|
+
Three gates fire in series, surfacing the same \`forbidden\` error if any blocks:
|
|
943
|
+
|
|
944
|
+
1. **Embeddable host gate** (\`CapabilityRPCHandler\`) \u2014 checks \`sandbox.manifest.permissions.includes('messaging:send')\` before the call leaves the sandbox.
|
|
945
|
+
2. **Backend handler gate** \u2014 checks \`claims.permissions?.includes('messaging:send')\` on the proxy-token JWT.
|
|
946
|
+
3. **Server-side instance check** \u2014 \`instance.config.messagingDisconnected\` short-circuits with \`reauth_required\` before any upstream call.
|
|
947
|
+
|
|
948
|
+
### Receiving replies \u2014 pair with \`events:messaging\`
|
|
949
|
+
|
|
950
|
+
\`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
|
|
951
|
+
|
|
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.
|
|
827
953
|
`;
|
|
828
954
|
};
|
|
829
955
|
|
|
@@ -1185,6 +1311,39 @@ export function Content(): React.ReactElement {
|
|
|
1185
1311
|
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
1186
1312
|
</Surface>
|
|
1187
1313
|
)
|
|
1314
|
+
}`,
|
|
1315
|
+
"messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
1316
|
+
|
|
1317
|
+
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
1318
|
+
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
1319
|
+
export function Content(): React.ReactElement {
|
|
1320
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
1321
|
+
|
|
1322
|
+
const onSayHello = async () => {
|
|
1323
|
+
try {
|
|
1324
|
+
await send({
|
|
1325
|
+
kind: 'text',
|
|
1326
|
+
body: 'Hello from the extension',
|
|
1327
|
+
actions: [
|
|
1328
|
+
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
|
|
1329
|
+
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
|
|
1330
|
+
],
|
|
1331
|
+
})
|
|
1332
|
+
} catch {
|
|
1333
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
return (
|
|
1338
|
+
<Surface id="slot.content">
|
|
1339
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
1340
|
+
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
|
|
1341
|
+
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
1342
|
+
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
1343
|
+
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
1344
|
+
</ui.Stack>
|
|
1345
|
+
</Surface>
|
|
1346
|
+
)
|
|
1188
1347
|
}`
|
|
1189
1348
|
};
|
|
1190
1349
|
|
|
@@ -1214,10 +1373,6 @@ Bootstrap the extension runtime. Call once in \`src/index.tsx\`.
|
|
|
1214
1373
|
${EXAMPLE_SNIPPETS.bootstrap}
|
|
1215
1374
|
\`\`\`
|
|
1216
1375
|
|
|
1217
|
-
## Surface component
|
|
1218
|
-
Wraps content for a target slot. The \`id\` must match a target in \`manifest.json\`.
|
|
1219
|
-
- \`<Surface id="slot.content">...</Surface>\`
|
|
1220
|
-
|
|
1221
1376
|
## useCapabilities()
|
|
1222
1377
|
Returns the capabilities object for calling host-mediated APIs.
|
|
1223
1378
|
\`\`\`tsx
|
|
@@ -1228,6 +1383,7 @@ const capabilities = useCapabilities()
|
|
|
1228
1383
|
// capabilities.actions.toast(payload)
|
|
1229
1384
|
// capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
|
|
1230
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.
|
|
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.
|
|
1231
1387
|
\`\`\`
|
|
1232
1388
|
|
|
1233
1389
|
## useStore(store, selector?)
|
|
@@ -1236,6 +1392,17 @@ Subscribe to a shared store. Re-renders when the selected state changes.
|
|
|
1236
1392
|
const viewState = useStore(appStore, (s) => s.viewState)
|
|
1237
1393
|
\`\`\`
|
|
1238
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
|
+
|
|
1239
1406
|
## useContextData()
|
|
1240
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).
|
|
1241
1408
|
\`\`\`tsx
|
|
@@ -1262,26 +1429,48 @@ Returns extension-level context.
|
|
|
1262
1429
|
const { extensionId } = useExtension()
|
|
1263
1430
|
\`\`\`
|
|
1264
1431
|
|
|
1265
|
-
##
|
|
1266
|
-
|
|
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
|
+
|
|
1267
1438
|
\`\`\`tsx
|
|
1268
|
-
|
|
1439
|
+
useEvent('activity', (event) => {
|
|
1440
|
+
console.log('Activity:', event.data)
|
|
1441
|
+
})
|
|
1269
1442
|
\`\`\`
|
|
1270
1443
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
- \`set(partial: Partial<T>): void\` \u2014 merge partial state update
|
|
1274
|
-
- \`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.
|
|
1275
1446
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
- \`
|
|
1279
|
-
-
|
|
1280
|
-
-
|
|
1281
|
-
-
|
|
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.
|
|
1282
1455
|
|
|
1283
1456
|
\`\`\`tsx
|
|
1284
|
-
|
|
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
|
+
)
|
|
1285
1474
|
\`\`\`
|
|
1286
1475
|
|
|
1287
1476
|
## useMessagingEvent(eventType, handler)
|
|
@@ -1312,16 +1501,15 @@ Subscribe to activity events pushed from the host via the framework. Requires \`
|
|
|
1312
1501
|
${stripImports(HOOK_SNIPPETS["events:activity"])}
|
|
1313
1502
|
\`\`\`
|
|
1314
1503
|
|
|
1315
|
-
##
|
|
1316
|
-
|
|
1317
|
-
- \`eventType:
|
|
1318
|
-
-
|
|
1319
|
-
- \`
|
|
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 }\`
|
|
1320
1510
|
|
|
1321
1511
|
\`\`\`tsx
|
|
1322
|
-
|
|
1323
|
-
console.log('Activity:', event.data)
|
|
1324
|
-
})
|
|
1512
|
+
${stripImports(HOOK_SNIPPETS["events:identity"])}
|
|
1325
1513
|
\`\`\`
|
|
1326
1514
|
|
|
1327
1515
|
## useExtendIdentity(handler)
|
|
@@ -1338,7 +1526,7 @@ With \`useCallback\` (for memoized handlers):
|
|
|
1338
1526
|
${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}
|
|
1339
1527
|
\`\`\`
|
|
1340
1528
|
|
|
1341
|
-
|
|
1529
|
+
**Identity via context.read()**
|
|
1342
1530
|
Identity state is available in the \`context.read()\` response as an \`identity\` field. Requires \`context:read\` permission (no separate identity permission needed).
|
|
1343
1531
|
\`\`\`tsx
|
|
1344
1532
|
const context = await capabilities.context.read()
|
|
@@ -3605,6 +3793,27 @@ updating custom fields.
|
|
|
3605
3793
|
\`\`\`tsx
|
|
3606
3794
|
${EXAMPLE_SNIPPETS["actions.invoke"]}
|
|
3607
3795
|
\`\`\`
|
|
3796
|
+
|
|
3797
|
+
## messaging.send \u2014 Sending Messages to Conversations
|
|
3798
|
+
|
|
3799
|
+
Post a message into the active conversation bound to this Instance via the
|
|
3800
|
+
\`useMessaging\` React hook. Returns the response on happy path and throws on
|
|
3801
|
+
**actionable** errors \u2014 inspect \`error\` for one of the typed
|
|
3802
|
+
\`SendMessageActionableErrorCode\` values (\`invalid_message\`, \`rate_limited\`,
|
|
3803
|
+
\`upstream_error\`). Host-handled cases (\`no_conversation\`, \`reauth_required\`,
|
|
3804
|
+
\`forbidden\`) resolve to \`null\` without throwing; the SDK logs a breadcrumb
|
|
3805
|
+
and the host surfaces remediation to admins (e.g. dashboard reconnect UI) \u2014
|
|
3806
|
+
extensions don't catch them.
|
|
3807
|
+
|
|
3808
|
+
**Permission:** \`messaging:send\`. The author label rendered above outbound
|
|
3809
|
+
messages is set **per-Instance by the admin** via the Instance settings field
|
|
3810
|
+
\`messagingDisplayName\` (visible on the dashboard once OAuth completes);
|
|
3811
|
+
falls back to \`extension.manifest.name\` when blank. Extensions do not set or
|
|
3812
|
+
think about the author identity.
|
|
3813
|
+
|
|
3814
|
+
\`\`\`tsx
|
|
3815
|
+
${EXAMPLE_SNIPPETS["messaging.send"]}
|
|
3816
|
+
\`\`\`
|
|
3608
3817
|
`;
|
|
3609
3818
|
};
|
|
3610
3819
|
var generateCookbookStructural = () => {
|
|
@@ -3688,6 +3897,108 @@ ${EXAMPLE_SNIPPETS.surfaceContext}
|
|
|
3688
3897
|
\`\`\`
|
|
3689
3898
|
`;
|
|
3690
3899
|
};
|
|
3900
|
+
var renderPayload = (kind) => JSON.stringify(MESSAGING_SEND_EXAMPLES[kind], null, 2);
|
|
3901
|
+
var renderAction = (type) => JSON.stringify(MESSAGING_ACTION_EXAMPLES[type], null, 2);
|
|
3902
|
+
var ACTION_DESCRIPTIONS = {
|
|
3903
|
+
reply: "Inserts a visible user-reply bubble carrying `payload` back into the conversation, as if the user typed it. **Mutually exclusive** \u2014 a `reply` action cannot share an `actions[]` array with any other action type; the host validates and surfaces `invalid_message` if mixed.",
|
|
3904
|
+
link: "Opens `url` in a new tab/window. No bubble inserted. Freely mixable with other non-reply actions.",
|
|
3905
|
+
postback: "Fires a server-side `conversation:postback` webhook to the Stackable backend carrying the full `payload` + any `metadata`. NO visible bubble. Extensions with the `events:messaging` permission receive postback events via `useMessagingEvent` \u2014 see the **Events & Identity** cookbook.",
|
|
3906
|
+
locationRequest: "Prompts the user to share device location. Response arrives as a separate `location`-type message inbound to the conversation. Freely mixable."
|
|
3907
|
+
};
|
|
3908
|
+
var actionTypeSections = Object.keys(MESSAGING_ACTION_EXAMPLES).map((type) => `### type: '${type}'
|
|
3909
|
+
|
|
3910
|
+
${ACTION_DESCRIPTIONS[type]}
|
|
3911
|
+
|
|
3912
|
+
\`\`\`tsx
|
|
3913
|
+
${renderAction(type)}
|
|
3914
|
+
\`\`\``).join("\n\n");
|
|
3915
|
+
var generateCookbookMessaging = () => {
|
|
3916
|
+
const fm = frontmatter({
|
|
3917
|
+
root: false,
|
|
3918
|
+
targets: ["*"],
|
|
3919
|
+
description: "Cookbook: example payloads per-kind for the messaging.send capability + action type shapes",
|
|
3920
|
+
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
3921
|
+
});
|
|
3922
|
+
return `${fm}
|
|
3923
|
+
|
|
3924
|
+
# Messaging Examples
|
|
3925
|
+
|
|
3926
|
+
Example payloads for the \`messaging.send\` capability \u2014 one per supported message
|
|
3927
|
+
kind, plus the action types you can attach to messages. Each payload is the
|
|
3928
|
+
exact shape the \`useMessaging\` hook accepts; copy and adapt as needed.
|
|
3929
|
+
|
|
3930
|
+
**Permission:** \`messaging:send\` in your \`manifest.json\`. The author label
|
|
3931
|
+
above outbound messages is set **per-Instance by the admin** via the Instance
|
|
3932
|
+
settings field \`messagingDisplayName\`; falls back to \`extension.manifest.name\`
|
|
3933
|
+
when blank. Extensions do not set or think about the author identity.
|
|
3934
|
+
|
|
3935
|
+
---
|
|
3936
|
+
|
|
3937
|
+
## kind: 'text' \u2014 Plain text + reply actions
|
|
3938
|
+
|
|
3939
|
+
Plain conversational message. Optional \`actions\` array attaches quick-reply
|
|
3940
|
+
buttons, links, postbacks, or location requests.
|
|
3941
|
+
|
|
3942
|
+
\`\`\`tsx
|
|
3943
|
+
const [send] = useMessaging()
|
|
3944
|
+
|
|
3945
|
+
await send(${renderPayload("text")})
|
|
3946
|
+
\`\`\`
|
|
3947
|
+
|
|
3948
|
+
## kind: 'image' \u2014 Embedded image + optional caption
|
|
3949
|
+
|
|
3950
|
+
Product photos, screenshots, visual help. \`url\` must respond with a
|
|
3951
|
+
\`Content-Type: image/*\` header (Sunco enforces; redirecting URLs like
|
|
3952
|
+
\`picsum.photos\` will fail). \`altText\` is optional \u2014 defaults to filename.
|
|
3953
|
+
Optional \`body\` renders a caption alongside the image.
|
|
3954
|
+
|
|
3955
|
+
\`\`\`tsx
|
|
3956
|
+
const [send] = useMessaging()
|
|
3957
|
+
|
|
3958
|
+
await send(${renderPayload("image")})
|
|
3959
|
+
\`\`\`
|
|
3960
|
+
|
|
3961
|
+
## kind: 'file' \u2014 Embedded file attachment
|
|
3962
|
+
|
|
3963
|
+
Return labels, receipts, NDAs, warranty paperwork. Sunco's \`fileMessage\`
|
|
3964
|
+
schema does NOT support \`actions\` \u2014 file messages can only carry an
|
|
3965
|
+
optional text caption.
|
|
3966
|
+
|
|
3967
|
+
\`\`\`tsx
|
|
3968
|
+
const [send] = useMessaging()
|
|
3969
|
+
|
|
3970
|
+
await send(${renderPayload("file")})
|
|
3971
|
+
\`\`\`
|
|
3972
|
+
|
|
3973
|
+
## kind: 'carousel' \u2014 Horizontally scrolling cards
|
|
3974
|
+
|
|
3975
|
+
The conversational-commerce primitive: product recommendations, size/color
|
|
3976
|
+
pickers, search results with images, multi-option selection.
|
|
3977
|
+
|
|
3978
|
+
- 1\u201310 items, each with required \`title\` + required \`actions\` (1\u20133 per item)
|
|
3979
|
+
- Item actions are a subset: \`link\` + \`postback\` only (no \`reply\`, no
|
|
3980
|
+
\`locationRequest\` \u2014 Sunco rejects those at the item level)
|
|
3981
|
+
- Carousel messages have NO message-level \`actions\` field \u2014 actions live
|
|
3982
|
+
per-card
|
|
3983
|
+
- Use a separate \`text\` message before the carousel if you need an intro
|
|
3984
|
+
|
|
3985
|
+
\`\`\`tsx
|
|
3986
|
+
const [send] = useMessaging()
|
|
3987
|
+
|
|
3988
|
+
await send(${renderPayload("carousel")})
|
|
3989
|
+
\`\`\`
|
|
3990
|
+
|
|
3991
|
+
---
|
|
3992
|
+
|
|
3993
|
+
## Action types
|
|
3994
|
+
|
|
3995
|
+
Attach to message-level \`actions\` (text / image) or item-level \`actions\`
|
|
3996
|
+
(carousel). All actions share \`label\` + optional \`metadata\` (primitives only,
|
|
3997
|
+
\u22644KB total).
|
|
3998
|
+
|
|
3999
|
+
${actionTypeSections}
|
|
4000
|
+
`;
|
|
4001
|
+
};
|
|
3691
4002
|
var identityEventTypes2 = Object.values(IDENTITY_EVENT).map((e) => `\`${e}\``).join(", ");
|
|
3692
4003
|
var activityEventTypes2 = Object.values(ACTIVITY_EVENT).map((e) => `\`${e}\``).join(", ");
|
|
3693
4004
|
var generateCookbookEvents = () => {
|
|
@@ -3709,7 +4020,7 @@ supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-l
|
|
|
3709
4020
|
|
|
3710
4021
|
## Messaging Events
|
|
3711
4022
|
|
|
3712
|
-
Subscribe to postback button clicks from the
|
|
4023
|
+
Subscribe to postback button clicks from the Messaging widget.
|
|
3713
4024
|
The \`actionName\` is the button's display text, not a programmatic identifier.
|
|
3714
4025
|
|
|
3715
4026
|
**Permission:** \`events:messaging\`
|
|
@@ -3846,7 +4157,7 @@ alongside existing surfaces.
|
|
|
3846
4157
|
Add the target to the \`targets\` array in \`packages/extension/public/manifest.json\`.
|
|
3847
4158
|
Also add any required permissions based on the target-permission mapping:
|
|
3848
4159
|
- \`slot.header\` \u2192 \`context:read\`
|
|
3849
|
-
- \`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\`
|
|
3850
4161
|
- \`slot.footer\` \u2192 (none)
|
|
3851
4162
|
- \`slot.footer-links\` \u2192 (none)
|
|
3852
4163
|
|
|
@@ -3922,6 +4233,18 @@ await capabilities.identity.extend({ verified: true })
|
|
|
3922
4233
|
useIdentityEvent('refresh', (event) => {
|
|
3923
4234
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
3924
4235
|
})
|
|
4236
|
+
`,
|
|
4237
|
+
"messaging.send": `${HOOK_SNIPPETS["messaging.send"]}
|
|
4238
|
+
|
|
4239
|
+
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
4240
|
+
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
4241
|
+
// catch / on state.error to show UX.
|
|
4242
|
+
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
4243
|
+
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
4244
|
+
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
4245
|
+
// can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
|
|
4246
|
+
// (permission-free) \u2014 wire to button disabled={!enabled || loading}.
|
|
4247
|
+
if (error === 'rate_limited') { /* show "slow down" UI */ }
|
|
3925
4248
|
`
|
|
3926
4249
|
};
|
|
3927
4250
|
var EVENT_SNIPPETS = {
|
|
@@ -3999,7 +4322,7 @@ export function Content(): React.ReactElement {
|
|
|
3999
4322
|
// ../../sdk/extension/ai-docs/src/commands/add-capability.ts
|
|
4000
4323
|
var generateAddCapabilityCommand = () => {
|
|
4001
4324
|
const fm = frontmatter({
|
|
4002
|
-
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",
|
|
4003
4326
|
targets: ["*"]
|
|
4004
4327
|
});
|
|
4005
4328
|
return `${fm}
|
|
@@ -4015,6 +4338,7 @@ Ask which capability to add. Valid capabilities:
|
|
|
4015
4338
|
- \`context.read\` \u2014 read host-provided context (customerId, customerEmail, messaging.conversationId, etc.)
|
|
4016
4339
|
- \`actions.toast\` \u2014 show toast notifications (success, error, info, warning)
|
|
4017
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)
|
|
4018
4342
|
- \`identity.extend\` \u2014 enrich identity JWT claims before signing
|
|
4019
4343
|
- \`events:identity\` \u2014 subscribe to identity events (login, logout, refresh, expired)
|
|
4020
4344
|
- \`events:messaging\` \u2014 subscribe to messaging events (postback button clicks)
|
|
@@ -4027,6 +4351,7 @@ Add the corresponding permission to \`packages/extension/public/manifest.json\`:
|
|
|
4027
4351
|
- \`context.read\` \u2192 \`"context:read"\`
|
|
4028
4352
|
- \`actions.toast\` \u2192 \`"actions:toast"\`
|
|
4029
4353
|
- \`actions.invoke\` \u2192 \`"actions:invoke"\`
|
|
4354
|
+
- \`messaging.send\` \u2192 \`"messaging:send"\`
|
|
4030
4355
|
- \`identity.extend\` \u2192 \`"identity:extend"\`
|
|
4031
4356
|
- \`events:identity\` \u2192 \`"events:identity"\` (also add entries to \`events\` array, e.g. \`["identity:login", "identity:logout"]\`)
|
|
4032
4357
|
- \`events:messaging\` \u2192 \`"events:messaging"\` (also add entries to \`events\` array, e.g. \`["messaging:postback:Buy Now"]\`)
|
|
@@ -4069,12 +4394,15 @@ const capabilities = useCapabilities()
|
|
|
4069
4394
|
// actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
|
|
4070
4395
|
\`\`\`
|
|
4071
4396
|
|
|
4072
|
-
### Special handling (events + identity.extend):
|
|
4397
|
+
### Special handling (events + identity.extend + messaging.send):
|
|
4073
4398
|
|
|
4074
4399
|
#### For events \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
|
|
4075
4400
|
Events are subscribed via \`useIdentityEvent\` / \`useMessagingEvent\` / \`useActivityEvent\` \u2014 never use \`capabilities.events.*\` directly (events are not part of the \`capabilities\` object).
|
|
4076
4401
|
|
|
4077
|
-
#### 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:
|
|
4078
4406
|
- **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires only once at initial login. ALWAYS use for known-at-login enrichment.
|
|
4079
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\`.
|
|
4080
4408
|
|
|
@@ -4093,6 +4421,11 @@ ${HOOK_SNIPPETS["events:messaging"]}
|
|
|
4093
4421
|
${HOOK_SNIPPETS["events:activity"]}
|
|
4094
4422
|
\`\`\`
|
|
4095
4423
|
|
|
4424
|
+
\`\`\`tsx
|
|
4425
|
+
// messaging.send \u2014 use useMessaging hook (returns [send, { enabled, loading, error }] tuple)
|
|
4426
|
+
${HOOK_SNIPPETS["messaging.send"]}
|
|
4427
|
+
\`\`\`
|
|
4428
|
+
|
|
4096
4429
|
\`\`\`tsx
|
|
4097
4430
|
// identity.extend \u2014 login-time useExtendIdentity hook OR post-login imperative capabilities.identity.extend
|
|
4098
4431
|
${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
@@ -4104,6 +4437,7 @@ ${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
|
4104
4437
|
- For data/context/actions: confirm accessed via \`useCapabilities()\` hook
|
|
4105
4438
|
- For events: confirm using \`useIdentityEvent\`, \`useMessagingEvent\`, or \`useActivityEvent\` hooks
|
|
4106
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
|
|
4107
4441
|
`;
|
|
4108
4442
|
};
|
|
4109
4443
|
|
|
@@ -4575,6 +4909,13 @@ var SKILLS = [
|
|
|
4575
4909
|
scopes: ["docs"],
|
|
4576
4910
|
content: () => generateCookbookEvents()
|
|
4577
4911
|
},
|
|
4912
|
+
{
|
|
4913
|
+
id: "cookbook-messaging",
|
|
4914
|
+
description: "Cookbook: per-kind example payloads (text / image / file / carousel) and action types (reply / link / postback / locationRequest) for the messaging.send capability. Use when wiring the useMessaging hook or composing outbound conversation messages.",
|
|
4915
|
+
type: "knowledge",
|
|
4916
|
+
scopes: ["docs"],
|
|
4917
|
+
content: () => generateCookbookMessaging()
|
|
4918
|
+
},
|
|
4578
4919
|
{
|
|
4579
4920
|
id: "marketplace-listing",
|
|
4580
4921
|
description: "Marketplace listing reference: the listing fields (icon, screenshots, description, categories), Public vs Protected visibility, and the two-stage review process (bundle scan + security review + approval). Use when publishing a deployed extension to the marketplace.",
|
|
@@ -5044,6 +5385,39 @@ export function Content() {
|
|
|
5044
5385
|
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
5045
5386
|
</Surface>
|
|
5046
5387
|
)
|
|
5388
|
+
}`,
|
|
5389
|
+
"messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
5390
|
+
|
|
5391
|
+
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
5392
|
+
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
5393
|
+
export function Content() {
|
|
5394
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
5395
|
+
|
|
5396
|
+
const onSayHello = async () => {
|
|
5397
|
+
try {
|
|
5398
|
+
await send({
|
|
5399
|
+
kind: 'text',
|
|
5400
|
+
body: 'Hello from the extension',
|
|
5401
|
+
actions: [
|
|
5402
|
+
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
|
|
5403
|
+
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
|
|
5404
|
+
],
|
|
5405
|
+
})
|
|
5406
|
+
} catch {
|
|
5407
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
return (
|
|
5412
|
+
<Surface id="slot.content">
|
|
5413
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
5414
|
+
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
|
|
5415
|
+
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
5416
|
+
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
5417
|
+
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
5418
|
+
</ui.Stack>
|
|
5419
|
+
</Surface>
|
|
5420
|
+
)
|
|
5047
5421
|
}`
|
|
5048
5422
|
};
|
|
5049
5423
|
|
|
@@ -5135,7 +5509,22 @@ await capabilities.identity.extend({ verified: true })
|
|
|
5135
5509
|
// Consumer side \u2014 same or sibling extension reacts via identity:refresh
|
|
5136
5510
|
useIdentityEvent('refresh', (event) => {
|
|
5137
5511
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
5138
|
-
})
|
|
5512
|
+
})`,
|
|
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' })
|
|
5518
|
+
|
|
5519
|
+
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
5520
|
+
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
5521
|
+
// catch / on state.error to show UX.
|
|
5522
|
+
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
5523
|
+
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
5524
|
+
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
5525
|
+
// can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
|
|
5526
|
+
// (permission-free) \u2014 wire to button disabled={!enabled || loading}.
|
|
5527
|
+
if (error === 'rate_limited') { /* show "slow down" UI */ }`
|
|
5139
5528
|
};
|
|
5140
5529
|
var EVENT_SNIPPETS2 = {
|
|
5141
5530
|
"events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
@@ -5467,7 +5856,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
5467
5856
|
usedPermissions.add(permission);
|
|
5468
5857
|
}
|
|
5469
5858
|
}
|
|
5470
|
-
for (const [hook, permission] of Object.entries(
|
|
5859
|
+
for (const [hook, permission] of Object.entries(HOOK_PERMISSION_MAP)) {
|
|
5471
5860
|
if (allSource.includes(hook)) {
|
|
5472
5861
|
usedPermissions.add(permission);
|
|
5473
5862
|
}
|