@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/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { IDENTITY_EVENT, ACTIVITY_EVENT, SURFACE_TARGET, TEMPLATE_FLAVORS, PERMISSIONS, CAPABILITY_PERMISSION_MAP, EVENT_HOOK_PERMISSION_MAP, ALLOWED_ICONS, UI_TAGS, UI_TAG_ATTRIBUTES, tagToComponentName } from '@stackable-labs/sdk-extension-contracts';
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';
@@ -375,6 +376,22 @@ var frontmatter = (meta) => {
375
376
  var generateCoreReference = () => CORE_CONTENT;
376
377
 
377
378
  // ../../sdk/extension/ai-docs/src/generators/components.ts
379
+ var CATEGORY_ORDER = [
380
+ "layout",
381
+ "text",
382
+ "input",
383
+ "navigation",
384
+ "feedback",
385
+ "composite"
386
+ ];
387
+ var CATEGORY_LABELS = {
388
+ layout: "Layout",
389
+ text: "Text",
390
+ input: "Input",
391
+ navigation: "Navigation",
392
+ feedback: "Feedback",
393
+ composite: "Composite"
394
+ };
378
395
  var generateComponents = () => {
379
396
  const fm = frontmatter({
380
397
  root: false,
@@ -382,13 +399,31 @@ var generateComponents = () => {
382
399
  description: "Available UI components with allowed attributes per tag",
383
400
  globs: ["packages/extension/src/**/*.tsx"]
384
401
  });
385
- const componentLines = [];
402
+ const tagsByCategory = /* @__PURE__ */ new Map();
386
403
  for (const tag of UI_TAGS) {
387
- const attrs = UI_TAG_ATTRIBUTES[tag];
388
- const name = tagToComponentName(tag);
389
- componentLines.push(`### \`<ui.${name}>\` (\`${tag}\`)`);
390
- componentLines.push(`Allowed attributes: ${attrs.map((a2) => `\`${a2}\``).join(", ")}`);
404
+ const category = UI_TAG_CATEGORIES[tag];
405
+ const bucket = tagsByCategory.get(category) ?? [];
406
+ bucket.push(tag);
407
+ tagsByCategory.set(category, bucket);
408
+ }
409
+ for (const tags of tagsByCategory.values()) {
410
+ tags.sort();
411
+ }
412
+ const componentLines = [];
413
+ for (const category of CATEGORY_ORDER) {
414
+ const tags = tagsByCategory.get(category);
415
+ if (!tags || tags.length === 0) {
416
+ continue;
417
+ }
418
+ componentLines.push(`## ${CATEGORY_LABELS[category]}`);
391
419
  componentLines.push("");
420
+ for (const tag of tags) {
421
+ const attrs = UI_TAG_ATTRIBUTES[tag];
422
+ const name = tagToComponentName(tag);
423
+ componentLines.push(`### \`<ui.${name}>\` (\`${tag}\`)`);
424
+ componentLines.push(`Allowed attributes: ${attrs.map((a2) => `\`${a2}\``).join(", ")}`);
425
+ componentLines.push("");
426
+ }
392
427
  }
393
428
  const iconList = ALLOWED_ICONS.map((icon) => `\`${icon}\``).join(", ");
394
429
  return `${fm}
@@ -485,12 +520,12 @@ Access capabilities via the \`useCapabilities()\` hook:
485
520
  const capabilities = useCapabilities()
486
521
  \`\`\`
487
522
 
488
- ## data.query \u2014 Host-Mediated Requests
489
- 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.
490
525
  - **Permission required:** \`data:query\`
491
526
  - **Usage:** \`capabilities.data.query<T>(payload: ApiRequest): Promise<T>\`
492
527
  - **ApiRequest shape:** \`{ action: string; [key: string]: unknown }\`
493
- - **When to use:** When the host application handles the API integration
528
+ - **When to use:** When the platform handles the API integration
494
529
 
495
530
  \`\`\`tsx
496
531
  const result = await capabilities.data.query<Customer>({
@@ -536,8 +571,8 @@ const result = await capabilities.data.fetch('https://api.example.com/orders', {
536
571
 
537
572
  > See [Instance Settings](./instance-settings) for the full schema-declaration + storage-mode story, including which field types accept \`secret: true\`.
538
573
 
539
- ## context.read \u2014 Read Host Context
540
- 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.).
541
576
  - **Permission required:** \`context:read\`
542
577
  - **Usage:** \`capabilities.context.read(): Promise<ContextData>\`
543
578
  - **ContextData shape:** \`{ customerId?: string, customerEmail?: string, settings?: Record<string, unknown>, [key: string]: unknown }\`
@@ -565,7 +600,7 @@ Non-secret settings declared in \`settingsSchema\` are automatically available v
565
600
  - No new permission needed \u2014 \`context:read\` is the only gate
566
601
 
567
602
  ## actions.toast \u2014 Show Toast Notifications
568
- Display a toast notification in the host UI.
603
+ Display a toast notification in the framework widget's UI.
569
604
  - **Permission required:** \`actions:toast\`
570
605
  - **Usage:** \`capabilities.actions.toast(payload: ToastPayload): Promise<void>\`
571
606
  - **ToastPayload:** \`{ message: string, type?: 'success'|'error'|'info'|'warning', duration?: number }\`
@@ -574,8 +609,8 @@ Display a toast notification in the host UI.
574
609
  capabilities.actions.toast({ message: 'Saved!', type: 'success' })
575
610
  \`\`\`
576
611
 
577
- ## actions.invoke \u2014 Invoke Host Actions
578
- 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).
579
614
  - **Permission required:** \`actions:invoke\`
580
615
  - **Usage:** \`capabilities.actions.invoke<T>(action: string, payload?: Record<string, unknown>): Promise<T>\`
581
616
  - **Available actions:**
@@ -604,7 +639,7 @@ await capabilities.actions.invoke('setConversationFields', [
604
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).
605
640
 
606
641
  ## events:identity \u2014 Identity Event Subscription
607
- 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.
608
643
  - **Permission required:** \`events:identity\`
609
644
  - **Manifest events array:** Declare specific events to listen for (e.g. \`["identity:login", "identity:logout"]\`)
610
645
  - **Hook:** \`useIdentityEvent(eventType, handler)\`
@@ -644,7 +679,7 @@ ${HOOK_SNIPPETS["events:messaging"]}
644
679
  \`\`\`
645
680
 
646
681
  ## events:activity \u2014 Activity Event Subscription
647
- 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.
648
683
  - **Permission required:** \`events:activity\`
649
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
650
685
  - **Hook:** \`useActivityEvent(eventType, handler)\` \u2014 \`ActivityEventHandler\` type exported for use with \`useCallback\`
@@ -677,7 +712,7 @@ ${HOOK_SNIPPETS["events:activity"]}
677
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.
678
713
 
679
714
  ## extend:identity \u2014 Identity Claim Enrichment
680
- 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.
681
716
  - **Permission required:** \`extend:identity\`
682
717
  - **Hook:** \`useExtendIdentity(handler)\` \u2014 \`ExtendIdentityHandler\` type exported for use with \`useCallback\`
683
718
  - **Handler signature:** \`(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>\`
@@ -1082,7 +1117,7 @@ const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
1082
1117
  - \`subscribe(listener: (state: T) => void): () => void\` \u2014 subscribe, returns unsubscribe fn
1083
1118
 
1084
1119
  ## useIdentityEvent(eventType, handler)
1085
- 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.
1086
1121
  - \`eventType: ${identityEventTypes}\`
1087
1122
  - \`handler: (event: IdentityEvent) => void\`
1088
1123
  - \`IdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }\`
@@ -1110,7 +1145,7 @@ ${HOOK_SNIPPETS_MEMOIZED["events:messaging"]}
1110
1145
  \`\`\`
1111
1146
 
1112
1147
  ## useActivityEvent(eventType, handler)
1113
- 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.
1114
1149
  - \`eventType: ${activityEventTypes} | '*'\` (domain-stripped)
1115
1150
  - \`handler: ActivityEventHandler\` \u2014 \`(event: ActivityEvent) => void\`
1116
1151
  - \`ActivityEvent: { eventName: string, data: Record<string, unknown> }\`
@@ -1498,13 +1533,13 @@ These rules must always be followed when writing extension code.
1498
1533
 
1499
1534
  ## Components
1500
1535
  - Use the \`ui.*\` namespace for components (\`<ui.Card>\`, \`<ui.Button>\`) \u2014 don't import components directly
1501
- - 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
1502
1537
  - Use \`<ui.ScrollArea>\` for content that may overflow
1503
1538
 
1504
1539
  ## Manifest
1505
1540
  - Always declare permissions in \`manifest.json\` before using capabilities
1506
1541
  - Don't use \`data.fetch\` without adding the domain to \`allowedDomains\` in manifest
1507
- - \`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
1508
1543
 
1509
1544
  ## Entry Point
1510
1545
  - Don't modify \`index.html\` \u2014 the extension entry point is always \`src/index.tsx\` via \`createExtension\`
@@ -1666,7 +1701,7 @@ var generateSurfaces = () => {
1666
1701
 
1667
1702
  # Surfaces
1668
1703
 
1669
- 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.
1670
1705
  Each surface maps to a specific layout position and is declared as a React component using
1671
1706
  the \`<Surface>\` wrapper from \`@stackable-labs/sdk-extension-react\`.
1672
1707
 
@@ -1703,15 +1738,15 @@ ${EXAMPLE_SNIPPETS.bootstrap}
1703
1738
  \`\`\`
1704
1739
 
1705
1740
  \`createExtension\` bootstraps the extension runtime \u2014 it handles the sandboxed iframe
1706
- communication, capability injection, and surface registration with the host.
1741
+ communication, capability injection, and surface registration with the framework.
1707
1742
 
1708
1743
  ## Surface Lifecycle
1709
1744
 
1710
- 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
1711
1746
  2. **Register** \u2014 \`createExtension\` scans the rendered tree for \`<Surface>\` components
1712
- 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
1713
1748
  4. **Update** \u2014 Surfaces re-render when props, state, or context data changes
1714
- 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
1715
1750
 
1716
1751
  ## Multi-Surface State
1717
1752
 
@@ -1741,31 +1776,32 @@ export const Header = () => {
1741
1776
  `;
1742
1777
  };
1743
1778
  var DLX = "pnpm --config.dlx-cache-max-age=0 dlx";
1779
+ var CLI_PKG = "@stackable-labs/cli-app-extension@latest";
1744
1780
  var CLI = {
1745
1781
  /** Scaffold a new extension project */
1746
- create: (name = "<extension-name>") => `${DLX} @stackable-labs/create-extension ${name}`,
1782
+ create: (name = "<extension-name>") => `${DLX} ${CLI_PKG} create ${name}`,
1747
1783
  /** Start dev servers with hot reload */
1748
- dev: `${DLX} @stackable-labs/cli-app-extension@latest dev`,
1784
+ dev: `${DLX} ${CLI_PKG} dev`,
1749
1785
  /** Deploy the extension (future) */
1750
- deploy: `${DLX} @stackable-labs/cli-app-extension@latest deploy`,
1786
+ deploy: `${DLX} ${CLI_PKG} deploy`,
1751
1787
  /** Validate the extension for common errors (coming soon) */
1752
- validate: `${DLX} @stackable-labs/cli-app-extension@latest validate`,
1788
+ validate: `${DLX} ${CLI_PKG} validate`,
1753
1789
  /** Scaffold from an existing extension */
1754
- scaffold: `${DLX} @stackable-labs/cli-app-extension@latest scaffold`,
1790
+ scaffold: `${DLX} ${CLI_PKG} scaffold`,
1755
1791
  /** Update an existing extension */
1756
- update: `${DLX} @stackable-labs/cli-app-extension@latest update`,
1792
+ update: `${DLX} ${CLI_PKG} update`,
1757
1793
  /** Manage CLI authentication (browser-based OAuth) */
1758
1794
  auth: {
1759
- login: `${DLX} @stackable-labs/cli-app-extension@latest auth login`,
1760
- logout: `${DLX} @stackable-labs/cli-app-extension@latest auth logout`,
1761
- status: `${DLX} @stackable-labs/cli-app-extension@latest auth status`
1795
+ login: `${DLX} ${CLI_PKG} auth login`,
1796
+ logout: `${DLX} ${CLI_PKG} auth logout`,
1797
+ status: `${DLX} ${CLI_PKG} auth status`
1762
1798
  },
1763
1799
  /** AI editor configuration tools (Skills + MCP) */
1764
1800
  ai: {
1765
1801
  /** Download AI editor config files (Stackable Skills) into your project */
1766
- scaffold: `${DLX} @stackable-labs/cli-app-extension@latest ai scaffold`,
1802
+ scaffold: `${DLX} ${CLI_PKG} ai scaffold`,
1767
1803
  /** Add Stackable MCP server config to your project */
1768
- mcp: `${DLX} @stackable-labs/cli-app-extension@latest ai mcp`
1804
+ mcp: `${DLX} ${CLI_PKG} ai mcp`
1769
1805
  }
1770
1806
  };
1771
1807
  var TEMPLATE_FLAVOR_META = {
@@ -1804,7 +1840,7 @@ Stackable supports two authoring paths. Both produce the same kind of extension
1804
1840
  | **Version control** | Auto-saved to cloud | Git-based |
1805
1841
  | **Deployment** | Link to extension + deploy | CLI deploy command |
1806
1842
 
1807
- 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).
1808
1844
 
1809
1845
  ## Prerequisites
1810
1846
 
@@ -1844,7 +1880,7 @@ The dev command:
1844
1880
  2. Creates a public Cloudflare tunnel to your local server
1845
1881
  3. Displays a **query parameter** you can use to preview your extension
1846
1882
 
1847
- ### Testing against your host app
1883
+ ### Testing against your extension
1848
1884
 
1849
1885
  The CLI outputs a query param like:
1850
1886
 
@@ -1852,16 +1888,17 @@ The CLI outputs a query param like:
1852
1888
  ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
1853
1889
  \`\`\`
1854
1890
 
1855
- Copy this and **append it to your host application URL** to load your local extension
1856
- 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:
1857
1894
 
1858
1895
  \`\`\`
1859
- 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
1860
1897
  \`\`\`
1861
1898
 
1862
1899
  This override is **browser-session only** \u2014 no database changes, no shared state.
1863
1900
  Each developer gets isolated overrides. Changes you make locally appear immediately
1864
- in the host app via hot reload.
1901
+ in your extension via hot reload.
1865
1902
 
1866
1903
  Use \`--no-tunnel\` if you only want to run the local Vite dev server without a tunnel.
1867
1904
 
@@ -2033,9 +2070,9 @@ ${CLI.dev}
2033
2070
  1. Reads \`.env.stackable\` for cached App/Extension context (prompts if missing)
2034
2071
  2. Creates Cloudflare tunnels for the extension dev server
2035
2072
  3. Starts Vite dev servers with hot reload
2036
- 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
2037
2074
 
2038
- ### Host App Override
2075
+ ### Host-Site Override
2039
2076
 
2040
2077
  The CLI outputs a query param like:
2041
2078
 
@@ -2043,9 +2080,10 @@ The CLI outputs a query param like:
2043
2080
  ?_stackable_dev=ext-123%3Ahttps%3A%2F%2Fabc.trycloudflare.com
2044
2081
  \`\`\`
2045
2082
 
2046
- Append this to your deployed host app URL to load your local extension instead
2047
- of the production bundle. The override is browser-session only \u2014 no DB changes,
2048
- 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.
2049
2087
 
2050
2088
  ## validate *(coming soon)*
2051
2089
 
@@ -2187,7 +2225,7 @@ var generateExternalApis = () => {
2187
2225
  const fm = frontmatter({
2188
2226
  root: false,
2189
2227
  targets: ["*"],
2190
- 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",
2191
2229
  globs: ["packages/extension/src/**/*.tsx", "packages/extension/src/**/*.ts", "packages/extension/public/manifest.json"]
2192
2230
  });
2193
2231
  return `${fm}
@@ -2195,7 +2233,7 @@ var generateExternalApis = () => {
2195
2233
  # External APIs
2196
2234
 
2197
2235
  Extensions can make direct HTTP requests to external services using the \`data.fetch\`
2198
- 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
2199
2237
  never makes raw network calls from the sandbox.
2200
2238
 
2201
2239
  ## Setup
@@ -2216,14 +2254,75 @@ Every domain your extension calls must be listed in \`allowedDomains\`:
2216
2254
 
2217
2255
  \`\`\`json
2218
2256
  {
2219
- "allowedDomains": ["api.example.com", "graphql.example.com"]
2257
+ "allowedDomains": ["api.example.com", "graphql.example.com", "*.myshopify.com"]
2220
2258
  }
2221
2259
  \`\`\`
2222
2260
 
2223
2261
  **Rules:**
2224
- - Exact hostnames only \u2014 no wildcards, no paths, no protocols
2225
- - The host rejects requests to unlisted domains
2226
- - 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.
2227
2326
 
2228
2327
  ### 3. Make Requests
2229
2328
 
@@ -2310,10 +2409,10 @@ const customer = await fetchCustomer(capabilities, customerId)
2310
2409
 
2311
2410
  | | data.fetch | data.query |
2312
2411
  |--|-----------|-----------|
2313
- | **Who handles the request** | Extension (via proxy) | Host application |
2412
+ | **Who handles the request** | Extension (via proxy) | Platform |
2314
2413
  | **Permission** | \`data:fetch\` | \`data:query\` |
2315
2414
  | **Domain config** | Required (\`allowedDomains\`) | Not needed |
2316
- | **Use when** | Calling external APIs directly | Host provides the API integration |
2415
+ | **Use when** | Calling external APIs directly | Platform provides the API integration |
2317
2416
 
2318
2417
  ## Error Handling
2319
2418
 
@@ -2372,7 +2471,7 @@ AI-powered smart insertion to place the component in the correct location.
2372
2471
 
2373
2472
  ### Surfaces
2374
2473
 
2375
- 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\`,
2376
2475
  \`slot.content\`, \`slot.footer\`). Surfaces already present in your code are
2377
2476
  filtered out. Clicking a surface adds it to your manifest and inserts a
2378
2477
  \`<Surface>\` block via AI smart insertion.
@@ -2441,7 +2540,7 @@ Studio toolbar.
2441
2540
 
2442
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.
2443
2542
 
2444
- 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.
2445
2544
  `;
2446
2545
  };
2447
2546
 
@@ -2592,7 +2691,7 @@ my-extension/
2592
2691
 
2593
2692
  ### manifest.json
2594
2693
 
2595
- The extension manifest declares what the extension needs from the host:
2694
+ The extension manifest declares what the extension needs from the framework:
2596
2695
 
2597
2696
  \`\`\`json
2598
2697
  {
@@ -2610,7 +2709,11 @@ The extension manifest declares what the extension needs from the host:
2610
2709
  | \`version\` | Semver version string |
2611
2710
  | \`targets\` | Surface slots the extension renders into |
2612
2711
  | \`permissions\` | Capabilities the extension uses |
2613
- | \`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.
2614
2717
 
2615
2718
  ### index.tsx \u2014 Entry Point
2616
2719
 
@@ -2621,9 +2724,9 @@ ${EXAMPLE_SNIPPETS.bootstrap}
2621
2724
  \`\`\`
2622
2725
 
2623
2726
  \`createExtension\` handles:
2624
- - Sandboxed iframe communication with the host
2727
+ - Sandboxed iframe communication with the framework
2625
2728
  - Capability injection (makes \`useCapabilities()\` work)
2626
- - Surface registration (maps \`<Surface id="...">\` to host layout slots)
2729
+ - Surface registration (maps \`<Surface id="...">\` to layout slots in the embedding application)
2627
2730
 
2628
2731
  **Do not modify \`index.html\`** \u2014 the extension always bootstraps through \`createExtension\`.
2629
2732
 
@@ -2683,17 +2786,17 @@ var generateStylingAndTheming = () => {
2683
2786
 
2684
2787
  # Styling & Theming
2685
2788
 
2686
- Extensions inherit the host application's theme automatically. The SDK component library
2687
- (\`ui.*\` namespace) renders inside the host's styling context, so colors, fonts, and
2688
- 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.
2689
2792
 
2690
- ## Host Theme Inheritance
2793
+ ## Theme Inheritance
2691
2794
 
2692
- The \`ui.*\` components automatically use the host's design tokens:
2693
- - **Colors** \u2014 text, backgrounds, borders adapt to the host's color scheme
2694
- - **Typography** \u2014 font family, sizes, and weights match the host
2695
- - **Spacing** \u2014 padding and margins follow the host's spacing scale
2696
- - **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
2697
2800
 
2698
2801
  No CSS or theme configuration is needed in the extension.
2699
2802
 
@@ -2765,7 +2868,7 @@ Use component props (not CSS) to control visual style:
2765
2868
 
2766
2869
  Extensions run in a sandboxed iframe. These constraints apply:
2767
2870
 
2768
- - **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
2769
2872
  - **No \`document\` access** \u2014 cannot inject stylesheets or modify the DOM directly
2770
2873
  - **No \`window.location\`** \u2014 cannot read or modify the URL
2771
2874
  - **Component-only styling** \u2014 use \`className\` on \`ui.*\` components, not raw HTML elements
@@ -2773,7 +2876,7 @@ Extensions run in a sandboxed iframe. These constraints apply:
2773
2876
 
2774
2877
  ## Responsive Design
2775
2878
 
2776
- 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
2777
2880
  narrow widths:
2778
2881
 
2779
2882
  \`\`\`tsx
@@ -2790,7 +2893,7 @@ narrow widths:
2790
2893
  - Assume a **narrow viewport** (~300-400px wide)
2791
2894
  - Use \`<ui.ScrollArea>\` for long lists or content
2792
2895
  - Avoid horizontal scrolling \u2014 stack elements vertically
2793
- - Test at the minimum panel width the host supports
2896
+ - Test at the minimum panel width the embedding application supports
2794
2897
  `;
2795
2898
  };
2796
2899
 
@@ -2878,16 +2981,36 @@ var parseSafelist = (css) => {
2878
2981
  return classes;
2879
2982
  };
2880
2983
  var classifyFamily = (cls) => {
2881
- if (cls.startsWith("dark:hover:text-")) return "Dark \u2014 hover text";
2882
- if (cls.startsWith("dark:hover:bg-")) return "Dark \u2014 hover background";
2883
- if (cls.startsWith("dark:text-")) return "Dark \u2014 text color";
2884
- if (cls.startsWith("dark:bg-")) return "Dark \u2014 background color";
2885
- if (cls.startsWith("dark:border-")) return "Dark \u2014 border color";
2886
- if (cls.startsWith("hover:opacity-")) return "Hover \u2014 opacity";
2887
- if (cls.startsWith("hover:text-")) return "Hover \u2014 text color";
2888
- if (cls.startsWith("hover:bg-")) return "Hover \u2014 background color";
2889
- if (cls.startsWith("focus:")) return "Focus";
2890
- if (cls.startsWith("disabled:")) return "Disabled";
2984
+ if (cls.startsWith("dark:hover:text-")) {
2985
+ return "Dark \u2014 hover text";
2986
+ }
2987
+ if (cls.startsWith("dark:hover:bg-")) {
2988
+ return "Dark \u2014 hover background";
2989
+ }
2990
+ if (cls.startsWith("dark:text-")) {
2991
+ return "Dark \u2014 text color";
2992
+ }
2993
+ if (cls.startsWith("dark:bg-")) {
2994
+ return "Dark \u2014 background color";
2995
+ }
2996
+ if (cls.startsWith("dark:border-")) {
2997
+ return "Dark \u2014 border color";
2998
+ }
2999
+ if (cls.startsWith("hover:opacity-")) {
3000
+ return "Hover \u2014 opacity";
3001
+ }
3002
+ if (cls.startsWith("hover:text-")) {
3003
+ return "Hover \u2014 text color";
3004
+ }
3005
+ if (cls.startsWith("hover:bg-")) {
3006
+ return "Hover \u2014 background color";
3007
+ }
3008
+ if (cls.startsWith("focus:")) {
3009
+ return "Focus";
3010
+ }
3011
+ if (cls.startsWith("disabled:")) {
3012
+ return "Disabled";
3013
+ }
2891
3014
  if (/^(m|mx|my|mt|mb|ml|mr)-/.test(cls)) {
2892
3015
  return "Margin";
2893
3016
  }
@@ -3096,14 +3219,14 @@ Apply them via the \`className\` prop on \`ui.*\` components:
3096
3219
  Always prefer:
3097
3220
  - Component variants (\`<ui.Button variant="primary">\`) over color overrides
3098
3221
  - Layout components (\`<ui.Stack>\`, \`<ui.Inline>\`) over manual flexbox class chains
3099
- - 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
3100
3223
 
3101
3224
  ## Safelist limits
3102
3225
 
3103
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:
3104
3227
 
3105
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.
3106
- - **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.
3107
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.
3108
3231
 
3109
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.
@@ -3256,9 +3379,9 @@ Focused examples showing each SDK capability in a working Surface component.
3256
3379
  Each example is self-contained \u2014 copy it into a surface file and add the
3257
3380
  corresponding permission to your \`manifest.json\`.
3258
3381
 
3259
- ## context.read \u2014 Reading Host Context
3382
+ ## context.read \u2014 Reading Platform Context
3260
3383
 
3261
- Read customer and session data provided by the host. The \`useContextData\`
3384
+ Read customer and session data provided by the framework. The \`useContextData\`
3262
3385
  hook handles loading state automatically.
3263
3386
 
3264
3387
  **Permission:** \`context:read\`
@@ -3267,9 +3390,9 @@ hook handles loading state automatically.
3267
3390
  ${EXAMPLE_SNIPPETS["context.read"]}
3268
3391
  \`\`\`
3269
3392
 
3270
- ## data.query \u2014 Host-Mediated Requests
3393
+ ## data.query \u2014 Platform-Mediated Requests
3271
3394
 
3272
- Send structured requests to the host application. The host handles the API
3395
+ Send structured requests to the platform. The framework handles the API
3273
3396
  call and returns the result \u2014 no \`allowedDomains\` needed.
3274
3397
 
3275
3398
  **Permission:** \`data:query\`
@@ -3281,7 +3404,8 @@ ${EXAMPLE_SNIPPETS["data.query"]}
3281
3404
  ## data.fetch \u2014 Direct HTTP Requests
3282
3405
 
3283
3406
  Make HTTP requests directly from the extension sandbox. The domain must be
3284
- 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.
3285
3409
 
3286
3410
  **Permission:** \`data:fetch\`
3287
3411
 
@@ -3291,7 +3415,7 @@ ${EXAMPLE_SNIPPETS["data.fetch"]}
3291
3415
 
3292
3416
  ## actions.toast \u2014 Toast Notifications
3293
3417
 
3294
- 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.
3295
3419
 
3296
3420
  **Permission:** \`actions:toast\`
3297
3421
 
@@ -3328,7 +3452,7 @@ surfaces, store-based navigation, and loading states.
3328
3452
  ## Bootstrap \u2014 Entry Point
3329
3453
 
3330
3454
  Set up your extension entry point in \`src/index.tsx\`. The \`createExtension\` factory
3331
- bootstraps the sandboxed runtime and registers all surfaces with the host.
3455
+ bootstraps the sandboxed runtime and registers all surfaces with the framework.
3332
3456
 
3333
3457
  \`\`\`tsx
3334
3458
  ${EXAMPLE_SNIPPETS.bootstrap}
@@ -3336,7 +3460,7 @@ ${EXAMPLE_SNIPPETS.bootstrap}
3336
3460
 
3337
3461
  ## Surfaces \u2014 Declaring UI Slots
3338
3462
 
3339
- 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.
3340
3464
  The \`id\` prop must match a target declared in your \`manifest.json\`.
3341
3465
 
3342
3466
  ### Header
@@ -3405,7 +3529,7 @@ var generateCookbookEvents = () => {
3405
3529
 
3406
3530
  # Events & Extensions
3407
3531
 
3408
- 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.
3409
3533
  Each event type has a dedicated hook \u2014 never use \`capabilities.events.*\` directly.
3410
3534
 
3411
3535
  ## Identity Events
@@ -3449,9 +3573,9 @@ ${EXAMPLE_SNIPPETS["events:messaging"]}
3449
3573
 
3450
3574
  ## Activity Events
3451
3575
 
3452
- Subscribe to host activity events like page views, clicks, and purchases.
3453
- Activity event names are domain-stripped \u2014 use \`useActivityEvent('product_view', ...)\`
3454
- 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'\`.
3455
3579
 
3456
3580
  **Permission:** \`events:activity\`
3457
3581
  **Well-known events:** ${activityEventTypes2}
@@ -4762,12 +4886,6 @@ pair(EXAMPLE_SNIPPETS, EXAMPLE_SNIPPETS2);
4762
4886
  pair(CAPABILITY_SNIPPETS, CAPABILITY_SNIPPETS2);
4763
4887
  pair(EVENT_SNIPPETS, EVENT_SNIPPETS2);
4764
4888
 
4765
- // ../../lib/contracts/src/custom.ts
4766
- var CUSTOM_ROLE = {
4767
- SUPER_ADMIN: "super_admin"
4768
- };
4769
- new Set(Object.values(CUSTOM_ROLE));
4770
-
4771
4889
  // ../../lib/utils-auth/src/constants.ts
4772
4890
  var STANDALONE_CLIENT_DATA = {
4773
4891
  CLI: { name: "@stackable-labs/cli-app-extension", authFile: "cli-auth.json" },
@@ -4954,13 +5072,21 @@ var createMcpServer = (options = {}) => {
4954
5072
  }
4955
5073
  if (!Array.isArray(manifest.allowedDomains)) {
4956
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
+ }
4957
5083
  }
4958
5084
  const permissions = manifest.permissions ?? [];
4959
5085
  if (permissions.includes("data:fetch") && Array.isArray(manifest.allowedDomains) && manifest.allowedDomains.length === 0) {
4960
5086
  errors.push('"data:fetch" permission declared but "allowedDomains" is empty. Add the domains your extension needs to fetch from.');
4961
5087
  }
4962
5088
  if (errors.length === 0) {
4963
- 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.");
4964
5090
  }
4965
5091
  return errorContent(`Manifest validation failed:
4966
5092
  ${errors.map((e) => `- ${e}`).join("\n")}`);