@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.
- package/dist/index.js +231 -105
- package/dist/server.js +231 -105
- 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
|
|
402
|
+
const tagsByCategory = /* @__PURE__ */ new Map();
|
|
386
403
|
for (const tag of UI_TAGS) {
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
489
|
-
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.
|
|
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
|
|
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
|
|
540
|
-
Read
|
|
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
|
|
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
|
|
578
|
-
Trigger
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1741
|
+
communication, capability injection, and surface registration with the framework.
|
|
1707
1742
|
|
|
1708
1743
|
## Surface Lifecycle
|
|
1709
1744
|
|
|
1710
|
-
1. **Mount** \u2014
|
|
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
|
|
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
|
|
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}
|
|
1782
|
+
create: (name = "<extension-name>") => `${DLX} ${CLI_PKG} create ${name}`,
|
|
1747
1783
|
/** Start dev servers with hot reload */
|
|
1748
|
-
dev: `${DLX}
|
|
1784
|
+
dev: `${DLX} ${CLI_PKG} dev`,
|
|
1749
1785
|
/** Deploy the extension (future) */
|
|
1750
|
-
deploy: `${DLX}
|
|
1786
|
+
deploy: `${DLX} ${CLI_PKG} deploy`,
|
|
1751
1787
|
/** Validate the extension for common errors (coming soon) */
|
|
1752
|
-
validate: `${DLX}
|
|
1788
|
+
validate: `${DLX} ${CLI_PKG} validate`,
|
|
1753
1789
|
/** Scaffold from an existing extension */
|
|
1754
|
-
scaffold: `${DLX}
|
|
1790
|
+
scaffold: `${DLX} ${CLI_PKG} scaffold`,
|
|
1755
1791
|
/** Update an existing extension */
|
|
1756
|
-
update: `${DLX}
|
|
1792
|
+
update: `${DLX} ${CLI_PKG} update`,
|
|
1757
1793
|
/** Manage CLI authentication (browser-based OAuth) */
|
|
1758
1794
|
auth: {
|
|
1759
|
-
login: `${DLX}
|
|
1760
|
-
logout: `${DLX}
|
|
1761
|
-
status: `${DLX}
|
|
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}
|
|
1802
|
+
scaffold: `${DLX} ${CLI_PKG} ai scaffold`,
|
|
1767
1803
|
/** Add Stackable MCP server config to your project */
|
|
1768
|
-
mcp: `${DLX}
|
|
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
|
|
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
|
|
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
|
|
1856
|
-
|
|
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-
|
|
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
|
|
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
|
|
2073
|
+
4. Displays a \`_stackable_dev\` query param to append to a host site's URL
|
|
2037
2074
|
|
|
2038
|
-
### Host
|
|
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
|
|
2047
|
-
|
|
2048
|
-
|
|
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
|
|
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
|
|
2225
|
-
-
|
|
2226
|
-
-
|
|
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) |
|
|
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 |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2727
|
+
- Sandboxed iframe communication with the framework
|
|
2625
2728
|
- Capability injection (makes \`useCapabilities()\` work)
|
|
2626
|
-
- Surface registration (maps \`<Surface id="...">\` to
|
|
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
|
|
2687
|
-
(\`ui.*\` namespace) renders inside the
|
|
2688
|
-
|
|
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
|
-
##
|
|
2793
|
+
## Theme Inheritance
|
|
2691
2794
|
|
|
2692
|
-
The \`ui.*\` components automatically use the
|
|
2693
|
-
- **Colors** \u2014 text, backgrounds, borders adapt to the
|
|
2694
|
-
- **Typography** \u2014 font family, sizes, and weights match the
|
|
2695
|
-
- **Spacing** \u2014 padding and margins follow the
|
|
2696
|
-
- **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
|
|
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
|
|
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
|
|
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
|
|
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-"))
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
if (cls.startsWith("dark:bg-"))
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
if (cls.startsWith("
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
if (cls.startsWith("
|
|
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
|
|
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
|
|
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
|
|
3382
|
+
## context.read \u2014 Reading Platform Context
|
|
3260
3383
|
|
|
3261
|
-
Read customer and session data provided by the
|
|
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
|
|
3393
|
+
## data.query \u2014 Platform-Mediated Requests
|
|
3271
3394
|
|
|
3272
|
-
Send structured requests to the
|
|
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
|
|
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
|
|
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
|
|
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")}`);
|