@stackable-labs/mcp-app-extension 1.22.0 → 1.23.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 +549 -214
- package/dist/server.js +549 -214
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -458,11 +458,19 @@ ${iconList}
|
|
|
458
458
|
var HOOK_SNIPPETS = {
|
|
459
459
|
"events:identity": `import { useIdentityEvent } from '@stackable-labs/sdk-extension-react'
|
|
460
460
|
|
|
461
|
+
// manifest events: ["identity:login", "identity:logout", "identity:refresh"]
|
|
461
462
|
useIdentityEvent('login', (event) => {
|
|
462
|
-
|
|
463
|
+
// event.data.state.user.metadata is populated with any enrichment from sibling
|
|
464
|
+
// extensions with identity:extend (declared in their manifest.identityClaims)
|
|
465
|
+
console.log('User logged in:', event.data.state.user?.email, event.data.state.user?.metadata)
|
|
463
466
|
})
|
|
464
467
|
useIdentityEvent('logout', () => {
|
|
465
468
|
console.log('User logged out')
|
|
469
|
+
})
|
|
470
|
+
// identity:refresh fires after any extension calls capabilities.identity.extend({...}).
|
|
471
|
+
// Listen here to react to post-login enrichment (verification, tier upgrades, etc.).
|
|
472
|
+
useIdentityEvent('refresh', (event) => {
|
|
473
|
+
console.log('Identity refreshed \u2014 metadata:', event.data.state.user?.metadata)
|
|
466
474
|
})`,
|
|
467
475
|
"events:messaging": `import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
|
|
468
476
|
|
|
@@ -474,11 +482,23 @@ useMessagingEvent('postback:Buy Now', (event) => {
|
|
|
474
482
|
useActivityEvent('product_view', (event) => {
|
|
475
483
|
console.log('Activity:', event.eventName, event.data)
|
|
476
484
|
})`,
|
|
477
|
-
"extend
|
|
478
|
-
|
|
485
|
+
"identity.extend": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
|
|
486
|
+
|
|
487
|
+
// manifest.json:
|
|
488
|
+
// {
|
|
489
|
+
// "permissions": ["identity:extend"],
|
|
490
|
+
// "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
|
|
491
|
+
// }
|
|
492
|
+
// Standard JWT claims (external_id, email, name) are exempt from declaration.
|
|
493
|
+
// Custom claims must be in manifest.identityClaims or they're dropped with a warn.
|
|
494
|
+
//
|
|
495
|
+
// Fires ONCE at initial login \u2014 return what's known synchronously. For post-login
|
|
496
|
+
// async updates (e.g., after verification completes via a webhook or polling),
|
|
497
|
+
// use capabilities.identity.extend(patch) \u2014 see the 'identity.extend' capability.
|
|
479
498
|
useExtendIdentity((claims) => ({
|
|
480
|
-
external_id: \`custom_\${claims.external_id}\`,
|
|
481
|
-
loyalty_tier: '
|
|
499
|
+
external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
|
|
500
|
+
loyalty_tier: 'bronze', // custom \u2014 sync, known at login
|
|
501
|
+
verified: false, // custom \u2014 default; updated async post-verification
|
|
482
502
|
}))`
|
|
483
503
|
};
|
|
484
504
|
var HOOK_SNIPPETS_MEMOIZED = {
|
|
@@ -490,13 +510,19 @@ const handlePostback = useCallback<MessagingEventHandler>((event) => {
|
|
|
490
510
|
console.log('Postback:', event.data.actionName, event.data.conversationId)
|
|
491
511
|
}, [])
|
|
492
512
|
useMessagingEvent('postback:Buy Now', handlePostback)`,
|
|
493
|
-
"extend
|
|
513
|
+
"identity.extend": `import { useCallback } from 'react'
|
|
494
514
|
import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
|
|
495
515
|
import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
|
|
496
516
|
|
|
517
|
+
// manifest.json:
|
|
518
|
+
// {
|
|
519
|
+
// "permissions": ["identity:extend"],
|
|
520
|
+
// "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
|
|
521
|
+
// }
|
|
497
522
|
const handleExtend = useCallback<ExtendIdentityHandler>((claims) => ({
|
|
498
|
-
external_id: \`custom_\${claims.external_id}\`,
|
|
499
|
-
loyalty_tier: '
|
|
523
|
+
external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
|
|
524
|
+
loyalty_tier: 'bronze', // custom \u2014 sync, known at login
|
|
525
|
+
verified: false, // custom \u2014 default; updated async post-verification
|
|
500
526
|
}), [])
|
|
501
527
|
useExtendIdentity(handleExtend)`
|
|
502
528
|
};
|
|
@@ -508,7 +534,7 @@ var generateCapabilities = () => {
|
|
|
508
534
|
const fm = frontmatter({
|
|
509
535
|
root: false,
|
|
510
536
|
targets: ["*"],
|
|
511
|
-
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, extend
|
|
537
|
+
description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
|
|
512
538
|
globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
|
|
513
539
|
});
|
|
514
540
|
return `${fm}
|
|
@@ -715,22 +741,89 @@ ${HOOK_SNIPPETS["events:activity"]}
|
|
|
715
741
|
|
|
716
742
|
**Generic alternative:** \`useEvent('activity:product_view', handler)\` \u2014 a cross-domain hook that accepts fully-qualified event types. Domain wildcard (e.g., \`'activity'\`) receives all events in that domain.
|
|
717
743
|
|
|
718
|
-
## extend
|
|
719
|
-
Enrich identity JWT claims
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
744
|
+
## identity.extend \u2014 Identity Claim Enrichment
|
|
745
|
+
Enrich identity JWT claims and \`identityState.user.metadata\` so the current user's signed token AND any sibling extension can react to them. Two complementary APIs:
|
|
746
|
+
|
|
747
|
+
1. **\`useExtendIdentity(handler)\`** \u2014 synchronous hook that fires ONCE at initial login.
|
|
748
|
+
2. **\`capabilities.identity.extend(patch)\`** \u2014 imperative call that fires at any time after login (post-verification, post-checkout, any user-triggered async flow). Re-signs the JWT, updates \`user.metadata\`, and broadcasts \`identity:refresh\`.
|
|
749
|
+
|
|
750
|
+
Both share the **\`identity:extend\`** permission and the **\`manifest.identityClaims\`** declaration gate.
|
|
751
|
+
|
|
752
|
+
### Manifest contract
|
|
724
753
|
|
|
725
754
|
\`\`\`json
|
|
726
755
|
{
|
|
727
|
-
"permissions": ["extend
|
|
756
|
+
"permissions": ["identity:extend"],
|
|
757
|
+
"identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
|
|
728
758
|
}
|
|
729
759
|
\`\`\`
|
|
730
760
|
|
|
761
|
+
- **Standard JWT claims (\`external_id\`, \`email\`, \`name\`) are exempt** \u2014 they're part of the signing contract and may be overridden without declaration.
|
|
762
|
+
- **Custom keys MUST be declared in \`identityClaims\`** or the host filter drops them with a \`console.warn\`.
|
|
763
|
+
- **Reserved JWT/Zendesk keys** (\`iss\`, \`sub\`, \`aud\`, \`exp\`, \`nbf\`, \`iat\`, \`jti\`, \`scope\`, \`email_verified\`, \`user_fields\`) **cannot** appear in \`identityClaims\` \u2014 the Lambda sanitizer always wins on collision.
|
|
764
|
+
- **Key format:** \`/^[a-z_][a-z0-9_]{0,63}$/\` (lowercase identifier, \u226464 chars).
|
|
765
|
+
- **Maximum 20 entries.**
|
|
766
|
+
|
|
767
|
+
### Initial-login enrichment (handler-style)
|
|
768
|
+
|
|
769
|
+
- **Hook:** \`useExtendIdentity(handler)\` \u2014 \`ExtendIdentityHandler\` type exported for use with \`useCallback\`
|
|
770
|
+
- **Handler signature:** \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
|
|
771
|
+
- **IdentityBaseClaims:** \`{ external_id: string, email?: string, name?: string, [key: string]: unknown }\`
|
|
772
|
+
|
|
731
773
|
\`\`\`tsx
|
|
732
|
-
${HOOK_SNIPPETS["extend
|
|
774
|
+
${HOOK_SNIPPETS["identity.extend"]}
|
|
733
775
|
\`\`\`
|
|
776
|
+
|
|
777
|
+
### Async post-login push (imperative)
|
|
778
|
+
|
|
779
|
+
\`\`\`tsx
|
|
780
|
+
const capabilities = useCapabilities()
|
|
781
|
+
|
|
782
|
+
// Anytime after login \u2014 webhook callback, user action, async verification, etc.
|
|
783
|
+
await capabilities.identity.extend({
|
|
784
|
+
verified: true,
|
|
785
|
+
verified_by: 'xyzProvider',
|
|
786
|
+
verified_at: new Date().toISOString(),
|
|
787
|
+
})
|
|
788
|
+
// \u2192 host filters against manifest.identityClaims
|
|
789
|
+
// \u2192 user.metadata updated
|
|
790
|
+
// \u2192 JWT re-signed, pushed to Zendesk loginUser
|
|
791
|
+
// \u2192 identity:refresh broadcast to all extensions with events:identity
|
|
792
|
+
\`\`\`
|
|
793
|
+
|
|
794
|
+
Per-extension calls are serialized to prevent concurrent Zendesk \`loginUser\` callbacks from orphaning each other (empirically verified in the loginUser re-auth spike).
|
|
795
|
+
|
|
796
|
+
### Consuming enriched state from another extension
|
|
797
|
+
|
|
798
|
+
Any extension with \`events:identity\` permission can react to enrichment updates. Same minimal pattern as the \`events:identity\` section above \u2014 \`'login'\` covers the initial enriched state, \`'refresh'\` covers post-login pushes:
|
|
799
|
+
|
|
800
|
+
\`\`\`tsx
|
|
801
|
+
${HOOK_SNIPPETS["events:identity"]}
|
|
802
|
+
\`\`\`
|
|
803
|
+
|
|
804
|
+
For a snapshot read instead of event-driven reaction (auto re-renders when context changes):
|
|
805
|
+
|
|
806
|
+
\`\`\`tsx
|
|
807
|
+
const { identity } = useContextData()
|
|
808
|
+
const verified = Boolean(identity?.user?.metadata?.verified)
|
|
809
|
+
\`\`\`
|
|
810
|
+
|
|
811
|
+
### Install-time enforcement
|
|
812
|
+
|
|
813
|
+
Two enabled extensions on the same instance **MUST NOT** declare overlapping \`identityClaims\` keys. The runtime merge is order-dependent (\`Object.assign\` across per-extension contributions), so one extension's value would silently overwrite the other's. The marketplace install API blocks the install with a 409 conflict, surfacing the specific overlapping key + conflicting extension name in the admin install dialog. Coordinate keys with downstream extensions or namespace them (e.g. \`<vendor>_loyalty_tier\`).
|
|
814
|
+
|
|
815
|
+
### Bundle-scan findings at upload
|
|
816
|
+
|
|
817
|
+
The publisher-side bundle scan validates your declaration at submission time:
|
|
818
|
+
|
|
819
|
+
| Finding | Severity | Triggers when |
|
|
820
|
+
| --- | --- | --- |
|
|
821
|
+
| \`identityClaims_missing\` | warning | \`identity:extend\` declared, \`identityClaims\` empty (custom claims would be dropped) |
|
|
822
|
+
| \`identityClaims_no_permission\` | warning | \`identityClaims\` declared, \`identity:extend\` permission missing |
|
|
823
|
+
| \`identityClaims_invalid_key\` | error | Key fails the format regex |
|
|
824
|
+
| \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
|
|
825
|
+
| \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
|
|
826
|
+
| \`identityClaims_too_many\` | error | More than 20 entries |
|
|
734
827
|
`;
|
|
735
828
|
};
|
|
736
829
|
|
|
@@ -968,35 +1061,94 @@ export function Content(): React.ReactElement {
|
|
|
968
1061
|
</Surface>
|
|
969
1062
|
)
|
|
970
1063
|
}`,
|
|
971
|
-
"extend
|
|
972
|
-
import type {
|
|
1064
|
+
"identity.extend": `import { useCapabilities, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
1065
|
+
import type { ContextData } from '@stackable-labs/sdk-extension-contracts'
|
|
973
1066
|
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1067
|
+
// manifest.json:
|
|
1068
|
+
// {
|
|
1069
|
+
// "permissions": ["identity:extend"],
|
|
1070
|
+
// "identityClaims": ["verified", "verified_by", "verified_at"]
|
|
1071
|
+
// }
|
|
1072
|
+
//
|
|
1073
|
+
// PUSH side \u2014 capabilities.identity.extend(patch) sends new claims to the host
|
|
1074
|
+
// AFTER initial login. The host filters the patch against manifest.identityClaims,
|
|
1075
|
+
// merges into user.metadata, re-signs the JWT, and broadcasts identity:refresh.
|
|
1076
|
+
//
|
|
1077
|
+
// CONSUMER side \u2014 useContextData() already re-renders on every host-pushed
|
|
1078
|
+
// context update (login/logout/refresh/expired). For pure rendering, read the
|
|
1079
|
+
// enriched value directly from ctx.identity.user.metadata \u2014 no event listener
|
|
1080
|
+
// needed. Use useIdentityEvent only when you need a SIDE EFFECT (analytics,
|
|
1081
|
+
// cache invalidation, etc.) on a specific event.
|
|
1082
|
+
//
|
|
1083
|
+
// Standard JWT claims (external_id, email, name) are exempt from declaration.
|
|
1084
|
+
// Undeclared custom keys are dropped client-side with a console.warn.
|
|
1085
|
+
export function Content(): React.ReactElement {
|
|
1086
|
+
const capabilities = useCapabilities()
|
|
1087
|
+
const ctx = useContextData() as ContextData & { loading: boolean }
|
|
1088
|
+
|
|
1089
|
+
// Push: trigger after async verification (webhook, polling, user action)
|
|
1090
|
+
const runVerification = async () => {
|
|
1091
|
+
await new Promise(r => setTimeout(r, 1000)) // simulated async work
|
|
1092
|
+
await capabilities.identity.extend({
|
|
1093
|
+
verified: true,
|
|
1094
|
+
verified_by: 'xyzProvider',
|
|
1095
|
+
verified_at: new Date().toISOString(),
|
|
1096
|
+
})
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Consume: read directly from ctx \u2014 re-renders automatically on identity:refresh.
|
|
1100
|
+
const verified = Boolean(ctx.identity?.user?.metadata?.verified)
|
|
1101
|
+
|
|
1102
|
+
return (
|
|
1103
|
+
<Surface id="slot.content">
|
|
1104
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
1105
|
+
<ui.Button onClick={runVerification}>Verify</ui.Button>
|
|
1106
|
+
<ui.Text>Status: {verified ? 'verified' : 'unverified'}</ui.Text>
|
|
1107
|
+
</ui.Stack>
|
|
1108
|
+
</Surface>
|
|
1109
|
+
)
|
|
1110
|
+
}`,
|
|
981
1111
|
// ── Per-event example snippets ─────────────────────────────────────────────
|
|
982
|
-
"events:identity": `import {
|
|
1112
|
+
"events:identity": `import { Surface, ui, useContextData, useIdentityEvent } from '@stackable-labs/sdk-extension-react'
|
|
983
1113
|
import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
|
|
984
|
-
import { useState } from 'react'
|
|
985
1114
|
|
|
1115
|
+
// useIdentityEvent \u2014 for SIDE EFFECTS on identity lifecycle events.
|
|
1116
|
+
// If you just want to RENDER based on identity state (e.g. show the current
|
|
1117
|
+
// user's email), use useContextData() \u2014 it re-renders reactively on every
|
|
1118
|
+
// host-pushed identity change. Use useIdentityEvent only when you need to
|
|
1119
|
+
// RUN imperative code on a specific event: analytics beacons, cookie writes,
|
|
1120
|
+
// downstream system notifications, cache invalidation, etc.
|
|
1121
|
+
//
|
|
1122
|
+
// manifest.json:
|
|
1123
|
+
// {
|
|
1124
|
+
// "permissions": ["context:read", "events:identity"],
|
|
1125
|
+
// "events": ["identity:login", "identity:logout"]
|
|
1126
|
+
// }
|
|
986
1127
|
export function Header(): React.ReactElement {
|
|
987
|
-
const
|
|
1128
|
+
const ctx = useContextData()
|
|
988
1129
|
|
|
1130
|
+
// Side effect: persist a "last login" hint to localStorage on every login.
|
|
1131
|
+
// Replace with your real-world side effect \u2014 analytics beacon, cookie write,
|
|
1132
|
+
// downstream system notification, etc.
|
|
989
1133
|
useIdentityEvent('login', (event: IdentityEvent) => {
|
|
990
|
-
|
|
1134
|
+
localStorage.setItem('last-login', JSON.stringify({
|
|
1135
|
+
userId: event.data.state.user?.id,
|
|
1136
|
+
timestamp: new Date().toISOString(),
|
|
1137
|
+
}))
|
|
991
1138
|
})
|
|
992
1139
|
|
|
1140
|
+
// Clear the hint on logout / session expiry.
|
|
993
1141
|
useIdentityEvent('logout', () => {
|
|
994
|
-
|
|
1142
|
+
localStorage.removeItem('last-login')
|
|
995
1143
|
})
|
|
996
1144
|
|
|
1145
|
+
// Rendering: read directly from ctx \u2014 DON'T mirror identity into local
|
|
1146
|
+
// useState via the event listener; useContextData is already reactive.
|
|
1147
|
+
const email = ctx?.identity?.user?.email ?? null
|
|
1148
|
+
|
|
997
1149
|
return (
|
|
998
1150
|
<Surface id="slot.header">
|
|
999
|
-
<ui.Text className="text-xs">{
|
|
1151
|
+
<ui.Text className="text-xs">{email ?? 'Not logged in'}</ui.Text>
|
|
1000
1152
|
</Surface>
|
|
1001
1153
|
)
|
|
1002
1154
|
}`,
|
|
@@ -1075,7 +1227,7 @@ const capabilities = useCapabilities()
|
|
|
1075
1227
|
// capabilities.data.fetch(url, init?)
|
|
1076
1228
|
// capabilities.actions.toast(payload)
|
|
1077
1229
|
// capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
|
|
1078
|
-
// capabilities.extend
|
|
1230
|
+
// 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.
|
|
1079
1231
|
\`\`\`
|
|
1080
1232
|
|
|
1081
1233
|
## useStore(store, selector?)
|
|
@@ -1173,17 +1325,17 @@ useEvent('activity', (event) => {
|
|
|
1173
1325
|
\`\`\`
|
|
1174
1326
|
|
|
1175
1327
|
## useExtendIdentity(handler)
|
|
1176
|
-
Register a handler to enrich identity JWT claims before signing. Requires \`extend
|
|
1328
|
+
Register a handler to enrich identity JWT claims before signing. Requires \`identity:extend\` permission.
|
|
1177
1329
|
- \`handler: ExtendIdentityHandler\` \u2014 \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
|
|
1178
1330
|
- \`IdentityBaseClaims: { external_id: string, email?: string, name?: string, [key: string]: unknown }\`
|
|
1179
1331
|
|
|
1180
1332
|
\`\`\`tsx
|
|
1181
|
-
${stripImports(HOOK_SNIPPETS["extend
|
|
1333
|
+
${stripImports(HOOK_SNIPPETS["identity.extend"])}
|
|
1182
1334
|
\`\`\`
|
|
1183
1335
|
|
|
1184
1336
|
With \`useCallback\` (for memoized handlers):
|
|
1185
1337
|
\`\`\`tsx
|
|
1186
|
-
${HOOK_SNIPPETS_MEMOIZED["extend
|
|
1338
|
+
${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}
|
|
1187
1339
|
\`\`\`
|
|
1188
1340
|
|
|
1189
1341
|
## Identity via context.read()
|
|
@@ -2499,7 +2651,7 @@ filtered out. Clicking a surface adds it to your manifest and inserts a
|
|
|
2499
2651
|
### Capabilities
|
|
2500
2652
|
|
|
2501
2653
|
The SDK capabilities your extension can use: \`data.query\`, \`data.fetch\`,
|
|
2502
|
-
\`context.read\`, \`actions.toast\`, \`actions.invoke\`, \`extend
|
|
2654
|
+
\`context.read\`, \`actions.toast\`, \`actions.invoke\`, \`identity.extend\`,
|
|
2503
2655
|
\`events:identity\`, \`events:messaging\`, and \`events:activity\`. Clicking a
|
|
2504
2656
|
capability adds the permission to your manifest and AI-inserts the hook usage.
|
|
2505
2657
|
|
|
@@ -3547,30 +3699,13 @@ var generateCookbookEvents = () => {
|
|
|
3547
3699
|
});
|
|
3548
3700
|
return `${fm}
|
|
3549
3701
|
|
|
3550
|
-
# Events &
|
|
3551
|
-
|
|
3552
|
-
Subscribe to real-time events pushed from the host via the framework, and extend identity claims.
|
|
3553
|
-
Each event type has a dedicated hook \u2014 never use \`capabilities.events.*\` directly.
|
|
3554
|
-
|
|
3555
|
-
## Identity Events
|
|
3556
|
-
|
|
3557
|
-
Subscribe to login, logout, refresh, and expired events. Useful for tracking
|
|
3558
|
-
agent authentication state in your extension.
|
|
3559
|
-
|
|
3560
|
-
**Permission:** \`events:identity\`
|
|
3561
|
-
**Event types:** ${identityEventTypes2}
|
|
3562
|
-
|
|
3563
|
-
### Hook usage
|
|
3564
|
-
|
|
3565
|
-
\`\`\`tsx
|
|
3566
|
-
${HOOK_SNIPPETS["events:identity"]}
|
|
3567
|
-
\`\`\`
|
|
3568
|
-
|
|
3569
|
-
### Full component example
|
|
3702
|
+
# Events & Identity
|
|
3570
3703
|
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3704
|
+
Subscribe to real-time events pushed from the host via the framework, and enrich identity
|
|
3705
|
+
claims. Each event type has a dedicated hook (\`useIdentityEvent\` / \`useMessagingEvent\` /
|
|
3706
|
+
\`useActivityEvent\`) \u2014 never use \`capabilities.events.*\` directly. Identity enrichment
|
|
3707
|
+
supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-login API
|
|
3708
|
+
(\`capabilities.identity.extend\`).
|
|
3574
3709
|
|
|
3575
3710
|
## Messaging Events
|
|
3576
3711
|
|
|
@@ -3612,24 +3747,54 @@ ${HOOK_SNIPPETS["events:activity"]}
|
|
|
3612
3747
|
${EXAMPLE_SNIPPETS["events:activity"]}
|
|
3613
3748
|
\`\`\`
|
|
3614
3749
|
|
|
3615
|
-
##
|
|
3750
|
+
## Identity Events
|
|
3616
3751
|
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3752
|
+
Subscribe to login, logout, refresh, and expired events. Useful for tracking
|
|
3753
|
+
agent authentication state and reacting to identity enrichment pushed from sibling
|
|
3754
|
+
extensions (via \`capabilities.identity.extend\` \u2014 see *Extend Identity* below).
|
|
3620
3755
|
|
|
3621
|
-
**Permission:** \`
|
|
3756
|
+
**Permission:** \`events:identity\`
|
|
3757
|
+
**Event types:** ${identityEventTypes2}
|
|
3622
3758
|
|
|
3623
3759
|
### Hook usage
|
|
3624
3760
|
|
|
3625
3761
|
\`\`\`tsx
|
|
3626
|
-
${HOOK_SNIPPETS["
|
|
3762
|
+
${HOOK_SNIPPETS["events:identity"]}
|
|
3627
3763
|
\`\`\`
|
|
3628
3764
|
|
|
3629
3765
|
### Full component example
|
|
3630
3766
|
|
|
3631
3767
|
\`\`\`tsx
|
|
3632
|
-
${EXAMPLE_SNIPPETS["
|
|
3768
|
+
${EXAMPLE_SNIPPETS["events:identity"]}
|
|
3769
|
+
\`\`\`
|
|
3770
|
+
|
|
3771
|
+
## Extend Identity
|
|
3772
|
+
|
|
3773
|
+
Enrich identity JWT claims and \`user.metadata\` so the signed token AND any sibling
|
|
3774
|
+
extension with \`events:identity\` can react. Two complementary paths:
|
|
3775
|
+
|
|
3776
|
+
- **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires ONCE at initial login. Use
|
|
3777
|
+
for known-at-login enrichment.
|
|
3778
|
+
- **\`capabilities.identity.extend(patch)\`** \u2014 imperative call, fires post-login (after
|
|
3779
|
+
async verification, webhook callbacks, user-triggered flows). Re-signs the JWT and
|
|
3780
|
+
broadcasts \`identity:refresh\` to all extensions with \`events:identity\`.
|
|
3781
|
+
|
|
3782
|
+
Both paths share the **\`identity:extend\`** permission and the **\`manifest.identityClaims\`**
|
|
3783
|
+
declaration gate. Standard JWT claims (\`external_id\`, \`email\`, \`name\`) are exempt;
|
|
3784
|
+
custom keys must be declared or they're dropped by the host filter with a \`console.warn\`.
|
|
3785
|
+
|
|
3786
|
+
**Permission:** \`identity:extend\`
|
|
3787
|
+
|
|
3788
|
+
### Login-time hook (useExtendIdentity)
|
|
3789
|
+
|
|
3790
|
+
\`\`\`tsx
|
|
3791
|
+
${HOOK_SNIPPETS["identity.extend"]}
|
|
3792
|
+
\`\`\`
|
|
3793
|
+
|
|
3794
|
+
### Imperative post-login (capabilities.identity.extend)
|
|
3795
|
+
|
|
3796
|
+
\`\`\`tsx
|
|
3797
|
+
${EXAMPLE_SNIPPETS["identity.extend"]}
|
|
3633
3798
|
\`\`\`
|
|
3634
3799
|
`;
|
|
3635
3800
|
};
|
|
@@ -3693,10 +3858,148 @@ Only add permissions that aren't already declared.
|
|
|
3693
3858
|
`;
|
|
3694
3859
|
};
|
|
3695
3860
|
|
|
3861
|
+
// ../../sdk/extension/ai-docs/src/snippets/capabilities.ts
|
|
3862
|
+
var EXTEND_IDENTITY_HOOK_EXAMPLE = `// Enrich identity JWT claims before signing.
|
|
3863
|
+
// The host sends base claims (external_id, email, name); your handler returns
|
|
3864
|
+
// additional claims to merge. Returned keys are filtered against manifest.identityClaims:
|
|
3865
|
+
// - Standard JWT claims (external_id, email, name) are exempt \u2014 always allowed
|
|
3866
|
+
// - Custom claims must be declared in manifest.identityClaims
|
|
3867
|
+
// - Undeclared keys are dropped with a console.warn
|
|
3868
|
+
// Merged claims land in BOTH identityState.user.metadata (for sibling extensions)
|
|
3869
|
+
// AND the signed JWT's custom_claims (for downstream JWT consumers).
|
|
3870
|
+
// Pattern: name the handler with useCallback<ExtendIdentityHandler> for stable
|
|
3871
|
+
// reference + clean separation; pass the named const to useExtendIdentity.
|
|
3872
|
+
//
|
|
3873
|
+
// useExtendIdentity fires ONCE at initial login \u2014 return what's known synchronously.
|
|
3874
|
+
// For post-login async updates (e.g., after verification completes via a webhook
|
|
3875
|
+
// or polling), use capabilities.identity.extend(patch) \u2014 see the 'identity.extend'
|
|
3876
|
+
// capability for the imperative path.
|
|
3877
|
+
|
|
3878
|
+
${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}`;
|
|
3879
|
+
var CAPABILITY_SNIPPETS = {
|
|
3880
|
+
"data.query": `
|
|
3881
|
+
const capabilities = useCapabilities()
|
|
3882
|
+
const result = await capabilities.data.query({ path: '/your-endpoint', method: 'GET' })
|
|
3883
|
+
`,
|
|
3884
|
+
"data.fetch": `
|
|
3885
|
+
const capabilities = useCapabilities()
|
|
3886
|
+
const response = await capabilities.data.fetch('https://api.example.com/endpoint')
|
|
3887
|
+
`,
|
|
3888
|
+
"context.read": `
|
|
3889
|
+
// Read host context + extension settings
|
|
3890
|
+
const { customerId, settings } = useContextData()
|
|
3891
|
+
|
|
3892
|
+
// Or use the convenience hook for settings only
|
|
3893
|
+
const settings = useSettings()
|
|
3894
|
+
`,
|
|
3895
|
+
"actions.toast": `
|
|
3896
|
+
const capabilities = useCapabilities()
|
|
3897
|
+
capabilities.actions.toast({ type: 'success', message: 'Done!' })
|
|
3898
|
+
`,
|
|
3899
|
+
"actions.invoke": `
|
|
3900
|
+
const capabilities = useCapabilities()
|
|
3901
|
+
|
|
3902
|
+
// New conversation with tags and fields
|
|
3903
|
+
await capabilities.actions.invoke('newConversation', {
|
|
3904
|
+
tags: ['stackable', 'order-lookup'],
|
|
3905
|
+
fields: [{ id: 'stackable_action', value: 'order_status' }],
|
|
3906
|
+
metadata: { orderId: '12345' },
|
|
3907
|
+
})
|
|
3908
|
+
|
|
3909
|
+
// Standalone: set tags on current/next conversation
|
|
3910
|
+
await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
|
|
3911
|
+
`,
|
|
3912
|
+
"events:identity": HOOK_SNIPPETS["events:identity"],
|
|
3913
|
+
"events:messaging": HOOK_SNIPPETS["events:messaging"],
|
|
3914
|
+
"events:activity": HOOK_SNIPPETS["events:activity"],
|
|
3915
|
+
"identity.extend": `${EXTEND_IDENTITY_HOOK_EXAMPLE}
|
|
3916
|
+
|
|
3917
|
+
// \u2500\u2500 For post-login async push (e.g., after async verification completes): \u2500\u2500\u2500\u2500\u2500
|
|
3918
|
+
const capabilities = useCapabilities()
|
|
3919
|
+
await capabilities.identity.extend({ verified: true })
|
|
3920
|
+
|
|
3921
|
+
// Consumer side \u2014 same or sibling extension reacts via identity:refresh
|
|
3922
|
+
useIdentityEvent('refresh', (event) => {
|
|
3923
|
+
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
3924
|
+
})
|
|
3925
|
+
`
|
|
3926
|
+
};
|
|
3927
|
+
var EVENT_SNIPPETS = {
|
|
3928
|
+
"events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
3929
|
+
import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
|
|
3930
|
+
import { useState } from 'react'
|
|
3931
|
+
|
|
3932
|
+
// manifest events: ["identity:login", "identity:logout", "identity:refresh"]
|
|
3933
|
+
//
|
|
3934
|
+
// 'login' and 'refresh' both deliver the full IdentityState, including
|
|
3935
|
+
// state.user.metadata \u2014 populated by sibling extensions with identity:extend
|
|
3936
|
+
// (declared in their manifest.identityClaims). Subscribe to BOTH to cover the
|
|
3937
|
+
// initial login AND any post-login push via capabilities.identity.extend().
|
|
3938
|
+
export function Header(): React.ReactElement {
|
|
3939
|
+
const [user, setUser] = useState<string | null>(null)
|
|
3940
|
+
const [verified, setVerified] = useState(false)
|
|
3941
|
+
|
|
3942
|
+
useIdentityEvent('login', (event: IdentityEvent) => {
|
|
3943
|
+
setUser(event.data.state.user?.email ?? null)
|
|
3944
|
+
setVerified(Boolean(event.data.state.user?.metadata?.verified))
|
|
3945
|
+
})
|
|
3946
|
+
|
|
3947
|
+
useIdentityEvent('refresh', (event: IdentityEvent) => {
|
|
3948
|
+
setVerified(Boolean(event.data.state.user?.metadata?.verified))
|
|
3949
|
+
})
|
|
3950
|
+
|
|
3951
|
+
useIdentityEvent('logout', () => {
|
|
3952
|
+
setUser(null)
|
|
3953
|
+
setVerified(false)
|
|
3954
|
+
})
|
|
3955
|
+
|
|
3956
|
+
return (
|
|
3957
|
+
<Surface id="slot.header">
|
|
3958
|
+
<ui.Text className="text-xs">{user ?? 'Not logged in'} {verified && '\u2713'}</ui.Text>
|
|
3959
|
+
</Surface>
|
|
3960
|
+
)
|
|
3961
|
+
}`,
|
|
3962
|
+
"events:messaging": `import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
3963
|
+
import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
|
|
3964
|
+
import { useState } from 'react'
|
|
3965
|
+
|
|
3966
|
+
export function Content(): React.ReactElement {
|
|
3967
|
+
const [lastPostback, setLastPostback] = useState<string | null>(null)
|
|
3968
|
+
|
|
3969
|
+
useMessagingEvent('postback', (event) => {
|
|
3970
|
+
setLastPostback(event.data.actionName)
|
|
3971
|
+
})
|
|
3972
|
+
|
|
3973
|
+
return (
|
|
3974
|
+
<Surface id="slot.content">
|
|
3975
|
+
<ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
|
|
3976
|
+
</Surface>
|
|
3977
|
+
)
|
|
3978
|
+
}`,
|
|
3979
|
+
"events:activity": `import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
3980
|
+
import type { ActivityEventHandler } from '@stackable-labs/sdk-extension-contracts'
|
|
3981
|
+
import { useState } from 'react'
|
|
3982
|
+
|
|
3983
|
+
export function Content(): React.ReactElement {
|
|
3984
|
+
const [lastEvent, setLastEvent] = useState<string | null>(null)
|
|
3985
|
+
|
|
3986
|
+
useActivityEvent('page_view', (event) => {
|
|
3987
|
+
setLastEvent(event.data.url as string)
|
|
3988
|
+
})
|
|
3989
|
+
|
|
3990
|
+
return (
|
|
3991
|
+
<Surface id="slot.content">
|
|
3992
|
+
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
3993
|
+
</Surface>
|
|
3994
|
+
)
|
|
3995
|
+
}`,
|
|
3996
|
+
"identity.extend": EXTEND_IDENTITY_HOOK_EXAMPLE
|
|
3997
|
+
};
|
|
3998
|
+
|
|
3696
3999
|
// ../../sdk/extension/ai-docs/src/commands/add-capability.ts
|
|
3697
4000
|
var generateAddCapabilityCommand = () => {
|
|
3698
4001
|
const fm = frontmatter({
|
|
3699
|
-
description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, extend
|
|
4002
|
+
description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity) in this extension",
|
|
3700
4003
|
targets: ["*"]
|
|
3701
4004
|
});
|
|
3702
4005
|
return `${fm}
|
|
@@ -3712,7 +4015,7 @@ Ask which capability to add. Valid capabilities:
|
|
|
3712
4015
|
- \`context.read\` \u2014 read host-provided context (customerId, customerEmail, messaging.conversationId, etc.)
|
|
3713
4016
|
- \`actions.toast\` \u2014 show toast notifications (success, error, info, warning)
|
|
3714
4017
|
- \`actions.invoke\` \u2014 invoke host actions (e.g., open new conversation)
|
|
3715
|
-
- \`extend
|
|
4018
|
+
- \`identity.extend\` \u2014 enrich identity JWT claims before signing
|
|
3716
4019
|
- \`events:identity\` \u2014 subscribe to identity events (login, logout, refresh, expired)
|
|
3717
4020
|
- \`events:messaging\` \u2014 subscribe to messaging events (postback button clicks)
|
|
3718
4021
|
- \`events:activity\` \u2014 subscribe to activity events (page views, clicks, purchases)
|
|
@@ -3724,7 +4027,7 @@ Add the corresponding permission to \`packages/extension/public/manifest.json\`:
|
|
|
3724
4027
|
- \`context.read\` \u2192 \`"context:read"\`
|
|
3725
4028
|
- \`actions.toast\` \u2192 \`"actions:toast"\`
|
|
3726
4029
|
- \`actions.invoke\` \u2192 \`"actions:invoke"\`
|
|
3727
|
-
- \`extend
|
|
4030
|
+
- \`identity.extend\` \u2192 \`"identity:extend"\`
|
|
3728
4031
|
- \`events:identity\` \u2192 \`"events:identity"\` (also add entries to \`events\` array, e.g. \`["identity:login", "identity:logout"]\`)
|
|
3729
4032
|
- \`events:messaging\` \u2192 \`"events:messaging"\` (also add entries to \`events\` array, e.g. \`["messaging:postback:Buy Now"]\`)
|
|
3730
4033
|
- \`events:activity\` \u2192 \`"events:activity"\` (also add entries to \`events\` array, e.g. \`["activity:product_view"]\`)
|
|
@@ -3766,8 +4069,14 @@ const capabilities = useCapabilities()
|
|
|
3766
4069
|
// actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
|
|
3767
4070
|
\`\`\`
|
|
3768
4071
|
|
|
3769
|
-
###
|
|
3770
|
-
|
|
4072
|
+
### Special handling (events + identity.extend):
|
|
4073
|
+
|
|
4074
|
+
#### For events \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
|
|
4075
|
+
Events are subscribed via \`useIdentityEvent\` / \`useMessagingEvent\` / \`useActivityEvent\` \u2014 never use \`capabilities.events.*\` directly (events are not part of the \`capabilities\` object).
|
|
4076
|
+
|
|
4077
|
+
#### For identity.extend \u2014 choose the CORRECT option:
|
|
4078
|
+
- **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires only once at initial login. ALWAYS use for known-at-login enrichment.
|
|
4079
|
+
- **\`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\`.
|
|
3771
4080
|
|
|
3772
4081
|
\`\`\`tsx
|
|
3773
4082
|
// events:identity \u2014 use useIdentityEvent hook
|
|
@@ -3785,8 +4094,8 @@ ${HOOK_SNIPPETS["events:activity"]}
|
|
|
3785
4094
|
\`\`\`
|
|
3786
4095
|
|
|
3787
4096
|
\`\`\`tsx
|
|
3788
|
-
// extend
|
|
3789
|
-
${
|
|
4097
|
+
// identity.extend \u2014 login-time useExtendIdentity hook OR post-login imperative capabilities.identity.extend
|
|
4098
|
+
${CAPABILITY_SNIPPETS["identity.extend"]}
|
|
3790
4099
|
\`\`\`
|
|
3791
4100
|
|
|
3792
4101
|
## 6. Verify
|
|
@@ -3794,7 +4103,7 @@ ${HOOK_SNIPPETS["extend.identity"]}
|
|
|
3794
4103
|
- If data.fetch, confirm the domain is in allowedDomains
|
|
3795
4104
|
- For data/context/actions: confirm accessed via \`useCapabilities()\` hook
|
|
3796
4105
|
- For events: confirm using \`useIdentityEvent\`, \`useMessagingEvent\`, or \`useActivityEvent\` hooks
|
|
3797
|
-
- For extend: confirm using \`useExtendIdentity\` hook
|
|
4106
|
+
- For identity.extend: confirm using \`useExtendIdentity\` hook (login-time) and/or \`capabilities.identity.extend(patch)\` (imperative post-login)
|
|
3798
4107
|
`;
|
|
3799
4108
|
};
|
|
3800
4109
|
|
|
@@ -3885,12 +4194,13 @@ Scan all \`.tsx\` files in \`packages/extension/src/\` for capability usage:
|
|
|
3885
4194
|
- \`capabilities.context.read\` or \`useContextData\` \u2192 needs \`context:read\` permission
|
|
3886
4195
|
- \`capabilities.actions.toast\` \u2192 needs \`actions:toast\` permission
|
|
3887
4196
|
- \`capabilities.actions.invoke\` \u2192 needs \`actions:invoke\` permission
|
|
3888
|
-
- \`useExtendIdentity\` \u2192 needs \`extend
|
|
4197
|
+
- \`useExtendIdentity\` or \`capabilities.identity.extend\` \u2192 needs \`identity:extend\` permission AND every custom key returned by the handler / passed to \`extend(patch)\` MUST be declared in \`manifest.identityClaims\` (standard JWT claims \`external_id\` / \`email\` / \`name\` are exempt). Undeclared keys are dropped by the host filter with a \`console.warn\`.
|
|
3889
4198
|
${eventHookBullets}
|
|
3890
4199
|
|
|
3891
4200
|
Report:
|
|
3892
4201
|
- **Missing permissions:** capabilities used in code but not declared in manifest
|
|
3893
4202
|
- **Unused permissions:** permissions declared in manifest but not used in code
|
|
4203
|
+
- **Undeclared identityClaims:** keys returned from \`useExtendIdentity\` or passed to \`capabilities.identity.extend\` that are not in \`manifest.identityClaims\` (excluding the standard-claim exemption list)
|
|
3894
4204
|
|
|
3895
4205
|
## 3. Surface-to-target matching
|
|
3896
4206
|
- Each \`.tsx\` file with a \`<Surface id="...">\` should have a matching target in manifest.json
|
|
@@ -4391,114 +4701,6 @@ var findRelevantSkills = (skills, query) => {
|
|
|
4391
4701
|
});
|
|
4392
4702
|
};
|
|
4393
4703
|
|
|
4394
|
-
// ../../sdk/extension/ai-docs/src/snippets/capabilities.ts
|
|
4395
|
-
var CAPABILITY_SNIPPETS = {
|
|
4396
|
-
"data.query": `
|
|
4397
|
-
const capabilities = useCapabilities()
|
|
4398
|
-
const result = await capabilities.data.query({ path: '/your-endpoint', method: 'GET' })
|
|
4399
|
-
`,
|
|
4400
|
-
"data.fetch": `
|
|
4401
|
-
const capabilities = useCapabilities()
|
|
4402
|
-
const response = await capabilities.data.fetch('https://api.example.com/endpoint')
|
|
4403
|
-
`,
|
|
4404
|
-
"context.read": `
|
|
4405
|
-
// Read host context + extension settings
|
|
4406
|
-
const { customerId, settings } = useContextData()
|
|
4407
|
-
|
|
4408
|
-
// Or use the convenience hook for settings only
|
|
4409
|
-
const settings = useSettings()
|
|
4410
|
-
`,
|
|
4411
|
-
"actions.toast": `
|
|
4412
|
-
const capabilities = useCapabilities()
|
|
4413
|
-
capabilities.actions.toast({ type: 'success', message: 'Done!' })
|
|
4414
|
-
`,
|
|
4415
|
-
"actions.invoke": `
|
|
4416
|
-
const capabilities = useCapabilities()
|
|
4417
|
-
|
|
4418
|
-
// New conversation with tags and fields
|
|
4419
|
-
await capabilities.actions.invoke('newConversation', {
|
|
4420
|
-
tags: ['stackable', 'order-lookup'],
|
|
4421
|
-
fields: [{ id: 'stackable_action', value: 'order_status' }],
|
|
4422
|
-
metadata: { orderId: '12345' },
|
|
4423
|
-
})
|
|
4424
|
-
|
|
4425
|
-
// Standalone: set tags on current/next conversation
|
|
4426
|
-
await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
|
|
4427
|
-
`,
|
|
4428
|
-
"events:identity": HOOK_SNIPPETS["events:identity"],
|
|
4429
|
-
"events:messaging": HOOK_SNIPPETS["events:messaging"],
|
|
4430
|
-
"events:activity": HOOK_SNIPPETS["events:activity"],
|
|
4431
|
-
"extend.identity": HOOK_SNIPPETS["extend.identity"]
|
|
4432
|
-
};
|
|
4433
|
-
var EVENT_SNIPPETS = {
|
|
4434
|
-
"events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
4435
|
-
import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
|
|
4436
|
-
import { useState } from 'react'
|
|
4437
|
-
|
|
4438
|
-
export function Header(): React.ReactElement {
|
|
4439
|
-
const [user, setUser] = useState<string | null>(null)
|
|
4440
|
-
|
|
4441
|
-
useIdentityEvent('login', (event: IdentityEvent) => {
|
|
4442
|
-
setUser(event.data.state.user?.email ?? null)
|
|
4443
|
-
})
|
|
4444
|
-
|
|
4445
|
-
useIdentityEvent('logout', () => {
|
|
4446
|
-
setUser(null)
|
|
4447
|
-
})
|
|
4448
|
-
|
|
4449
|
-
return (
|
|
4450
|
-
<Surface id="slot.header">
|
|
4451
|
-
<ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
|
|
4452
|
-
</Surface>
|
|
4453
|
-
)
|
|
4454
|
-
}`,
|
|
4455
|
-
"events:messaging": `import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
4456
|
-
import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
|
|
4457
|
-
import { useState } from 'react'
|
|
4458
|
-
|
|
4459
|
-
export function Content(): React.ReactElement {
|
|
4460
|
-
const [lastPostback, setLastPostback] = useState<string | null>(null)
|
|
4461
|
-
|
|
4462
|
-
useMessagingEvent('postback', (event) => {
|
|
4463
|
-
setLastPostback(event.data.actionName)
|
|
4464
|
-
})
|
|
4465
|
-
|
|
4466
|
-
return (
|
|
4467
|
-
<Surface id="slot.content">
|
|
4468
|
-
<ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
|
|
4469
|
-
</Surface>
|
|
4470
|
-
)
|
|
4471
|
-
}`,
|
|
4472
|
-
"events:activity": `import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
4473
|
-
import type { ActivityEventHandler } from '@stackable-labs/sdk-extension-contracts'
|
|
4474
|
-
import { useState } from 'react'
|
|
4475
|
-
|
|
4476
|
-
export function Content(): React.ReactElement {
|
|
4477
|
-
const [lastEvent, setLastEvent] = useState<string | null>(null)
|
|
4478
|
-
|
|
4479
|
-
useActivityEvent('page_view', (event) => {
|
|
4480
|
-
setLastEvent(event.data.url as string)
|
|
4481
|
-
})
|
|
4482
|
-
|
|
4483
|
-
return (
|
|
4484
|
-
<Surface id="slot.content">
|
|
4485
|
-
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
|
|
4486
|
-
</Surface>
|
|
4487
|
-
)
|
|
4488
|
-
}`,
|
|
4489
|
-
"extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
|
|
4490
|
-
import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
|
|
4491
|
-
|
|
4492
|
-
// Enrich identity JWT claims before signing.
|
|
4493
|
-
// The host sends base claims (external_id, email, name),
|
|
4494
|
-
// and your handler returns additional claims to merge.
|
|
4495
|
-
// Use ExtendIdentityHandler type with useCallback for memoized handlers.
|
|
4496
|
-
useExtendIdentity((claims) => ({
|
|
4497
|
-
external_id: \`shopify_\${claims.external_id}\`,
|
|
4498
|
-
loyalty_tier: 'gold',
|
|
4499
|
-
}))`
|
|
4500
|
-
};
|
|
4501
|
-
|
|
4502
4704
|
// ../../sdk/extension/ai-docs/src/generated/example-snippets-jsx.ts
|
|
4503
4705
|
var EXAMPLE_SNIPPETS2 = {
|
|
4504
4706
|
"bootstrap": `import { createExtension } from '@stackable-labs/sdk-extension-react'
|
|
@@ -4723,32 +4925,91 @@ export function Content() {
|
|
|
4723
4925
|
</Surface>
|
|
4724
4926
|
)
|
|
4725
4927
|
}`,
|
|
4726
|
-
"extend
|
|
4928
|
+
"identity.extend": `import { useCapabilities, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
4727
4929
|
|
|
4728
|
-
//
|
|
4729
|
-
//
|
|
4730
|
-
//
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4930
|
+
// manifest.json:
|
|
4931
|
+
// {
|
|
4932
|
+
// "permissions": ["identity:extend"],
|
|
4933
|
+
// "identityClaims": ["verified", "verified_by", "verified_at"]
|
|
4934
|
+
// }
|
|
4935
|
+
//
|
|
4936
|
+
// PUSH side \u2014 capabilities.identity.extend(patch) sends new claims to the host
|
|
4937
|
+
// AFTER initial login. The host filters the patch against manifest.identityClaims,
|
|
4938
|
+
// merges into user.metadata, re-signs the JWT, and broadcasts identity:refresh.
|
|
4939
|
+
//
|
|
4940
|
+
// CONSUMER side \u2014 useContextData() already re-renders on every host-pushed
|
|
4941
|
+
// context update (login/logout/refresh/expired). For pure rendering, read the
|
|
4942
|
+
// enriched value directly from ctx.identity.user.metadata \u2014 no event listener
|
|
4943
|
+
// needed. Use useIdentityEvent only when you need a SIDE EFFECT (analytics,
|
|
4944
|
+
// cache invalidation, etc.) on a specific event.
|
|
4945
|
+
//
|
|
4946
|
+
// Standard JWT claims (external_id, email, name) are exempt from declaration.
|
|
4947
|
+
// Undeclared custom keys are dropped client-side with a console.warn.
|
|
4948
|
+
export function Content() {
|
|
4949
|
+
const capabilities = useCapabilities()
|
|
4950
|
+
const ctx = useContextData()
|
|
4951
|
+
|
|
4952
|
+
// Push: trigger after async verification (webhook, polling, user action)
|
|
4953
|
+
const runVerification = async () => {
|
|
4954
|
+
await new Promise(r => setTimeout(r, 1000)) // simulated async work
|
|
4955
|
+
await capabilities.identity.extend({
|
|
4956
|
+
verified: true,
|
|
4957
|
+
verified_by: 'xyzProvider',
|
|
4958
|
+
verified_at: new Date().toISOString(),
|
|
4959
|
+
})
|
|
4960
|
+
}
|
|
4737
4961
|
|
|
4962
|
+
// Consume: read directly from ctx \u2014 re-renders automatically on identity:refresh.
|
|
4963
|
+
const verified = Boolean(ctx.identity?.user?.metadata?.verified)
|
|
4964
|
+
|
|
4965
|
+
return (
|
|
4966
|
+
<Surface id="slot.content">
|
|
4967
|
+
<ui.Stack direction="column" gap="2" className="p-3">
|
|
4968
|
+
<ui.Button onClick={runVerification}>Verify</ui.Button>
|
|
4969
|
+
<ui.Text>Status: {verified ? 'verified' : 'unverified'}</ui.Text>
|
|
4970
|
+
</ui.Stack>
|
|
4971
|
+
</Surface>
|
|
4972
|
+
)
|
|
4973
|
+
}`,
|
|
4974
|
+
"events:identity": `import { Surface, ui, useContextData, useIdentityEvent } from '@stackable-labs/sdk-extension-react'
|
|
4975
|
+
|
|
4976
|
+
// useIdentityEvent \u2014 for SIDE EFFECTS on identity lifecycle events.
|
|
4977
|
+
// If you just want to RENDER based on identity state (e.g. show the current
|
|
4978
|
+
// user's email), use useContextData() \u2014 it re-renders reactively on every
|
|
4979
|
+
// host-pushed identity change. Use useIdentityEvent only when you need to
|
|
4980
|
+
// RUN imperative code on a specific event: analytics beacons, cookie writes,
|
|
4981
|
+
// downstream system notifications, cache invalidation, etc.
|
|
4982
|
+
//
|
|
4983
|
+
// manifest.json:
|
|
4984
|
+
// {
|
|
4985
|
+
// "permissions": ["context:read", "events:identity"],
|
|
4986
|
+
// "events": ["identity:login", "identity:logout"]
|
|
4987
|
+
// }
|
|
4738
4988
|
export function Header() {
|
|
4739
|
-
const
|
|
4989
|
+
const ctx = useContextData()
|
|
4740
4990
|
|
|
4991
|
+
// Side effect: persist a "last login" hint to localStorage on every login.
|
|
4992
|
+
// Replace with your real-world side effect \u2014 analytics beacon, cookie write,
|
|
4993
|
+
// downstream system notification, etc.
|
|
4741
4994
|
useIdentityEvent('login', (event) => {
|
|
4742
|
-
|
|
4995
|
+
localStorage.setItem('last-login', JSON.stringify({
|
|
4996
|
+
userId: event.data.state.user?.id,
|
|
4997
|
+
timestamp: new Date().toISOString(),
|
|
4998
|
+
}))
|
|
4743
4999
|
})
|
|
4744
5000
|
|
|
5001
|
+
// Clear the hint on logout / session expiry.
|
|
4745
5002
|
useIdentityEvent('logout', () => {
|
|
4746
|
-
|
|
5003
|
+
localStorage.removeItem('last-login')
|
|
4747
5004
|
})
|
|
4748
5005
|
|
|
5006
|
+
// Rendering: read directly from ctx \u2014 DON'T mirror identity into local
|
|
5007
|
+
// useState via the event listener; useContextData is already reactive.
|
|
5008
|
+
const email = ctx?.identity?.user?.email ?? null
|
|
5009
|
+
|
|
4749
5010
|
return (
|
|
4750
5011
|
<Surface id="slot.header">
|
|
4751
|
-
<ui.Text className="text-xs">{
|
|
5012
|
+
<ui.Text className="text-xs">{email ?? 'Not logged in'}</ui.Text>
|
|
4752
5013
|
</Surface>
|
|
4753
5014
|
)
|
|
4754
5015
|
}`,
|
|
@@ -4812,11 +5073,19 @@ await capabilities.actions.invoke('newConversation', {
|
|
|
4812
5073
|
await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])`,
|
|
4813
5074
|
"events:identity": `import { useIdentityEvent } from '@stackable-labs/sdk-extension-react'
|
|
4814
5075
|
|
|
5076
|
+
// manifest events: ["identity:login", "identity:logout", "identity:refresh"]
|
|
4815
5077
|
useIdentityEvent('login', (event) => {
|
|
4816
|
-
|
|
5078
|
+
// event.data.state.user.metadata is populated with any enrichment from sibling
|
|
5079
|
+
// extensions with identity:extend (declared in their manifest.identityClaims)
|
|
5080
|
+
console.log('User logged in:', event.data.state.user?.email, event.data.state.user?.metadata)
|
|
4817
5081
|
})
|
|
4818
5082
|
useIdentityEvent('logout', () => {
|
|
4819
5083
|
console.log('User logged out')
|
|
5084
|
+
})
|
|
5085
|
+
// identity:refresh fires after any extension calls capabilities.identity.extend({...}).
|
|
5086
|
+
// Listen here to react to post-login enrichment (verification, tier upgrades, etc.).
|
|
5087
|
+
useIdentityEvent('refresh', (event) => {
|
|
5088
|
+
console.log('Identity refreshed \u2014 metadata:', event.data.state.user?.metadata)
|
|
4820
5089
|
})`,
|
|
4821
5090
|
"events:messaging": `import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
|
|
4822
5091
|
|
|
@@ -4828,31 +5097,77 @@ useMessagingEvent('postback:Buy Now', (event) => {
|
|
|
4828
5097
|
useActivityEvent('product_view', (event) => {
|
|
4829
5098
|
console.log('Activity:', event.eventName, event.data)
|
|
4830
5099
|
})`,
|
|
4831
|
-
"extend
|
|
5100
|
+
"identity.extend": `// Enrich identity JWT claims before signing.
|
|
5101
|
+
// The host sends base claims (external_id, email, name); your handler returns
|
|
5102
|
+
// additional claims to merge. Returned keys are filtered against manifest.identityClaims:
|
|
5103
|
+
// - Standard JWT claims (external_id, email, name) are exempt \u2014 always allowed
|
|
5104
|
+
// - Custom claims must be declared in manifest.identityClaims
|
|
5105
|
+
// - Undeclared keys are dropped with a console.warn
|
|
5106
|
+
// Merged claims land in BOTH identityState.user.metadata (for sibling extensions)
|
|
5107
|
+
// AND the signed JWT's custom_claims (for downstream JWT consumers).
|
|
5108
|
+
// Pattern: name the handler with useCallback<ExtendIdentityHandler> for stable
|
|
5109
|
+
// reference + clean separation; pass the named const to useExtendIdentity.
|
|
5110
|
+
//
|
|
5111
|
+
// useExtendIdentity fires ONCE at initial login \u2014 return what's known synchronously.
|
|
5112
|
+
// For post-login async updates (e.g., after verification completes via a webhook
|
|
5113
|
+
// or polling), use capabilities.identity.extend(patch) \u2014 see the 'identity.extend'
|
|
5114
|
+
// capability for the imperative path.
|
|
4832
5115
|
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
5116
|
+
import { useCallback } from 'react'
|
|
5117
|
+
import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
|
|
5118
|
+
|
|
5119
|
+
// manifest.json:
|
|
5120
|
+
// {
|
|
5121
|
+
// "permissions": ["identity:extend"],
|
|
5122
|
+
// "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
|
|
5123
|
+
// }
|
|
5124
|
+
const handleExtend = useCallback((claims) => ({
|
|
5125
|
+
external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
|
|
5126
|
+
loyalty_tier: 'bronze', // custom \u2014 sync, known at login
|
|
5127
|
+
verified: false, // custom \u2014 default; updated async post-verification
|
|
5128
|
+
}), [])
|
|
5129
|
+
useExtendIdentity(handleExtend)
|
|
5130
|
+
|
|
5131
|
+
// \u2500\u2500 For post-login async push (e.g., after async verification completes): \u2500\u2500\u2500\u2500\u2500
|
|
5132
|
+
const capabilities = useCapabilities()
|
|
5133
|
+
await capabilities.identity.extend({ verified: true })
|
|
5134
|
+
|
|
5135
|
+
// Consumer side \u2014 same or sibling extension reacts via identity:refresh
|
|
5136
|
+
useIdentityEvent('refresh', (event) => {
|
|
5137
|
+
console.log('verified =>', event.data.state.user?.metadata?.verified)
|
|
5138
|
+
})`
|
|
4837
5139
|
};
|
|
4838
5140
|
var EVENT_SNIPPETS2 = {
|
|
4839
5141
|
"events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
|
|
4840
5142
|
import { useState } from 'react'
|
|
4841
5143
|
|
|
5144
|
+
// manifest events: ["identity:login", "identity:logout", "identity:refresh"]
|
|
5145
|
+
//
|
|
5146
|
+
// 'login' and 'refresh' both deliver the full IdentityState, including
|
|
5147
|
+
// state.user.metadata \u2014 populated by sibling extensions with identity:extend
|
|
5148
|
+
// (declared in their manifest.identityClaims). Subscribe to BOTH to cover the
|
|
5149
|
+
// initial login AND any post-login push via capabilities.identity.extend().
|
|
4842
5150
|
export function Header() {
|
|
4843
5151
|
const [user, setUser] = useState(null)
|
|
5152
|
+
const [verified, setVerified] = useState(false)
|
|
4844
5153
|
|
|
4845
5154
|
useIdentityEvent('login', (event) => {
|
|
4846
5155
|
setUser(event.data.state.user?.email ?? null)
|
|
5156
|
+
setVerified(Boolean(event.data.state.user?.metadata?.verified))
|
|
5157
|
+
})
|
|
5158
|
+
|
|
5159
|
+
useIdentityEvent('refresh', (event) => {
|
|
5160
|
+
setVerified(Boolean(event.data.state.user?.metadata?.verified))
|
|
4847
5161
|
})
|
|
4848
5162
|
|
|
4849
5163
|
useIdentityEvent('logout', () => {
|
|
4850
5164
|
setUser(null)
|
|
5165
|
+
setVerified(false)
|
|
4851
5166
|
})
|
|
4852
5167
|
|
|
4853
5168
|
return (
|
|
4854
5169
|
<Surface id="slot.header">
|
|
4855
|
-
<ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
|
|
5170
|
+
<ui.Text className="text-xs">{user ?? 'Not logged in'} {verified && '\u2713'}</ui.Text>
|
|
4856
5171
|
</Surface>
|
|
4857
5172
|
)
|
|
4858
5173
|
}`,
|
|
@@ -4888,16 +5203,36 @@ export function Content() {
|
|
|
4888
5203
|
</Surface>
|
|
4889
5204
|
)
|
|
4890
5205
|
}`,
|
|
4891
|
-
"extend
|
|
5206
|
+
"identity.extend": `// Enrich identity JWT claims before signing.
|
|
5207
|
+
// The host sends base claims (external_id, email, name); your handler returns
|
|
5208
|
+
// additional claims to merge. Returned keys are filtered against manifest.identityClaims:
|
|
5209
|
+
// - Standard JWT claims (external_id, email, name) are exempt \u2014 always allowed
|
|
5210
|
+
// - Custom claims must be declared in manifest.identityClaims
|
|
5211
|
+
// - Undeclared keys are dropped with a console.warn
|
|
5212
|
+
// Merged claims land in BOTH identityState.user.metadata (for sibling extensions)
|
|
5213
|
+
// AND the signed JWT's custom_claims (for downstream JWT consumers).
|
|
5214
|
+
// Pattern: name the handler with useCallback<ExtendIdentityHandler> for stable
|
|
5215
|
+
// reference + clean separation; pass the named const to useExtendIdentity.
|
|
5216
|
+
//
|
|
5217
|
+
// useExtendIdentity fires ONCE at initial login \u2014 return what's known synchronously.
|
|
5218
|
+
// For post-login async updates (e.g., after verification completes via a webhook
|
|
5219
|
+
// or polling), use capabilities.identity.extend(patch) \u2014 see the 'identity.extend'
|
|
5220
|
+
// capability for the imperative path.
|
|
4892
5221
|
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
//
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
}
|
|
5222
|
+
import { useCallback } from 'react'
|
|
5223
|
+
import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
|
|
5224
|
+
|
|
5225
|
+
// manifest.json:
|
|
5226
|
+
// {
|
|
5227
|
+
// "permissions": ["identity:extend"],
|
|
5228
|
+
// "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
|
|
5229
|
+
// }
|
|
5230
|
+
const handleExtend = useCallback((claims) => ({
|
|
5231
|
+
external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
|
|
5232
|
+
loyalty_tier: 'bronze', // custom \u2014 sync, known at login
|
|
5233
|
+
verified: false, // custom \u2014 default; updated async post-verification
|
|
5234
|
+
}), [])
|
|
5235
|
+
useExtendIdentity(handleExtend)`
|
|
4901
5236
|
};
|
|
4902
5237
|
|
|
4903
5238
|
// ../../sdk/extension/ai-docs/src/index.ts
|