@stackable-labs/mcp-app-extension 1.29.0 → 1.30.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 +388 -5
- package/dist/server.js +388 -5
- 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, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
|
|
4
|
+
import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, MESSAGING_ACTION_EXAMPLES, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, 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';
|
|
@@ -541,7 +541,7 @@ var generateCapabilities = () => {
|
|
|
541
541
|
const fm = frontmatter({
|
|
542
542
|
root: false,
|
|
543
543
|
targets: ["*"],
|
|
544
|
-
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
|
|
544
|
+
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, messaging.send, events:identity, events:messaging, events:activity",
|
|
545
545
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
546
546
|
});
|
|
547
547
|
return `${fm}
|
|
@@ -798,7 +798,7 @@ await capabilities.identity.extend({
|
|
|
798
798
|
// \u2192 identity:refresh broadcast to all extensions with events:identity
|
|
799
799
|
\`\`\`
|
|
800
800
|
|
|
801
|
-
Per-extension calls are serialized to prevent concurrent
|
|
801
|
+
Per-extension calls are serialized to prevent concurrent \`loginUser\` callbacks from orphaning each other.
|
|
802
802
|
|
|
803
803
|
### Consuming enriched state from another extension
|
|
804
804
|
|
|
@@ -831,6 +831,123 @@ The publisher-side bundle scan validates your declaration at submission time:
|
|
|
831
831
|
| \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
|
|
832
832
|
| \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
|
|
833
833
|
| \`identityClaims_too_many\` | error | More than 20 entries |
|
|
834
|
+
|
|
835
|
+
## messaging.send \u2014 Send Messages to Conversations
|
|
836
|
+
|
|
837
|
+
Post a message into the **active conversation** bound to the current Instance. The host attributes each message to the extension via the per-Instance author label set by the admin (see "Author label" below). Two complementary APIs:
|
|
838
|
+
|
|
839
|
+
1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { loading, error, data }]\` \u2014 lets callers rename \`send\` per-instance when used multiple times in one component. Preferred for UI components; narrows the error surface to actionable codes only.
|
|
840
|
+
2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
|
|
841
|
+
|
|
842
|
+
### Manifest contract
|
|
843
|
+
|
|
844
|
+
\`\`\`json
|
|
845
|
+
{
|
|
846
|
+
"permissions": ["messaging:send"]
|
|
847
|
+
}
|
|
848
|
+
\`\`\`
|
|
849
|
+
|
|
850
|
+
- **\`messaging:send\` permission** \u2014 without it the host gate rejects the call before any network request.
|
|
851
|
+
|
|
852
|
+
### Author label (host-resolved, per-Instance)
|
|
853
|
+
|
|
854
|
+
Extensions do **not** set the author label. The host resolves it from:
|
|
855
|
+
1. \`instance.config.settings.messagingDisplayName\` \u2014 admin sets via the dashboard Instance form (visible only after OAuth completes; gated on \`messagingAppId\` being populated)
|
|
856
|
+
2. **Fallback**: \`extension.manifest.name\` \u2014 used when admin hasn't set a label
|
|
857
|
+
|
|
858
|
+
This prevents impersonation (extension A can't author as extension B) and gives admins per-deployment branding control.
|
|
859
|
+
|
|
860
|
+
### Runtime requirements
|
|
861
|
+
|
|
862
|
+
1. **Instance connected to a messaging provider** \u2014 bearer token stored, ready to post
|
|
863
|
+
2. **An active conversation** when \`send()\` is called (else host-handled \`no_conversation\` \u2014 \`send()\` returns \`null\`)
|
|
864
|
+
3. **Instance not disconnected** from the messaging provider (else host-handled \`reauth_required\` \u2014 admin reconnect surfaced in dashboard)
|
|
865
|
+
|
|
866
|
+
### Payload \u2014 discriminated by \`kind\`
|
|
867
|
+
|
|
868
|
+
Four kinds:
|
|
869
|
+
|
|
870
|
+
| kind | Required fields | Optional |
|
|
871
|
+
|---|---|---|
|
|
872
|
+
| \`'text'\` | \`body: string\` | \`actions?: MessageItemAction[]\`, \`metadata?\`, \`htmlText?\`, \`markdownText?\`, \`disableUserInput?\` |
|
|
873
|
+
| \`'image'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`actions?\`, \`metadata?\` |
|
|
874
|
+
| \`'file'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`metadata?\` (no \`actions\` \u2014 file kind doesn't accept item-level actions) |
|
|
875
|
+
| \`'carousel'\` | \`items: [MessageItem, ...MessageItem[]]\` (1\u201310 items, each with \`title\`, \`description?\`, \`imageUrl?\`, \`actions?\`) | \`displaySettings?: { imageAspectRatio?: 'square' \\| 'horizontal' }\` |
|
|
876
|
+
|
|
877
|
+
**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).
|
|
878
|
+
|
|
879
|
+
### Hook usage
|
|
880
|
+
|
|
881
|
+
\`\`\`tsx
|
|
882
|
+
import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
|
|
883
|
+
|
|
884
|
+
const { messaging } = useContextData()
|
|
885
|
+
const [send, { loading, error }] = useMessaging()
|
|
886
|
+
|
|
887
|
+
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
888
|
+
// host-handled no_conversation log + null return.
|
|
889
|
+
const canSend = !!messaging?.conversationId
|
|
890
|
+
|
|
891
|
+
const onApprove = async () => {
|
|
892
|
+
try {
|
|
893
|
+
await send({
|
|
894
|
+
kind: 'text',
|
|
895
|
+
body: 'Order approved \u2713',
|
|
896
|
+
actions: [{ type: 'reply', label: 'Got it', payload: 'ACK' }],
|
|
897
|
+
})
|
|
898
|
+
} catch {
|
|
899
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (error === 'rate_limited') {
|
|
904
|
+
// Render a "slow down" notice
|
|
905
|
+
}
|
|
906
|
+
\`\`\`
|
|
907
|
+
|
|
908
|
+
### Imperative usage
|
|
909
|
+
|
|
910
|
+
\`\`\`tsx
|
|
911
|
+
const capabilities = useCapabilities()
|
|
912
|
+
const result = await capabilities.messaging.send({ kind: 'text', body: 'Hello' })
|
|
913
|
+
// result is either { messageId, receivedAt } OR { error: SendMessageErrorCode }
|
|
914
|
+
\`\`\`
|
|
915
|
+
|
|
916
|
+
### Typed error codes \u2014 split by who acts
|
|
917
|
+
|
|
918
|
+
The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two groups:
|
|
919
|
+
|
|
920
|
+
**Actionable (extension catches + renders UI)** \u2014 \`send()\` throws and \`state.error\` is populated:
|
|
921
|
+
|
|
922
|
+
| Code | When |
|
|
923
|
+
| --- | --- |
|
|
924
|
+
| \`invalid_message\` | Payload failed validation (e.g. empty \`body\` for text, carousel >10 items, list kind sent) |
|
|
925
|
+
| \`rate_limited\` | Upstream rate limit hit \u2014 back off + retry |
|
|
926
|
+
| \`upstream_error\` | Provider returned 5xx \u2014 transient; retry with backoff |
|
|
927
|
+
|
|
928
|
+
**Host-handled (extension ignores)** \u2014 \`send()\` resolves to \`null\`, \`state.error\` stays \`null\`, SDK logs a breadcrumb:
|
|
929
|
+
|
|
930
|
+
| Code | What the framework does |
|
|
931
|
+
| --- | --- |
|
|
932
|
+
| \`no_conversation\` | \`console.info\` \u2014 pre-empt via \`useContextData().messaging?.conversationId\` |
|
|
933
|
+
| \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
|
|
934
|
+
| \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
|
|
935
|
+
|
|
936
|
+
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).
|
|
937
|
+
|
|
938
|
+
### Defense-in-depth gates
|
|
939
|
+
|
|
940
|
+
Three gates fire in series, surfacing the same \`forbidden\` error if any blocks:
|
|
941
|
+
|
|
942
|
+
1. **Embeddable host gate** (\`CapabilityRPCHandler\`) \u2014 checks \`sandbox.manifest.permissions.includes('messaging:send')\` before the call leaves the sandbox.
|
|
943
|
+
2. **Backend handler gate** \u2014 checks \`claims.permissions?.includes('messaging:send')\` on the proxy-token JWT.
|
|
944
|
+
3. **Server-side instance check** \u2014 \`instance.config.messagingDisconnected\` short-circuits with \`reauth_required\` before any upstream call.
|
|
945
|
+
|
|
946
|
+
### Receiving replies \u2014 pair with \`events:messaging\`
|
|
947
|
+
|
|
948
|
+
\`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
|
|
949
|
+
|
|
950
|
+
> **Note:** the postback \`payload\` field is currently not surfaced by the Web Widget (only the button text \`actionName\`). Until that gap is closed, design action labels to be self-describing or pair sends with a follow-up \`data.query\` lookup.
|
|
834
951
|
`;
|
|
835
952
|
};
|
|
836
953
|
|
|
@@ -1192,6 +1309,43 @@ export function Content(): React.ReactElement {
|
|
|
1192
1309
|
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
1193
1310
|
</Surface>
|
|
1194
1311
|
)
|
|
1312
|
+
}`,
|
|
1313
|
+
"messaging.send": `import { useMessaging, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
1314
|
+
|
|
1315
|
+
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
1316
|
+
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
1317
|
+
export function Content(): React.ReactElement {
|
|
1318
|
+
const { messaging } = useContextData()
|
|
1319
|
+
const [send, { loading, error }] = useMessaging()
|
|
1320
|
+
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
1321
|
+
// host-handled no_conversation log + null return.
|
|
1322
|
+
const canSend = !!messaging?.conversationId
|
|
1323
|
+
|
|
1324
|
+
const onSayHello = async () => {
|
|
1325
|
+
try {
|
|
1326
|
+
await send({
|
|
1327
|
+
kind: 'text',
|
|
1328
|
+
body: 'Hello from the extension',
|
|
1329
|
+
actions: [
|
|
1330
|
+
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
|
|
1331
|
+
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
|
|
1332
|
+
],
|
|
1333
|
+
})
|
|
1334
|
+
} catch {
|
|
1335
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
return (
|
|
1340
|
+
<Surface id="slot.content">
|
|
1341
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
1342
|
+
<ui.Button onClick={onSayHello} disabled={loading || !canSend}>Say hello</ui.Button>
|
|
1343
|
+
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
1344
|
+
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
1345
|
+
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
1346
|
+
</ui.Stack>
|
|
1347
|
+
</Surface>
|
|
1348
|
+
)
|
|
1195
1349
|
}`
|
|
1196
1350
|
};
|
|
1197
1351
|
|
|
@@ -1235,6 +1389,7 @@ const capabilities = useCapabilities()
|
|
|
1235
1389
|
// capabilities.actions.toast(payload)
|
|
1236
1390
|
// capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
|
|
1237
1391
|
// capabilities.identity.extend(patch) \u2014 push enrichment claims to user.metadata + JWT custom_claims (imperative; for handler-style at login, use useExtendIdentity hook). Each patch key MUST be declared in manifest.identityClaims or the host filter drops it.
|
|
1392
|
+
// capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback. For React state ([send, { loading, error, data }]), prefer the useMessaging hook.
|
|
1238
1393
|
\`\`\`
|
|
1239
1394
|
|
|
1240
1395
|
## useStore(store, selector?)
|
|
@@ -1351,6 +1506,35 @@ Identity state is available in the \`context.read()\` response as an \`identity\
|
|
|
1351
1506
|
const context = await capabilities.context.read()
|
|
1352
1507
|
// context.identity \u2014 { authenticated, user, expiresAt? }
|
|
1353
1508
|
\`\`\`
|
|
1509
|
+
|
|
1510
|
+
## useMessaging()
|
|
1511
|
+
Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`loading\` / \`error\` / \`data\`. Returns a tuple \`[send, state]\` so callers can rename \`send\` when the hook is used multiple times in one component. Requires \`messaging:send\` permission. The author label rendered above outbound messages is set per-Instance by the admin (Instance settings \`messagingDisplayName\`); falls back to \`extension.manifest.name\` when blank.
|
|
1512
|
+
|
|
1513
|
+
- **Returns:** \`readonly [send, { loading, error, data }]\`
|
|
1514
|
+
- **\`send(payload: SendMessagePayload): Promise<SendMessageResponse | null>\`** \u2014 returns the response on success; throws on **actionable** errors; resolves to \`null\` on host-handled errors (SDK logs a breadcrumb, host surfaces remediation)
|
|
1515
|
+
- **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
|
|
1516
|
+
- **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
|
|
1517
|
+
- **\`error: SendMessageActionableErrorCode | null\`** \u2014 one of \`invalid_message\` / \`rate_limited\` / \`upstream_error\` after a failed send. Host-handled codes (\`no_conversation\` / \`reauth_required\` / \`forbidden\`) never surface here.
|
|
1518
|
+
|
|
1519
|
+
Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
|
|
1520
|
+
|
|
1521
|
+
\`\`\`tsx
|
|
1522
|
+
import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
|
|
1523
|
+
|
|
1524
|
+
const { messaging } = useContextData()
|
|
1525
|
+
const [send, { loading, error }] = useMessaging()
|
|
1526
|
+
|
|
1527
|
+
// Proactive gate: skip the call when there's no conversation
|
|
1528
|
+
const canSend = !!messaging?.conversationId
|
|
1529
|
+
|
|
1530
|
+
const onApprove = async () => {
|
|
1531
|
+
try {
|
|
1532
|
+
await send({ kind: 'text', body: 'Approved \u2713' })
|
|
1533
|
+
} catch {
|
|
1534
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
\`\`\`
|
|
1354
1538
|
`;
|
|
1355
1539
|
};
|
|
1356
1540
|
|
|
@@ -3612,6 +3796,27 @@ updating custom fields.
|
|
|
3612
3796
|
\`\`\`tsx
|
|
3613
3797
|
${EXAMPLE_SNIPPETS["actions.invoke"]}
|
|
3614
3798
|
\`\`\`
|
|
3799
|
+
|
|
3800
|
+
## messaging.send \u2014 Sending Messages to Conversations
|
|
3801
|
+
|
|
3802
|
+
Post a message into the active conversation bound to this Instance via the
|
|
3803
|
+
\`useMessaging\` React hook. Returns the response on happy path and throws on
|
|
3804
|
+
**actionable** errors \u2014 inspect \`error\` for one of the typed
|
|
3805
|
+
\`SendMessageActionableErrorCode\` values (\`invalid_message\`, \`rate_limited\`,
|
|
3806
|
+
\`upstream_error\`). Host-handled cases (\`no_conversation\`, \`reauth_required\`,
|
|
3807
|
+
\`forbidden\`) resolve to \`null\` without throwing; the SDK logs a breadcrumb
|
|
3808
|
+
and the host surfaces remediation to admins (e.g. dashboard reconnect UI) \u2014
|
|
3809
|
+
extensions don't catch them.
|
|
3810
|
+
|
|
3811
|
+
**Permission:** \`messaging:send\`. The author label rendered above outbound
|
|
3812
|
+
messages is set **per-Instance by the admin** via the Instance settings field
|
|
3813
|
+
\`messagingDisplayName\` (visible on the dashboard once OAuth completes);
|
|
3814
|
+
falls back to \`extension.manifest.name\` when blank. Extensions do not set or
|
|
3815
|
+
think about the author identity.
|
|
3816
|
+
|
|
3817
|
+
\`\`\`tsx
|
|
3818
|
+
${EXAMPLE_SNIPPETS["messaging.send"]}
|
|
3819
|
+
\`\`\`
|
|
3615
3820
|
`;
|
|
3616
3821
|
};
|
|
3617
3822
|
var generateCookbookStructural = () => {
|
|
@@ -3695,6 +3900,108 @@ ${EXAMPLE_SNIPPETS.surfaceContext}
|
|
|
3695
3900
|
\`\`\`
|
|
3696
3901
|
`;
|
|
3697
3902
|
};
|
|
3903
|
+
var renderPayload = (kind) => JSON.stringify(MESSAGING_SEND_EXAMPLES[kind], null, 2);
|
|
3904
|
+
var renderAction = (type) => JSON.stringify(MESSAGING_ACTION_EXAMPLES[type], null, 2);
|
|
3905
|
+
var ACTION_DESCRIPTIONS = {
|
|
3906
|
+
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.",
|
|
3907
|
+
link: "Opens `url` in a new tab/window. No bubble inserted. Freely mixable with other non-reply actions.",
|
|
3908
|
+
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.",
|
|
3909
|
+
locationRequest: "Prompts the user to share device location. Response arrives as a separate `location`-type message inbound to the conversation. Freely mixable."
|
|
3910
|
+
};
|
|
3911
|
+
var actionTypeSections = Object.keys(MESSAGING_ACTION_EXAMPLES).map((type) => `### type: '${type}'
|
|
3912
|
+
|
|
3913
|
+
${ACTION_DESCRIPTIONS[type]}
|
|
3914
|
+
|
|
3915
|
+
\`\`\`tsx
|
|
3916
|
+
${renderAction(type)}
|
|
3917
|
+
\`\`\``).join("\n\n");
|
|
3918
|
+
var generateCookbookMessaging = () => {
|
|
3919
|
+
const fm = frontmatter({
|
|
3920
|
+
root: false,
|
|
3921
|
+
targets: ["*"],
|
|
3922
|
+
description: "Cookbook: example payloads per-kind for the messaging.send capability + action type shapes",
|
|
3923
|
+
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
3924
|
+
});
|
|
3925
|
+
return `${fm}
|
|
3926
|
+
|
|
3927
|
+
# Messaging Examples
|
|
3928
|
+
|
|
3929
|
+
Example payloads for the \`messaging.send\` capability \u2014 one per supported message
|
|
3930
|
+
kind, plus the action types you can attach to messages. Each payload is the
|
|
3931
|
+
exact shape the \`useMessaging\` hook accepts; copy and adapt as needed.
|
|
3932
|
+
|
|
3933
|
+
**Permission:** \`messaging:send\` in your \`manifest.json\`. The author label
|
|
3934
|
+
above outbound messages is set **per-Instance by the admin** via the Instance
|
|
3935
|
+
settings field \`messagingDisplayName\`; falls back to \`extension.manifest.name\`
|
|
3936
|
+
when blank. Extensions do not set or think about the author identity.
|
|
3937
|
+
|
|
3938
|
+
---
|
|
3939
|
+
|
|
3940
|
+
## kind: 'text' \u2014 Plain text + reply actions
|
|
3941
|
+
|
|
3942
|
+
Plain conversational message. Optional \`actions\` array attaches quick-reply
|
|
3943
|
+
buttons, links, postbacks, or location requests.
|
|
3944
|
+
|
|
3945
|
+
\`\`\`tsx
|
|
3946
|
+
const [send] = useMessaging()
|
|
3947
|
+
|
|
3948
|
+
await send(${renderPayload("text")})
|
|
3949
|
+
\`\`\`
|
|
3950
|
+
|
|
3951
|
+
## kind: 'image' \u2014 Embedded image + optional caption
|
|
3952
|
+
|
|
3953
|
+
Product photos, screenshots, visual help. \`url\` must respond with a
|
|
3954
|
+
\`Content-Type: image/*\` header (Sunco enforces; redirecting URLs like
|
|
3955
|
+
\`picsum.photos\` will fail). \`altText\` is optional \u2014 defaults to filename.
|
|
3956
|
+
Optional \`body\` renders a caption alongside the image.
|
|
3957
|
+
|
|
3958
|
+
\`\`\`tsx
|
|
3959
|
+
const [send] = useMessaging()
|
|
3960
|
+
|
|
3961
|
+
await send(${renderPayload("image")})
|
|
3962
|
+
\`\`\`
|
|
3963
|
+
|
|
3964
|
+
## kind: 'file' \u2014 Embedded file attachment
|
|
3965
|
+
|
|
3966
|
+
Return labels, receipts, NDAs, warranty paperwork. Sunco's \`fileMessage\`
|
|
3967
|
+
schema does NOT support \`actions\` \u2014 file messages can only carry an
|
|
3968
|
+
optional text caption.
|
|
3969
|
+
|
|
3970
|
+
\`\`\`tsx
|
|
3971
|
+
const [send] = useMessaging()
|
|
3972
|
+
|
|
3973
|
+
await send(${renderPayload("file")})
|
|
3974
|
+
\`\`\`
|
|
3975
|
+
|
|
3976
|
+
## kind: 'carousel' \u2014 Horizontally scrolling cards
|
|
3977
|
+
|
|
3978
|
+
The conversational-commerce primitive: product recommendations, size/color
|
|
3979
|
+
pickers, search results with images, multi-option selection.
|
|
3980
|
+
|
|
3981
|
+
- 1\u201310 items, each with required \`title\` + required \`actions\` (1\u20133 per item)
|
|
3982
|
+
- Item actions are a subset: \`link\` + \`postback\` only (no \`reply\`, no
|
|
3983
|
+
\`locationRequest\` \u2014 Sunco rejects those at the item level)
|
|
3984
|
+
- Carousel messages have NO message-level \`actions\` field \u2014 actions live
|
|
3985
|
+
per-card
|
|
3986
|
+
- Use a separate \`text\` message before the carousel if you need an intro
|
|
3987
|
+
|
|
3988
|
+
\`\`\`tsx
|
|
3989
|
+
const [send] = useMessaging()
|
|
3990
|
+
|
|
3991
|
+
await send(${renderPayload("carousel")})
|
|
3992
|
+
\`\`\`
|
|
3993
|
+
|
|
3994
|
+
---
|
|
3995
|
+
|
|
3996
|
+
## Action types
|
|
3997
|
+
|
|
3998
|
+
Attach to message-level \`actions\` (text / image) or item-level \`actions\`
|
|
3999
|
+
(carousel). All actions share \`label\` + optional \`metadata\` (primitives only,
|
|
4000
|
+
\u22644KB total).
|
|
4001
|
+
|
|
4002
|
+
${actionTypeSections}
|
|
4003
|
+
`;
|
|
4004
|
+
};
|
|
3698
4005
|
var identityEventTypes2 = Object.values(IDENTITY_EVENT).map((e) => `\`${e}\``).join(", ");
|
|
3699
4006
|
var activityEventTypes2 = Object.values(ACTIVITY_EVENT).map((e) => `\`${e}\``).join(", ");
|
|
3700
4007
|
var generateCookbookEvents = () => {
|
|
@@ -3716,7 +4023,7 @@ supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-l
|
|
|
3716
4023
|
|
|
3717
4024
|
## Messaging Events
|
|
3718
4025
|
|
|
3719
|
-
Subscribe to postback button clicks from the
|
|
4026
|
+
Subscribe to postback button clicks from the Messaging widget.
|
|
3720
4027
|
The \`actionName\` is the button's display text, not a programmatic identifier.
|
|
3721
4028
|
|
|
3722
4029
|
**Permission:** \`events:messaging\`
|
|
@@ -3929,6 +4236,23 @@ await capabilities.identity.extend({ verified: true })
|
|
|
3929
4236
|
useIdentityEvent('refresh', (event) => {
|
|
3930
4237
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
3931
4238
|
})
|
|
4239
|
+
`,
|
|
4240
|
+
"messaging.send": `
|
|
4241
|
+
// Send a message into the active conversation bound to this Instance.
|
|
4242
|
+
// Requires the 'messaging:send' permission. The author label rendered above
|
|
4243
|
+
// outbound messages is set per-Instance by the admin (Instance settings
|
|
4244
|
+
// messagingDisplayName); falls back to extension.manifest.name when blank.
|
|
4245
|
+
const [send, { loading, error }] = useMessaging()
|
|
4246
|
+
const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
4247
|
+
|
|
4248
|
+
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
4249
|
+
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
4250
|
+
// catch / on state.error to show UX.
|
|
4251
|
+
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
4252
|
+
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
4253
|
+
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
4254
|
+
// can pre-empt 'no_conversation' via useContextData().messaging?.conversationId.
|
|
4255
|
+
if (error === 'rate_limited') { /* show "slow down" UI */ }
|
|
3932
4256
|
`
|
|
3933
4257
|
};
|
|
3934
4258
|
var EVENT_SNIPPETS = {
|
|
@@ -4582,6 +4906,13 @@ var SKILLS = [
|
|
|
4582
4906
|
scopes: ["docs"],
|
|
4583
4907
|
content: () => generateCookbookEvents()
|
|
4584
4908
|
},
|
|
4909
|
+
{
|
|
4910
|
+
id: "cookbook-messaging",
|
|
4911
|
+
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.",
|
|
4912
|
+
type: "knowledge",
|
|
4913
|
+
scopes: ["docs"],
|
|
4914
|
+
content: () => generateCookbookMessaging()
|
|
4915
|
+
},
|
|
4585
4916
|
{
|
|
4586
4917
|
id: "marketplace-listing",
|
|
4587
4918
|
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 +5382,43 @@ export function Content() {
|
|
|
5051
5382
|
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
5052
5383
|
</Surface>
|
|
5053
5384
|
)
|
|
5385
|
+
}`,
|
|
5386
|
+
"messaging.send": `import { useMessaging, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
5387
|
+
|
|
5388
|
+
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
5389
|
+
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
5390
|
+
export function Content() {
|
|
5391
|
+
const { messaging } = useContextData()
|
|
5392
|
+
const [send, { loading, error }] = useMessaging()
|
|
5393
|
+
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
5394
|
+
// host-handled no_conversation log + null return.
|
|
5395
|
+
const canSend = !!messaging?.conversationId
|
|
5396
|
+
|
|
5397
|
+
const onSayHello = async () => {
|
|
5398
|
+
try {
|
|
5399
|
+
await send({
|
|
5400
|
+
kind: 'text',
|
|
5401
|
+
body: 'Hello from the extension',
|
|
5402
|
+
actions: [
|
|
5403
|
+
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
|
|
5404
|
+
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
|
|
5405
|
+
],
|
|
5406
|
+
})
|
|
5407
|
+
} catch {
|
|
5408
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
|
|
5412
|
+
return (
|
|
5413
|
+
<Surface id="slot.content">
|
|
5414
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
5415
|
+
<ui.Button onClick={onSayHello} disabled={loading || !canSend}>Say hello</ui.Button>
|
|
5416
|
+
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
5417
|
+
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
5418
|
+
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
5419
|
+
</ui.Stack>
|
|
5420
|
+
</Surface>
|
|
5421
|
+
)
|
|
5054
5422
|
}`
|
|
5055
5423
|
};
|
|
5056
5424
|
|
|
@@ -5142,7 +5510,22 @@ await capabilities.identity.extend({ verified: true })
|
|
|
5142
5510
|
// Consumer side \u2014 same or sibling extension reacts via identity:refresh
|
|
5143
5511
|
useIdentityEvent('refresh', (event) => {
|
|
5144
5512
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
5145
|
-
})
|
|
5513
|
+
})`,
|
|
5514
|
+
"messaging.send": `// Send a message into the active conversation bound to this Instance.
|
|
5515
|
+
// Requires the 'messaging:send' permission. The author label rendered above
|
|
5516
|
+
// outbound messages is set per-Instance by the admin (Instance settings
|
|
5517
|
+
// messagingDisplayName); falls back to extension.manifest.name when blank.
|
|
5518
|
+
const [send, { loading, error }] = useMessaging()
|
|
5519
|
+
const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
5520
|
+
|
|
5521
|
+
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
5522
|
+
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
5523
|
+
// catch / on state.error to show UX.
|
|
5524
|
+
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
5525
|
+
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
5526
|
+
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
5527
|
+
// can pre-empt 'no_conversation' via useContextData().messaging?.conversationId.
|
|
5528
|
+
if (error === 'rate_limited') { /* show "slow down" UI */ }`
|
|
5146
5529
|
};
|
|
5147
5530
|
var EVENT_SNIPPETS2 = {
|
|
5148
5531
|
"events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
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, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName, 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';
|
|
@@ -534,7 +534,7 @@ var generateCapabilities = () => {
|
|
|
534
534
|
const fm = frontmatter({
|
|
535
535
|
root: false,
|
|
536
536
|
targets: ["*"],
|
|
537
|
-
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
|
|
537
|
+
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, messaging.send, events:identity, events:messaging, events:activity",
|
|
538
538
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
539
539
|
});
|
|
540
540
|
return `${fm}
|
|
@@ -791,7 +791,7 @@ await capabilities.identity.extend({
|
|
|
791
791
|
// \u2192 identity:refresh broadcast to all extensions with events:identity
|
|
792
792
|
\`\`\`
|
|
793
793
|
|
|
794
|
-
Per-extension calls are serialized to prevent concurrent
|
|
794
|
+
Per-extension calls are serialized to prevent concurrent \`loginUser\` callbacks from orphaning each other.
|
|
795
795
|
|
|
796
796
|
### Consuming enriched state from another extension
|
|
797
797
|
|
|
@@ -824,6 +824,123 @@ The publisher-side bundle scan validates your declaration at submission time:
|
|
|
824
824
|
| \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
|
|
825
825
|
| \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
|
|
826
826
|
| \`identityClaims_too_many\` | error | More than 20 entries |
|
|
827
|
+
|
|
828
|
+
## messaging.send \u2014 Send Messages to Conversations
|
|
829
|
+
|
|
830
|
+
Post a message into the **active conversation** bound to the current Instance. The host attributes each message to the extension via the per-Instance author label set by the admin (see "Author label" below). Two complementary APIs:
|
|
831
|
+
|
|
832
|
+
1. **\`useMessaging()\`** \u2014 React hook returning tuple \`[send, { loading, error, data }]\` \u2014 lets callers rename \`send\` per-instance when used multiple times in one component. Preferred for UI components; narrows the error surface to actionable codes only.
|
|
833
|
+
2. **\`messagingSendCapability(payload)\`** / **\`capabilities.messaging.send(payload)\`** \u2014 imperative; useful outside React render. Exposes the full wire-level error taxonomy.
|
|
834
|
+
|
|
835
|
+
### Manifest contract
|
|
836
|
+
|
|
837
|
+
\`\`\`json
|
|
838
|
+
{
|
|
839
|
+
"permissions": ["messaging:send"]
|
|
840
|
+
}
|
|
841
|
+
\`\`\`
|
|
842
|
+
|
|
843
|
+
- **\`messaging:send\` permission** \u2014 without it the host gate rejects the call before any network request.
|
|
844
|
+
|
|
845
|
+
### Author label (host-resolved, per-Instance)
|
|
846
|
+
|
|
847
|
+
Extensions do **not** set the author label. The host resolves it from:
|
|
848
|
+
1. \`instance.config.settings.messagingDisplayName\` \u2014 admin sets via the dashboard Instance form (visible only after OAuth completes; gated on \`messagingAppId\` being populated)
|
|
849
|
+
2. **Fallback**: \`extension.manifest.name\` \u2014 used when admin hasn't set a label
|
|
850
|
+
|
|
851
|
+
This prevents impersonation (extension A can't author as extension B) and gives admins per-deployment branding control.
|
|
852
|
+
|
|
853
|
+
### Runtime requirements
|
|
854
|
+
|
|
855
|
+
1. **Instance connected to a messaging provider** \u2014 bearer token stored, ready to post
|
|
856
|
+
2. **An active conversation** when \`send()\` is called (else host-handled \`no_conversation\` \u2014 \`send()\` returns \`null\`)
|
|
857
|
+
3. **Instance not disconnected** from the messaging provider (else host-handled \`reauth_required\` \u2014 admin reconnect surfaced in dashboard)
|
|
858
|
+
|
|
859
|
+
### Payload \u2014 discriminated by \`kind\`
|
|
860
|
+
|
|
861
|
+
Four kinds:
|
|
862
|
+
|
|
863
|
+
| kind | Required fields | Optional |
|
|
864
|
+
|---|---|---|
|
|
865
|
+
| \`'text'\` | \`body: string\` | \`actions?: MessageItemAction[]\`, \`metadata?\`, \`htmlText?\`, \`markdownText?\`, \`disableUserInput?\` |
|
|
866
|
+
| \`'image'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`actions?\`, \`metadata?\` |
|
|
867
|
+
| \`'file'\` | \`url: string\`, \`altText: string\` | \`body?\`, \`metadata?\` (no \`actions\` \u2014 file kind doesn't accept item-level actions) |
|
|
868
|
+
| \`'carousel'\` | \`items: [MessageItem, ...MessageItem[]]\` (1\u201310 items, each with \`title\`, \`description?\`, \`imageUrl?\`, \`actions?\`) | \`displaySettings?: { imageAspectRatio?: 'square' \\| 'horizontal' }\` |
|
|
869
|
+
|
|
870
|
+
**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).
|
|
871
|
+
|
|
872
|
+
### Hook usage
|
|
873
|
+
|
|
874
|
+
\`\`\`tsx
|
|
875
|
+
import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
|
|
876
|
+
|
|
877
|
+
const { messaging } = useContextData()
|
|
878
|
+
const [send, { loading, error }] = useMessaging()
|
|
879
|
+
|
|
880
|
+
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
881
|
+
// host-handled no_conversation log + null return.
|
|
882
|
+
const canSend = !!messaging?.conversationId
|
|
883
|
+
|
|
884
|
+
const onApprove = async () => {
|
|
885
|
+
try {
|
|
886
|
+
await send({
|
|
887
|
+
kind: 'text',
|
|
888
|
+
body: 'Order approved \u2713',
|
|
889
|
+
actions: [{ type: 'reply', label: 'Got it', payload: 'ACK' }],
|
|
890
|
+
})
|
|
891
|
+
} catch {
|
|
892
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (error === 'rate_limited') {
|
|
897
|
+
// Render a "slow down" notice
|
|
898
|
+
}
|
|
899
|
+
\`\`\`
|
|
900
|
+
|
|
901
|
+
### Imperative usage
|
|
902
|
+
|
|
903
|
+
\`\`\`tsx
|
|
904
|
+
const capabilities = useCapabilities()
|
|
905
|
+
const result = await capabilities.messaging.send({ kind: 'text', body: 'Hello' })
|
|
906
|
+
// result is either { messageId, receivedAt } OR { error: SendMessageErrorCode }
|
|
907
|
+
\`\`\`
|
|
908
|
+
|
|
909
|
+
### Typed error codes \u2014 split by who acts
|
|
910
|
+
|
|
911
|
+
The wire taxonomy has 6 codes. The hook (\`useMessaging\`) narrows them into two groups:
|
|
912
|
+
|
|
913
|
+
**Actionable (extension catches + renders UI)** \u2014 \`send()\` throws and \`state.error\` is populated:
|
|
914
|
+
|
|
915
|
+
| Code | When |
|
|
916
|
+
| --- | --- |
|
|
917
|
+
| \`invalid_message\` | Payload failed validation (e.g. empty \`body\` for text, carousel >10 items, list kind sent) |
|
|
918
|
+
| \`rate_limited\` | Upstream rate limit hit \u2014 back off + retry |
|
|
919
|
+
| \`upstream_error\` | Provider returned 5xx \u2014 transient; retry with backoff |
|
|
920
|
+
|
|
921
|
+
**Host-handled (extension ignores)** \u2014 \`send()\` resolves to \`null\`, \`state.error\` stays \`null\`, SDK logs a breadcrumb:
|
|
922
|
+
|
|
923
|
+
| Code | What the framework does |
|
|
924
|
+
| --- | --- |
|
|
925
|
+
| \`no_conversation\` | \`console.info\` \u2014 pre-empt via \`useContextData().messaging?.conversationId\` |
|
|
926
|
+
| \`reauth_required\` | \`console.warn\` \u2014 admin sees a "Reconnect" CTA in the dashboard (server flips \`messagingDisconnected: true\`) |
|
|
927
|
+
| \`forbidden\` | \`console.warn\` \u2014 should not reach in production with correct manifest |
|
|
928
|
+
|
|
929
|
+
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).
|
|
930
|
+
|
|
931
|
+
### Defense-in-depth gates
|
|
932
|
+
|
|
933
|
+
Three gates fire in series, surfacing the same \`forbidden\` error if any blocks:
|
|
934
|
+
|
|
935
|
+
1. **Embeddable host gate** (\`CapabilityRPCHandler\`) \u2014 checks \`sandbox.manifest.permissions.includes('messaging:send')\` before the call leaves the sandbox.
|
|
936
|
+
2. **Backend handler gate** \u2014 checks \`claims.permissions?.includes('messaging:send')\` on the proxy-token JWT.
|
|
937
|
+
3. **Server-side instance check** \u2014 \`instance.config.messagingDisconnected\` short-circuits with \`reauth_required\` before any upstream call.
|
|
938
|
+
|
|
939
|
+
### Receiving replies \u2014 pair with \`events:messaging\`
|
|
940
|
+
|
|
941
|
+
\`reply\` and \`postback\` action clicks fire on extensions with the \`events:messaging\` permission via \`useMessagingEvent\` \u2014 see the \`events:messaging\` section above.
|
|
942
|
+
|
|
943
|
+
> **Note:** the postback \`payload\` field is currently not surfaced by the Web Widget (only the button text \`actionName\`). Until that gap is closed, design action labels to be self-describing or pair sends with a follow-up \`data.query\` lookup.
|
|
827
944
|
`;
|
|
828
945
|
};
|
|
829
946
|
|
|
@@ -1185,6 +1302,43 @@ export function Content(): React.ReactElement {
|
|
|
1185
1302
|
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
1186
1303
|
</Surface>
|
|
1187
1304
|
)
|
|
1305
|
+
}`,
|
|
1306
|
+
"messaging.send": `import { useMessaging, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
1307
|
+
|
|
1308
|
+
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
1309
|
+
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
1310
|
+
export function Content(): React.ReactElement {
|
|
1311
|
+
const { messaging } = useContextData()
|
|
1312
|
+
const [send, { loading, error }] = useMessaging()
|
|
1313
|
+
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
1314
|
+
// host-handled no_conversation log + null return.
|
|
1315
|
+
const canSend = !!messaging?.conversationId
|
|
1316
|
+
|
|
1317
|
+
const onSayHello = async () => {
|
|
1318
|
+
try {
|
|
1319
|
+
await send({
|
|
1320
|
+
kind: 'text',
|
|
1321
|
+
body: 'Hello from the extension',
|
|
1322
|
+
actions: [
|
|
1323
|
+
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
|
|
1324
|
+
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
|
|
1325
|
+
],
|
|
1326
|
+
})
|
|
1327
|
+
} catch {
|
|
1328
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
return (
|
|
1333
|
+
<Surface id="slot.content">
|
|
1334
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
1335
|
+
<ui.Button onClick={onSayHello} disabled={loading || !canSend}>Say hello</ui.Button>
|
|
1336
|
+
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
1337
|
+
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
1338
|
+
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
1339
|
+
</ui.Stack>
|
|
1340
|
+
</Surface>
|
|
1341
|
+
)
|
|
1188
1342
|
}`
|
|
1189
1343
|
};
|
|
1190
1344
|
|
|
@@ -1228,6 +1382,7 @@ const capabilities = useCapabilities()
|
|
|
1228
1382
|
// capabilities.actions.toast(payload)
|
|
1229
1383
|
// capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
|
|
1230
1384
|
// capabilities.identity.extend(patch) \u2014 push enrichment claims to user.metadata + JWT custom_claims (imperative; for handler-style at login, use useExtendIdentity hook). Each patch key MUST be declared in manifest.identityClaims or the host filter drops it.
|
|
1385
|
+
// capabilities.messaging.send(payload) \u2014 post a message into the active conversation bound to this Instance. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback. For React state ([send, { loading, error, data }]), prefer the useMessaging hook.
|
|
1231
1386
|
\`\`\`
|
|
1232
1387
|
|
|
1233
1388
|
## useStore(store, selector?)
|
|
@@ -1344,6 +1499,35 @@ Identity state is available in the \`context.read()\` response as an \`identity\
|
|
|
1344
1499
|
const context = await capabilities.context.read()
|
|
1345
1500
|
// context.identity \u2014 { authenticated, user, expiresAt? }
|
|
1346
1501
|
\`\`\`
|
|
1502
|
+
|
|
1503
|
+
## useMessaging()
|
|
1504
|
+
Send messages into the active conversation bound to this Instance. Wraps the \`messaging.send\` capability with React state, tracking \`loading\` / \`error\` / \`data\`. Returns a tuple \`[send, state]\` so callers can rename \`send\` when the hook is used multiple times in one component. Requires \`messaging:send\` permission. The author label rendered above outbound messages is set per-Instance by the admin (Instance settings \`messagingDisplayName\`); falls back to \`extension.manifest.name\` when blank.
|
|
1505
|
+
|
|
1506
|
+
- **Returns:** \`readonly [send, { loading, error, data }]\`
|
|
1507
|
+
- **\`send(payload: SendMessagePayload): Promise<SendMessageResponse | null>\`** \u2014 returns the response on success; throws on **actionable** errors; resolves to \`null\` on host-handled errors (SDK logs a breadcrumb, host surfaces remediation)
|
|
1508
|
+
- **\`loading: boolean\`** \u2014 true while a call is in flight (matches \`useContextData\`'s \`loading\` for SDK-wide consistency)
|
|
1509
|
+
- **\`data: SendMessageResponse | null\`** \u2014 \`{ messageId, receivedAt }\` from last successful send
|
|
1510
|
+
- **\`error: SendMessageActionableErrorCode | null\`** \u2014 one of \`invalid_message\` / \`rate_limited\` / \`upstream_error\` after a failed send. Host-handled codes (\`no_conversation\` / \`reauth_required\` / \`forbidden\`) never surface here.
|
|
1511
|
+
|
|
1512
|
+
Payload is discriminated by \`kind\`: \`'text'\` / \`'image'\` / \`'file'\` / \`'carousel'\`. See the \`messaging.send\` capability section for the full payload table + action types.
|
|
1513
|
+
|
|
1514
|
+
\`\`\`tsx
|
|
1515
|
+
import { useMessaging, useContextData } from '@stackable-labs/sdk-extension-react'
|
|
1516
|
+
|
|
1517
|
+
const { messaging } = useContextData()
|
|
1518
|
+
const [send, { loading, error }] = useMessaging()
|
|
1519
|
+
|
|
1520
|
+
// Proactive gate: skip the call when there's no conversation
|
|
1521
|
+
const canSend = !!messaging?.conversationId
|
|
1522
|
+
|
|
1523
|
+
const onApprove = async () => {
|
|
1524
|
+
try {
|
|
1525
|
+
await send({ kind: 'text', body: 'Approved \u2713' })
|
|
1526
|
+
} catch {
|
|
1527
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
\`\`\`
|
|
1347
1531
|
`;
|
|
1348
1532
|
};
|
|
1349
1533
|
|
|
@@ -3605,6 +3789,27 @@ updating custom fields.
|
|
|
3605
3789
|
\`\`\`tsx
|
|
3606
3790
|
${EXAMPLE_SNIPPETS["actions.invoke"]}
|
|
3607
3791
|
\`\`\`
|
|
3792
|
+
|
|
3793
|
+
## messaging.send \u2014 Sending Messages to Conversations
|
|
3794
|
+
|
|
3795
|
+
Post a message into the active conversation bound to this Instance via the
|
|
3796
|
+
\`useMessaging\` React hook. Returns the response on happy path and throws on
|
|
3797
|
+
**actionable** errors \u2014 inspect \`error\` for one of the typed
|
|
3798
|
+
\`SendMessageActionableErrorCode\` values (\`invalid_message\`, \`rate_limited\`,
|
|
3799
|
+
\`upstream_error\`). Host-handled cases (\`no_conversation\`, \`reauth_required\`,
|
|
3800
|
+
\`forbidden\`) resolve to \`null\` without throwing; the SDK logs a breadcrumb
|
|
3801
|
+
and the host surfaces remediation to admins (e.g. dashboard reconnect UI) \u2014
|
|
3802
|
+
extensions don't catch them.
|
|
3803
|
+
|
|
3804
|
+
**Permission:** \`messaging:send\`. The author label rendered above outbound
|
|
3805
|
+
messages is set **per-Instance by the admin** via the Instance settings field
|
|
3806
|
+
\`messagingDisplayName\` (visible on the dashboard once OAuth completes);
|
|
3807
|
+
falls back to \`extension.manifest.name\` when blank. Extensions do not set or
|
|
3808
|
+
think about the author identity.
|
|
3809
|
+
|
|
3810
|
+
\`\`\`tsx
|
|
3811
|
+
${EXAMPLE_SNIPPETS["messaging.send"]}
|
|
3812
|
+
\`\`\`
|
|
3608
3813
|
`;
|
|
3609
3814
|
};
|
|
3610
3815
|
var generateCookbookStructural = () => {
|
|
@@ -3688,6 +3893,108 @@ ${EXAMPLE_SNIPPETS.surfaceContext}
|
|
|
3688
3893
|
\`\`\`
|
|
3689
3894
|
`;
|
|
3690
3895
|
};
|
|
3896
|
+
var renderPayload = (kind) => JSON.stringify(MESSAGING_SEND_EXAMPLES[kind], null, 2);
|
|
3897
|
+
var renderAction = (type) => JSON.stringify(MESSAGING_ACTION_EXAMPLES[type], null, 2);
|
|
3898
|
+
var ACTION_DESCRIPTIONS = {
|
|
3899
|
+
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.",
|
|
3900
|
+
link: "Opens `url` in a new tab/window. No bubble inserted. Freely mixable with other non-reply actions.",
|
|
3901
|
+
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.",
|
|
3902
|
+
locationRequest: "Prompts the user to share device location. Response arrives as a separate `location`-type message inbound to the conversation. Freely mixable."
|
|
3903
|
+
};
|
|
3904
|
+
var actionTypeSections = Object.keys(MESSAGING_ACTION_EXAMPLES).map((type) => `### type: '${type}'
|
|
3905
|
+
|
|
3906
|
+
${ACTION_DESCRIPTIONS[type]}
|
|
3907
|
+
|
|
3908
|
+
\`\`\`tsx
|
|
3909
|
+
${renderAction(type)}
|
|
3910
|
+
\`\`\``).join("\n\n");
|
|
3911
|
+
var generateCookbookMessaging = () => {
|
|
3912
|
+
const fm = frontmatter({
|
|
3913
|
+
root: false,
|
|
3914
|
+
targets: ["*"],
|
|
3915
|
+
description: "Cookbook: example payloads per-kind for the messaging.send capability + action type shapes",
|
|
3916
|
+
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
3917
|
+
});
|
|
3918
|
+
return `${fm}
|
|
3919
|
+
|
|
3920
|
+
# Messaging Examples
|
|
3921
|
+
|
|
3922
|
+
Example payloads for the \`messaging.send\` capability \u2014 one per supported message
|
|
3923
|
+
kind, plus the action types you can attach to messages. Each payload is the
|
|
3924
|
+
exact shape the \`useMessaging\` hook accepts; copy and adapt as needed.
|
|
3925
|
+
|
|
3926
|
+
**Permission:** \`messaging:send\` in your \`manifest.json\`. The author label
|
|
3927
|
+
above outbound messages is set **per-Instance by the admin** via the Instance
|
|
3928
|
+
settings field \`messagingDisplayName\`; falls back to \`extension.manifest.name\`
|
|
3929
|
+
when blank. Extensions do not set or think about the author identity.
|
|
3930
|
+
|
|
3931
|
+
---
|
|
3932
|
+
|
|
3933
|
+
## kind: 'text' \u2014 Plain text + reply actions
|
|
3934
|
+
|
|
3935
|
+
Plain conversational message. Optional \`actions\` array attaches quick-reply
|
|
3936
|
+
buttons, links, postbacks, or location requests.
|
|
3937
|
+
|
|
3938
|
+
\`\`\`tsx
|
|
3939
|
+
const [send] = useMessaging()
|
|
3940
|
+
|
|
3941
|
+
await send(${renderPayload("text")})
|
|
3942
|
+
\`\`\`
|
|
3943
|
+
|
|
3944
|
+
## kind: 'image' \u2014 Embedded image + optional caption
|
|
3945
|
+
|
|
3946
|
+
Product photos, screenshots, visual help. \`url\` must respond with a
|
|
3947
|
+
\`Content-Type: image/*\` header (Sunco enforces; redirecting URLs like
|
|
3948
|
+
\`picsum.photos\` will fail). \`altText\` is optional \u2014 defaults to filename.
|
|
3949
|
+
Optional \`body\` renders a caption alongside the image.
|
|
3950
|
+
|
|
3951
|
+
\`\`\`tsx
|
|
3952
|
+
const [send] = useMessaging()
|
|
3953
|
+
|
|
3954
|
+
await send(${renderPayload("image")})
|
|
3955
|
+
\`\`\`
|
|
3956
|
+
|
|
3957
|
+
## kind: 'file' \u2014 Embedded file attachment
|
|
3958
|
+
|
|
3959
|
+
Return labels, receipts, NDAs, warranty paperwork. Sunco's \`fileMessage\`
|
|
3960
|
+
schema does NOT support \`actions\` \u2014 file messages can only carry an
|
|
3961
|
+
optional text caption.
|
|
3962
|
+
|
|
3963
|
+
\`\`\`tsx
|
|
3964
|
+
const [send] = useMessaging()
|
|
3965
|
+
|
|
3966
|
+
await send(${renderPayload("file")})
|
|
3967
|
+
\`\`\`
|
|
3968
|
+
|
|
3969
|
+
## kind: 'carousel' \u2014 Horizontally scrolling cards
|
|
3970
|
+
|
|
3971
|
+
The conversational-commerce primitive: product recommendations, size/color
|
|
3972
|
+
pickers, search results with images, multi-option selection.
|
|
3973
|
+
|
|
3974
|
+
- 1\u201310 items, each with required \`title\` + required \`actions\` (1\u20133 per item)
|
|
3975
|
+
- Item actions are a subset: \`link\` + \`postback\` only (no \`reply\`, no
|
|
3976
|
+
\`locationRequest\` \u2014 Sunco rejects those at the item level)
|
|
3977
|
+
- Carousel messages have NO message-level \`actions\` field \u2014 actions live
|
|
3978
|
+
per-card
|
|
3979
|
+
- Use a separate \`text\` message before the carousel if you need an intro
|
|
3980
|
+
|
|
3981
|
+
\`\`\`tsx
|
|
3982
|
+
const [send] = useMessaging()
|
|
3983
|
+
|
|
3984
|
+
await send(${renderPayload("carousel")})
|
|
3985
|
+
\`\`\`
|
|
3986
|
+
|
|
3987
|
+
---
|
|
3988
|
+
|
|
3989
|
+
## Action types
|
|
3990
|
+
|
|
3991
|
+
Attach to message-level \`actions\` (text / image) or item-level \`actions\`
|
|
3992
|
+
(carousel). All actions share \`label\` + optional \`metadata\` (primitives only,
|
|
3993
|
+
\u22644KB total).
|
|
3994
|
+
|
|
3995
|
+
${actionTypeSections}
|
|
3996
|
+
`;
|
|
3997
|
+
};
|
|
3691
3998
|
var identityEventTypes2 = Object.values(IDENTITY_EVENT).map((e) => `\`${e}\``).join(", ");
|
|
3692
3999
|
var activityEventTypes2 = Object.values(ACTIVITY_EVENT).map((e) => `\`${e}\``).join(", ");
|
|
3693
4000
|
var generateCookbookEvents = () => {
|
|
@@ -3709,7 +4016,7 @@ supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-l
|
|
|
3709
4016
|
|
|
3710
4017
|
## Messaging Events
|
|
3711
4018
|
|
|
3712
|
-
Subscribe to postback button clicks from the
|
|
4019
|
+
Subscribe to postback button clicks from the Messaging widget.
|
|
3713
4020
|
The \`actionName\` is the button's display text, not a programmatic identifier.
|
|
3714
4021
|
|
|
3715
4022
|
**Permission:** \`events:messaging\`
|
|
@@ -3922,6 +4229,23 @@ await capabilities.identity.extend({ verified: true })
|
|
|
3922
4229
|
useIdentityEvent('refresh', (event) => {
|
|
3923
4230
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
3924
4231
|
})
|
|
4232
|
+
`,
|
|
4233
|
+
"messaging.send": `
|
|
4234
|
+
// Send a message into the active conversation bound to this Instance.
|
|
4235
|
+
// Requires the 'messaging:send' permission. The author label rendered above
|
|
4236
|
+
// outbound messages is set per-Instance by the admin (Instance settings
|
|
4237
|
+
// messagingDisplayName); falls back to extension.manifest.name when blank.
|
|
4238
|
+
const [send, { loading, error }] = useMessaging()
|
|
4239
|
+
const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
4240
|
+
|
|
4241
|
+
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
4242
|
+
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
4243
|
+
// catch / on state.error to show UX.
|
|
4244
|
+
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
4245
|
+
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
4246
|
+
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
4247
|
+
// can pre-empt 'no_conversation' via useContextData().messaging?.conversationId.
|
|
4248
|
+
if (error === 'rate_limited') { /* show "slow down" UI */ }
|
|
3925
4249
|
`
|
|
3926
4250
|
};
|
|
3927
4251
|
var EVENT_SNIPPETS = {
|
|
@@ -4575,6 +4899,13 @@ var SKILLS = [
|
|
|
4575
4899
|
scopes: ["docs"],
|
|
4576
4900
|
content: () => generateCookbookEvents()
|
|
4577
4901
|
},
|
|
4902
|
+
{
|
|
4903
|
+
id: "cookbook-messaging",
|
|
4904
|
+
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.",
|
|
4905
|
+
type: "knowledge",
|
|
4906
|
+
scopes: ["docs"],
|
|
4907
|
+
content: () => generateCookbookMessaging()
|
|
4908
|
+
},
|
|
4578
4909
|
{
|
|
4579
4910
|
id: "marketplace-listing",
|
|
4580
4911
|
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 +5375,43 @@ export function Content() {
|
|
|
5044
5375
|
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
5045
5376
|
</Surface>
|
|
5046
5377
|
)
|
|
5378
|
+
}`,
|
|
5379
|
+
"messaging.send": `import { useMessaging, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
5380
|
+
|
|
5381
|
+
// Requires 'messaging:send' permission. send() throws on actionable errors
|
|
5382
|
+
// (state.error holds the typed code); resolves to null on host-handled cases.
|
|
5383
|
+
export function Content() {
|
|
5384
|
+
const { messaging } = useContextData()
|
|
5385
|
+
const [send, { loading, error }] = useMessaging()
|
|
5386
|
+
// Proactive gate: skip the call when there's no conversation \u2014 avoids the
|
|
5387
|
+
// host-handled no_conversation log + null return.
|
|
5388
|
+
const canSend = !!messaging?.conversationId
|
|
5389
|
+
|
|
5390
|
+
const onSayHello = async () => {
|
|
5391
|
+
try {
|
|
5392
|
+
await send({
|
|
5393
|
+
kind: 'text',
|
|
5394
|
+
body: 'Hello from the extension',
|
|
5395
|
+
actions: [
|
|
5396
|
+
{ type: 'reply', label: 'Sounds good', payload: 'ACK' },
|
|
5397
|
+
{ type: 'reply', label: 'Maybe later', payload: 'DEFER' },
|
|
5398
|
+
],
|
|
5399
|
+
})
|
|
5400
|
+
} catch {
|
|
5401
|
+
// error holds the typed SendMessageActionableErrorCode
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
|
|
5405
|
+
return (
|
|
5406
|
+
<Surface id="slot.content">
|
|
5407
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
5408
|
+
<ui.Button onClick={onSayHello} disabled={loading || !canSend}>Say hello</ui.Button>
|
|
5409
|
+
{(error === 'rate_limited') && <ui.Alert variant="warning">Slow down \u2014 sent too many.</ui.Alert>}
|
|
5410
|
+
{(error === 'upstream_error') && <ui.Alert variant="error">Send failed \u2014 please try again.</ui.Alert>}
|
|
5411
|
+
{(error === 'invalid_message') && <ui.Alert variant="error">Send failed \u2014 message format invalid.</ui.Alert>}
|
|
5412
|
+
</ui.Stack>
|
|
5413
|
+
</Surface>
|
|
5414
|
+
)
|
|
5047
5415
|
}`
|
|
5048
5416
|
};
|
|
5049
5417
|
|
|
@@ -5135,7 +5503,22 @@ await capabilities.identity.extend({ verified: true })
|
|
|
5135
5503
|
// Consumer side \u2014 same or sibling extension reacts via identity:refresh
|
|
5136
5504
|
useIdentityEvent('refresh', (event) => {
|
|
5137
5505
|
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
5138
|
-
})
|
|
5506
|
+
})`,
|
|
5507
|
+
"messaging.send": `// Send a message into the active conversation bound to this Instance.
|
|
5508
|
+
// Requires the 'messaging:send' permission. The author label rendered above
|
|
5509
|
+
// outbound messages is set per-Instance by the admin (Instance settings
|
|
5510
|
+
// messagingDisplayName); falls back to extension.manifest.name when blank.
|
|
5511
|
+
const [send, { loading, error }] = useMessaging()
|
|
5512
|
+
const result = await send({ kind: 'text', body: 'Hello from the extension' })
|
|
5513
|
+
|
|
5514
|
+
// Actionable errors throw + populate state.error: 'invalid_message' (bad payload),
|
|
5515
|
+
// 'rate_limited' (slow down), 'upstream_error' (transient \u2014 retry). Branch in
|
|
5516
|
+
// catch / on state.error to show UX.
|
|
5517
|
+
// Host-handled cases ('no_conversation' / 'reauth_required' / 'forbidden')
|
|
5518
|
+
// resolve to null without throwing; the SDK logs a breadcrumb and the host
|
|
5519
|
+
// surfaces remediation to admins (e.g. dashboard Reconnect UI). Extensions
|
|
5520
|
+
// can pre-empt 'no_conversation' via useContextData().messaging?.conversationId.
|
|
5521
|
+
if (error === 'rate_limited') { /* show "slow down" UI */ }`
|
|
5139
5522
|
};
|
|
5140
5523
|
var EVENT_SNIPPETS2 = {
|
|
5141
5524
|
"events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|