@stackable-labs/mcp-app-extension 1.5.0 → 1.7.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 +231 -105
  2. package/dist/server.js +231 -105
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
- import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
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';
@@ -382,6 +383,22 @@ var frontmatter = (meta) => {
382
383
  var generateCoreReference = () => CORE_CONTENT;
383
384
 
384
385
  // ../../sdk/extension/ai-docs/src/generators/components.ts
386
+ var CATEGORY_ORDER = [
387
+ "layout",
388
+ "text",
389
+ "input",
390
+ "navigation",
391
+ "feedback",
392
+ "composite"
393
+ ];
394
+ var CATEGORY_LABELS = {
395
+ layout: "Layout",
396
+ text: "Text",
397
+ input: "Input",
398
+ navigation: "Navigation",
399
+ feedback: "Feedback",
400
+ composite: "Composite"
401
+ };
385
402
  var generateComponents = () => {
386
403
  const fm = frontmatter({
387
404
  root: false,
@@ -389,13 +406,31 @@ var generateComponents = () => {
389
406
  description: "Available UI components with allowed attributes per tag",
390
407
  globs: ["packages/extension/src/**/*.tsx"]
391
408
  });
392
- const componentLines = [];
409
+ const tagsByCategory = /* @__PURE__ */ new Map();
393
410
  for (const tag of UI_TAGS) {
394
- const attrs = UI_TAG_ATTRIBUTES[tag];
395
- const name = tagToComponentName(tag);
396
- componentLines.push(`### \`<ui.${name}>\` (\`${tag}\`)`);
397
- componentLines.push(`Allowed attributes: ${attrs.map((a2) => `\`${a2}\``).join(", ")}`);
411
+ const category = UI_TAG_CATEGORIES[tag];
412
+ const bucket = tagsByCategory.get(category) ?? [];
413
+ bucket.push(tag);
414
+ tagsByCategory.set(category, bucket);
415
+ }
416
+ for (const tags of tagsByCategory.values()) {
417
+ tags.sort();
418
+ }
419
+ const componentLines = [];
420
+ for (const category of CATEGORY_ORDER) {
421
+ const tags = tagsByCategory.get(category);
422
+ if (!tags || tags.length === 0) {
423
+ continue;
424
+ }
425
+ componentLines.push(`## ${CATEGORY_LABELS[category]}`);
398
426
  componentLines.push("");
427
+ for (const tag of tags) {
428
+ const attrs = UI_TAG_ATTRIBUTES[tag];
429
+ const name = tagToComponentName(tag);
430
+ componentLines.push(`### \`<ui.${name}>\` (\`${tag}\`)`);
431
+ componentLines.push(`Allowed attributes: ${attrs.map((a2) => `\`${a2}\``).join(", ")}`);
432
+ componentLines.push("");
433
+ }
399
434
  }
400
435
  const iconList = ALLOWED_ICONS.map((icon) => `\`${icon}\``).join(", ");
401
436
  return `${fm}
@@ -492,12 +527,12 @@ Access capabilities via the \`useCapabilities()\` hook:
492
527
  const capabilities = useCapabilities()
493
528
  \`\`\`
494
529
 
495
- ## data.query \u2014 Host-Mediated Requests
496
- 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.
497
532
  - **Permission required:** \`data:query\`
498
533
  - **Usage:** \`capabilities.data.query<T>(payload: ApiRequest): Promise<T>\`
499
534
  - **ApiRequest shape:** \`{ action: string; [key: string]: unknown }\`
500
- - **When to use:** When the host application handles the API integration
535
+ - **When to use:** When the platform handles the API integration
501
536
 
502
537
  \`\`\`tsx
503
538
  const result = await capabilities.data.query<Customer>({
@@ -543,8 +578,8 @@ const result = await capabilities.data.fetch('https://api.example.com/orders', {
543
578
 
544
579
  > See [Instance Settings](./instance-settings) for the full schema-declaration + storage-mode story, including which field types accept \`secret: true\`.
545
580
 
546
- ## context.read \u2014 Read Host Context
547
- 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.).
548
583
  - **Permission required:** \`context:read\`
549
584
  - **Usage:** \`capabilities.context.read(): Promise<ContextData>\`
550
585
  - **ContextData shape:** \`{ customerId?: string, customerEmail?: string, settings?: Record<string, unknown>, [key: string]: unknown }\`
@@ -572,7 +607,7 @@ Non-secret settings declared in \`settingsSchema\` are automatically available v
572
607
  - No new permission needed \u2014 \`context:read\` is the only gate
573
608
 
574
609
  ## actions.toast \u2014 Show Toast Notifications
575
- Display a toast notification in the host UI.
610
+ Display a toast notification in the framework widget's UI.
576
611
  - **Permission required:** \`actions:toast\`
577
612
  - **Usage:** \`capabilities.actions.toast(payload: ToastPayload): Promise<void>\`
578
613
  - **ToastPayload:** \`{ message: string, type?: 'success'|'error'|'info'|'warning', duration?: number }\`
@@ -581,8 +616,8 @@ Display a toast notification in the host UI.
581
616
  capabilities.actions.toast({ message: 'Saved!', type: 'success' })
582
617
  \`\`\`
583
618
 
584
- ## actions.invoke \u2014 Invoke Host Actions
585
- 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).
586
621
  - **Permission required:** \`actions:invoke\`
587
622
  - **Usage:** \`capabilities.actions.invoke<T>(action: string, payload?: Record<string, unknown>): Promise<T>\`
588
623
  - **Available actions:**
@@ -611,7 +646,7 @@ await capabilities.actions.invoke('setConversationFields', [
611
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).
612
647
 
613
648
  ## events:identity \u2014 Identity Event Subscription
614
- 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.
615
650
  - **Permission required:** \`events:identity\`
616
651
  - **Manifest events array:** Declare specific events to listen for (e.g. \`["identity:login", "identity:logout"]\`)
617
652
  - **Hook:** \`useIdentityEvent(eventType, handler)\`
@@ -651,7 +686,7 @@ ${HOOK_SNIPPETS["events:messaging"]}
651
686
  \`\`\`
652
687
 
653
688
  ## events:activity \u2014 Activity Event Subscription
654
- 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.
655
690
  - **Permission required:** \`events:activity\`
656
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
657
692
  - **Hook:** \`useActivityEvent(eventType, handler)\` \u2014 \`ActivityEventHandler\` type exported for use with \`useCallback\`
@@ -684,7 +719,7 @@ ${HOOK_SNIPPETS["events:activity"]}
684
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.
685
720
 
686
721
  ## extend:identity \u2014 Identity Claim Enrichment
687
- 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.
688
723
  - **Permission required:** \`extend:identity\`
689
724
  - **Hook:** \`useExtendIdentity(handler)\` \u2014 \`ExtendIdentityHandler\` type exported for use with \`useCallback\`
690
725
  - **Handler signature:** \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
@@ -1089,7 +1124,7 @@ const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
1089
1124
  - \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
1090
1125
 
1091
1126
  ## useIdentityEvent(eventType, handler)
1092
- 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.
1093
1128
  - \`eventType: ${identityEventTypes}\`
1094
1129
  - \`handler: (event: IdentityEvent) => void\`
1095
1130
  - \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
@@ -1117,7 +1152,7 @@ ${HOOK_SNIPPETS_MEMOIZED["events:messaging"]}
1117
1152
  \`\`\`
1118
1153
 
1119
1154
  ## useActivityEvent(eventType, handler)
1120
- 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.
1121
1156
  - \`eventType: ${activityEventTypes} | '*'\` (domain-stripped)
1122
1157
  - \`handler: ActivityEventHandler\` \u2014 \`(event: ActivityEvent) => void\`
1123
1158
  - \`ActivityEvent: { eventName: string, data: Record<string, unknown> }\`
@@ -1505,13 +1540,13 @@ These rules must always be followed when writing extension code.
1505
1540
 
1506
1541
  ## Components
1507
1542
  - Use the \`ui.*\` namespace for components (\`<ui.Card>\`, \`<ui.Button>\`) \u2014 don't import components directly
1508
- - 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
1509
1544
  - Use \`<ui.ScrollArea>\` for content that may overflow
1510
1545
 
1511
1546
  ## Manifest
1512
1547
  - Always declare permissions in \`manifest.json\` before using capabilities
1513
1548
  - Don't use \`data.fetch\` without adding the domain to \`allowedDomains\` in manifest
1514
- - \`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
1515
1550
 
1516
1551
  ## Entry Point
1517
1552
  - Don't modify \`index.html\` \u2014 the extension entry point is always \`src/index.tsx\` via \`createExtension\`
@@ -1673,7 +1708,7 @@ var generateSurfaces = () => {
1673
1708
 
1674
1709
  # Surfaces
1675
1710
 
1676
- 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.
1677
1712
  Each surface maps to a specific layout position and is declared as a React component using
1678
1713
  the \`<Surface>\` wrapper from \`@stackable-labs/sdk-extension-react\`.
1679
1714
 
@@ -1710,15 +1745,15 @@ ${EXAMPLE_SNIPPETS.bootstrap}
1710
1745
  \`\`\`
1711
1746
 
1712
1747
  \`createExtension\` bootstraps the extension runtime \u2014 it handles the sandboxed iframe
1713
- communication, capability injection, and surface registration with the host.
1748
+ communication, capability injection, and surface registration with the framework.
1714
1749
 
1715
1750
  ## Surface Lifecycle
1716
1751
 
1717
- 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
1718
1753
  2. **Register** \u2014 \`createExtension\` scans the rendered tree for \`<Surface>\` components
1719
- 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
1720
1755
  4. **Update** \u2014 Surfaces re-render when props, state, or context data changes
1721
- 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
1722
1757
 
1723
1758
  ## Multi-Surface State
1724
1759
 
@@ -1748,31 +1783,32 @@ export const Header = () => {
1748
1783
  `;
1749
1784
  };
1750
1785
  var DLX = "pnpm --config.dlx-cache-max-age=0 dlx";
1786
+ var CLI_PKG = "@stackable-labs/cli-app-extension@latest";
1751
1787
  var CLI = {
1752
1788
  /** Scaffold a new extension project */
1753
- create: (name = "<extension-name>") => `${DLX} @stackable-labs/create-extension ${name}`,
1789
+ create: (name = "<extension-name>") => `${DLX} ${CLI_PKG} create ${name}`,
1754
1790
  /** Start dev servers with hot reload */
1755
- dev: `${DLX} @stackable-labs/cli-app-extension@latest dev`,
1791
+ dev: `${DLX} ${CLI_PKG} dev`,
1756
1792
  /** Deploy the extension (future) */
1757
- deploy: `${DLX} @stackable-labs/cli-app-extension@latest deploy`,
1793
+ deploy: `${DLX} ${CLI_PKG} deploy`,
1758
1794
  /** Validate the extension for common errors (coming soon) */
1759
- validate: `${DLX} @stackable-labs/cli-app-extension@latest validate`,
1795
+ validate: `${DLX} ${CLI_PKG} validate`,
1760
1796
  /** Scaffold from an existing extension */
1761
- scaffold: `${DLX} @stackable-labs/cli-app-extension@latest scaffold`,
1797
+ scaffold: `${DLX} ${CLI_PKG} scaffold`,
1762
1798
  /** Update an existing extension */
1763
- update: `${DLX} @stackable-labs/cli-app-extension@latest update`,
1799
+ update: `${DLX} ${CLI_PKG} update`,
1764
1800
  /** Manage CLI authentication (browser-based OAuth) */
1765
1801
  auth: {
1766
- login: `${DLX} @stackable-labs/cli-app-extension@latest auth login`,
1767
- logout: `${DLX} @stackable-labs/cli-app-extension@latest auth logout`,
1768
- status: `${DLX} @stackable-labs/cli-app-extension@latest auth status`
1802
+ login: `${DLX} ${CLI_PKG} auth login`,
1803
+ logout: `${DLX} ${CLI_PKG} auth logout`,
1804
+ status: `${DLX} ${CLI_PKG} auth status`
1769
1805
  },
1770
1806
  /** AI editor configuration tools (Skills + MCP) */
1771
1807
  ai: {
1772
1808
  /** Download AI editor config files (Stackable Skills) into your project */
1773
- scaffold: `${DLX} @stackable-labs/cli-app-extension@latest ai scaffold`,
1809
+ scaffold: `${DLX} ${CLI_PKG} ai scaffold`,
1774
1810
  /** Add Stackable MCP server config to your project */
1775
- mcp: `${DLX} @stackable-labs/cli-app-extension@latest ai mcp`
1811
+ mcp: `${DLX} ${CLI_PKG} ai mcp`
1776
1812
  }
1777
1813
  };
1778
1814
  var TEMPLATE_FLAVOR_META = {
@@ -1811,7 +1847,7 @@ Stackable supports two authoring paths. Both produce the same kind of extension
1811
1847
  | **Version control** | Auto-saved to cloud | Git-based |
1812
1848
  | **Deployment** | Link to extension + deploy | CLI deploy command |
1813
1849
 
1814
- 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).
1815
1851
 
1816
1852
  ## Prerequisites
1817
1853
 
@@ -1851,7 +1887,7 @@ The dev command:
1851
1887
  2. Creates a public Cloudflare tunnel to your local server
1852
1888
  3. Displays a **query parameter** you can use to preview your extension
1853
1889
 
1854
- ### Testing against your host app
1890
+ ### Testing against your extension
1855
1891
 
1856
1892
  The CLI outputs a query param like:
1857
1893
 
@@ -1859,16 +1895,17 @@ The CLI outputs a query param like:
1859
1895
  ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1860
1896
  \`\`\`
1861
1897
 
1862
- Copy this and **append it to your host application URL** to load your local extension
1863
- 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:
1864
1901
 
1865
1902
  \`\`\`
1866
- 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
1867
1904
  \`\`\`
1868
1905
 
1869
1906
  This override is **browser-session only** \u2014 no database changes, no shared state.
1870
1907
  Each developer gets isolated overrides. Changes you make locally appear immediately
1871
- in the host app via hot reload.
1908
+ in your extension via hot reload.
1872
1909
 
1873
1910
  Use \`--no-tunnel\` if you only want to run the local Vite dev server without a tunnel.
1874
1911
 
@@ -2040,9 +2077,9 @@ ${CLI.dev}
2040
2077
  1. Reads \`.env.stackable\` for cached App/Extension context (prompts if missing)
2041
2078
  2. Creates Cloudflare tunnels for the extension dev server
2042
2079
  3. Starts Vite dev servers with hot reload
2043
- 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
2044
2081
 
2045
- ### Host App Override
2082
+ ### Host-Site Override
2046
2083
 
2047
2084
  The CLI outputs a query param like:
2048
2085
 
@@ -2050,9 +2087,10 @@ The CLI outputs a query param like:
2050
2087
  ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
2051
2088
  \`\`\`
2052
2089
 
2053
- Append this to your deployed host app URL to load your local extension instead
2054
- of the production bundle. The override is browser-session only \u2014 no DB changes,
2055
- 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.
2056
2094
 
2057
2095
  ## validate *(coming soon)*
2058
2096
 
@@ -2194,7 +2232,7 @@ var generateExternalApis = () => {
2194
2232
  const fm = frontmatter({
2195
2233
  root: false,
2196
2234
  targets: ["*"],
2197
- 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",
2198
2236
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
2199
2237
  });
2200
2238
  return `${fm}
@@ -2202,7 +2240,7 @@ var generateExternalApis = () => {
2202
2240
  # External APIs
2203
2241
 
2204
2242
  Extensions can make direct HTTP requests to external services using the \`data.fetch\`
2205
- 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
2206
2244
  never makes raw network calls from the sandbox.
2207
2245
 
2208
2246
  ## Setup
@@ -2223,14 +2261,75 @@ Every domain your extension calls must be listed in \`allowedDomains\`:
2223
2261
 
2224
2262
  \`\`\`json
2225
2263
  {
2226
- "allowedDomains": ["api.example.com", "graphql.example.com"]
2264
+ "allowedDomains": ["api.example.com", "graphql.example.com", "*.myshopify.com"]
2227
2265
  }
2228
2266
  \`\`\`
2229
2267
 
2230
2268
  **Rules:**
2231
- - Exact hostnames only \u2014 no wildcards, no paths, no protocols
2232
- - The host rejects requests to unlisted domains
2233
- - 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.
2234
2333
 
2235
2334
  ### 3. Make Requests
2236
2335
 
@@ -2317,10 +2416,10 @@ const customer = await fetchCustomer(capabilities, customerId)
2317
2416
 
2318
2417
  | | data.fetch | data.query |
2319
2418
  |--|-----------|-----------|
2320
- | **Who handles the request** | Extension (via proxy) | Host application |
2419
+ | **Who handles the request** | Extension (via proxy) | Platform |
2321
2420
  | **Permission** | \`data:fetch\` | \`data:query\` |
2322
2421
  | **Domain config** | Required (\`allowedDomains\`) | Not needed |
2323
- | **Use when** | Calling external APIs directly | Host provides the API integration |
2422
+ | **Use when** | Calling external APIs directly | Platform provides the API integration |
2324
2423
 
2325
2424
  ## Error Handling
2326
2425
 
@@ -2379,7 +2478,7 @@ AI-powered smart insertion to place the component in the correct location.
2379
2478
 
2380
2479
  ### Surfaces
2381
2480
 
2382
- 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\`,
2383
2482
  \`slot.content\`, \`slot.footer\`). Surfaces already present in your code are
2384
2483
  filtered out. Clicking a surface adds it to your manifest and inserts a
2385
2484
  \`<Surface>\` block via AI smart insertion.
@@ -2448,7 +2547,7 @@ Studio toolbar.
2448
2547
 
2449
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.
2450
2549
 
2451
- 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.
2452
2551
  `;
2453
2552
  };
2454
2553
 
@@ -2599,7 +2698,7 @@ my-extension/
2599
2698
 
2600
2699
  ### manifest.json
2601
2700
 
2602
- The extension manifest declares what the extension needs from the host:
2701
+ The extension manifest declares what the extension needs from the framework:
2603
2702
 
2604
2703
  \`\`\`json
2605
2704
  {
@@ -2617,7 +2716,11 @@ The extension manifest declares what the extension needs from the host:
2617
2716
  | \`version\` | Semver version string |
2618
2717
  | \`targets\` | Surface slots the extension renders into |
2619
2718
  | \`permissions\` | Capabilities the extension uses |
2620
- | \`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.
2621
2724
 
2622
2725
  ### index.tsx \u2014 Entry Point
2623
2726
 
@@ -2628,9 +2731,9 @@ ${EXAMPLE_SNIPPETS.bootstrap}
2628
2731
  \`\`\`
2629
2732
 
2630
2733
  \`createExtension\` handles:
2631
- - Sandboxed iframe communication with the host
2734
+ - Sandboxed iframe communication with the framework
2632
2735
  - Capability injection (makes \`useCapabilities()\` work)
2633
- - Surface registration (maps \`<Surface id="...">\` to host layout slots)
2736
+ - Surface registration (maps \`<Surface id="...">\` to layout slots in the embedding application)
2634
2737
 
2635
2738
  **Do not modify \`index.html\`** \u2014 the extension always bootstraps through \`createExtension\`.
2636
2739
 
@@ -2690,17 +2793,17 @@ var generateStylingAndTheming = () => {
2690
2793
 
2691
2794
  # Styling & Theming
2692
2795
 
2693
- Extensions inherit the host application's theme automatically. The SDK component library
2694
- (\`ui.*\` namespace) renders inside the host's styling context, so colors, fonts, and
2695
- 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.
2696
2799
 
2697
- ## Host Theme Inheritance
2800
+ ## Theme Inheritance
2698
2801
 
2699
- The \`ui.*\` components automatically use the host's design tokens:
2700
- - **Colors** \u2014 text, backgrounds, borders adapt to the host's color scheme
2701
- - **Typography** \u2014 font family, sizes, and weights match the host
2702
- - **Spacing** \u2014 padding and margins follow the host's spacing scale
2703
- - **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
2704
2807
 
2705
2808
  No CSS or theme configuration is needed in the extension.
2706
2809
 
@@ -2772,7 +2875,7 @@ Use component props (not CSS) to control visual style:
2772
2875
 
2773
2876
  Extensions run in a sandboxed iframe. These constraints apply:
2774
2877
 
2775
- - **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
2776
2879
  - **No \`document\` access** \u2014 cannot inject stylesheets or modify the DOM directly
2777
2880
  - **No \`window.location\`** \u2014 cannot read or modify the URL
2778
2881
  - **Component-only styling** \u2014 use \`className\` on \`ui.*\` components, not raw HTML elements
@@ -2780,7 +2883,7 @@ Extensions run in a sandboxed iframe. These constraints apply:
2780
2883
 
2781
2884
  ## Responsive Design
2782
2885
 
2783
- 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
2784
2887
  narrow widths:
2785
2888
 
2786
2889
  \`\`\`tsx
@@ -2797,7 +2900,7 @@ narrow widths:
2797
2900
  - Assume a **narrow viewport** (~300-400px wide)
2798
2901
  - Use \`<ui.ScrollArea>\` for long lists or content
2799
2902
  - Avoid horizontal scrolling \u2014 stack elements vertically
2800
- - Test at the minimum panel width the host supports
2903
+ - Test at the minimum panel width the embedding application supports
2801
2904
  `;
2802
2905
  };
2803
2906
 
@@ -2885,16 +2988,36 @@ var parseSafelist = (css) => {
2885
2988
  return classes;
2886
2989
  };
2887
2990
  var classifyFamily = (cls) => {
2888
- if (cls.startsWith("dark:hover:text-")) return "Dark \u2014 hover text";
2889
- if (cls.startsWith("dark:hover:bg-")) return "Dark \u2014 hover background";
2890
- if (cls.startsWith("dark:text-")) return "Dark \u2014 text color";
2891
- if (cls.startsWith("dark:bg-")) return "Dark \u2014 background color";
2892
- if (cls.startsWith("dark:border-")) return "Dark \u2014 border color";
2893
- if (cls.startsWith("hover:opacity-")) return "Hover \u2014 opacity";
2894
- if (cls.startsWith("hover:text-")) return "Hover \u2014 text color";
2895
- if (cls.startsWith("hover:bg-")) return "Hover \u2014 background color";
2896
- if (cls.startsWith("focus:")) return "Focus";
2897
- if (cls.startsWith("disabled:")) return "Disabled";
2991
+ if (cls.startsWith("dark:hover:text-")) {
2992
+ return "Dark \u2014 hover text";
2993
+ }
2994
+ if (cls.startsWith("dark:hover:bg-")) {
2995
+ return "Dark \u2014 hover background";
2996
+ }
2997
+ if (cls.startsWith("dark:text-")) {
2998
+ return "Dark \u2014 text color";
2999
+ }
3000
+ if (cls.startsWith("dark:bg-")) {
3001
+ return "Dark \u2014 background color";
3002
+ }
3003
+ if (cls.startsWith("dark:border-")) {
3004
+ return "Dark \u2014 border color";
3005
+ }
3006
+ if (cls.startsWith("hover:opacity-")) {
3007
+ return "Hover \u2014 opacity";
3008
+ }
3009
+ if (cls.startsWith("hover:text-")) {
3010
+ return "Hover \u2014 text color";
3011
+ }
3012
+ if (cls.startsWith("hover:bg-")) {
3013
+ return "Hover \u2014 background color";
3014
+ }
3015
+ if (cls.startsWith("focus:")) {
3016
+ return "Focus";
3017
+ }
3018
+ if (cls.startsWith("disabled:")) {
3019
+ return "Disabled";
3020
+ }
2898
3021
  if (/^(m|mx|my|mt|mb|ml|mr)-/.test(cls)) {
2899
3022
  return "Margin";
2900
3023
  }
@@ -3103,14 +3226,14 @@ Apply them via the \`className\` prop on \`ui.*\` components:
3103
3226
  Always prefer:
3104
3227
  - Component variants (\`<ui.Button variant="primary">\`) over color overrides
3105
3228
  - Layout components (\`<ui.Stack>\`, \`<ui.Inline>\`) over manual flexbox class chains
3106
- - 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
3107
3230
 
3108
3231
  ## Safelist limits
3109
3232
 
3110
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:
3111
3234
 
3112
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.
3113
- - **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.
3114
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.
3115
3238
 
3116
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.
@@ -3263,9 +3386,9 @@ Focused examples showing each SDK capability in a working Surface component.
3263
3386
  Each example is self-contained \u2014 copy it into a surface file and add the
3264
3387
  corresponding permission to your \`manifest.json\`.
3265
3388
 
3266
- ## context.read \u2014 Reading Host Context
3389
+ ## context.read \u2014 Reading Platform Context
3267
3390
 
3268
- Read customer and session data provided by the host. The \`useContextData\`
3391
+ Read customer and session data provided by the framework. The \`useContextData\`
3269
3392
  hook handles loading state automatically.
3270
3393
 
3271
3394
  **Permission:** \`context:read\`
@@ -3274,9 +3397,9 @@ hook handles loading state automatically.
3274
3397
  ${EXAMPLE_SNIPPETS["context.read"]}
3275
3398
  \`\`\`
3276
3399
 
3277
- ## data.query \u2014 Host-Mediated Requests
3400
+ ## data.query \u2014 Platform-Mediated Requests
3278
3401
 
3279
- Send structured requests to the host application. The host handles the API
3402
+ Send structured requests to the platform. The framework handles the API
3280
3403
  call and returns the result \u2014 no \`allowedDomains\` needed.
3281
3404
 
3282
3405
  **Permission:** \`data:query\`
@@ -3288,7 +3411,8 @@ ${EXAMPLE_SNIPPETS["data.query"]}
3288
3411
  ## data.fetch \u2014 Direct HTTP Requests
3289
3412
 
3290
3413
  Make HTTP requests directly from the extension sandbox. The domain must be
3291
- 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.
3292
3416
 
3293
3417
  **Permission:** \`data:fetch\`
3294
3418
 
@@ -3298,7 +3422,7 @@ ${EXAMPLE_SNIPPETS["data.fetch"]}
3298
3422
 
3299
3423
  ## actions.toast \u2014 Toast Notifications
3300
3424
 
3301
- 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.
3302
3426
 
3303
3427
  **Permission:** \`actions:toast\`
3304
3428
 
@@ -3335,7 +3459,7 @@ surfaces, store-based navigation, and loading states.
3335
3459
  ## Bootstrap \u2014 Entry Point
3336
3460
 
3337
3461
  Set up your extension entry point in \`src/index.tsx\`. The \`createExtension\` factory
3338
- bootstraps the sandboxed runtime and registers all surfaces with the host.
3462
+ bootstraps the sandboxed runtime and registers all surfaces with the framework.
3339
3463
 
3340
3464
  \`\`\`tsx
3341
3465
  ${EXAMPLE_SNIPPETS.bootstrap}
@@ -3343,7 +3467,7 @@ ${EXAMPLE_SNIPPETS.bootstrap}
3343
3467
 
3344
3468
  ## Surfaces \u2014 Declaring UI Slots
3345
3469
 
3346
- 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.
3347
3471
  The \`id\` prop must match a target declared in your \`manifest.json\`.
3348
3472
 
3349
3473
  ### Header
@@ -3412,7 +3536,7 @@ var generateCookbookEvents = () => {
3412
3536
 
3413
3537
  # Events & Extensions
3414
3538
 
3415
- 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.
3416
3540
  Each event type has a dedicated hook \u2014 never use \`capabilities.events.*\` directly.
3417
3541
 
3418
3542
  ## Identity Events
@@ -3456,9 +3580,9 @@ ${EXAMPLE_SNIPPETS["events:messaging"]}
3456
3580
 
3457
3581
  ## Activity Events
3458
3582
 
3459
- Subscribe to host activity events like page views, clicks, and purchases.
3460
- Activity event names are domain-stripped \u2014 use \`useActivityEvent('product_view', ...)\`
3461
- 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'\`.
3462
3586
 
3463
3587
  **Permission:** \`events:activity\`
3464
3588
  **Well-known events:** ${activityEventTypes2}
@@ -4769,12 +4893,6 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
4769
4893
  pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
4770
4894
  pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
4771
4895
 
4772
- // ../../lib/contracts/src/custom.ts
4773
- var CUSTOM_ROLE = {
4774
- SUPER_ADMIN: "super_admin"
4775
- };
4776
- new Set(Object.values(CUSTOM_ROLE));
4777
-
4778
4896
  // ../../lib/utils-js/src/crypto.ts
4779
4897
  var getCrypto = () => {
4780
4898
  if (typeof globalThis !== "undefined" && globalThis.crypto) {
@@ -5005,13 +5123,21 @@ var createMcpServer = (options = {}) => {
5005
5123
  }
5006
5124
  if (!Array.isArray(manifest.allowedDomains)) {
5007
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
+ }
5008
5134
  }
5009
5135
  const permissions = manifest.permissions ?? [];
5010
5136
  if (permissions.includes("data:fetch") && Array.isArray(manifest.allowedDomains) && manifest.allowedDomains.length === 0) {
5011
5137
  errors.push('"data:fetch" permission declared but "allowedDomains" is empty. Add the domains your extension needs to fetch from.');
5012
5138
  }
5013
5139
  if (errors.length === 0) {
5014
- 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.");
5015
5141
  }
5016
5142
  return errorContent(`Manifest validation failed:
5017
5143
  ${errors.map((e) => `- ${e}`).join("\n")}`);