@stackable-labs/mcp-app-extension 1.6.0 → 1.7.1

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 +149 -78
  2. package/dist/server.js +149 -78
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
5
+ import { validatePatternFormat } from '@stackable-labs/lib-contracts';
5
6
  import fs4, { readFile, mkdir, writeFile, constants } from 'fs/promises';
6
7
  import path, { join } from 'path';
7
8
  import os, { homedir } from 'os';
@@ -526,12 +527,12 @@ Access capabilities via the \`useCapabilities()\` hook:
526
527
  const capabilities = useCapabilities()
527
528
  \`\`\`
528
529
 
529
- ## data.query \u2014 Host-Mediated Requests
530
- The host handles the API call. Extension sends an action name + params, host returns data.
530
+ ## data.query \u2014 Platform-Mediated Requests
531
+ The Stackable platform handles the API call. Extension sends an action name + params, the platform returns data.
531
532
  - **Permission required:** \`data:query\`
532
533
  - **Usage:** \`capabilities.data.query<T>(payload: ApiRequest): Promise<T>\`
533
534
  - **ApiRequest shape:** \`{ action: string; [key: string]: unknown }\`
534
- - **When to use:** When the host application handles the API integration
535
+ - **When to use:** When the platform handles the API integration
535
536
 
536
537
  \`\`\`tsx
537
538
  const result = await capabilities.data.query<Customer>({
@@ -577,8 +578,8 @@ const result = await capabilities.data.fetch('https://api.example.com/orders', {
577
578
 
578
579
  > See [Instance Settings](./instance-settings) for the full schema-declaration + storage-mode story, including which field types accept \`secret: true\`.
579
580
 
580
- ## context.read \u2014 Read Host Context
581
- Read host-provided context (customer ID, email, extension settings, etc.).
581
+ ## context.read \u2014 Read Platform Context
582
+ Read framework-provided context (customer ID, email, extension settings, etc.).
582
583
  - **Permission required:** \`context:read\`
583
584
  - **Usage:** \`capabilities.context.read(): Promise<ContextData>\`
584
585
  - **ContextData shape:** \`{ customerId?: string, customerEmail?: string, settings?: Record<string, unknown>, [key: string]: unknown }\`
@@ -606,7 +607,7 @@ Non-secret settings declared in \`settingsSchema\` are automatically available v
606
607
  - No new permission needed \u2014 \`context:read\` is the only gate
607
608
 
608
609
  ## actions.toast \u2014 Show Toast Notifications
609
- Display a toast notification in the host UI.
610
+ Display a toast notification in the framework widget's UI.
610
611
  - **Permission required:** \`actions:toast\`
611
612
  - **Usage:** \`capabilities.actions.toast(payload: ToastPayload): Promise<void>\`
612
613
  - **ToastPayload:** \`{ message: string, type?: 'success'|'error'|'info'|'warning', duration?: number }\`
@@ -615,8 +616,8 @@ Display a toast notification in the host UI.
615
616
  capabilities.actions.toast({ message: 'Saved!', type: 'success' })
616
617
  \`\`\`
617
618
 
618
- ## actions.invoke \u2014 Invoke Host Actions
619
- Trigger host-defined actions (e.g., open a new conversation, set conversation tags/fields).
619
+ ## actions.invoke \u2014 Invoke Platform Actions
620
+ Trigger framework-defined actions (e.g., open a new conversation, set conversation tags/fields).
620
621
  - **Permission required:** \`actions:invoke\`
621
622
  - **Usage:** \`capabilities.actions.invoke<T>(action: string, payload?: Record<string, unknown>): Promise<T>\`
622
623
  - **Available actions:**
@@ -645,7 +646,7 @@ await capabilities.actions.invoke('setConversationFields', [
645
646
  **Zendesk constraints:** Tags max 20, auto-lowercased/sanitized. Fields require \`web_widget_conversation_ticket_metadata\` feature flag. Both \`conversationTags\` and \`conversationFields\` **replace** on each call (not additive).
646
647
 
647
648
  ## events:identity \u2014 Identity Event Subscription
648
- Subscribe to real-time identity events (login, logout, refresh, expired) pushed from the host.
649
+ Subscribe to real-time identity events (login, logout, refresh, expired) pushed from the host via the framework.
649
650
  - **Permission required:** \`events:identity\`
650
651
  - **Manifest events array:** Declare specific events to listen for (e.g. \`["identity:login", "identity:logout"]\`)
651
652
  - **Hook:** \`useIdentityEvent(eventType, handler)\`
@@ -685,7 +686,7 @@ ${HOOK_SNIPPETS["events:messaging"]}
685
686
  \`\`\`
686
687
 
687
688
  ## events:activity \u2014 Activity Event Subscription
688
- Subscribe to host activity events (e.g. page views, clicks, purchases) pushed from the host application.
689
+ Subscribe to activity events (e.g. page views, clicks, purchases) pushed from the host via the framework.
689
690
  - **Permission required:** \`events:activity\`
690
691
  - **Manifest events array:** Declare specific events to listen for (e.g. \`["activity:product_view", "activity:add_to_cart"]\`) \u2014 manifest uses fully-qualified strings
691
692
  - **Hook:** \`useActivityEvent(eventType, handler)\` \u2014 \`ActivityEventHandler\` type exported for use with \`useCallback\`
@@ -718,7 +719,7 @@ ${HOOK_SNIPPETS["events:activity"]}
718
719
  **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.
719
720
 
720
721
  ## extend:identity \u2014 Identity Claim Enrichment
721
- Enrich identity JWT claims before signing. The host sends base claims to your extension, and you return additional claims to merge into the token.
722
+ Enrich identity JWT claims before signing. The framework sends base claims to your extension, and you return additional claims to merge into the token.
722
723
  - **Permission required:** \`extend:identity\`
723
724
  - **Hook:** \`useExtendIdentity(handler)\` \u2014 \`ExtendIdentityHandler\` type exported for use with \`useCallback\`
724
725
  - **Handler signature:** \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
@@ -1123,7 +1124,7 @@ const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
1123
1124
  - \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
1124
1125
 
1125
1126
  ## useIdentityEvent(eventType, handler)
1126
- Subscribe to identity events pushed from the host. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
1127
+ Subscribe to identity events pushed from the host via the framework. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
1127
1128
  - \`eventType: ${identityEventTypes}\`
1128
1129
  - \`handler: (event: IdentityEvent) => void\`
1129
1130
  - \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
@@ -1151,7 +1152,7 @@ ${HOOK_SNIPPETS_MEMOIZED["events:messaging"]}
1151
1152
  \`\`\`
1152
1153
 
1153
1154
  ## useActivityEvent(eventType, handler)
1154
- Subscribe to host activity events. Requires \`events:activity\` permission and matching entries in manifest \`events\` array.
1155
+ Subscribe to activity events pushed from the host via the framework. Requires \`events:activity\` permission and matching entries in manifest \`events\` array.
1155
1156
  - \`eventType: ${activityEventTypes} | '*'\` (domain-stripped)
1156
1157
  - \`handler: ActivityEventHandler\` \u2014 \`(event: ActivityEvent) => void\`
1157
1158
  - \`ActivityEvent: { eventName: string, data: Record<string, unknown> }\`
@@ -1539,13 +1540,13 @@ These rules must always be followed when writing extension code.
1539
1540
 
1540
1541
  ## Components
1541
1542
  - Use the \`ui.*\` namespace for components (\`<ui.Card>\`, \`<ui.Button>\`) \u2014 don't import components directly
1542
- - Only use the attributes listed in the component reference \u2014 the host rejects unknown attributes
1543
+ - Only use the attributes listed in the component reference \u2014 the framework rejects unknown attributes
1543
1544
  - Use \`<ui.ScrollArea>\` for content that may overflow
1544
1545
 
1545
1546
  ## Manifest
1546
1547
  - Always declare permissions in \`manifest.json\` before using capabilities
1547
1548
  - Don't use \`data.fetch\` without adding the domain to \`allowedDomains\` in manifest
1548
- - \`allowedDomains\` must be exact hostnames \u2014 no wildcards, no paths
1549
+ - \`allowedDomains\`: prefer exact hostnames. Use \`*.<suffix>\` wildcards only if you need any subdomain; the apex is separate. No paths, no protocols, no mid-string wildcards. Wildcards must use a multi-label suffix (e.g., \`*.example.com\`, not \`*.com\`); TLD patterns such as \`*.co.uk\` may pass format checks but will be rejected at submission
1549
1550
 
1550
1551
  ## Entry Point
1551
1552
  - Don't modify \`index.html\` \u2014 the extension entry point is always \`src/index.tsx\` via \`createExtension\`
@@ -1707,7 +1708,7 @@ var generateSurfaces = () => {
1707
1708
 
1708
1709
  # Surfaces
1709
1710
 
1710
- Surfaces are the UI slots where your extension renders content inside the host application.
1711
+ Surfaces are the UI slots where your extension renders content inside the embedding application.
1711
1712
  Each surface maps to a specific layout position and is declared as a React component using
1712
1713
  the \`<Surface>\` wrapper from \`@stackable-labs/sdk-extension-react\`.
1713
1714
 
@@ -1744,15 +1745,15 @@ ${EXAMPLE_SNIPPETS.bootstrap}
1744
1745
  \`\`\`
1745
1746
 
1746
1747
  \`createExtension\` bootstraps the extension runtime \u2014 it handles the sandboxed iframe
1747
- communication, capability injection, and surface registration with the host.
1748
+ communication, capability injection, and surface registration with the framework.
1748
1749
 
1749
1750
  ## Surface Lifecycle
1750
1751
 
1751
- 1. **Mount** \u2014 Host creates an iframe for the extension and loads the entry point
1752
+ 1. **Mount** \u2014 The framework creates an iframe for the extension and loads the entry point
1752
1753
  2. **Register** \u2014 \`createExtension\` scans the rendered tree for \`<Surface>\` components
1753
- 3. **Render** \u2014 Each \`<Surface>\` renders its children into the matching host slot
1754
+ 3. **Render** \u2014 Each \`<Surface>\` renders its children into the matching slot in the embedding application
1754
1755
  4. **Update** \u2014 Surfaces re-render when props, state, or context data changes
1755
- 5. **Unmount** \u2014 Host removes the extension iframe when no longer needed
1756
+ 5. **Unmount** \u2014 The framework removes the extension iframe when no longer needed
1756
1757
 
1757
1758
  ## Multi-Surface State
1758
1759
 
@@ -1846,7 +1847,7 @@ Stackable supports two authoring paths. Both produce the same kind of extension
1846
1847
  | **Version control** | Auto-saved to cloud | Git-based |
1847
1848
  | **Deployment** | Link to extension + deploy | CLI deploy command |
1848
1849
 
1849
- Both produce the same output \u2014 a Stackable extension that runs in the host application via the same Remote DOM pipeline. **This guide covers the [CLI](/docs/reference/cli-reference) path.** For Studio, see [AI Extension Studio](/docs/reference/extension-studio).
1850
+ Both produce the same output \u2014 a Stackable extension that runs inside the embedding application via the same Remote DOM pipeline. **This guide covers the [CLI](/docs/reference/cli-reference) path.** For Studio, see [AI Extension Studio](/docs/reference/extension-studio).
1850
1851
 
1851
1852
  ## Prerequisites
1852
1853
 
@@ -1886,7 +1887,7 @@ The dev command:
1886
1887
  2. Creates a public Cloudflare tunnel to your local server
1887
1888
  3. Displays a **query parameter** you can use to preview your extension
1888
1889
 
1889
- ### Testing against your host app
1890
+ ### Testing against your extension
1890
1891
 
1891
1892
  The CLI outputs a query param like:
1892
1893
 
@@ -1894,16 +1895,17 @@ The CLI outputs a query param like:
1894
1895
  ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1895
1896
  \`\`\`
1896
1897
 
1897
- Copy this and **append it to your host application URL** to load your local extension
1898
- instead of the production bundle. For example:
1898
+ Copy this and **append it to the host site's URL** (the site or product where your
1899
+ extension is installed/authorized) to load your local extension instead of the
1900
+ production bundle. For example:
1899
1901
 
1900
1902
  \`\`\`
1901
- https://your-app.com/dashboard?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1903
+ https://your-host-site.com/dashboard?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1902
1904
  \`\`\`
1903
1905
 
1904
1906
  This override is **browser-session only** \u2014 no database changes, no shared state.
1905
1907
  Each developer gets isolated overrides. Changes you make locally appear immediately
1906
- in the host app via hot reload.
1908
+ in your extension via hot reload.
1907
1909
 
1908
1910
  Use \`--no-tunnel\` if you only want to run the local Vite dev server without a tunnel.
1909
1911
 
@@ -2075,9 +2077,9 @@ ${CLI.dev}
2075
2077
  1. Reads \`.env.stackable\` for cached App/Extension context (prompts if missing)
2076
2078
  2. Creates Cloudflare tunnels for the extension dev server
2077
2079
  3. Starts Vite dev servers with hot reload
2078
- 4. Displays a \`_stackable_dev\` query param to append to your host app URL
2080
+ 4. Displays a \`_stackable_dev\` query param to append to a host site's URL
2079
2081
 
2080
- ### Host App Override
2082
+ ### Host-Site Override
2081
2083
 
2082
2084
  The CLI outputs a query param like:
2083
2085
 
@@ -2085,9 +2087,10 @@ The CLI outputs a query param like:
2085
2087
  ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
2086
2088
  \`\`\`
2087
2089
 
2088
- Append this to your deployed host app URL to load your local extension instead
2089
- of the production bundle. The override is browser-session only \u2014 no DB changes,
2090
- no shared state. Each developer gets isolated overrides.
2090
+ Append this to your deployed host site's URL (the site or product where your
2091
+ extension is installed/authorized) to load your local extension instead of the production
2092
+ bundle. The override is browser-session only \u2014 no DB changes, no shared state.
2093
+ Each developer gets isolated overrides.
2091
2094
 
2092
2095
  ## validate *(coming soon)*
2093
2096
 
@@ -2229,7 +2232,7 @@ var generateExternalApis = () => {
2229
2232
  const fm = frontmatter({
2230
2233
  root: false,
2231
2234
  targets: ["*"],
2232
- description: "Direct HTTP requests via data.fetch, allowedDomains configuration, and API wrapper patterns",
2235
+ description: "Direct HTTP requests via data.fetch, allowedDomains configuration, wildcard domains, and API wrapper patterns",
2233
2236
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
2234
2237
  });
2235
2238
  return `${fm}
@@ -2237,7 +2240,7 @@ var generateExternalApis = () => {
2237
2240
  # External APIs
2238
2241
 
2239
2242
  Extensions can make direct HTTP requests to external services using the \`data.fetch\`
2240
- capability. All requests are proxied through the host for security \u2014 the extension
2243
+ capability. All requests are proxied through the framework for security \u2014 the extension
2241
2244
  never makes raw network calls from the sandbox.
2242
2245
 
2243
2246
  ## Setup
@@ -2258,14 +2261,75 @@ Every domain your extension calls must be listed in \`allowedDomains\`:
2258
2261
 
2259
2262
  \`\`\`json
2260
2263
  {
2261
- "allowedDomains": ["api.example.com", "graphql.example.com"]
2264
+ "allowedDomains": ["api.example.com", "graphql.example.com", "*.myshopify.com"]
2262
2265
  }
2263
2266
  \`\`\`
2264
2267
 
2265
2268
  **Rules:**
2266
- - Exact hostnames only \u2014 no wildcards, no paths, no protocols
2267
- - The host rejects requests to unlisted domains
2268
- - Add each subdomain separately (e.g., \`api.example.com\` and \`cdn.example.com\`)
2269
+ - Exact hostnames or \`*.<suffix>\` wildcards (subdomain match) \u2014 no paths, no protocols
2270
+ - Wildcards take the form \`*.<suffix>\` and must use a multi-label suffix (e.g., \`*.example.com\`, not \`*.com\`)
2271
+ - The framework will reject requests to unlisted domains
2272
+ - Add each subdomain separately when listing exact hosts (e.g., \`api.example.com\` and \`cdn.example.com\`)
2273
+
2274
+ ### Wildcards
2275
+
2276
+ **Prefer exact hostnames where possible.** Use \`*.example.com\` only when you
2277
+ need to match any subdomain \u2014 for example, tenant-per-subdomain platforms
2278
+ (Shopify shops, Salesforce subdomains, Zendesk subdomains, multi-region API hosts) where
2279
+ you don't know the full set of hostnames at authoring time or will differ by instance.
2280
+
2281
+ \`\`\`json
2282
+ {
2283
+ "allowedDomains": ["*.myshopify.com"]
2284
+ }
2285
+ \`\`\`
2286
+
2287
+ With this entry, the framework allows requests to \`acme.myshopify.com\`,
2288
+ \`widgets.myshopify.com\`, and any subdomain at any depth (e.g.,
2289
+ \`shop.eu.myshopify.com\`). A request to \`evil.com\` is still rejected.
2290
+
2291
+ **Apex is separate.** \`*.myshopify.com\` does **not** match \`myshopify.com\`
2292
+ itself \u2014 this matches CORS, TLS-certificate, and DNS-wildcard convention. To
2293
+ allow both the apex and any subdomain, list both:
2294
+
2295
+ \`\`\`json
2296
+ {
2297
+ "allowedDomains": ["myshopify.com", "*.myshopify.com"]
2298
+ }
2299
+ \`\`\`
2300
+
2301
+ **Worked example:**
2302
+
2303
+ \`\`\`json
2304
+ {
2305
+ "allowedDomains": [
2306
+ "*.myshopify.com",
2307
+ "myshopify.com",
2308
+ "api.example.com",
2309
+ "*.staging.example.com"
2310
+ ]
2311
+ }
2312
+ \`\`\`
2313
+
2314
+ This allows any customer shop subdomain, the Shopify apex, an exact API host,
2315
+ and any subdomain (at any depth) under \`staging.example.com\`.
2316
+
2317
+ **What's rejected and where.** Wildcards must use a multi-label suffix
2318
+ (e.g., \`*.example.com\`, not \`*.com\`). Two layers enforce this:
2319
+
2320
+ - *Format-level* (instant feedback in the CLI, MCP, and Studio AI panel) \u2014
2321
+ \`*\` alone, multiple wildcards, mid-string wildcards (\`api-*.example.com\`),
2322
+ single-label suffixes (\`*.com\`, \`*.localhost\`, \`*.io\`), protocols
2323
+ (\`https://...\`), paths, ports, and IP literals.
2324
+ - *Server-level* (at submission) \u2014 TLD patterns such as \`*.co.uk\`,
2325
+ \`*.com.au\`, and \`*.github.io\` may pass the format check but will be
2326
+ rejected at submission, since they would otherwise allow any registrant
2327
+ under a shared registry.
2328
+
2329
+ **Local development.** Exact \`localhost\` and IPv4 literals (\`127.0.0.1\`) are
2330
+ still valid for exact-host entries. Dev/staging extension modes bypass the
2331
+ domain check entirely, so wildcards aren't needed for local iteration \u2014 they
2332
+ matter at submission and in production.
2269
2333
 
2270
2334
  ### 3. Make Requests
2271
2335
 
@@ -2352,10 +2416,10 @@ const customer = await fetchCustomer(capabilities, customerId)
2352
2416
 
2353
2417
  | | data.fetch | data.query |
2354
2418
  |--|-----------|-----------|
2355
- | **Who handles the request** | Extension (via proxy) | Host application |
2419
+ | **Who handles the request** | Extension (via proxy) | Platform |
2356
2420
  | **Permission** | \`data:fetch\` | \`data:query\` |
2357
2421
  | **Domain config** | Required (\`allowedDomains\`) | Not needed |
2358
- | **Use when** | Calling external APIs directly | Host provides the API integration |
2422
+ | **Use when** | Calling external APIs directly | Platform provides the API integration |
2359
2423
 
2360
2424
  ## Error Handling
2361
2425
 
@@ -2414,7 +2478,7 @@ AI-powered smart insertion to place the component in the correct location.
2414
2478
 
2415
2479
  ### Surfaces
2416
2480
 
2417
- Lists the surface targets available for your app (e.g., \`slot.header\`,
2481
+ Lists the surface targets available for your extension (e.g., \`slot.header\`,
2418
2482
  \`slot.content\`, \`slot.footer\`). Surfaces already present in your code are
2419
2483
  filtered out. Clicking a surface adds it to your manifest and inserts a
2420
2484
  \`<Surface>\` block via AI smart insertion.
@@ -2483,7 +2547,7 @@ Studio toolbar.
2483
2547
 
2484
2548
  Comparing approaches? See [Choosing your path](/docs/guides/quick-start#choosing-your-path) in the Quick Start guide for the full Studio vs CLI comparison.
2485
2549
 
2486
- Both workflows produce the same output \u2014 a Stackable extension that runs in the host application via the same Remote DOM pipeline. Start in Studio to prototype, then scaffold to CLI when you need the full development workflow.
2550
+ Both workflows produce the same output \u2014 a Stackable extension that runs inside the embedding application via the same Remote DOM pipeline. Start in Studio to prototype, then scaffold to CLI when you need the full development workflow.
2487
2551
  `;
2488
2552
  };
2489
2553
 
@@ -2634,7 +2698,7 @@ my-extension/
2634
2698
 
2635
2699
  ### manifest.json
2636
2700
 
2637
- The extension manifest declares what the extension needs from the host:
2701
+ The extension manifest declares what the extension needs from the framework:
2638
2702
 
2639
2703
  \`\`\`json
2640
2704
  {
@@ -2652,7 +2716,11 @@ The extension manifest declares what the extension needs from the host:
2652
2716
  | \`version\` | Semver version string |
2653
2717
  | \`targets\` | Surface slots the extension renders into |
2654
2718
  | \`permissions\` | Capabilities the extension uses |
2655
- | \`allowedDomains\` | Hostnames for data.fetch requests (exact match, no wildcards) |
2719
+ | \`allowedDomains\` | Hostnames for data.fetch requests (exact hostnames or \`*.<suffix>\` wildcards) |
2720
+
2721
+ See **External APIs > Wildcards** for the full \`allowedDomains\` rules,
2722
+ including the apex-vs-subdomain distinction and what's rejected at format
2723
+ vs. submission time.
2656
2724
 
2657
2725
  ### index.tsx \u2014 Entry Point
2658
2726
 
@@ -2663,9 +2731,9 @@ ${EXAMPLE_SNIPPETS.bootstrap}
2663
2731
  \`\`\`
2664
2732
 
2665
2733
  \`createExtension\` handles:
2666
- - Sandboxed iframe communication with the host
2734
+ - Sandboxed iframe communication with the framework
2667
2735
  - Capability injection (makes \`useCapabilities()\` work)
2668
- - Surface registration (maps \`<Surface id="...">\` to host layout slots)
2736
+ - Surface registration (maps \`<Surface id="...">\` to layout slots in the embedding application)
2669
2737
 
2670
2738
  **Do not modify \`index.html\`** \u2014 the extension always bootstraps through \`createExtension\`.
2671
2739
 
@@ -2725,17 +2793,17 @@ var generateStylingAndTheming = () => {
2725
2793
 
2726
2794
  # Styling & Theming
2727
2795
 
2728
- Extensions inherit the host application's theme automatically. The SDK component library
2729
- (\`ui.*\` namespace) renders inside the host's styling context, so colors, fonts, and
2730
- spacing match the host UI without any configuration.
2796
+ Extensions inherit the embedding application's theme automatically. The SDK component
2797
+ library (\`ui.*\` namespace) renders inside the surrounding application's styling context,
2798
+ so colors, fonts, and spacing match without any configuration.
2731
2799
 
2732
- ## Host Theme Inheritance
2800
+ ## Theme Inheritance
2733
2801
 
2734
- The \`ui.*\` components automatically use the host's design tokens:
2735
- - **Colors** \u2014 text, backgrounds, borders adapt to the host's color scheme
2736
- - **Typography** \u2014 font family, sizes, and weights match the host
2737
- - **Spacing** \u2014 padding and margins follow the host's spacing scale
2738
- - **Dark mode** \u2014 components respond to the host's light/dark mode setting
2802
+ The \`ui.*\` components automatically use the embedding application's design tokens:
2803
+ - **Colors** \u2014 text, backgrounds, borders adapt to the application's color scheme
2804
+ - **Typography** \u2014 font family, sizes, and weights match the application
2805
+ - **Spacing** \u2014 padding and margins follow the application's spacing scale
2806
+ - **Dark mode** \u2014 components respond to the application's light/dark mode setting
2739
2807
 
2740
2808
  No CSS or theme configuration is needed in the extension.
2741
2809
 
@@ -2807,7 +2875,7 @@ Use component props (not CSS) to control visual style:
2807
2875
 
2808
2876
  Extensions run in a sandboxed iframe. These constraints apply:
2809
2877
 
2810
- - **No global CSS** \u2014 styles don't leak between extensions or into the host
2878
+ - **No global CSS** \u2014 styles don't leak between extensions or into the embedding application
2811
2879
  - **No \`document\` access** \u2014 cannot inject stylesheets or modify the DOM directly
2812
2880
  - **No \`window.location\`** \u2014 cannot read or modify the URL
2813
2881
  - **Component-only styling** \u2014 use \`className\` on \`ui.*\` components, not raw HTML elements
@@ -2815,7 +2883,7 @@ Extensions run in a sandboxed iframe. These constraints apply:
2815
2883
 
2816
2884
  ## Responsive Design
2817
2885
 
2818
- Extensions render in a constrained viewport (the host's sidebar or panel). Design for
2886
+ Extensions render in a constrained viewport (the embedding application's sidebar or panel). Design for
2819
2887
  narrow widths:
2820
2888
 
2821
2889
  \`\`\`tsx
@@ -2832,7 +2900,7 @@ narrow widths:
2832
2900
  - Assume a **narrow viewport** (~300-400px wide)
2833
2901
  - Use \`<ui.ScrollArea>\` for long lists or content
2834
2902
  - Avoid horizontal scrolling \u2014 stack elements vertically
2835
- - Test at the minimum panel width the host supports
2903
+ - Test at the minimum panel width the embedding application supports
2836
2904
  `;
2837
2905
  };
2838
2906
 
@@ -3158,14 +3226,14 @@ Apply them via the \`className\` prop on \`ui.*\` components:
3158
3226
  Always prefer:
3159
3227
  - Component variants (\`<ui.Button variant="primary">\`) over color overrides
3160
3228
  - Layout components (\`<ui.Stack>\`, \`<ui.Inline>\`) over manual flexbox class chains
3161
- - The semantic color tokens (\`text-foreground\`, \`bg-background\`) which adapt to the host theme
3229
+ - The semantic color tokens (\`text-foreground\`, \`bg-background\`) which adapt to the embedding application's theme
3162
3230
 
3163
3231
  ## Safelist limits
3164
3232
 
3165
3233
  The safelist is intentionally a fixed list, **NOT the full Tailwind catalog** to provide UI consistency and reduce bundle size. Anything beyond what is listed below will be missing from the platform stylesheet at runtime \u2014 the class will appear in your DOM but the corresponding CSS rule will not exist, and the style will silently no-op. In particular:
3166
3234
 
3167
3235
  - **Arbitrary values are NOT supported** in general (e.g. \`w-[123px]\`, \`bg-[#1a1a1a]\`, \`text-[15px]\`). The single exception is \`aspect-[9/16]\` (portrait video). For any other arbitrary value, use the named utility closest to your target.
3168
- - **Off-list color shades** (e.g. \`bg-cyan-500\`, \`text-rose-500\`) are not pre-emitted. The supported color set below is curated for visual consistency with the host theme.
3236
+ - **Off-list color shades** (e.g. \`bg-cyan-500\`, \`text-rose-500\`) are not pre-emitted. The supported color set below is curated for visual consistency with the embedding application's theme.
3169
3237
  - **Off-list ladder values** (e.g. \`mt-32\`, \`text-4xl\`, \`w-128\`) are not pre-emitted. The ladders below cover sizes appropriate for the embedded widget's narrow viewport.
3170
3238
 
3171
3239
  If you need something that's not on this list, file an issue with your use case \u2014 the safelist is curated, not frozen.
@@ -3318,9 +3386,9 @@ Focused examples showing each SDK capability in a working Surface component.
3318
3386
  Each example is self-contained \u2014 copy it into a surface file and add the
3319
3387
  corresponding permission to your \`manifest.json\`.
3320
3388
 
3321
- ## context.read \u2014 Reading Host Context
3389
+ ## context.read \u2014 Reading Platform Context
3322
3390
 
3323
- Read customer and session data provided by the host. The \`useContextData\`
3391
+ Read customer and session data provided by the framework. The \`useContextData\`
3324
3392
  hook handles loading state automatically.
3325
3393
 
3326
3394
  **Permission:** \`context:read\`
@@ -3329,9 +3397,9 @@ hook handles loading state automatically.
3329
3397
  ${EXAMPLE_SNIPPETS["context.read"]}
3330
3398
  \`\`\`
3331
3399
 
3332
- ## data.query \u2014 Host-Mediated Requests
3400
+ ## data.query \u2014 Platform-Mediated Requests
3333
3401
 
3334
- Send structured requests to the host application. The host handles the API
3402
+ Send structured requests to the platform. The framework handles the API
3335
3403
  call and returns the result \u2014 no \`allowedDomains\` needed.
3336
3404
 
3337
3405
  **Permission:** \`data:query\`
@@ -3343,7 +3411,8 @@ ${EXAMPLE_SNIPPETS["data.query"]}
3343
3411
  ## data.fetch \u2014 Direct HTTP Requests
3344
3412
 
3345
3413
  Make HTTP requests directly from the extension sandbox. The domain must be
3346
- listed in \`allowedDomains\` in your manifest.
3414
+ listed in \`allowedDomains\` in your manifest \u2014 exact hostnames or
3415
+ \`*.<suffix>\` wildcards. See **External APIs > Wildcards** for the full rules.
3347
3416
 
3348
3417
  **Permission:** \`data:fetch\`
3349
3418
 
@@ -3353,7 +3422,7 @@ ${EXAMPLE_SNIPPETS["data.fetch"]}
3353
3422
 
3354
3423
  ## actions.toast \u2014 Toast Notifications
3355
3424
 
3356
- Display toast notifications in the host UI to provide feedback to users.
3425
+ Display toast notifications in the host widget's UI to provide feedback to users.
3357
3426
 
3358
3427
  **Permission:** \`actions:toast\`
3359
3428
 
@@ -3390,7 +3459,7 @@ surfaces, store-based navigation, and loading states.
3390
3459
  ## Bootstrap \u2014 Entry Point
3391
3460
 
3392
3461
  Set up your extension entry point in \`src/index.tsx\`. The \`createExtension\` factory
3393
- bootstraps the sandboxed runtime and registers all surfaces with the host.
3462
+ bootstraps the sandboxed runtime and registers all surfaces with the framework.
3394
3463
 
3395
3464
  \`\`\`tsx
3396
3465
  ${EXAMPLE_SNIPPETS.bootstrap}
@@ -3398,7 +3467,7 @@ ${EXAMPLE_SNIPPETS.bootstrap}
3398
3467
 
3399
3468
  ## Surfaces \u2014 Declaring UI Slots
3400
3469
 
3401
- Each surface renders into a specific layout slot in the host application.
3470
+ Each surface renders into a specific layout slot in the embedding application.
3402
3471
  The \`id\` prop must match a target declared in your \`manifest.json\`.
3403
3472
 
3404
3473
  ### Header
@@ -3467,7 +3536,7 @@ var generateCookbookEvents = () => {
3467
3536
 
3468
3537
  # Events & Extensions
3469
3538
 
3470
- Subscribe to real-time events pushed from the host and extend identity claims.
3539
+ Subscribe to real-time events pushed from the host via the framework, and extend identity claims.
3471
3540
  Each event type has a dedicated hook \u2014 never use \`capabilities.events.*\` directly.
3472
3541
 
3473
3542
  ## Identity Events
@@ -3511,9 +3580,9 @@ ${EXAMPLE_SNIPPETS["events:messaging"]}
3511
3580
 
3512
3581
  ## Activity Events
3513
3582
 
3514
- Subscribe to host activity events like page views, clicks, and purchases.
3515
- Activity event names are domain-stripped \u2014 use \`useActivityEvent('product_view', ...)\`
3516
- not \`'activity:product_view'\`.
3583
+ Subscribe to host site activity events like page views, clicks, and purchases pushed from
3584
+ the host via the framework. Activity event names are domain-stripped \u2014
3585
+ use \`useActivityEvent('product_view', ...)\` not \`'activity:product_view'\`.
3517
3586
 
3518
3587
  **Permission:** \`events:activity\`
3519
3588
  **Well-known events:** ${activityEventTypes2}
@@ -4824,12 +4893,6 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
4824
4893
  pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
4825
4894
  pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
4826
4895
 
4827
- // ../../lib/contracts/src/custom.ts
4828
- var CUSTOM_ROLE = {
4829
- SUPER_ADMIN: "super_admin"
4830
- };
4831
- new Set(Object.values(CUSTOM_ROLE));
4832
-
4833
4896
  // ../../lib/utils-js/src/crypto.ts
4834
4897
  var getCrypto = () => {
4835
4898
  if (typeof globalThis !== "undefined" && globalThis.crypto) {
@@ -5060,13 +5123,21 @@ var createMcpServer = (options = {}) => {
5060
5123
  }
5061
5124
  if (!Array.isArray(manifest.allowedDomains)) {
5062
5125
  errors.push('Missing "allowedDomains" array. Use an empty array if no external API calls are needed.');
5126
+ } else {
5127
+ for (let i = 0; i < manifest.allowedDomains.length; i++) {
5128
+ const entry = manifest.allowedDomains[i];
5129
+ const reason = validatePatternFormat(entry);
5130
+ if (reason) {
5131
+ errors.push(`allowedDomains[${i}] "${String(entry)}": ${reason}`);
5132
+ }
5133
+ }
5063
5134
  }
5064
5135
  const permissions = manifest.permissions ?? [];
5065
5136
  if (permissions.includes("data:fetch") && Array.isArray(manifest.allowedDomains) && manifest.allowedDomains.length === 0) {
5066
5137
  errors.push('"data:fetch" permission declared but "allowedDomains" is empty. Add the domains your extension needs to fetch from.');
5067
5138
  }
5068
5139
  if (errors.length === 0) {
5069
- return textContent("Manifest is valid.");
5140
+ return textContent("Manifest is valid. Note: server will additionally validate wildcard suffixes against the public suffix list at submission time.");
5070
5141
  }
5071
5142
  return errorContent(`Manifest validation failed:
5072
5143
  ${errors.map((e) => `- ${e}`).join("\n")}`);
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_CATEGORIES, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
3
+ import { validatePatternFormat } from '@stackable-labs/lib-contracts';
3
4
  import { readFile } from 'fs/promises';
4
5
  import { join } from 'path';
5
6
  import { homedir } from 'os';
@@ -519,12 +520,12 @@ Access capabilities via the \`useCapabilities()\` hook:
519
520
  const capabilities = useCapabilities()
520
521
  \`\`\`
521
522
 
522
- ## data.query \u2014 Host-Mediated Requests
523
- The host handles the API call. Extension sends an action name + params, host returns data.
523
+ ## data.query \u2014 Platform-Mediated Requests
524
+ The Stackable platform handles the API call. Extension sends an action name + params, the platform returns data.
524
525
  - **Permission required:** \`data:query\`
525
526
  - **Usage:** \`capabilities.data.query<T>(payload: ApiRequest): Promise<T>\`
526
527
  - **ApiRequest shape:** \`{ action: string; [key: string]: unknown }\`
527
- - **When to use:** When the host application handles the API integration
528
+ - **When to use:** When the platform handles the API integration
528
529
 
529
530
  \`\`\`tsx
530
531
  const result = await capabilities.data.query<Customer>({
@@ -570,8 +571,8 @@ const result = await capabilities.data.fetch('https://api.example.com/orders', {
570
571
 
571
572
  > See [Instance Settings](./instance-settings) for the full schema-declaration + storage-mode story, including which field types accept \`secret: true\`.
572
573
 
573
- ## context.read \u2014 Read Host Context
574
- Read host-provided context (customer ID, email, extension settings, etc.).
574
+ ## context.read \u2014 Read Platform Context
575
+ Read framework-provided context (customer ID, email, extension settings, etc.).
575
576
  - **Permission required:** \`context:read\`
576
577
  - **Usage:** \`capabilities.context.read(): Promise<ContextData>\`
577
578
  - **ContextData shape:** \`{ customerId?: string, customerEmail?: string, settings?: Record<string, unknown>, [key: string]: unknown }\`
@@ -599,7 +600,7 @@ Non-secret settings declared in \`settingsSchema\` are automatically available v
599
600
  - No new permission needed \u2014 \`context:read\` is the only gate
600
601
 
601
602
  ## actions.toast \u2014 Show Toast Notifications
602
- Display a toast notification in the host UI.
603
+ Display a toast notification in the framework widget's UI.
603
604
  - **Permission required:** \`actions:toast\`
604
605
  - **Usage:** \`capabilities.actions.toast(payload: ToastPayload): Promise<void>\`
605
606
  - **ToastPayload:** \`{ message: string, type?: 'success'|'error'|'info'|'warning', duration?: number }\`
@@ -608,8 +609,8 @@ Display a toast notification in the host UI.
608
609
  capabilities.actions.toast({ message: 'Saved!', type: 'success' })
609
610
  \`\`\`
610
611
 
611
- ## actions.invoke \u2014 Invoke Host Actions
612
- Trigger host-defined actions (e.g., open a new conversation, set conversation tags/fields).
612
+ ## actions.invoke \u2014 Invoke Platform Actions
613
+ Trigger framework-defined actions (e.g., open a new conversation, set conversation tags/fields).
613
614
  - **Permission required:** \`actions:invoke\`
614
615
  - **Usage:** \`capabilities.actions.invoke<T>(action: string, payload?: Record<string, unknown>): Promise<T>\`
615
616
  - **Available actions:**
@@ -638,7 +639,7 @@ await capabilities.actions.invoke('setConversationFields', [
638
639
  **Zendesk constraints:** Tags max 20, auto-lowercased/sanitized. Fields require \`web_widget_conversation_ticket_metadata\` feature flag. Both \`conversationTags\` and \`conversationFields\` **replace** on each call (not additive).
639
640
 
640
641
  ## events:identity \u2014 Identity Event Subscription
641
- Subscribe to real-time identity events (login, logout, refresh, expired) pushed from the host.
642
+ Subscribe to real-time identity events (login, logout, refresh, expired) pushed from the host via the framework.
642
643
  - **Permission required:** \`events:identity\`
643
644
  - **Manifest events array:** Declare specific events to listen for (e.g. \`["identity:login", "identity:logout"]\`)
644
645
  - **Hook:** \`useIdentityEvent(eventType, handler)\`
@@ -678,7 +679,7 @@ ${HOOK_SNIPPETS["events:messaging"]}
678
679
  \`\`\`
679
680
 
680
681
  ## events:activity \u2014 Activity Event Subscription
681
- Subscribe to host activity events (e.g. page views, clicks, purchases) pushed from the host application.
682
+ Subscribe to activity events (e.g. page views, clicks, purchases) pushed from the host via the framework.
682
683
  - **Permission required:** \`events:activity\`
683
684
  - **Manifest events array:** Declare specific events to listen for (e.g. \`["activity:product_view", "activity:add_to_cart"]\`) \u2014 manifest uses fully-qualified strings
684
685
  - **Hook:** \`useActivityEvent(eventType, handler)\` \u2014 \`ActivityEventHandler\` type exported for use with \`useCallback\`
@@ -711,7 +712,7 @@ ${HOOK_SNIPPETS["events:activity"]}
711
712
  **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.
712
713
 
713
714
  ## extend:identity \u2014 Identity Claim Enrichment
714
- Enrich identity JWT claims before signing. The host sends base claims to your extension, and you return additional claims to merge into the token.
715
+ Enrich identity JWT claims before signing. The framework sends base claims to your extension, and you return additional claims to merge into the token.
715
716
  - **Permission required:** \`extend:identity\`
716
717
  - **Hook:** \`useExtendIdentity(handler)\` \u2014 \`ExtendIdentityHandler\` type exported for use with \`useCallback\`
717
718
  - **Handler signature:** \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
@@ -1116,7 +1117,7 @@ const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
1116
1117
  - \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
1117
1118
 
1118
1119
  ## useIdentityEvent(eventType, handler)
1119
- Subscribe to identity events pushed from the host. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
1120
+ Subscribe to identity events pushed from the host via the framework. Requires \`events:identity\` permission and matching entries in manifest \`events\` array.
1120
1121
  - \`eventType: ${identityEventTypes}\`
1121
1122
  - \`handler: (event: IdentityEvent) => void\`
1122
1123
  - \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
@@ -1144,7 +1145,7 @@ ${HOOK_SNIPPETS_MEMOIZED["events:messaging"]}
1144
1145
  \`\`\`
1145
1146
 
1146
1147
  ## useActivityEvent(eventType, handler)
1147
- Subscribe to host activity events. Requires \`events:activity\` permission and matching entries in manifest \`events\` array.
1148
+ Subscribe to activity events pushed from the host via the framework. Requires \`events:activity\` permission and matching entries in manifest \`events\` array.
1148
1149
  - \`eventType: ${activityEventTypes} | '*'\` (domain-stripped)
1149
1150
  - \`handler: ActivityEventHandler\` \u2014 \`(event: ActivityEvent) => void\`
1150
1151
  - \`ActivityEvent: { eventName: string, data: Record<string, unknown> }\`
@@ -1532,13 +1533,13 @@ These rules must always be followed when writing extension code.
1532
1533
 
1533
1534
  ## Components
1534
1535
  - Use the \`ui.*\` namespace for components (\`<ui.Card>\`, \`<ui.Button>\`) \u2014 don't import components directly
1535
- - Only use the attributes listed in the component reference \u2014 the host rejects unknown attributes
1536
+ - Only use the attributes listed in the component reference \u2014 the framework rejects unknown attributes
1536
1537
  - Use \`<ui.ScrollArea>\` for content that may overflow
1537
1538
 
1538
1539
  ## Manifest
1539
1540
  - Always declare permissions in \`manifest.json\` before using capabilities
1540
1541
  - Don't use \`data.fetch\` without adding the domain to \`allowedDomains\` in manifest
1541
- - \`allowedDomains\` must be exact hostnames \u2014 no wildcards, no paths
1542
+ - \`allowedDomains\`: prefer exact hostnames. Use \`*.<suffix>\` wildcards only if you need any subdomain; the apex is separate. No paths, no protocols, no mid-string wildcards. Wildcards must use a multi-label suffix (e.g., \`*.example.com\`, not \`*.com\`); TLD patterns such as \`*.co.uk\` may pass format checks but will be rejected at submission
1542
1543
 
1543
1544
  ## Entry Point
1544
1545
  - Don't modify \`index.html\` \u2014 the extension entry point is always \`src/index.tsx\` via \`createExtension\`
@@ -1700,7 +1701,7 @@ var generateSurfaces = () => {
1700
1701
 
1701
1702
  # Surfaces
1702
1703
 
1703
- Surfaces are the UI slots where your extension renders content inside the host application.
1704
+ Surfaces are the UI slots where your extension renders content inside the embedding application.
1704
1705
  Each surface maps to a specific layout position and is declared as a React component using
1705
1706
  the \`<Surface>\` wrapper from \`@stackable-labs/sdk-extension-react\`.
1706
1707
 
@@ -1737,15 +1738,15 @@ ${EXAMPLE_SNIPPETS.bootstrap}
1737
1738
  \`\`\`
1738
1739
 
1739
1740
  \`createExtension\` bootstraps the extension runtime \u2014 it handles the sandboxed iframe
1740
- communication, capability injection, and surface registration with the host.
1741
+ communication, capability injection, and surface registration with the framework.
1741
1742
 
1742
1743
  ## Surface Lifecycle
1743
1744
 
1744
- 1. **Mount** \u2014 Host creates an iframe for the extension and loads the entry point
1745
+ 1. **Mount** \u2014 The framework creates an iframe for the extension and loads the entry point
1745
1746
  2. **Register** \u2014 \`createExtension\` scans the rendered tree for \`<Surface>\` components
1746
- 3. **Render** \u2014 Each \`<Surface>\` renders its children into the matching host slot
1747
+ 3. **Render** \u2014 Each \`<Surface>\` renders its children into the matching slot in the embedding application
1747
1748
  4. **Update** \u2014 Surfaces re-render when props, state, or context data changes
1748
- 5. **Unmount** \u2014 Host removes the extension iframe when no longer needed
1749
+ 5. **Unmount** \u2014 The framework removes the extension iframe when no longer needed
1749
1750
 
1750
1751
  ## Multi-Surface State
1751
1752
 
@@ -1839,7 +1840,7 @@ Stackable supports two authoring paths. Both produce the same kind of extension
1839
1840
  | **Version control** | Auto-saved to cloud | Git-based |
1840
1841
  | **Deployment** | Link to extension + deploy | CLI deploy command |
1841
1842
 
1842
- Both produce the same output \u2014 a Stackable extension that runs in the host application via the same Remote DOM pipeline. **This guide covers the [CLI](/docs/reference/cli-reference) path.** For Studio, see [AI Extension Studio](/docs/reference/extension-studio).
1843
+ Both produce the same output \u2014 a Stackable extension that runs inside the embedding application via the same Remote DOM pipeline. **This guide covers the [CLI](/docs/reference/cli-reference) path.** For Studio, see [AI Extension Studio](/docs/reference/extension-studio).
1843
1844
 
1844
1845
  ## Prerequisites
1845
1846
 
@@ -1879,7 +1880,7 @@ The dev command:
1879
1880
  2. Creates a public Cloudflare tunnel to your local server
1880
1881
  3. Displays a **query parameter** you can use to preview your extension
1881
1882
 
1882
- ### Testing against your host app
1883
+ ### Testing against your extension
1883
1884
 
1884
1885
  The CLI outputs a query param like:
1885
1886
 
@@ -1887,16 +1888,17 @@ The CLI outputs a query param like:
1887
1888
  ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1888
1889
  \`\`\`
1889
1890
 
1890
- Copy this and **append it to your host application URL** to load your local extension
1891
- instead of the production bundle. For example:
1891
+ Copy this and **append it to the host site's URL** (the site or product where your
1892
+ extension is installed/authorized) to load your local extension instead of the
1893
+ production bundle. For example:
1892
1894
 
1893
1895
  \`\`\`
1894
- https://your-app.com/dashboard?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1896
+ https://your-host-site.com/dashboard?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1895
1897
  \`\`\`
1896
1898
 
1897
1899
  This override is **browser-session only** \u2014 no database changes, no shared state.
1898
1900
  Each developer gets isolated overrides. Changes you make locally appear immediately
1899
- in the host app via hot reload.
1901
+ in your extension via hot reload.
1900
1902
 
1901
1903
  Use \`--no-tunnel\` if you only want to run the local Vite dev server without a tunnel.
1902
1904
 
@@ -2068,9 +2070,9 @@ ${CLI.dev}
2068
2070
  1. Reads \`.env.stackable\` for cached App/Extension context (prompts if missing)
2069
2071
  2. Creates Cloudflare tunnels for the extension dev server
2070
2072
  3. Starts Vite dev servers with hot reload
2071
- 4. Displays a \`_stackable_dev\` query param to append to your host app URL
2073
+ 4. Displays a \`_stackable_dev\` query param to append to a host site's URL
2072
2074
 
2073
- ### Host App Override
2075
+ ### Host-Site Override
2074
2076
 
2075
2077
  The CLI outputs a query param like:
2076
2078
 
@@ -2078,9 +2080,10 @@ The CLI outputs a query param like:
2078
2080
  ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
2079
2081
  \`\`\`
2080
2082
 
2081
- Append this to your deployed host app URL to load your local extension instead
2082
- of the production bundle. The override is browser-session only \u2014 no DB changes,
2083
- no shared state. Each developer gets isolated overrides.
2083
+ Append this to your deployed host site's URL (the site or product where your
2084
+ extension is installed/authorized) to load your local extension instead of the production
2085
+ bundle. The override is browser-session only \u2014 no DB changes, no shared state.
2086
+ Each developer gets isolated overrides.
2084
2087
 
2085
2088
  ## validate *(coming soon)*
2086
2089
 
@@ -2222,7 +2225,7 @@ var generateExternalApis = () => {
2222
2225
  const fm = frontmatter({
2223
2226
  root: false,
2224
2227
  targets: ["*"],
2225
- description: "Direct HTTP requests via data.fetch, allowedDomains configuration, and API wrapper patterns",
2228
+ description: "Direct HTTP requests via data.fetch, allowedDomains configuration, wildcard domains, and API wrapper patterns",
2226
2229
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
2227
2230
  });
2228
2231
  return `${fm}
@@ -2230,7 +2233,7 @@ var generateExternalApis = () => {
2230
2233
  # External APIs
2231
2234
 
2232
2235
  Extensions can make direct HTTP requests to external services using the \`data.fetch\`
2233
- capability. All requests are proxied through the host for security \u2014 the extension
2236
+ capability. All requests are proxied through the framework for security \u2014 the extension
2234
2237
  never makes raw network calls from the sandbox.
2235
2238
 
2236
2239
  ## Setup
@@ -2251,14 +2254,75 @@ Every domain your extension calls must be listed in \`allowedDomains\`:
2251
2254
 
2252
2255
  \`\`\`json
2253
2256
  {
2254
- "allowedDomains": ["api.example.com", "graphql.example.com"]
2257
+ "allowedDomains": ["api.example.com", "graphql.example.com", "*.myshopify.com"]
2255
2258
  }
2256
2259
  \`\`\`
2257
2260
 
2258
2261
  **Rules:**
2259
- - Exact hostnames only \u2014 no wildcards, no paths, no protocols
2260
- - The host rejects requests to unlisted domains
2261
- - Add each subdomain separately (e.g., \`api.example.com\` and \`cdn.example.com\`)
2262
+ - Exact hostnames or \`*.<suffix>\` wildcards (subdomain match) \u2014 no paths, no protocols
2263
+ - Wildcards take the form \`*.<suffix>\` and must use a multi-label suffix (e.g., \`*.example.com\`, not \`*.com\`)
2264
+ - The framework will reject requests to unlisted domains
2265
+ - Add each subdomain separately when listing exact hosts (e.g., \`api.example.com\` and \`cdn.example.com\`)
2266
+
2267
+ ### Wildcards
2268
+
2269
+ **Prefer exact hostnames where possible.** Use \`*.example.com\` only when you
2270
+ need to match any subdomain \u2014 for example, tenant-per-subdomain platforms
2271
+ (Shopify shops, Salesforce subdomains, Zendesk subdomains, multi-region API hosts) where
2272
+ you don't know the full set of hostnames at authoring time or will differ by instance.
2273
+
2274
+ \`\`\`json
2275
+ {
2276
+ "allowedDomains": ["*.myshopify.com"]
2277
+ }
2278
+ \`\`\`
2279
+
2280
+ With this entry, the framework allows requests to \`acme.myshopify.com\`,
2281
+ \`widgets.myshopify.com\`, and any subdomain at any depth (e.g.,
2282
+ \`shop.eu.myshopify.com\`). A request to \`evil.com\` is still rejected.
2283
+
2284
+ **Apex is separate.** \`*.myshopify.com\` does **not** match \`myshopify.com\`
2285
+ itself \u2014 this matches CORS, TLS-certificate, and DNS-wildcard convention. To
2286
+ allow both the apex and any subdomain, list both:
2287
+
2288
+ \`\`\`json
2289
+ {
2290
+ "allowedDomains": ["myshopify.com", "*.myshopify.com"]
2291
+ }
2292
+ \`\`\`
2293
+
2294
+ **Worked example:**
2295
+
2296
+ \`\`\`json
2297
+ {
2298
+ "allowedDomains": [
2299
+ "*.myshopify.com",
2300
+ "myshopify.com",
2301
+ "api.example.com",
2302
+ "*.staging.example.com"
2303
+ ]
2304
+ }
2305
+ \`\`\`
2306
+
2307
+ This allows any customer shop subdomain, the Shopify apex, an exact API host,
2308
+ and any subdomain (at any depth) under \`staging.example.com\`.
2309
+
2310
+ **What's rejected and where.** Wildcards must use a multi-label suffix
2311
+ (e.g., \`*.example.com\`, not \`*.com\`). Two layers enforce this:
2312
+
2313
+ - *Format-level* (instant feedback in the CLI, MCP, and Studio AI panel) \u2014
2314
+ \`*\` alone, multiple wildcards, mid-string wildcards (\`api-*.example.com\`),
2315
+ single-label suffixes (\`*.com\`, \`*.localhost\`, \`*.io\`), protocols
2316
+ (\`https://...\`), paths, ports, and IP literals.
2317
+ - *Server-level* (at submission) \u2014 TLD patterns such as \`*.co.uk\`,
2318
+ \`*.com.au\`, and \`*.github.io\` may pass the format check but will be
2319
+ rejected at submission, since they would otherwise allow any registrant
2320
+ under a shared registry.
2321
+
2322
+ **Local development.** Exact \`localhost\` and IPv4 literals (\`127.0.0.1\`) are
2323
+ still valid for exact-host entries. Dev/staging extension modes bypass the
2324
+ domain check entirely, so wildcards aren't needed for local iteration \u2014 they
2325
+ matter at submission and in production.
2262
2326
 
2263
2327
  ### 3. Make Requests
2264
2328
 
@@ -2345,10 +2409,10 @@ const customer = await fetchCustomer(capabilities, customerId)
2345
2409
 
2346
2410
  | | data.fetch | data.query |
2347
2411
  |--|-----------|-----------|
2348
- | **Who handles the request** | Extension (via proxy) | Host application |
2412
+ | **Who handles the request** | Extension (via proxy) | Platform |
2349
2413
  | **Permission** | \`data:fetch\` | \`data:query\` |
2350
2414
  | **Domain config** | Required (\`allowedDomains\`) | Not needed |
2351
- | **Use when** | Calling external APIs directly | Host provides the API integration |
2415
+ | **Use when** | Calling external APIs directly | Platform provides the API integration |
2352
2416
 
2353
2417
  ## Error Handling
2354
2418
 
@@ -2407,7 +2471,7 @@ AI-powered smart insertion to place the component in the correct location.
2407
2471
 
2408
2472
  ### Surfaces
2409
2473
 
2410
- Lists the surface targets available for your app (e.g., \`slot.header\`,
2474
+ Lists the surface targets available for your extension (e.g., \`slot.header\`,
2411
2475
  \`slot.content\`, \`slot.footer\`). Surfaces already present in your code are
2412
2476
  filtered out. Clicking a surface adds it to your manifest and inserts a
2413
2477
  \`<Surface>\` block via AI smart insertion.
@@ -2476,7 +2540,7 @@ Studio toolbar.
2476
2540
 
2477
2541
  Comparing approaches? See [Choosing your path](/docs/guides/quick-start#choosing-your-path) in the Quick Start guide for the full Studio vs CLI comparison.
2478
2542
 
2479
- Both workflows produce the same output \u2014 a Stackable extension that runs in the host application via the same Remote DOM pipeline. Start in Studio to prototype, then scaffold to CLI when you need the full development workflow.
2543
+ Both workflows produce the same output \u2014 a Stackable extension that runs inside the embedding application via the same Remote DOM pipeline. Start in Studio to prototype, then scaffold to CLI when you need the full development workflow.
2480
2544
  `;
2481
2545
  };
2482
2546
 
@@ -2627,7 +2691,7 @@ my-extension/
2627
2691
 
2628
2692
  ### manifest.json
2629
2693
 
2630
- The extension manifest declares what the extension needs from the host:
2694
+ The extension manifest declares what the extension needs from the framework:
2631
2695
 
2632
2696
  \`\`\`json
2633
2697
  {
@@ -2645,7 +2709,11 @@ The extension manifest declares what the extension needs from the host:
2645
2709
  | \`version\` | Semver version string |
2646
2710
  | \`targets\` | Surface slots the extension renders into |
2647
2711
  | \`permissions\` | Capabilities the extension uses |
2648
- | \`allowedDomains\` | Hostnames for data.fetch requests (exact match, no wildcards) |
2712
+ | \`allowedDomains\` | Hostnames for data.fetch requests (exact hostnames or \`*.<suffix>\` wildcards) |
2713
+
2714
+ See **External APIs > Wildcards** for the full \`allowedDomains\` rules,
2715
+ including the apex-vs-subdomain distinction and what's rejected at format
2716
+ vs. submission time.
2649
2717
 
2650
2718
  ### index.tsx \u2014 Entry Point
2651
2719
 
@@ -2656,9 +2724,9 @@ ${EXAMPLE_SNIPPETS.bootstrap}
2656
2724
  \`\`\`
2657
2725
 
2658
2726
  \`createExtension\` handles:
2659
- - Sandboxed iframe communication with the host
2727
+ - Sandboxed iframe communication with the framework
2660
2728
  - Capability injection (makes \`useCapabilities()\` work)
2661
- - Surface registration (maps \`<Surface id="...">\` to host layout slots)
2729
+ - Surface registration (maps \`<Surface id="...">\` to layout slots in the embedding application)
2662
2730
 
2663
2731
  **Do not modify \`index.html\`** \u2014 the extension always bootstraps through \`createExtension\`.
2664
2732
 
@@ -2718,17 +2786,17 @@ var generateStylingAndTheming = () => {
2718
2786
 
2719
2787
  # Styling & Theming
2720
2788
 
2721
- Extensions inherit the host application's theme automatically. The SDK component library
2722
- (\`ui.*\` namespace) renders inside the host's styling context, so colors, fonts, and
2723
- spacing match the host UI without any configuration.
2789
+ Extensions inherit the embedding application's theme automatically. The SDK component
2790
+ library (\`ui.*\` namespace) renders inside the surrounding application's styling context,
2791
+ so colors, fonts, and spacing match without any configuration.
2724
2792
 
2725
- ## Host Theme Inheritance
2793
+ ## Theme Inheritance
2726
2794
 
2727
- The \`ui.*\` components automatically use the host's design tokens:
2728
- - **Colors** \u2014 text, backgrounds, borders adapt to the host's color scheme
2729
- - **Typography** \u2014 font family, sizes, and weights match the host
2730
- - **Spacing** \u2014 padding and margins follow the host's spacing scale
2731
- - **Dark mode** \u2014 components respond to the host's light/dark mode setting
2795
+ The \`ui.*\` components automatically use the embedding application's design tokens:
2796
+ - **Colors** \u2014 text, backgrounds, borders adapt to the application's color scheme
2797
+ - **Typography** \u2014 font family, sizes, and weights match the application
2798
+ - **Spacing** \u2014 padding and margins follow the application's spacing scale
2799
+ - **Dark mode** \u2014 components respond to the application's light/dark mode setting
2732
2800
 
2733
2801
  No CSS or theme configuration is needed in the extension.
2734
2802
 
@@ -2800,7 +2868,7 @@ Use component props (not CSS) to control visual style:
2800
2868
 
2801
2869
  Extensions run in a sandboxed iframe. These constraints apply:
2802
2870
 
2803
- - **No global CSS** \u2014 styles don't leak between extensions or into the host
2871
+ - **No global CSS** \u2014 styles don't leak between extensions or into the embedding application
2804
2872
  - **No \`document\` access** \u2014 cannot inject stylesheets or modify the DOM directly
2805
2873
  - **No \`window.location\`** \u2014 cannot read or modify the URL
2806
2874
  - **Component-only styling** \u2014 use \`className\` on \`ui.*\` components, not raw HTML elements
@@ -2808,7 +2876,7 @@ Extensions run in a sandboxed iframe. These constraints apply:
2808
2876
 
2809
2877
  ## Responsive Design
2810
2878
 
2811
- Extensions render in a constrained viewport (the host's sidebar or panel). Design for
2879
+ Extensions render in a constrained viewport (the embedding application's sidebar or panel). Design for
2812
2880
  narrow widths:
2813
2881
 
2814
2882
  \`\`\`tsx
@@ -2825,7 +2893,7 @@ narrow widths:
2825
2893
  - Assume a **narrow viewport** (~300-400px wide)
2826
2894
  - Use \`<ui.ScrollArea>\` for long lists or content
2827
2895
  - Avoid horizontal scrolling \u2014 stack elements vertically
2828
- - Test at the minimum panel width the host supports
2896
+ - Test at the minimum panel width the embedding application supports
2829
2897
  `;
2830
2898
  };
2831
2899
 
@@ -3151,14 +3219,14 @@ Apply them via the \`className\` prop on \`ui.*\` components:
3151
3219
  Always prefer:
3152
3220
  - Component variants (\`<ui.Button variant="primary">\`) over color overrides
3153
3221
  - Layout components (\`<ui.Stack>\`, \`<ui.Inline>\`) over manual flexbox class chains
3154
- - The semantic color tokens (\`text-foreground\`, \`bg-background\`) which adapt to the host theme
3222
+ - The semantic color tokens (\`text-foreground\`, \`bg-background\`) which adapt to the embedding application's theme
3155
3223
 
3156
3224
  ## Safelist limits
3157
3225
 
3158
3226
  The safelist is intentionally a fixed list, **NOT the full Tailwind catalog** to provide UI consistency and reduce bundle size. Anything beyond what is listed below will be missing from the platform stylesheet at runtime \u2014 the class will appear in your DOM but the corresponding CSS rule will not exist, and the style will silently no-op. In particular:
3159
3227
 
3160
3228
  - **Arbitrary values are NOT supported** in general (e.g. \`w-[123px]\`, \`bg-[#1a1a1a]\`, \`text-[15px]\`). The single exception is \`aspect-[9/16]\` (portrait video). For any other arbitrary value, use the named utility closest to your target.
3161
- - **Off-list color shades** (e.g. \`bg-cyan-500\`, \`text-rose-500\`) are not pre-emitted. The supported color set below is curated for visual consistency with the host theme.
3229
+ - **Off-list color shades** (e.g. \`bg-cyan-500\`, \`text-rose-500\`) are not pre-emitted. The supported color set below is curated for visual consistency with the embedding application's theme.
3162
3230
  - **Off-list ladder values** (e.g. \`mt-32\`, \`text-4xl\`, \`w-128\`) are not pre-emitted. The ladders below cover sizes appropriate for the embedded widget's narrow viewport.
3163
3231
 
3164
3232
  If you need something that's not on this list, file an issue with your use case \u2014 the safelist is curated, not frozen.
@@ -3311,9 +3379,9 @@ Focused examples showing each SDK capability in a working Surface component.
3311
3379
  Each example is self-contained \u2014 copy it into a surface file and add the
3312
3380
  corresponding permission to your \`manifest.json\`.
3313
3381
 
3314
- ## context.read \u2014 Reading Host Context
3382
+ ## context.read \u2014 Reading Platform Context
3315
3383
 
3316
- Read customer and session data provided by the host. The \`useContextData\`
3384
+ Read customer and session data provided by the framework. The \`useContextData\`
3317
3385
  hook handles loading state automatically.
3318
3386
 
3319
3387
  **Permission:** \`context:read\`
@@ -3322,9 +3390,9 @@ hook handles loading state automatically.
3322
3390
  ${EXAMPLE_SNIPPETS["context.read"]}
3323
3391
  \`\`\`
3324
3392
 
3325
- ## data.query \u2014 Host-Mediated Requests
3393
+ ## data.query \u2014 Platform-Mediated Requests
3326
3394
 
3327
- Send structured requests to the host application. The host handles the API
3395
+ Send structured requests to the platform. The framework handles the API
3328
3396
  call and returns the result \u2014 no \`allowedDomains\` needed.
3329
3397
 
3330
3398
  **Permission:** \`data:query\`
@@ -3336,7 +3404,8 @@ ${EXAMPLE_SNIPPETS["data.query"]}
3336
3404
  ## data.fetch \u2014 Direct HTTP Requests
3337
3405
 
3338
3406
  Make HTTP requests directly from the extension sandbox. The domain must be
3339
- listed in \`allowedDomains\` in your manifest.
3407
+ listed in \`allowedDomains\` in your manifest \u2014 exact hostnames or
3408
+ \`*.<suffix>\` wildcards. See **External APIs > Wildcards** for the full rules.
3340
3409
 
3341
3410
  **Permission:** \`data:fetch\`
3342
3411
 
@@ -3346,7 +3415,7 @@ ${EXAMPLE_SNIPPETS["data.fetch"]}
3346
3415
 
3347
3416
  ## actions.toast \u2014 Toast Notifications
3348
3417
 
3349
- Display toast notifications in the host UI to provide feedback to users.
3418
+ Display toast notifications in the host widget's UI to provide feedback to users.
3350
3419
 
3351
3420
  **Permission:** \`actions:toast\`
3352
3421
 
@@ -3383,7 +3452,7 @@ surfaces, store-based navigation, and loading states.
3383
3452
  ## Bootstrap \u2014 Entry Point
3384
3453
 
3385
3454
  Set up your extension entry point in \`src/index.tsx\`. The \`createExtension\` factory
3386
- bootstraps the sandboxed runtime and registers all surfaces with the host.
3455
+ bootstraps the sandboxed runtime and registers all surfaces with the framework.
3387
3456
 
3388
3457
  \`\`\`tsx
3389
3458
  ${EXAMPLE_SNIPPETS.bootstrap}
@@ -3391,7 +3460,7 @@ ${EXAMPLE_SNIPPETS.bootstrap}
3391
3460
 
3392
3461
  ## Surfaces \u2014 Declaring UI Slots
3393
3462
 
3394
- Each surface renders into a specific layout slot in the host application.
3463
+ Each surface renders into a specific layout slot in the embedding application.
3395
3464
  The \`id\` prop must match a target declared in your \`manifest.json\`.
3396
3465
 
3397
3466
  ### Header
@@ -3460,7 +3529,7 @@ var generateCookbookEvents = () => {
3460
3529
 
3461
3530
  # Events & Extensions
3462
3531
 
3463
- Subscribe to real-time events pushed from the host and extend identity claims.
3532
+ Subscribe to real-time events pushed from the host via the framework, and extend identity claims.
3464
3533
  Each event type has a dedicated hook \u2014 never use \`capabilities.events.*\` directly.
3465
3534
 
3466
3535
  ## Identity Events
@@ -3504,9 +3573,9 @@ ${EXAMPLE_SNIPPETS["events:messaging"]}
3504
3573
 
3505
3574
  ## Activity Events
3506
3575
 
3507
- Subscribe to host activity events like page views, clicks, and purchases.
3508
- Activity event names are domain-stripped \u2014 use \`useActivityEvent('product_view', ...)\`
3509
- not \`'activity:product_view'\`.
3576
+ Subscribe to host site activity events like page views, clicks, and purchases pushed from
3577
+ the host via the framework. Activity event names are domain-stripped \u2014
3578
+ use \`useActivityEvent('product_view', ...)\` not \`'activity:product_view'\`.
3510
3579
 
3511
3580
  **Permission:** \`events:activity\`
3512
3581
  **Well-known events:** ${activityEventTypes2}
@@ -4817,12 +4886,6 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
4817
4886
  pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
4818
4887
  pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
4819
4888
 
4820
- // ../../lib/contracts/src/custom.ts
4821
- var CUSTOM_ROLE = {
4822
- SUPER_ADMIN: "super_admin"
4823
- };
4824
- new Set(Object.values(CUSTOM_ROLE));
4825
-
4826
4889
  // ../../lib/utils-auth/src/constants.ts
4827
4890
  var STANDALONE_CLIENT_DATA = {
4828
4891
  CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
@@ -5009,13 +5072,21 @@ var createMcpServer = (options = {}) => {
5009
5072
  }
5010
5073
  if (!Array.isArray(manifest.allowedDomains)) {
5011
5074
  errors.push('Missing "allowedDomains" array. Use an empty array if no external API calls are needed.');
5075
+ } else {
5076
+ for (let i = 0; i < manifest.allowedDomains.length; i++) {
5077
+ const entry = manifest.allowedDomains[i];
5078
+ const reason = validatePatternFormat(entry);
5079
+ if (reason) {
5080
+ errors.push(`allowedDomains[${i}] "${String(entry)}": ${reason}`);
5081
+ }
5082
+ }
5012
5083
  }
5013
5084
  const permissions = manifest.permissions ?? [];
5014
5085
  if (permissions.includes("data:fetch") && Array.isArray(manifest.allowedDomains) && manifest.allowedDomains.length === 0) {
5015
5086
  errors.push('"data:fetch" permission declared but "allowedDomains" is empty. Add the domains your extension needs to fetch from.');
5016
5087
  }
5017
5088
  if (errors.length === 0) {
5018
- return textContent("Manifest is valid.");
5089
+ return textContent("Manifest is valid. Note: server will additionally validate wildcard suffixes against the public suffix list at submission time.");
5019
5090
  }
5020
5091
  return errorContent(`Manifest validation failed:
5021
5092
  ${errors.map((e) => `- ${e}`).join("\n")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackable-labs/mcp-app-extension",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mcp-app-extension": "./dist/index.js"