@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.
Files changed (3) hide show
  1. package/dist/index.js +549 -214
  2. package/dist/server.js +549 -214
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -465,11 +465,19 @@ ${iconList}
465
465
  var HOOK_SNIPPETS = {
466
466
  "events:identity": `import { useIdentityEvent } from '@stackable-labs/sdk-extension-react'
467
467
 
468
+ // manifest events: ["identity:login", "identity:logout", "identity:refresh"]
468
469
  useIdentityEvent('login', (event) => {
469
- console.log('User logged in:', event.data.state.user?.email)
470
+ // event.data.state.user.metadata is populated with any enrichment from sibling
471
+ // extensions with identity:extend (declared in their manifest.identityClaims)
472
+ console.log('User logged in:', event.data.state.user?.email, event.data.state.user?.metadata)
470
473
  })
471
474
  useIdentityEvent('logout', () => {
472
475
  console.log('User logged out')
476
+ })
477
+ // identity:refresh fires after any extension calls capabilities.identity.extend({...}).
478
+ // Listen here to react to post-login enrichment (verification, tier upgrades, etc.).
479
+ useIdentityEvent('refresh', (event) => {
480
+ console.log('Identity refreshed \u2014 metadata:', event.data.state.user?.metadata)
473
481
  })`,
474
482
  "events:messaging": `import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
475
483
 
@@ -481,11 +489,23 @@ useMessagingEvent('postback:Buy Now', (event) => {
481
489
  useActivityEvent('product_view', (event) => {
482
490
  console.log('Activity:', event.eventName, event.data)
483
491
  })`,
484
- "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
485
-
492
+ "identity.extend": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
493
+
494
+ // manifest.json:
495
+ // {
496
+ // "permissions": ["identity:extend"],
497
+ // "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
498
+ // }
499
+ // Standard JWT claims (external_id, email, name) are exempt from declaration.
500
+ // Custom claims must be in manifest.identityClaims or they're dropped with a warn.
501
+ //
502
+ // Fires ONCE at initial login \u2014 return what's known synchronously. For post-login
503
+ // async updates (e.g., after verification completes via a webhook or polling),
504
+ // use capabilities.identity.extend(patch) \u2014 see the 'identity.extend' capability.
486
505
  useExtendIdentity((claims) => ({
487
- external_id: \`custom_\${claims.external_id}\`,
488
- loyalty_tier: 'gold',
506
+ external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
507
+ loyalty_tier: 'bronze', // custom \u2014 sync, known at login
508
+ verified: false, // custom \u2014 default; updated async post-verification
489
509
  }))`
490
510
  };
491
511
  var HOOK_SNIPPETS_MEMOIZED = {
@@ -497,13 +517,19 @@ const handlePostback = useCallback<MessagingEventHandler>((event) => {
497
517
  console.log('Postback:', event.data.actionName, event.data.conversationId)
498
518
  }, [])
499
519
  useMessagingEvent('postback:Buy Now', handlePostback)`,
500
- "extend.identity": `import { useCallback } from 'react'
520
+ "identity.extend": `import { useCallback } from 'react'
501
521
  import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
502
522
  import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
503
523
 
524
+ // manifest.json:
525
+ // {
526
+ // "permissions": ["identity:extend"],
527
+ // "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
528
+ // }
504
529
  const handleExtend = useCallback<ExtendIdentityHandler>((claims) => ({
505
- external_id: \`custom_\${claims.external_id}\`,
506
- loyalty_tier: 'gold',
530
+ external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
531
+ loyalty_tier: 'bronze', // custom \u2014 sync, known at login
532
+ verified: false, // custom \u2014 default; updated async post-verification
507
533
  }), [])
508
534
  useExtendIdentity(handleExtend)`
509
535
  };
@@ -515,7 +541,7 @@ var generateCapabilities = () => {
515
541
  const fm = frontmatter({
516
542
  root: false,
517
543
  targets: ["*"],
518
- description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, extend:identity, events:identity, events:messaging, events:activity",
544
+ description: "Extension capabilities: data.query, data.fetch, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity",
519
545
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts"]
520
546
  });
521
547
  return `${fm}
@@ -722,22 +748,89 @@ ${HOOK_SNIPPETS["events:activity"]}
722
748
 
723
749
  **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.
724
750
 
725
- ## extend:identity \u2014 Identity Claim Enrichment
726
- Enrich identity JWT claims before signing. The framework sends base claims to your extension, and you return additional claims to merge into the token.
727
- - **Permission required:** \`extend:identity\`
728
- - **Hook:** \`useExtendIdentity(handler)\` \u2014 \`ExtendIdentityHandler\` type exported for use with \`useCallback\`
729
- - **Handler signature:** \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
730
- - **IdentityBaseClaims:** \`{ external_id: string, email?: string, name?: string, [key: string]: unknown }\`
751
+ ## identity.extend \u2014 Identity Claim Enrichment
752
+ 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:
753
+
754
+ 1. **\`useExtendIdentity(handler)\`** \u2014 synchronous hook that fires ONCE at initial login.
755
+ 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\`.
756
+
757
+ Both share the **\`identity:extend\`** permission and the **\`manifest.identityClaims\`** declaration gate.
758
+
759
+ ### Manifest contract
731
760
 
732
761
  \`\`\`json
733
762
  {
734
- "permissions": ["extend:identity"]
763
+ "permissions": ["identity:extend"],
764
+ "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
735
765
  }
736
766
  \`\`\`
737
767
 
768
+ - **Standard JWT claims (\`external_id\`, \`email\`, \`name\`) are exempt** \u2014 they're part of the signing contract and may be overridden without declaration.
769
+ - **Custom keys MUST be declared in \`identityClaims\`** or the host filter drops them with a \`console.warn\`.
770
+ - **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.
771
+ - **Key format:** \`/^[a-z_][a-z0-9_]{0,63}$/\` (lowercase identifier, \u226464 chars).
772
+ - **Maximum 20 entries.**
773
+
774
+ ### Initial-login enrichment (handler-style)
775
+
776
+ - **Hook:** \`useExtendIdentity(handler)\` \u2014 \`ExtendIdentityHandler\` type exported for use with \`useCallback\`
777
+ - **Handler signature:** \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
778
+ - **IdentityBaseClaims:** \`{ external_id: string, email?: string, name?: string, [key: string]: unknown }\`
779
+
738
780
  \`\`\`tsx
739
- ${HOOK_SNIPPETS["extend.identity"]}
781
+ ${HOOK_SNIPPETS["identity.extend"]}
740
782
  \`\`\`
783
+
784
+ ### Async post-login push (imperative)
785
+
786
+ \`\`\`tsx
787
+ const capabilities = useCapabilities()
788
+
789
+ // Anytime after login \u2014 webhook callback, user action, async verification, etc.
790
+ await capabilities.identity.extend({
791
+ verified: true,
792
+ verified_by: 'xyzProvider',
793
+ verified_at: new Date().toISOString(),
794
+ })
795
+ // \u2192 host filters against manifest.identityClaims
796
+ // \u2192 user.metadata updated
797
+ // \u2192 JWT re-signed, pushed to Zendesk loginUser
798
+ // \u2192 identity:refresh broadcast to all extensions with events:identity
799
+ \`\`\`
800
+
801
+ Per-extension calls are serialized to prevent concurrent Zendesk \`loginUser\` callbacks from orphaning each other (empirically verified in the loginUser re-auth spike).
802
+
803
+ ### Consuming enriched state from another extension
804
+
805
+ 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:
806
+
807
+ \`\`\`tsx
808
+ ${HOOK_SNIPPETS["events:identity"]}
809
+ \`\`\`
810
+
811
+ For a snapshot read instead of event-driven reaction (auto re-renders when context changes):
812
+
813
+ \`\`\`tsx
814
+ const { identity } = useContextData()
815
+ const verified = Boolean(identity?.user?.metadata?.verified)
816
+ \`\`\`
817
+
818
+ ### Install-time enforcement
819
+
820
+ 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\`).
821
+
822
+ ### Bundle-scan findings at upload
823
+
824
+ The publisher-side bundle scan validates your declaration at submission time:
825
+
826
+ | Finding | Severity | Triggers when |
827
+ | --- | --- | --- |
828
+ | \`identityClaims_missing\` | warning | \`identity:extend\` declared, \`identityClaims\` empty (custom claims would be dropped) |
829
+ | \`identityClaims_no_permission\` | warning | \`identityClaims\` declared, \`identity:extend\` permission missing |
830
+ | \`identityClaims_invalid_key\` | error | Key fails the format regex |
831
+ | \`identityClaims_reserved_key\` | error | Collides with a reserved JWT / Zendesk claim |
832
+ | \`identityClaims_standard_key\` | warning | Redundant \u2014 standard claims (\`external_id\`, \`email\`, \`name\`) are exempt |
833
+ | \`identityClaims_too_many\` | error | More than 20 entries |
741
834
  `;
742
835
  };
743
836
 
@@ -975,35 +1068,94 @@ export function Content(): React.ReactElement {
975
1068
  </Surface>
976
1069
  )
977
1070
  }`,
978
- "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
979
- import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
1071
+ "identity.extend": `import { useCapabilities, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
1072
+ import type { ContextData } from '@stackable-labs/sdk-extension-contracts'
980
1073
 
981
- // Enrich identity JWT claims before signing.
982
- // The host sends base claims (external_id, email, name),
983
- // and your handler returns additional claims to merge.
984
- useExtendIdentity((claims) => ({
985
- external_id: \`shopify_\${claims.external_id}\`,
986
- loyalty_tier: 'gold',
987
- }))`,
1074
+ // manifest.json:
1075
+ // {
1076
+ // "permissions": ["identity:extend"],
1077
+ // "identityClaims": ["verified", "verified_by", "verified_at"]
1078
+ // }
1079
+ //
1080
+ // PUSH side \u2014 capabilities.identity.extend(patch) sends new claims to the host
1081
+ // AFTER initial login. The host filters the patch against manifest.identityClaims,
1082
+ // merges into user.metadata, re-signs the JWT, and broadcasts identity:refresh.
1083
+ //
1084
+ // CONSUMER side \u2014 useContextData() already re-renders on every host-pushed
1085
+ // context update (login/logout/refresh/expired). For pure rendering, read the
1086
+ // enriched value directly from ctx.identity.user.metadata \u2014 no event listener
1087
+ // needed. Use useIdentityEvent only when you need a SIDE EFFECT (analytics,
1088
+ // cache invalidation, etc.) on a specific event.
1089
+ //
1090
+ // Standard JWT claims (external_id, email, name) are exempt from declaration.
1091
+ // Undeclared custom keys are dropped client-side with a console.warn.
1092
+ export function Content(): React.ReactElement {
1093
+ const capabilities = useCapabilities()
1094
+ const ctx = useContextData() as ContextData & { loading: boolean }
1095
+
1096
+ // Push: trigger after async verification (webhook, polling, user action)
1097
+ const runVerification = async () => {
1098
+ await new Promise(r => setTimeout(r, 1000)) // simulated async work
1099
+ await capabilities.identity.extend({
1100
+ verified: true,
1101
+ verified_by: 'xyzProvider',
1102
+ verified_at: new Date().toISOString(),
1103
+ })
1104
+ }
1105
+
1106
+ // Consume: read directly from ctx \u2014 re-renders automatically on identity:refresh.
1107
+ const verified = Boolean(ctx.identity?.user?.metadata?.verified)
1108
+
1109
+ return (
1110
+ <Surface id="slot.content">
1111
+ <ui.Stack direction="column" gap="2" className="p-3">
1112
+ <ui.Button onClick={runVerification}>Verify</ui.Button>
1113
+ <ui.Text>Status: {verified ? 'verified' : 'unverified'}</ui.Text>
1114
+ </ui.Stack>
1115
+ </Surface>
1116
+ )
1117
+ }`,
988
1118
  // ── Per-event example snippets ─────────────────────────────────────────────
989
- "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
1119
+ "events:identity": `import { Surface, ui, useContextData, useIdentityEvent } from '@stackable-labs/sdk-extension-react'
990
1120
  import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
991
- import { useState } from 'react'
992
1121
 
1122
+ // useIdentityEvent \u2014 for SIDE EFFECTS on identity lifecycle events.
1123
+ // If you just want to RENDER based on identity state (e.g. show the current
1124
+ // user's email), use useContextData() \u2014 it re-renders reactively on every
1125
+ // host-pushed identity change. Use useIdentityEvent only when you need to
1126
+ // RUN imperative code on a specific event: analytics beacons, cookie writes,
1127
+ // downstream system notifications, cache invalidation, etc.
1128
+ //
1129
+ // manifest.json:
1130
+ // {
1131
+ // "permissions": ["context:read", "events:identity"],
1132
+ // "events": ["identity:login", "identity:logout"]
1133
+ // }
993
1134
  export function Header(): React.ReactElement {
994
- const [user, setUser] = useState<string | null>(null)
1135
+ const ctx = useContextData()
995
1136
 
1137
+ // Side effect: persist a "last login" hint to localStorage on every login.
1138
+ // Replace with your real-world side effect \u2014 analytics beacon, cookie write,
1139
+ // downstream system notification, etc.
996
1140
  useIdentityEvent('login', (event: IdentityEvent) => {
997
- setUser(event.data.state.user?.email ?? null)
1141
+ localStorage.setItem('last-login', JSON.stringify({
1142
+ userId: event.data.state.user?.id,
1143
+ timestamp: new Date().toISOString(),
1144
+ }))
998
1145
  })
999
1146
 
1147
+ // Clear the hint on logout / session expiry.
1000
1148
  useIdentityEvent('logout', () => {
1001
- setUser(null)
1149
+ localStorage.removeItem('last-login')
1002
1150
  })
1003
1151
 
1152
+ // Rendering: read directly from ctx \u2014 DON'T mirror identity into local
1153
+ // useState via the event listener; useContextData is already reactive.
1154
+ const email = ctx?.identity?.user?.email ?? null
1155
+
1004
1156
  return (
1005
1157
  <Surface id="slot.header">
1006
- <ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
1158
+ <ui.Text className="text-xs">{email ?? 'Not logged in'}</ui.Text>
1007
1159
  </Surface>
1008
1160
  )
1009
1161
  }`,
@@ -1082,7 +1234,7 @@ const capabilities = useCapabilities()
1082
1234
  // capabilities.data.fetch(url, init?)
1083
1235
  // capabilities.actions.toast(payload)
1084
1236
  // capabilities.actions.invoke(action, payload?) \u2014 actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
1085
- // capabilities.extend.identity(payload) \u2014 enrich identity claims (prefer useExtendIdentity hook)
1237
+ // 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.
1086
1238
  \`\`\`
1087
1239
 
1088
1240
  ## useStore(store, selector?)
@@ -1180,17 +1332,17 @@ useEvent('activity', (event) => {
1180
1332
  \`\`\`
1181
1333
 
1182
1334
  ## useExtendIdentity(handler)
1183
- Register a handler to enrich identity JWT claims before signing. Requires \`extend:identity\` permission.
1335
+ Register a handler to enrich identity JWT claims before signing. Requires \`identity:extend\` permission.
1184
1336
  - \`handler: ExtendIdentityHandler\` \u2014 \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
1185
1337
  - \`IdentityBaseClaims: { external_id: string, email?: string, name?: string, [key: string]: unknown }\`
1186
1338
 
1187
1339
  \`\`\`tsx
1188
- ${stripImports(HOOK_SNIPPETS["extend.identity"])}
1340
+ ${stripImports(HOOK_SNIPPETS["identity.extend"])}
1189
1341
  \`\`\`
1190
1342
 
1191
1343
  With \`useCallback\` (for memoized handlers):
1192
1344
  \`\`\`tsx
1193
- ${HOOK_SNIPPETS_MEMOIZED["extend.identity"]}
1345
+ ${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}
1194
1346
  \`\`\`
1195
1347
 
1196
1348
  ## Identity via context.read()
@@ -2506,7 +2658,7 @@ filtered out. Clicking a surface adds it to your manifest and inserts a
2506
2658
  ### Capabilities
2507
2659
 
2508
2660
  The SDK capabilities your extension can use: \`data.query\`, \`data.fetch\`,
2509
- \`context.read\`, \`actions.toast\`, \`actions.invoke\`, \`extend.identity\`,
2661
+ \`context.read\`, \`actions.toast\`, \`actions.invoke\`, \`identity.extend\`,
2510
2662
  \`events:identity\`, \`events:messaging\`, and \`events:activity\`. Clicking a
2511
2663
  capability adds the permission to your manifest and AI-inserts the hook usage.
2512
2664
 
@@ -3554,30 +3706,13 @@ var generateCookbookEvents = () => {
3554
3706
  });
3555
3707
  return `${fm}
3556
3708
 
3557
- # Events & Extensions
3558
-
3559
- Subscribe to real-time events pushed from the host via the framework, and extend identity claims.
3560
- Each event type has a dedicated hook \u2014 never use \`capabilities.events.*\` directly.
3561
-
3562
- ## Identity Events
3563
-
3564
- Subscribe to login, logout, refresh, and expired events. Useful for tracking
3565
- agent authentication state in your extension.
3566
-
3567
- **Permission:** \`events:identity\`
3568
- **Event types:** ${identityEventTypes2}
3569
-
3570
- ### Hook usage
3571
-
3572
- \`\`\`tsx
3573
- ${HOOK_SNIPPETS["events:identity"]}
3574
- \`\`\`
3575
-
3576
- ### Full component example
3709
+ # Events & Identity
3577
3710
 
3578
- \`\`\`tsx
3579
- ${EXAMPLE_SNIPPETS["events:identity"]}
3580
- \`\`\`
3711
+ Subscribe to real-time events pushed from the host via the framework, and enrich identity
3712
+ claims. Each event type has a dedicated hook (\`useIdentityEvent\` / \`useMessagingEvent\` /
3713
+ \`useActivityEvent\`) \u2014 never use \`capabilities.events.*\` directly. Identity enrichment
3714
+ supports both a login-time hook (\`useExtendIdentity\`) and an imperative post-login API
3715
+ (\`capabilities.identity.extend\`).
3581
3716
 
3582
3717
  ## Messaging Events
3583
3718
 
@@ -3619,24 +3754,54 @@ ${HOOK_SNIPPETS["events:activity"]}
3619
3754
  ${EXAMPLE_SNIPPETS["events:activity"]}
3620
3755
  \`\`\`
3621
3756
 
3622
- ## Extend Identity
3757
+ ## Identity Events
3623
3758
 
3624
- Enrich identity JWT claims before signing. The host sends base claims
3625
- (\`external_id\`, \`email\`, \`name\`) and your handler returns additional
3626
- claims to merge into the token.
3759
+ Subscribe to login, logout, refresh, and expired events. Useful for tracking
3760
+ agent authentication state and reacting to identity enrichment pushed from sibling
3761
+ extensions (via \`capabilities.identity.extend\` \u2014 see *Extend Identity* below).
3627
3762
 
3628
- **Permission:** \`extend:identity\`
3763
+ **Permission:** \`events:identity\`
3764
+ **Event types:** ${identityEventTypes2}
3629
3765
 
3630
3766
  ### Hook usage
3631
3767
 
3632
3768
  \`\`\`tsx
3633
- ${HOOK_SNIPPETS["extend.identity"]}
3769
+ ${HOOK_SNIPPETS["events:identity"]}
3634
3770
  \`\`\`
3635
3771
 
3636
3772
  ### Full component example
3637
3773
 
3638
3774
  \`\`\`tsx
3639
- ${EXAMPLE_SNIPPETS["extend.identity"]}
3775
+ ${EXAMPLE_SNIPPETS["events:identity"]}
3776
+ \`\`\`
3777
+
3778
+ ## Extend Identity
3779
+
3780
+ Enrich identity JWT claims and \`user.metadata\` so the signed token AND any sibling
3781
+ extension with \`events:identity\` can react. Two complementary paths:
3782
+
3783
+ - **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires ONCE at initial login. Use
3784
+ for known-at-login enrichment.
3785
+ - **\`capabilities.identity.extend(patch)\`** \u2014 imperative call, fires post-login (after
3786
+ async verification, webhook callbacks, user-triggered flows). Re-signs the JWT and
3787
+ broadcasts \`identity:refresh\` to all extensions with \`events:identity\`.
3788
+
3789
+ Both paths share the **\`identity:extend\`** permission and the **\`manifest.identityClaims\`**
3790
+ declaration gate. Standard JWT claims (\`external_id\`, \`email\`, \`name\`) are exempt;
3791
+ custom keys must be declared or they're dropped by the host filter with a \`console.warn\`.
3792
+
3793
+ **Permission:** \`identity:extend\`
3794
+
3795
+ ### Login-time hook (useExtendIdentity)
3796
+
3797
+ \`\`\`tsx
3798
+ ${HOOK_SNIPPETS["identity.extend"]}
3799
+ \`\`\`
3800
+
3801
+ ### Imperative post-login (capabilities.identity.extend)
3802
+
3803
+ \`\`\`tsx
3804
+ ${EXAMPLE_SNIPPETS["identity.extend"]}
3640
3805
  \`\`\`
3641
3806
  `;
3642
3807
  };
@@ -3700,10 +3865,148 @@ Only add permissions that aren't already declared.
3700
3865
  `;
3701
3866
  };
3702
3867
 
3868
+ // ../../sdk/extension/ai-docs/src/snippets/capabilities.ts
3869
+ var EXTEND_IDENTITY_HOOK_EXAMPLE = `// Enrich identity JWT claims before signing.
3870
+ // The host sends base claims (external_id, email, name); your handler returns
3871
+ // additional claims to merge. Returned keys are filtered against manifest.identityClaims:
3872
+ // - Standard JWT claims (external_id, email, name) are exempt \u2014 always allowed
3873
+ // - Custom claims must be declared in manifest.identityClaims
3874
+ // - Undeclared keys are dropped with a console.warn
3875
+ // Merged claims land in BOTH identityState.user.metadata (for sibling extensions)
3876
+ // AND the signed JWT's custom_claims (for downstream JWT consumers).
3877
+ // Pattern: name the handler with useCallback<ExtendIdentityHandler> for stable
3878
+ // reference + clean separation; pass the named const to useExtendIdentity.
3879
+ //
3880
+ // useExtendIdentity fires ONCE at initial login \u2014 return what's known synchronously.
3881
+ // For post-login async updates (e.g., after verification completes via a webhook
3882
+ // or polling), use capabilities.identity.extend(patch) \u2014 see the 'identity.extend'
3883
+ // capability for the imperative path.
3884
+
3885
+ ${HOOK_SNIPPETS_MEMOIZED["identity.extend"]}`;
3886
+ var CAPABILITY_SNIPPETS = {
3887
+ "data.query": `
3888
+ const capabilities = useCapabilities()
3889
+ const result = await capabilities.data.query({ path: '/your-endpoint', method: 'GET' })
3890
+ `,
3891
+ "data.fetch": `
3892
+ const capabilities = useCapabilities()
3893
+ const response = await capabilities.data.fetch('https://api.example.com/endpoint')
3894
+ `,
3895
+ "context.read": `
3896
+ // Read host context + extension settings
3897
+ const { customerId, settings } = useContextData()
3898
+
3899
+ // Or use the convenience hook for settings only
3900
+ const settings = useSettings()
3901
+ `,
3902
+ "actions.toast": `
3903
+ const capabilities = useCapabilities()
3904
+ capabilities.actions.toast({ type: 'success', message: 'Done!' })
3905
+ `,
3906
+ "actions.invoke": `
3907
+ const capabilities = useCapabilities()
3908
+
3909
+ // New conversation with tags and fields
3910
+ await capabilities.actions.invoke('newConversation', {
3911
+ tags: ['stackable', 'order-lookup'],
3912
+ fields: [{ id: 'stackable_action', value: 'order_status' }],
3913
+ metadata: { orderId: '12345' },
3914
+ })
3915
+
3916
+ // Standalone: set tags on current/next conversation
3917
+ await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
3918
+ `,
3919
+ "events:identity": HOOK_SNIPPETS["events:identity"],
3920
+ "events:messaging": HOOK_SNIPPETS["events:messaging"],
3921
+ "events:activity": HOOK_SNIPPETS["events:activity"],
3922
+ "identity.extend": `${EXTEND_IDENTITY_HOOK_EXAMPLE}
3923
+
3924
+ // \u2500\u2500 For post-login async push (e.g., after async verification completes): \u2500\u2500\u2500\u2500\u2500
3925
+ const capabilities = useCapabilities()
3926
+ await capabilities.identity.extend({ verified: true })
3927
+
3928
+ // Consumer side \u2014 same or sibling extension reacts via identity:refresh
3929
+ useIdentityEvent('refresh', (event) => {
3930
+ console.log('verified =>', event.data.state.user?.metadata?.verified)
3931
+ })
3932
+ `
3933
+ };
3934
+ var EVENT_SNIPPETS = {
3935
+ "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3936
+ import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
3937
+ import { useState } from 'react'
3938
+
3939
+ // manifest events: ["identity:login", "identity:logout", "identity:refresh"]
3940
+ //
3941
+ // 'login' and 'refresh' both deliver the full IdentityState, including
3942
+ // state.user.metadata \u2014 populated by sibling extensions with identity:extend
3943
+ // (declared in their manifest.identityClaims). Subscribe to BOTH to cover the
3944
+ // initial login AND any post-login push via capabilities.identity.extend().
3945
+ export function Header(): React.ReactElement {
3946
+ const [user, setUser] = useState<string | null>(null)
3947
+ const [verified, setVerified] = useState(false)
3948
+
3949
+ useIdentityEvent('login', (event: IdentityEvent) => {
3950
+ setUser(event.data.state.user?.email ?? null)
3951
+ setVerified(Boolean(event.data.state.user?.metadata?.verified))
3952
+ })
3953
+
3954
+ useIdentityEvent('refresh', (event: IdentityEvent) => {
3955
+ setVerified(Boolean(event.data.state.user?.metadata?.verified))
3956
+ })
3957
+
3958
+ useIdentityEvent('logout', () => {
3959
+ setUser(null)
3960
+ setVerified(false)
3961
+ })
3962
+
3963
+ return (
3964
+ <Surface id="slot.header">
3965
+ <ui.Text className="text-xs">{user ?? 'Not logged in'} {verified && '\u2713'}</ui.Text>
3966
+ </Surface>
3967
+ )
3968
+ }`,
3969
+ "events:messaging": `import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3970
+ import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
3971
+ import { useState } from 'react'
3972
+
3973
+ export function Content(): React.ReactElement {
3974
+ const [lastPostback, setLastPostback] = useState<string | null>(null)
3975
+
3976
+ useMessagingEvent('postback', (event) => {
3977
+ setLastPostback(event.data.actionName)
3978
+ })
3979
+
3980
+ return (
3981
+ <Surface id="slot.content">
3982
+ <ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
3983
+ </Surface>
3984
+ )
3985
+ }`,
3986
+ "events:activity": `import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
3987
+ import type { ActivityEventHandler } from '@stackable-labs/sdk-extension-contracts'
3988
+ import { useState } from 'react'
3989
+
3990
+ export function Content(): React.ReactElement {
3991
+ const [lastEvent, setLastEvent] = useState<string | null>(null)
3992
+
3993
+ useActivityEvent('page_view', (event) => {
3994
+ setLastEvent(event.data.url as string)
3995
+ })
3996
+
3997
+ return (
3998
+ <Surface id="slot.content">
3999
+ <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
4000
+ </Surface>
4001
+ )
4002
+ }`,
4003
+ "identity.extend": EXTEND_IDENTITY_HOOK_EXAMPLE
4004
+ };
4005
+
3703
4006
  // ../../sdk/extension/ai-docs/src/commands/add-capability.ts
3704
4007
  var generateAddCapabilityCommand = () => {
3705
4008
  const fm = frontmatter({
3706
- description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, extend:identity, events:identity, events:messaging, events:activity) in this extension",
4009
+ description: "Wire up a new capability (data.fetch, data.query, context.read, actions.toast, actions.invoke, identity.extend, events:identity, events:messaging, events:activity) in this extension",
3707
4010
  targets: ["*"]
3708
4011
  });
3709
4012
  return `${fm}
@@ -3719,7 +4022,7 @@ Ask which capability to add. Valid capabilities:
3719
4022
  - \`context.read\` \u2014 read host-provided context (customerId, customerEmail, messaging.conversationId, etc.)
3720
4023
  - \`actions.toast\` \u2014 show toast notifications (success, error, info, warning)
3721
4024
  - \`actions.invoke\` \u2014 invoke host actions (e.g., open new conversation)
3722
- - \`extend:identity\` \u2014 enrich identity JWT claims before signing
4025
+ - \`identity.extend\` \u2014 enrich identity JWT claims before signing
3723
4026
  - \`events:identity\` \u2014 subscribe to identity events (login, logout, refresh, expired)
3724
4027
  - \`events:messaging\` \u2014 subscribe to messaging events (postback button clicks)
3725
4028
  - \`events:activity\` \u2014 subscribe to activity events (page views, clicks, purchases)
@@ -3731,7 +4034,7 @@ Add the corresponding permission to \`packages/extension/public/manifest.json\`:
3731
4034
  - \`context.read\` \u2192 \`"context:read"\`
3732
4035
  - \`actions.toast\` \u2192 \`"actions:toast"\`
3733
4036
  - \`actions.invoke\` \u2192 \`"actions:invoke"\`
3734
- - \`extend:identity\` \u2192 \`"extend:identity"\`
4037
+ - \`identity.extend\` \u2192 \`"identity:extend"\`
3735
4038
  - \`events:identity\` \u2192 \`"events:identity"\` (also add entries to \`events\` array, e.g. \`["identity:login", "identity:logout"]\`)
3736
4039
  - \`events:messaging\` \u2192 \`"events:messaging"\` (also add entries to \`events\` array, e.g. \`["messaging:postback:Buy Now"]\`)
3737
4040
  - \`events:activity\` \u2192 \`"events:activity"\` (also add entries to \`events\` array, e.g. \`["activity:product_view"]\`)
@@ -3773,8 +4076,14 @@ const capabilities = useCapabilities()
3773
4076
  // actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
3774
4077
  \`\`\`
3775
4078
 
3776
- ### For events and extend \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
3777
- **IMPORTANT:** Event and extend capabilities have their own React hooks. Never use \`capabilities.events.*\` or \`capabilities.extend.*\` \u2014 those do not exist.
4079
+ ### Special handling (events + identity.extend):
4080
+
4081
+ #### For events \u2014 ALWAYS use dedicated hooks (INSTEAD of useCapabilities direct):
4082
+ Events are subscribed via \`useIdentityEvent\` / \`useMessagingEvent\` / \`useActivityEvent\` \u2014 never use \`capabilities.events.*\` directly (events are not part of the \`capabilities\` object).
4083
+
4084
+ #### For identity.extend \u2014 choose the CORRECT option:
4085
+ - **\`useExtendIdentity(handler)\`** \u2014 synchronous hook, fires only once at initial login. ALWAYS use for known-at-login enrichment.
4086
+ - **\`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\`.
3778
4087
 
3779
4088
  \`\`\`tsx
3780
4089
  // events:identity \u2014 use useIdentityEvent hook
@@ -3792,8 +4101,8 @@ ${HOOK_SNIPPETS["events:activity"]}
3792
4101
  \`\`\`
3793
4102
 
3794
4103
  \`\`\`tsx
3795
- // extend:identity \u2014 use useExtendIdentity hook
3796
- ${HOOK_SNIPPETS["extend.identity"]}
4104
+ // identity.extend \u2014 login-time useExtendIdentity hook OR post-login imperative capabilities.identity.extend
4105
+ ${CAPABILITY_SNIPPETS["identity.extend"]}
3797
4106
  \`\`\`
3798
4107
 
3799
4108
  ## 6. Verify
@@ -3801,7 +4110,7 @@ ${HOOK_SNIPPETS["extend.identity"]}
3801
4110
  - If data.fetch, confirm the domain is in allowedDomains
3802
4111
  - For data/context/actions: confirm accessed via \`useCapabilities()\` hook
3803
4112
  - For events: confirm using \`useIdentityEvent\`, \`useMessagingEvent\`, or \`useActivityEvent\` hooks
3804
- - For extend: confirm using \`useExtendIdentity\` hook
4113
+ - For identity.extend: confirm using \`useExtendIdentity\` hook (login-time) and/or \`capabilities.identity.extend(patch)\` (imperative post-login)
3805
4114
  `;
3806
4115
  };
3807
4116
 
@@ -3892,12 +4201,13 @@ Scan all \`.tsx\` files in \`packages/extension/src/\` for capability usage:
3892
4201
  - \`capabilities.context.read\` or \`useContextData\` \u2192 needs \`context:read\` permission
3893
4202
  - \`capabilities.actions.toast\` \u2192 needs \`actions:toast\` permission
3894
4203
  - \`capabilities.actions.invoke\` \u2192 needs \`actions:invoke\` permission
3895
- - \`useExtendIdentity\` \u2192 needs \`extend:identity\` permission
4204
+ - \`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\`.
3896
4205
  ${eventHookBullets}
3897
4206
 
3898
4207
  Report:
3899
4208
  - **Missing permissions:** capabilities used in code but not declared in manifest
3900
4209
  - **Unused permissions:** permissions declared in manifest but not used in code
4210
+ - **Undeclared identityClaims:** keys returned from \`useExtendIdentity\` or passed to \`capabilities.identity.extend\` that are not in \`manifest.identityClaims\` (excluding the standard-claim exemption list)
3901
4211
 
3902
4212
  ## 3. Surface-to-target matching
3903
4213
  - Each \`.tsx\` file with a \`<Surface id="...">\` should have a matching target in manifest.json
@@ -4398,114 +4708,6 @@ var findRelevantSkills = (skills, query) => {
4398
4708
  });
4399
4709
  };
4400
4710
 
4401
- // ../../sdk/extension/ai-docs/src/snippets/capabilities.ts
4402
- var CAPABILITY_SNIPPETS = {
4403
- "data.query": `
4404
- const capabilities = useCapabilities()
4405
- const result = await capabilities.data.query({ path: '/your-endpoint', method: 'GET' })
4406
- `,
4407
- "data.fetch": `
4408
- const capabilities = useCapabilities()
4409
- const response = await capabilities.data.fetch('https://api.example.com/endpoint')
4410
- `,
4411
- "context.read": `
4412
- // Read host context + extension settings
4413
- const { customerId, settings } = useContextData()
4414
-
4415
- // Or use the convenience hook for settings only
4416
- const settings = useSettings()
4417
- `,
4418
- "actions.toast": `
4419
- const capabilities = useCapabilities()
4420
- capabilities.actions.toast({ type: 'success', message: 'Done!' })
4421
- `,
4422
- "actions.invoke": `
4423
- const capabilities = useCapabilities()
4424
-
4425
- // New conversation with tags and fields
4426
- await capabilities.actions.invoke('newConversation', {
4427
- tags: ['stackable', 'order-lookup'],
4428
- fields: [{ id: 'stackable_action', value: 'order_status' }],
4429
- metadata: { orderId: '12345' },
4430
- })
4431
-
4432
- // Standalone: set tags on current/next conversation
4433
- await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])
4434
- `,
4435
- "events:identity": HOOK_SNIPPETS["events:identity"],
4436
- "events:messaging": HOOK_SNIPPETS["events:messaging"],
4437
- "events:activity": HOOK_SNIPPETS["events:activity"],
4438
- "extend.identity": HOOK_SNIPPETS["extend.identity"]
4439
- };
4440
- var EVENT_SNIPPETS = {
4441
- "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
4442
- import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
4443
- import { useState } from 'react'
4444
-
4445
- export function Header(): React.ReactElement {
4446
- const [user, setUser] = useState<string | null>(null)
4447
-
4448
- useIdentityEvent('login', (event: IdentityEvent) => {
4449
- setUser(event.data.state.user?.email ?? null)
4450
- })
4451
-
4452
- useIdentityEvent('logout', () => {
4453
- setUser(null)
4454
- })
4455
-
4456
- return (
4457
- <Surface id="slot.header">
4458
- <ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
4459
- </Surface>
4460
- )
4461
- }`,
4462
- "events:messaging": `import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
4463
- import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
4464
- import { useState } from 'react'
4465
-
4466
- export function Content(): React.ReactElement {
4467
- const [lastPostback, setLastPostback] = useState<string | null>(null)
4468
-
4469
- useMessagingEvent('postback', (event) => {
4470
- setLastPostback(event.data.actionName)
4471
- })
4472
-
4473
- return (
4474
- <Surface id="slot.content">
4475
- <ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
4476
- </Surface>
4477
- )
4478
- }`,
4479
- "events:activity": `import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
4480
- import type { ActivityEventHandler } from '@stackable-labs/sdk-extension-contracts'
4481
- import { useState } from 'react'
4482
-
4483
- export function Content(): React.ReactElement {
4484
- const [lastEvent, setLastEvent] = useState<string | null>(null)
4485
-
4486
- useActivityEvent('page_view', (event) => {
4487
- setLastEvent(event.data.url as string)
4488
- })
4489
-
4490
- return (
4491
- <Surface id="slot.content">
4492
- <ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
4493
- </Surface>
4494
- )
4495
- }`,
4496
- "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
4497
- import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
4498
-
4499
- // Enrich identity JWT claims before signing.
4500
- // The host sends base claims (external_id, email, name),
4501
- // and your handler returns additional claims to merge.
4502
- // Use ExtendIdentityHandler type with useCallback for memoized handlers.
4503
- useExtendIdentity((claims) => ({
4504
- external_id: \`shopify_\${claims.external_id}\`,
4505
- loyalty_tier: 'gold',
4506
- }))`
4507
- };
4508
-
4509
4711
  // ../../sdk/extension/ai-docs/src/generated/example-snippets-jsx.ts
4510
4712
  var EXAMPLE_SNIPPETS2 = {
4511
4713
  "bootstrap": `import { createExtension } from '@stackable-labs/sdk-extension-react'
@@ -4730,32 +4932,91 @@ export function Content() {
4730
4932
  </Surface>
4731
4933
  )
4732
4934
  }`,
4733
- "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
4935
+ "identity.extend": `import { useCapabilities, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
4734
4936
 
4735
- // Enrich identity JWT claims before signing.
4736
- // The host sends base claims (external_id, email, name),
4737
- // and your handler returns additional claims to merge.
4738
- useExtendIdentity((claims) => ({
4739
- external_id: \`shopify_\${claims.external_id}\`,
4740
- loyalty_tier: 'gold',
4741
- }))`,
4742
- "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
4743
- import { useState } from 'react'
4937
+ // manifest.json:
4938
+ // {
4939
+ // "permissions": ["identity:extend"],
4940
+ // "identityClaims": ["verified", "verified_by", "verified_at"]
4941
+ // }
4942
+ //
4943
+ // PUSH side \u2014 capabilities.identity.extend(patch) sends new claims to the host
4944
+ // AFTER initial login. The host filters the patch against manifest.identityClaims,
4945
+ // merges into user.metadata, re-signs the JWT, and broadcasts identity:refresh.
4946
+ //
4947
+ // CONSUMER side \u2014 useContextData() already re-renders on every host-pushed
4948
+ // context update (login/logout/refresh/expired). For pure rendering, read the
4949
+ // enriched value directly from ctx.identity.user.metadata \u2014 no event listener
4950
+ // needed. Use useIdentityEvent only when you need a SIDE EFFECT (analytics,
4951
+ // cache invalidation, etc.) on a specific event.
4952
+ //
4953
+ // Standard JWT claims (external_id, email, name) are exempt from declaration.
4954
+ // Undeclared custom keys are dropped client-side with a console.warn.
4955
+ export function Content() {
4956
+ const capabilities = useCapabilities()
4957
+ const ctx = useContextData()
4958
+
4959
+ // Push: trigger after async verification (webhook, polling, user action)
4960
+ const runVerification = async () => {
4961
+ await new Promise(r => setTimeout(r, 1000)) // simulated async work
4962
+ await capabilities.identity.extend({
4963
+ verified: true,
4964
+ verified_by: 'xyzProvider',
4965
+ verified_at: new Date().toISOString(),
4966
+ })
4967
+ }
4744
4968
 
4969
+ // Consume: read directly from ctx \u2014 re-renders automatically on identity:refresh.
4970
+ const verified = Boolean(ctx.identity?.user?.metadata?.verified)
4971
+
4972
+ return (
4973
+ <Surface id="slot.content">
4974
+ <ui.Stack direction="column" gap="2" className="p-3">
4975
+ <ui.Button onClick={runVerification}>Verify</ui.Button>
4976
+ <ui.Text>Status: {verified ? 'verified' : 'unverified'}</ui.Text>
4977
+ </ui.Stack>
4978
+ </Surface>
4979
+ )
4980
+ }`,
4981
+ "events:identity": `import { Surface, ui, useContextData, useIdentityEvent } from '@stackable-labs/sdk-extension-react'
4982
+
4983
+ // useIdentityEvent \u2014 for SIDE EFFECTS on identity lifecycle events.
4984
+ // If you just want to RENDER based on identity state (e.g. show the current
4985
+ // user's email), use useContextData() \u2014 it re-renders reactively on every
4986
+ // host-pushed identity change. Use useIdentityEvent only when you need to
4987
+ // RUN imperative code on a specific event: analytics beacons, cookie writes,
4988
+ // downstream system notifications, cache invalidation, etc.
4989
+ //
4990
+ // manifest.json:
4991
+ // {
4992
+ // "permissions": ["context:read", "events:identity"],
4993
+ // "events": ["identity:login", "identity:logout"]
4994
+ // }
4745
4995
  export function Header() {
4746
- const [user, setUser] = useState(null)
4996
+ const ctx = useContextData()
4747
4997
 
4998
+ // Side effect: persist a "last login" hint to localStorage on every login.
4999
+ // Replace with your real-world side effect \u2014 analytics beacon, cookie write,
5000
+ // downstream system notification, etc.
4748
5001
  useIdentityEvent('login', (event) => {
4749
- setUser(event.data.state.user?.email ?? null)
5002
+ localStorage.setItem('last-login', JSON.stringify({
5003
+ userId: event.data.state.user?.id,
5004
+ timestamp: new Date().toISOString(),
5005
+ }))
4750
5006
  })
4751
5007
 
5008
+ // Clear the hint on logout / session expiry.
4752
5009
  useIdentityEvent('logout', () => {
4753
- setUser(null)
5010
+ localStorage.removeItem('last-login')
4754
5011
  })
4755
5012
 
5013
+ // Rendering: read directly from ctx \u2014 DON'T mirror identity into local
5014
+ // useState via the event listener; useContextData is already reactive.
5015
+ const email = ctx?.identity?.user?.email ?? null
5016
+
4756
5017
  return (
4757
5018
  <Surface id="slot.header">
4758
- <ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
5019
+ <ui.Text className="text-xs">{email ?? 'Not logged in'}</ui.Text>
4759
5020
  </Surface>
4760
5021
  )
4761
5022
  }`,
@@ -4819,11 +5080,19 @@ await capabilities.actions.invoke('newConversation', {
4819
5080
  await capabilities.actions.invoke('setConversationTags', ['escalated', 'order-issue'])`,
4820
5081
  "events:identity": `import { useIdentityEvent } from '@stackable-labs/sdk-extension-react'
4821
5082
 
5083
+ // manifest events: ["identity:login", "identity:logout", "identity:refresh"]
4822
5084
  useIdentityEvent('login', (event) => {
4823
- console.log('User logged in:', event.data.state.user?.email)
5085
+ // event.data.state.user.metadata is populated with any enrichment from sibling
5086
+ // extensions with identity:extend (declared in their manifest.identityClaims)
5087
+ console.log('User logged in:', event.data.state.user?.email, event.data.state.user?.metadata)
4824
5088
  })
4825
5089
  useIdentityEvent('logout', () => {
4826
5090
  console.log('User logged out')
5091
+ })
5092
+ // identity:refresh fires after any extension calls capabilities.identity.extend({...}).
5093
+ // Listen here to react to post-login enrichment (verification, tier upgrades, etc.).
5094
+ useIdentityEvent('refresh', (event) => {
5095
+ console.log('Identity refreshed \u2014 metadata:', event.data.state.user?.metadata)
4827
5096
  })`,
4828
5097
  "events:messaging": `import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
4829
5098
 
@@ -4835,31 +5104,77 @@ useMessagingEvent('postback:Buy Now', (event) => {
4835
5104
  useActivityEvent('product_view', (event) => {
4836
5105
  console.log('Activity:', event.eventName, event.data)
4837
5106
  })`,
4838
- "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
5107
+ "identity.extend": `// Enrich identity JWT claims before signing.
5108
+ // The host sends base claims (external_id, email, name); your handler returns
5109
+ // additional claims to merge. Returned keys are filtered against manifest.identityClaims:
5110
+ // - Standard JWT claims (external_id, email, name) are exempt \u2014 always allowed
5111
+ // - Custom claims must be declared in manifest.identityClaims
5112
+ // - Undeclared keys are dropped with a console.warn
5113
+ // Merged claims land in BOTH identityState.user.metadata (for sibling extensions)
5114
+ // AND the signed JWT's custom_claims (for downstream JWT consumers).
5115
+ // Pattern: name the handler with useCallback<ExtendIdentityHandler> for stable
5116
+ // reference + clean separation; pass the named const to useExtendIdentity.
5117
+ //
5118
+ // useExtendIdentity fires ONCE at initial login \u2014 return what's known synchronously.
5119
+ // For post-login async updates (e.g., after verification completes via a webhook
5120
+ // or polling), use capabilities.identity.extend(patch) \u2014 see the 'identity.extend'
5121
+ // capability for the imperative path.
4839
5122
 
4840
- useExtendIdentity((claims) => ({
4841
- external_id: \`custom_\${claims.external_id}\`,
4842
- loyalty_tier: 'gold',
4843
- }))`
5123
+ import { useCallback } from 'react'
5124
+ import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
5125
+
5126
+ // manifest.json:
5127
+ // {
5128
+ // "permissions": ["identity:extend"],
5129
+ // "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
5130
+ // }
5131
+ const handleExtend = useCallback((claims) => ({
5132
+ external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
5133
+ loyalty_tier: 'bronze', // custom \u2014 sync, known at login
5134
+ verified: false, // custom \u2014 default; updated async post-verification
5135
+ }), [])
5136
+ useExtendIdentity(handleExtend)
5137
+
5138
+ // \u2500\u2500 For post-login async push (e.g., after async verification completes): \u2500\u2500\u2500\u2500\u2500
5139
+ const capabilities = useCapabilities()
5140
+ await capabilities.identity.extend({ verified: true })
5141
+
5142
+ // Consumer side \u2014 same or sibling extension reacts via identity:refresh
5143
+ useIdentityEvent('refresh', (event) => {
5144
+ console.log('verified =>', event.data.state.user?.metadata?.verified)
5145
+ })`
4844
5146
  };
4845
5147
  var EVENT_SNIPPETS2 = {
4846
5148
  "events:identity": `import { useIdentityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
4847
5149
  import { useState } from 'react'
4848
5150
 
5151
+ // manifest events: ["identity:login", "identity:logout", "identity:refresh"]
5152
+ //
5153
+ // 'login' and 'refresh' both deliver the full IdentityState, including
5154
+ // state.user.metadata \u2014 populated by sibling extensions with identity:extend
5155
+ // (declared in their manifest.identityClaims). Subscribe to BOTH to cover the
5156
+ // initial login AND any post-login push via capabilities.identity.extend().
4849
5157
  export function Header() {
4850
5158
  const [user, setUser] = useState(null)
5159
+ const [verified, setVerified] = useState(false)
4851
5160
 
4852
5161
  useIdentityEvent('login', (event) => {
4853
5162
  setUser(event.data.state.user?.email ?? null)
5163
+ setVerified(Boolean(event.data.state.user?.metadata?.verified))
5164
+ })
5165
+
5166
+ useIdentityEvent('refresh', (event) => {
5167
+ setVerified(Boolean(event.data.state.user?.metadata?.verified))
4854
5168
  })
4855
5169
 
4856
5170
  useIdentityEvent('logout', () => {
4857
5171
  setUser(null)
5172
+ setVerified(false)
4858
5173
  })
4859
5174
 
4860
5175
  return (
4861
5176
  <Surface id="slot.header">
4862
- <ui.Text className="text-xs">{user ?? 'Not logged in'}</ui.Text>
5177
+ <ui.Text className="text-xs">{user ?? 'Not logged in'} {verified && '\u2713'}</ui.Text>
4863
5178
  </Surface>
4864
5179
  )
4865
5180
  }`,
@@ -4895,16 +5210,36 @@ export function Content() {
4895
5210
  </Surface>
4896
5211
  )
4897
5212
  }`,
4898
- "extend.identity": `import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
5213
+ "identity.extend": `// Enrich identity JWT claims before signing.
5214
+ // The host sends base claims (external_id, email, name); your handler returns
5215
+ // additional claims to merge. Returned keys are filtered against manifest.identityClaims:
5216
+ // - Standard JWT claims (external_id, email, name) are exempt \u2014 always allowed
5217
+ // - Custom claims must be declared in manifest.identityClaims
5218
+ // - Undeclared keys are dropped with a console.warn
5219
+ // Merged claims land in BOTH identityState.user.metadata (for sibling extensions)
5220
+ // AND the signed JWT's custom_claims (for downstream JWT consumers).
5221
+ // Pattern: name the handler with useCallback<ExtendIdentityHandler> for stable
5222
+ // reference + clean separation; pass the named const to useExtendIdentity.
5223
+ //
5224
+ // useExtendIdentity fires ONCE at initial login \u2014 return what's known synchronously.
5225
+ // For post-login async updates (e.g., after verification completes via a webhook
5226
+ // or polling), use capabilities.identity.extend(patch) \u2014 see the 'identity.extend'
5227
+ // capability for the imperative path.
4899
5228
 
4900
- // Enrich identity JWT claims before signing.
4901
- // The host sends base claims (external_id, email, name),
4902
- // and your handler returns additional claims to merge.
4903
- // Use ExtendIdentityHandler type with useCallback for memoized handlers.
4904
- useExtendIdentity((claims) => ({
4905
- external_id: \`shopify_\${claims.external_id}\`,
4906
- loyalty_tier: 'gold',
4907
- }))`
5229
+ import { useCallback } from 'react'
5230
+ import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
5231
+
5232
+ // manifest.json:
5233
+ // {
5234
+ // "permissions": ["identity:extend"],
5235
+ // "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
5236
+ // }
5237
+ const handleExtend = useCallback((claims) => ({
5238
+ external_id: \`custom_\${claims.external_id}\`, // standard claim override (exempt)
5239
+ loyalty_tier: 'bronze', // custom \u2014 sync, known at login
5240
+ verified: false, // custom \u2014 default; updated async post-verification
5241
+ }), [])
5242
+ useExtendIdentity(handleExtend)`
4908
5243
  };
4909
5244
 
4910
5245
  // ../../sdk/extension/ai-docs/src/index.ts