@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.
Files changed (3) hide show
  1. package/dist/index.js +427 -38
  2. package/dist/server.js +427 -38
  3. 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, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
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 Zendesk \`loginUser\` callbacks from orphaning each other (empirically verified in the loginUser re-auth spike).
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
- ## createStore(initialState)
1266
- Create a shared store for cross-surface state coordination.
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
- const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
1439
+ useEvent('activity', (event) => {
1440
+ console.log('Activity:', event.data)
1441
+ })
1269
1442
  \`\`\`
1270
1443
 
1271
- ### Store\\<T\\> interface
1272
- - \`get(): T\` \u2014 read current state
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
- ## useIdentityEvent(eventType, handler)
1277
- Subscribe to identity events pushed from the host via the framework. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
1278
- - \`eventType: ${identityEventTypes}\`
1279
- - \`handler: (event: IdentityEvent) => void\`
1280
- - \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
1281
- - \`IdentityState: { authenticated: boolean, user: UserIdentity | null, expiresAt?: string }\`
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
- ${stripImports(HOOK_SNIPPETS["events:identity"])}
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
- ## useEvent(eventType, handler)
1316
- Generic cross-domain event hook (should not be used unless absolutely required). Subscribe to any event using fully-qualified event types.
1317
- - \`eventType: EventType\` \u2014 fully-qualified (e.g., \`'activity:product_view'\`, \`'identity:login'\`, \`'messaging:postback'\`)
1318
- - Domain wildcard (e.g., \`'activity'\`) receives all events in that domain
1319
- - \`handler: (event: BaseEvent) => void\`
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
- useEvent('activity', (event) => {
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
- ## Identity via context.read()
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 Zendesk messaging widget.
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 identity.extend \u2014 choose the CORRECT option:
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(EVENT_HOOK_PERMISSION_MAP)) {
5859
+ for (const [hook, permission] of Object.entries(HOOK_PERMISSION_MAP)) {
5471
5860
  if (allSource.includes(hook)) {
5472
5861
  usedPermissions.add(permission);
5473
5862
  }