@stackable-labs/mcp-app-extension 1.6.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.
- package/dist/index.js +149 -78
- package/dist/server.js +149 -78
- 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
|
|
530
|
-
The
|
|
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
|
|
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
|
|
581
|
-
Read
|
|
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
|
|
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
|
|
619
|
-
Trigger
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1748
|
+
communication, capability injection, and surface registration with the framework.
|
|
1748
1749
|
|
|
1749
1750
|
## Surface Lifecycle
|
|
1750
1751
|
|
|
1751
|
-
1. **Mount** \u2014
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1898
|
-
|
|
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-
|
|
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
|
|
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
|
|
2080
|
+
4. Displays a \`_stackable_dev\` query param to append to a host site's URL
|
|
2079
2081
|
|
|
2080
|
-
### Host
|
|
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
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
|
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
|
|
2267
|
-
-
|
|
2268
|
-
-
|
|
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) |
|
|
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 |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2734
|
+
- Sandboxed iframe communication with the framework
|
|
2667
2735
|
- Capability injection (makes \`useCapabilities()\` work)
|
|
2668
|
-
- Surface registration (maps \`<Surface id="...">\` to
|
|
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
|
|
2729
|
-
(\`ui.*\` namespace) renders inside the
|
|
2730
|
-
|
|
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
|
-
##
|
|
2800
|
+
## Theme Inheritance
|
|
2733
2801
|
|
|
2734
|
-
The \`ui.*\` components automatically use the
|
|
2735
|
-
- **Colors** \u2014 text, backgrounds, borders adapt to the
|
|
2736
|
-
- **Typography** \u2014 font family, sizes, and weights match the
|
|
2737
|
-
- **Spacing** \u2014 padding and margins follow the
|
|
2738
|
-
- **Dark mode** \u2014 components respond to the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3389
|
+
## context.read \u2014 Reading Platform Context
|
|
3322
3390
|
|
|
3323
|
-
Read customer and session data provided by the
|
|
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
|
|
3400
|
+
## data.query \u2014 Platform-Mediated Requests
|
|
3333
3401
|
|
|
3334
|
-
Send structured requests to the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
523
|
-
The
|
|
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
|
|
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
|
|
574
|
-
Read
|
|
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
|
|
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
|
|
612
|
-
Trigger
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1741
|
+
communication, capability injection, and surface registration with the framework.
|
|
1741
1742
|
|
|
1742
1743
|
## Surface Lifecycle
|
|
1743
1744
|
|
|
1744
|
-
1. **Mount** \u2014
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1891
|
-
|
|
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-
|
|
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
|
|
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
|
|
2073
|
+
4. Displays a \`_stackable_dev\` query param to append to a host site's URL
|
|
2072
2074
|
|
|
2073
|
-
### Host
|
|
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
|
|
2082
|
-
|
|
2083
|
-
|
|
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
|
|
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
|
|
2260
|
-
-
|
|
2261
|
-
-
|
|
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) |
|
|
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 |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2727
|
+
- Sandboxed iframe communication with the framework
|
|
2660
2728
|
- Capability injection (makes \`useCapabilities()\` work)
|
|
2661
|
-
- Surface registration (maps \`<Surface id="...">\` to
|
|
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
|
|
2722
|
-
(\`ui.*\` namespace) renders inside the
|
|
2723
|
-
|
|
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
|
-
##
|
|
2793
|
+
## Theme Inheritance
|
|
2726
2794
|
|
|
2727
|
-
The \`ui.*\` components automatically use the
|
|
2728
|
-
- **Colors** \u2014 text, backgrounds, borders adapt to the
|
|
2729
|
-
- **Typography** \u2014 font family, sizes, and weights match the
|
|
2730
|
-
- **Spacing** \u2014 padding and margins follow the
|
|
2731
|
-
- **Dark mode** \u2014 components respond to the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3382
|
+
## context.read \u2014 Reading Platform Context
|
|
3315
3383
|
|
|
3316
|
-
Read customer and session data provided by the
|
|
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
|
|
3393
|
+
## data.query \u2014 Platform-Mediated Requests
|
|
3326
3394
|
|
|
3327
|
-
Send structured requests to the
|
|
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
|
|
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
|
|
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
|
|
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")}`);
|