@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/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, 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'
|
|
@@ -541,7 +551,7 @@ var generateCapabilities = () => {
|
|
|
541
551
|
const fm = frontmatter({
|
|
542
552
|
root: false,
|
|
543
553
|
targets: ["*"],
|
|
544
|
-
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
|
|
554
|
+
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, messaging.send, events:identity, events:messaging, events:activity",
|
|
545
555
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
546
556
|
});
|
|
547
557
|
return `${fm}
|
|
@@ -798,7 +808,7 @@ await capabilities.identity.extend({
|
|
|
798
808
|
// \u2192 identity:refresh broadcast to all extensions with events:identity
|
|
799
809
|
\`\`\`
|
|
800
810
|
|
|
801
|
-
Per-extension calls are serialized to prevent concurrent
|
|
811
|
+
Per-extension calls are serialized to prevent concurrent \`loginUser\` callbacks from orphaning each other.
|
|
802
812
|
|
|
803
813
|
### Consuming enriched state from another extension
|
|
804
814
|
|
|
@@ -831,6 +841,122 @@ The publisher-side bundle scan validates your declaration at submission time:
|
|
|
831
841
|
| \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
|
|
832
842
|
| \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
|
|
833
843
|
| \`identityClaims_too_many\` | error | More than 20 entries |
|
|
844
|
+
|
|
845
|
+
## messaging.send \u2014 Send Messages to Conversations
|
|
846
|
+
|
|
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:
|
|
848
|
+
|
|
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\`.
|
|
850
|
+
2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
|
|
851
|
+
|
|
852
|
+
### Manifest contract
|
|
853
|
+
|
|
854
|
+
\`\`\`json
|
|
855
|
+
{
|
|
856
|
+
"permissions": ["messaging:send"]
|
|
857
|
+
}
|
|
858
|
+
\`\`\`
|
|
859
|
+
|
|
860
|
+
- **\`messaging:send\` permission** \u2014 without it the host gate rejects the call before any network request.
|
|
861
|
+
|
|
862
|
+
### Author label (host-resolved, per-Instance)
|
|
863
|
+
|
|
864
|
+
Extensions do **not** set the author label. The host resolves it from:
|
|
865
|
+
1. \`instance.config.settings.messagingDisplayName\` \u2014 admin sets via the dashboard Instance form (visible only after OAuth completes; gated on \`messagingAppId\` being populated)
|
|
866
|
+
2. **Fallback**: \`extension.manifest.name\` \u2014 used when admin hasn't set a label
|
|
867
|
+
|
|
868
|
+
This prevents impersonation (extension A can't author as extension B) and gives admins per-deployment branding control.
|
|
869
|
+
|
|
870
|
+
### Runtime requirements
|
|
871
|
+
|
|
872
|
+
1. **Instance connected to a messaging provider** \u2014 bearer token stored, ready to post
|
|
873
|
+
2. **An active conversation** when \`send()\` is called (else host-handled \`no_conversation\` \u2014 \`send()\` returns \`null\`)
|
|
874
|
+
3. **Instance not disconnected** from the messaging provider (else host-handled \`reauth_required\` \u2014 admin reconnect surfaced in dashboard)
|
|
875
|
+
|
|
876
|
+
### Payload \u2014 discriminated by \`kind\`
|
|
877
|
+
|
|
878
|
+
Four kinds:
|
|
879
|
+
|
|
880
|
+
| kind | Required fields | Optional |
|
|
881
|
+
|---|---|---|
|
|
882
|
+
| \`'text'\` | \`body: string\` | \`actions?: MessageItemAction[]\`, \`metadata?\`, \`htmlText?\`, \`markdownText?\`, \`disableUserInput?\` |
|
|
883
|
+
| \`'image'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`actions?\`, \`metadata?\` |
|
|
884
|
+
| \`'file'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`metadata?\` (no \`actions\` \u2014 file kind doesn't accept item-level actions) |
|
|
885
|
+
| \`'carousel'\` | \`items: [MessageItem, ...MessageItem[]]\` (1\u201310 items, each with \`title\`, \`description?\`, \`imageUrl?\`, \`actions?\`) | \`displaySettings?: { imageAspectRatio?: 'square' \\| 'horizontal' }\` |
|
|
886
|
+
|
|
887
|
+
**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).
|
|
888
|
+
|
|
889
|
+
### Hook usage
|
|
890
|
+
|
|
891
|
+
\`\`\`tsx
|
|
892
|
+
import { useMessaging } from '@stackable-labs/sdk-extension-react'
|
|
893
|
+
|
|
894
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
895
|
+
|
|
896
|
+
const onApprove = async () => {
|
|
897
|
+
try {
|
|
898
|
+
await send({
|
|
899
|
+
kind: 'text',
|
|
900
|
+
body: 'Order approved \u2713',
|
|
901
|
+
actions: [{ type: 'reply', label: 'Got it', payload: 'ACK' }],
|
|
902
|
+
})
|
|
903
|
+
} catch {
|
|
904
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return (
|
|
909
|
+
<button disabled={!enabled || loading} onClick={onApprove}>
|
|
910
|
+
{error === 'rate_limited' ? 'Slow down\u2026' : 'Approve'}
|
|
911
|
+
</button>
|
|
912
|
+
)
|
|
913
|
+
\`\`\`
|
|
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
|
+
|
|
917
|
+
### Imperative usage
|
|
918
|
+
|
|
919
|
+
\`\`\`tsx
|
|
920
|
+
const capabilities = useCapabilities()
|
|
921
|
+
const result = await capabilities.messaging.send({ kind: 'text', body: 'Hello' })
|
|
922
|
+
// result is either { messageId, receivedAt } OR { error: SendMessageErrorCode }
|
|
923
|
+
\`\`\`
|
|
924
|
+
|
|
925
|
+
### Typed error codes \u2014 split by who acts
|
|
926
|
+
|
|
927
|
+
The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two groups:
|
|
928
|
+
|
|
929
|
+
**Actionable (extension catches + renders UI)** \u2014 \`send()\` throws and \`state.error\` is populated:
|
|
930
|
+
|
|
931
|
+
| Code | When |
|
|
932
|
+
| --- | --- |
|
|
933
|
+
| \`invalid_message\` | Payload failed validation (e.g. empty \`body\` for text, carousel >10 items, list kind sent) |
|
|
934
|
+
| \`rate_limited\` | Upstream rate limit hit \u2014 back off + retry |
|
|
935
|
+
| \`upstream_error\` | Provider returned 5xx \u2014 transient; retry with backoff |
|
|
936
|
+
|
|
937
|
+
**Host-handled (extension ignores)** \u2014 \`send()\` resolves to \`null\`, \`state.error\` stays \`null\`, SDK logs a breadcrumb:
|
|
938
|
+
|
|
939
|
+
| Code | What the framework does |
|
|
940
|
+
| --- | --- |
|
|
941
|
+
| \`no_conversation\` | \`console.info\` \u2014 pre-empt via the \`enabled\` flag from \`useMessaging()\` (no permission needed) |
|
|
942
|
+
| \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
|
|
943
|
+
| \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
|
|
944
|
+
|
|
945
|
+
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).
|
|
946
|
+
|
|
947
|
+
### Defense-in-depth gates
|
|
948
|
+
|
|
949
|
+
Three gates fire in series, surfacing the same \`forbidden\` error if any blocks:
|
|
950
|
+
|
|
951
|
+
1. **Embeddable host gate** (\`CapabilityRPCHandler\`) \u2014 checks \`sandbox.manifest.permissions.includes('messaging:send')\` before the call leaves the sandbox.
|
|
952
|
+
2. **Backend handler gate** \u2014 checks \`claims.permissions?.includes('messaging:send')\` on the proxy-token JWT.
|
|
953
|
+
3. **Server-side instance check** \u2014 \`instance.config.messagingDisconnected\` short-circuits with \`reauth_required\` before any upstream call.
|
|
954
|
+
|
|
955
|
+
### Receiving replies \u2014 pair with \`events:messaging\`
|
|
956
|
+
|
|
957
|
+
\`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
|
|
958
|
+
|
|
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.
|
|
834
960
|
`;
|
|
835
961
|
};
|
|
836
962
|
|
|
@@ -1192,6 +1318,39 @@ export function Content(): React.ReactElement {
|
|
|
1192
1318
|
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
1193
1319
|
</Surface>
|
|
1194
1320
|
)
|
|
1321
|
+
}`,
|
|
1322
|
+
"messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
1323
|
+
|
|
1324
|
+
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
1325
|
+
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
1326
|
+
export function Content(): React.ReactElement {
|
|
1327
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
1328
|
+
|
|
1329
|
+
const onSayHello = async () => {
|
|
1330
|
+
try {
|
|
1331
|
+
await send({
|
|
1332
|
+
kind: 'text',
|
|
1333
|
+
body: 'Hello from the extension',
|
|
1334
|
+
actions: [
|
|
1335
|
+
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
|
|
1336
|
+
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
|
|
1337
|
+
],
|
|
1338
|
+
})
|
|
1339
|
+
} catch {
|
|
1340
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
return (
|
|
1345
|
+
<Surface id="slot.content">
|
|
1346
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
1347
|
+
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
|
|
1348
|
+
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
1349
|
+
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
1350
|
+
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
1351
|
+
</ui.Stack>
|
|
1352
|
+
</Surface>
|
|
1353
|
+
)
|
|
1195
1354
|
}`
|
|
1196
1355
|
};
|
|
1197
1356
|
|
|
@@ -1221,10 +1380,6 @@ Bootstrap the extension runtime. Call once in \`src/index.tsx\`.
|
|
|
1221
1380
|
${EXAMPLE_SNIPPETS.bootstrap}
|
|
1222
1381
|
\`\`\`
|
|
1223
1382
|
|
|
1224
|
-
## Surface component
|
|
1225
|
-
Wraps content for a target slot. The \`id\` must match a target in \`manifest.json\`.
|
|
1226
|
-
- \`<Surface id="slot.content">...</Surface>\`
|
|
1227
|
-
|
|
1228
1383
|
## useCapabilities()
|
|
1229
1384
|
Returns the capabilities object for calling host-mediated APIs.
|
|
1230
1385
|
\`\`\`tsx
|
|
@@ -1235,6 +1390,7 @@ const capabilities = useCapabilities()
|
|
|
1235
1390
|
// capabilities.actions.toast(payload)
|
|
1236
1391
|
// capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
|
|
1237
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.
|
|
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.
|
|
1238
1394
|
\`\`\`
|
|
1239
1395
|
|
|
1240
1396
|
## useStore(store, selector?)
|
|
@@ -1243,6 +1399,17 @@ Subscribe to a shared store. Re-renders when the selected state changes.
|
|
|
1243
1399
|
const viewState = useStore(appStore, (s) => s.viewState)
|
|
1244
1400
|
\`\`\`
|
|
1245
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
|
+
|
|
1246
1413
|
## useContextData()
|
|
1247
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).
|
|
1248
1415
|
\`\`\`tsx
|
|
@@ -1269,26 +1436,48 @@ Returns extension-level context.
|
|
|
1269
1436
|
const { extensionId } = useExtension()
|
|
1270
1437
|
\`\`\`
|
|
1271
1438
|
|
|
1272
|
-
##
|
|
1273
|
-
|
|
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
|
+
|
|
1274
1445
|
\`\`\`tsx
|
|
1275
|
-
|
|
1446
|
+
useEvent('activity', (event) => {
|
|
1447
|
+
console.log('Activity:', event.data)
|
|
1448
|
+
})
|
|
1276
1449
|
\`\`\`
|
|
1277
1450
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
- \`set(partial: Partial<T>): void\` \u2014 merge partial state update
|
|
1281
|
-
- \`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.
|
|
1282
1453
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
- \`
|
|
1286
|
-
-
|
|
1287
|
-
-
|
|
1288
|
-
-
|
|
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.
|
|
1289
1462
|
|
|
1290
1463
|
\`\`\`tsx
|
|
1291
|
-
|
|
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
|
+
)
|
|
1292
1481
|
\`\`\`
|
|
1293
1482
|
|
|
1294
1483
|
## useMessagingEvent(eventType, handler)
|
|
@@ -1319,16 +1508,15 @@ Subscribe to activity events pushed from the host via the framework. Requires \`
|
|
|
1319
1508
|
${stripImports(HOOK_SNIPPETS["events:activity"])}
|
|
1320
1509
|
\`\`\`
|
|
1321
1510
|
|
|
1322
|
-
##
|
|
1323
|
-
|
|
1324
|
-
- \`eventType:
|
|
1325
|
-
-
|
|
1326
|
-
- \`
|
|
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 }\`
|
|
1327
1517
|
|
|
1328
1518
|
\`\`\`tsx
|
|
1329
|
-
|
|
1330
|
-
console.log('Activity:', event.data)
|
|
1331
|
-
})
|
|
1519
|
+
${stripImports(HOOK_SNIPPETS["events:identity"])}
|
|
1332
1520
|
\`\`\`
|
|
1333
1521
|
|
|
1334
1522
|
## useExtendIdentity(handler)
|
|
@@ -1345,7 +1533,7 @@ With \`useCallback\` (for memoized handlers):
|
|
|
1345
1533
|
${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}
|
|
1346
1534
|
\`\`\`
|
|
1347
1535
|
|
|
1348
|
-
|
|
1536
|
+
**Identity via context.read()**
|
|
1349
1537
|
Identity state is available in the \`context.read()\` response as an \`identity\` field. Requires \`context:read\` permission (no separate identity permission needed).
|
|
1350
1538
|
\`\`\`tsx
|
|
1351
1539
|
const context = await capabilities.context.read()
|
|
@@ -3612,6 +3800,27 @@ updating custom fields.
|
|
|
3612
3800
|
\`\`\`tsx
|
|
3613
3801
|
${EXAMPLE_SNIPPETS["actions.invoke"]}
|
|
3614
3802
|
\`\`\`
|
|
3803
|
+
|
|
3804
|
+
## messaging.send \u2014 Sending Messages to Conversations
|
|
3805
|
+
|
|
3806
|
+
Post a message into the active conversation bound to this Instance via the
|
|
3807
|
+
\`useMessaging\` React hook. Returns the response on happy path and throws on
|
|
3808
|
+
**actionable** errors \u2014 inspect \`error\` for one of the typed
|
|
3809
|
+
\`SendMessageActionableErrorCode\` values (\`invalid_message\`, \`rate_limited\`,
|
|
3810
|
+
\`upstream_error\`). Host-handled cases (\`no_conversation\`, \`reauth_required\`,
|
|
3811
|
+
\`forbidden\`) resolve to \`null\` without throwing; the SDK logs a breadcrumb
|
|
3812
|
+
and the host surfaces remediation to admins (e.g. dashboard reconnect UI) \u2014
|
|
3813
|
+
extensions don't catch them.
|
|
3814
|
+
|
|
3815
|
+
**Permission:** \`messaging:send\`. The author label rendered above outbound
|
|
3816
|
+
messages is set **per-Instance by the admin** via the Instance settings field
|
|
3817
|
+
\`messagingDisplayName\` (visible on the dashboard once OAuth completes);
|
|
3818
|
+
falls back to \`extension.manifest.name\` when blank. Extensions do not set or
|
|
3819
|
+
think about the author identity.
|
|
3820
|
+
|
|
3821
|
+
\`\`\`tsx
|
|
3822
|
+
${EXAMPLE_SNIPPETS["messaging.send"]}
|
|
3823
|
+
\`\`\`
|
|
3615
3824
|
`;
|
|
3616
3825
|
};
|
|
3617
3826
|
var generateCookbookStructural = () => {
|
|
@@ -3695,6 +3904,108 @@ ${EXAMPLE_SNIPPETS.surfaceContext}
|
|
|
3695
3904
|
\`\`\`
|
|
3696
3905
|
`;
|
|
3697
3906
|
};
|
|
3907
|
+
var renderPayload = (kind) => JSON.stringify(MESSAGING_SEND_EXAMPLES[kind], null, 2);
|
|
3908
|
+
var renderAction = (type) => JSON.stringify(MESSAGING_ACTION_EXAMPLES[type], null, 2);
|
|
3909
|
+
var ACTION_DESCRIPTIONS = {
|
|
3910
|
+
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.",
|
|
3911
|
+
link: "Opens `url` in a new tab/window. No bubble inserted. Freely mixable with other non-reply actions.",
|
|
3912
|
+
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.",
|
|
3913
|
+
locationRequest: "Prompts the user to share device location. Response arrives as a separate `location`-type message inbound to the conversation. Freely mixable."
|
|
3914
|
+
};
|
|
3915
|
+
var actionTypeSections = Object.keys(MESSAGING_ACTION_EXAMPLES).map((type) => `### type: '${type}'
|
|
3916
|
+
|
|
3917
|
+
${ACTION_DESCRIPTIONS[type]}
|
|
3918
|
+
|
|
3919
|
+
\`\`\`tsx
|
|
3920
|
+
${renderAction(type)}
|
|
3921
|
+
\`\`\``).join("\n\n");
|
|
3922
|
+
var generateCookbookMessaging = () => {
|
|
3923
|
+
const fm = frontmatter({
|
|
3924
|
+
root: false,
|
|
3925
|
+
targets: ["*"],
|
|
3926
|
+
description: "Cookbook: example payloads per-kind for the messaging.send capability + action type shapes",
|
|
3927
|
+
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
3928
|
+
});
|
|
3929
|
+
return `${fm}
|
|
3930
|
+
|
|
3931
|
+
# Messaging Examples
|
|
3932
|
+
|
|
3933
|
+
Example payloads for the \`messaging.send\` capability \u2014 one per supported message
|
|
3934
|
+
kind, plus the action types you can attach to messages. Each payload is the
|
|
3935
|
+
exact shape the \`useMessaging\` hook accepts; copy and adapt as needed.
|
|
3936
|
+
|
|
3937
|
+
**Permission:** \`messaging:send\` in your \`manifest.json\`. The author label
|
|
3938
|
+
above outbound messages is set **per-Instance by the admin** via the Instance
|
|
3939
|
+
settings field \`messagingDisplayName\`; falls back to \`extension.manifest.name\`
|
|
3940
|
+
when blank. Extensions do not set or think about the author identity.
|
|
3941
|
+
|
|
3942
|
+
---
|
|
3943
|
+
|
|
3944
|
+
## kind: 'text' \u2014 Plain text + reply actions
|
|
3945
|
+
|
|
3946
|
+
Plain conversational message. Optional \`actions\` array attaches quick-reply
|
|
3947
|
+
buttons, links, postbacks, or location requests.
|
|
3948
|
+
|
|
3949
|
+
\`\`\`tsx
|
|
3950
|
+
const [send] = useMessaging()
|
|
3951
|
+
|
|
3952
|
+
await send(${renderPayload("text")})
|
|
3953
|
+
\`\`\`
|
|
3954
|
+
|
|
3955
|
+
## kind: 'image' \u2014 Embedded image + optional caption
|
|
3956
|
+
|
|
3957
|
+
Product photos, screenshots, visual help. \`url\` must respond with a
|
|
3958
|
+
\`Content-Type: image/*\` header (Sunco enforces; redirecting URLs like
|
|
3959
|
+
\`picsum.photos\` will fail). \`altText\` is optional \u2014 defaults to filename.
|
|
3960
|
+
Optional \`body\` renders a caption alongside the image.
|
|
3961
|
+
|
|
3962
|
+
\`\`\`tsx
|
|
3963
|
+
const [send] = useMessaging()
|
|
3964
|
+
|
|
3965
|
+
await send(${renderPayload("image")})
|
|
3966
|
+
\`\`\`
|
|
3967
|
+
|
|
3968
|
+
## kind: 'file' \u2014 Embedded file attachment
|
|
3969
|
+
|
|
3970
|
+
Return labels, receipts, NDAs, warranty paperwork. Sunco's \`fileMessage\`
|
|
3971
|
+
schema does NOT support \`actions\` \u2014 file messages can only carry an
|
|
3972
|
+
optional text caption.
|
|
3973
|
+
|
|
3974
|
+
\`\`\`tsx
|
|
3975
|
+
const [send] = useMessaging()
|
|
3976
|
+
|
|
3977
|
+
await send(${renderPayload("file")})
|
|
3978
|
+
\`\`\`
|
|
3979
|
+
|
|
3980
|
+
## kind: 'carousel' \u2014 Horizontally scrolling cards
|
|
3981
|
+
|
|
3982
|
+
The conversational-commerce primitive: product recommendations, size/color
|
|
3983
|
+
pickers, search results with images, multi-option selection.
|
|
3984
|
+
|
|
3985
|
+
- 1\u201310 items, each with required \`title\` + required \`actions\` (1\u20133 per item)
|
|
3986
|
+
- Item actions are a subset: \`link\` + \`postback\` only (no \`reply\`, no
|
|
3987
|
+
\`locationRequest\` \u2014 Sunco rejects those at the item level)
|
|
3988
|
+
- Carousel messages have NO message-level \`actions\` field \u2014 actions live
|
|
3989
|
+
per-card
|
|
3990
|
+
- Use a separate \`text\` message before the carousel if you need an intro
|
|
3991
|
+
|
|
3992
|
+
\`\`\`tsx
|
|
3993
|
+
const [send] = useMessaging()
|
|
3994
|
+
|
|
3995
|
+
await send(${renderPayload("carousel")})
|
|
3996
|
+
\`\`\`
|
|
3997
|
+
|
|
3998
|
+
---
|
|
3999
|
+
|
|
4000
|
+
## Action types
|
|
4001
|
+
|
|
4002
|
+
Attach to message-level \`actions\` (text / image) or item-level \`actions\`
|
|
4003
|
+
(carousel). All actions share \`label\` + optional \`metadata\` (primitives only,
|
|
4004
|
+
\u22644KB total).
|
|
4005
|
+
|
|
4006
|
+
${actionTypeSections}
|
|
4007
|
+
`;
|
|
4008
|
+
};
|
|
3698
4009
|
var identityEventTypes2 = Object.values(IDENTITY_EVENT).map((e) => `\`${e}\``).join(", ");
|
|
3699
4010
|
var activityEventTypes2 = Object.values(ACTIVITY_EVENT).map((e) => `\`${e}\``).join(", ");
|
|
3700
4011
|
var generateCookbookEvents = () => {
|
|
@@ -3716,7 +4027,7 @@ supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-l
|
|
|
3716
4027
|
|
|
3717
4028
|
## Messaging Events
|
|
3718
4029
|
|
|
3719
|
-
Subscribe to postback button clicks from the
|
|
4030
|
+
Subscribe to postback button clicks from the Messaging widget.
|
|
3720
4031
|
The \`actionName\` is the button's display text, not a programmatic identifier.
|
|
3721
4032
|
|
|
3722
4033
|
**Permission:** \`events:messaging\`
|
|
@@ -3853,7 +4164,7 @@ alongside existing surfaces.
|
|
|
3853
4164
|
Add the target to the \`targets\` array in \`packages/extension/public/manifest.json\`.
|
|
3854
4165
|
Also add any required permissions based on the target-permission mapping:
|
|
3855
4166
|
- \`slot.header\` \u2192 \`context:read\`
|
|
3856
|
-
- \`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\`
|
|
3857
4168
|
- \`slot.footer\` \u2192 (none)
|
|
3858
4169
|
- \`slot.footer-links\` \u2192 (none)
|
|
3859
4170
|
|
|
@@ -3929,6 +4240,18 @@ await capabilities.identity.extend({ verified: true })
|
|
|
3929
4240
|
useIdentityEvent('refresh', (event) => {
|
|
3930
4241
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
3931
4242
|
})
|
|
4243
|
+
`,
|
|
4244
|
+
"messaging.send": `${HOOK_SNIPPETS["messaging.send"]}
|
|
4245
|
+
|
|
4246
|
+
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
4247
|
+
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
4248
|
+
// catch / on state.error to show UX.
|
|
4249
|
+
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
4250
|
+
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
4251
|
+
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
4252
|
+
// can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
|
|
4253
|
+
// (permission-free) \u2014 wire to button disabled={!enabled || loading}.
|
|
4254
|
+
if (error === 'rate_limited') { /* show "slow down" UI */ }
|
|
3932
4255
|
`
|
|
3933
4256
|
};
|
|
3934
4257
|
var EVENT_SNIPPETS = {
|
|
@@ -4006,7 +4329,7 @@ export function Content(): React.ReactElement {
|
|
|
4006
4329
|
// ../../sdk/extension/ai-docs/src/commands/add-capability.ts
|
|
4007
4330
|
var generateAddCapabilityCommand = () => {
|
|
4008
4331
|
const fm = frontmatter({
|
|
4009
|
-
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",
|
|
4010
4333
|
targets: ["*"]
|
|
4011
4334
|
});
|
|
4012
4335
|
return `${fm}
|
|
@@ -4022,6 +4345,7 @@ Ask which capability to add. Valid capabilities:
|
|
|
4022
4345
|
- \`context.read\` \u2014 read host-provided context (customerId, customerEmail, messaging.conversationId, etc.)
|
|
4023
4346
|
- \`actions.toast\` \u2014 show toast notifications (success, error, info, warning)
|
|
4024
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)
|
|
4025
4349
|
- \`identity.extend\` \u2014 enrich identity JWT claims before signing
|
|
4026
4350
|
- \`events:identity\` \u2014 subscribe to identity events (login, logout, refresh, expired)
|
|
4027
4351
|
- \`events:messaging\` \u2014 subscribe to messaging events (postback button clicks)
|
|
@@ -4034,6 +4358,7 @@ Add the corresponding permission to \`packages/extension/public/manifest.json\`:
|
|
|
4034
4358
|
- \`context.read\` \u2192 \`"context:read"\`
|
|
4035
4359
|
- \`actions.toast\` \u2192 \`"actions:toast"\`
|
|
4036
4360
|
- \`actions.invoke\` \u2192 \`"actions:invoke"\`
|
|
4361
|
+
- \`messaging.send\` \u2192 \`"messaging:send"\`
|
|
4037
4362
|
- \`identity.extend\` \u2192 \`"identity:extend"\`
|
|
4038
4363
|
- \`events:identity\` \u2192 \`"events:identity"\` (also add entries to \`events\` array, e.g. \`["identity:login", "identity:logout"]\`)
|
|
4039
4364
|
- \`events:messaging\` \u2192 \`"events:messaging"\` (also add entries to \`events\` array, e.g. \`["messaging:postback:Buy Now"]\`)
|
|
@@ -4076,12 +4401,15 @@ const capabilities = useCapabilities()
|
|
|
4076
4401
|
// actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
|
|
4077
4402
|
\`\`\`
|
|
4078
4403
|
|
|
4079
|
-
### Special handling (events + identity.extend):
|
|
4404
|
+
### Special handling (events + identity.extend + messaging.send):
|
|
4080
4405
|
|
|
4081
4406
|
#### For events \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
|
|
4082
4407
|
Events are subscribed via \`useIdentityEvent\` / \`useMessagingEvent\` / \`useActivityEvent\` \u2014 never use \`capabilities.events.*\` directly (events are not part of the \`capabilities\` object).
|
|
4083
4408
|
|
|
4084
|
-
#### 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:
|
|
4085
4413
|
- **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires only once at initial login. ALWAYS use for known-at-login enrichment.
|
|
4086
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\`.
|
|
4087
4415
|
|
|
@@ -4100,6 +4428,11 @@ ${HOOK_SNIPPETS["events:messaging"]}
|
|
|
4100
4428
|
${HOOK_SNIPPETS["events:activity"]}
|
|
4101
4429
|
\`\`\`
|
|
4102
4430
|
|
|
4431
|
+
\`\`\`tsx
|
|
4432
|
+
// messaging.send \u2014 use useMessaging hook (returns [send, { enabled, loading, error }] tuple)
|
|
4433
|
+
${HOOK_SNIPPETS["messaging.send"]}
|
|
4434
|
+
\`\`\`
|
|
4435
|
+
|
|
4103
4436
|
\`\`\`tsx
|
|
4104
4437
|
// identity.extend \u2014 login-time useExtendIdentity hook OR post-login imperative capabilities.identity.extend
|
|
4105
4438
|
${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
@@ -4111,6 +4444,7 @@ ${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
|
4111
4444
|
- For data/context/actions: confirm accessed via \`useCapabilities()\` hook
|
|
4112
4445
|
- For events: confirm using \`useIdentityEvent\`, \`useMessagingEvent\`, or \`useActivityEvent\` hooks
|
|
4113
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
|
|
4114
4448
|
`;
|
|
4115
4449
|
};
|
|
4116
4450
|
|
|
@@ -4582,6 +4916,13 @@ var SKILLS = [
|
|
|
4582
4916
|
scopes: ["docs"],
|
|
4583
4917
|
content: () => generateCookbookEvents()
|
|
4584
4918
|
},
|
|
4919
|
+
{
|
|
4920
|
+
id: "cookbook-messaging",
|
|
4921
|
+
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.",
|
|
4922
|
+
type: "knowledge",
|
|
4923
|
+
scopes: ["docs"],
|
|
4924
|
+
content: () => generateCookbookMessaging()
|
|
4925
|
+
},
|
|
4585
4926
|
{
|
|
4586
4927
|
id: "marketplace-listing",
|
|
4587
4928
|
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.",
|
|
@@ -5051,6 +5392,39 @@ export function Content() {
|
|
|
5051
5392
|
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
5052
5393
|
</Surface>
|
|
5053
5394
|
)
|
|
5395
|
+
}`,
|
|
5396
|
+
"messaging.send": `import { useMessaging, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
5397
|
+
|
|
5398
|
+
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
5399
|
+
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
5400
|
+
export function Content() {
|
|
5401
|
+
const [send, { enabled, loading, error }] = useMessaging()
|
|
5402
|
+
|
|
5403
|
+
const onSayHello = async () => {
|
|
5404
|
+
try {
|
|
5405
|
+
await send({
|
|
5406
|
+
kind: 'text',
|
|
5407
|
+
body: 'Hello from the extension',
|
|
5408
|
+
actions: [
|
|
5409
|
+
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
|
|
5410
|
+
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
|
|
5411
|
+
],
|
|
5412
|
+
})
|
|
5413
|
+
} catch {
|
|
5414
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
5415
|
+
}
|
|
5416
|
+
}
|
|
5417
|
+
|
|
5418
|
+
return (
|
|
5419
|
+
<Surface id="slot.content">
|
|
5420
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
5421
|
+
<ui.Button onClick={onSayHello} disabled={!enabled || loading}>Say hello</ui.Button>
|
|
5422
|
+
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
5423
|
+
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
5424
|
+
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
5425
|
+
</ui.Stack>
|
|
5426
|
+
</Surface>
|
|
5427
|
+
)
|
|
5054
5428
|
}`
|
|
5055
5429
|
};
|
|
5056
5430
|
|
|
@@ -5142,7 +5516,22 @@ await capabilities.identity.extend({ verified: true })
|
|
|
5142
5516
|
// Consumer side \u2014 same or sibling extension reacts via identity:refresh
|
|
5143
5517
|
useIdentityEvent('refresh', (event) => {
|
|
5144
5518
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
5145
|
-
})
|
|
5519
|
+
})`,
|
|
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' })
|
|
5525
|
+
|
|
5526
|
+
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
5527
|
+
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
5528
|
+
// catch / on state.error to show UX.
|
|
5529
|
+
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
5530
|
+
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
5531
|
+
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
5532
|
+
// can pre-empt 'no_conversation' via the 'enabled' flag from useMessaging()
|
|
5533
|
+
// (permission-free) \u2014 wire to button disabled={!enabled || loading}.
|
|
5534
|
+
if (error === 'rate_limited') { /* show "slow down" UI */ }`
|
|
5146
5535
|
};
|
|
5147
5536
|
var EVENT_SNIPPETS2 = {
|
|
5148
5537
|
"events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
@@ -5518,7 +5907,7 @@ ${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
|
5518
5907
|
usedPermissions.add(permission);
|
|
5519
5908
|
}
|
|
5520
5909
|
}
|
|
5521
|
-
for (const [hook, permission] of Object.entries(
|
|
5910
|
+
for (const [hook, permission] of Object.entries(HOOK_PERMISSION_MAP)) {
|
|
5522
5911
|
if (allSource.includes(hook)) {
|
|
5523
5912
|
usedPermissions.add(permission);
|
|
5524
5913
|
}
|