@colixsystems/widget-sdk 0.3.0 → 0.6.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/README.md +40 -4
- package/dist/_theme-tokens.js +10 -0
- package/dist/contract.cjs +433 -0
- package/dist/contract.js +425 -0
- package/dist/hooks.js +259 -23
- package/dist/index.d.ts +122 -12
- package/dist/index.js +16 -2
- package/dist/index.native.js +16 -2
- package/dist/linter.cjs +104 -0
- package/dist/linter.js +72 -24
- package/dist/manifest.cjs +144 -0
- package/dist/manifest.js +47 -36
- package/dist/primitives.js +35 -51
- package/dist/primitives.native.js +20 -10
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -6,11 +6,47 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
`v0.
|
|
9
|
+
`v0.6.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
10
10
|
|
|
11
|
-
### What's new in 0.
|
|
11
|
+
### What's new in 0.6.0
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- **Primitives are now React Native through-and-through.** Web (Studio + Player) bundles with `react-native-web`; native (exported Expo app) uses the real `react-native`. The hand-written DOM wrappers in `primitives.js` are gone — both web and native files are now one-line re-exports from `react-native`. The widget API surface is unchanged for the components that already existed (`Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`), so existing 0.5.0 widgets keep running without source edits.
|
|
14
|
+
- **More primitives available for free.** `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, and `StyleSheet` are now exported alongside the original five. Authors who want them just import from `@colixsystems/widget-sdk` like any other primitive.
|
|
15
|
+
- **Per-component API docs link out to React Native.** The Developer Widgets page and the AI agent's system prompt now point at https://reactnative.dev/docs/<component> for prop details instead of duplicating them in the SDK contract. Less drift, more breadth (every RN prop — `accessibilityLabel`, `testID`, gesture handlers, …, works without explicit allowlisting).
|
|
16
|
+
- **Adding a new primitive is now a single edit.** Append the export name to `primitives.js` + `primitives.native.js` + the `CONTRACT.primitives` array. No web/native implementation split.
|
|
17
|
+
- **New peer dep.** `react-native-web >=0.19.0` (optional, marked under `peerDependenciesMeta`). The web bundler picks it up via an alias (`react-native` → `react-native-web` in the host's Vite / webpack config); the native bundler ignores it.
|
|
18
|
+
|
|
19
|
+
### What was in 0.5.0
|
|
20
|
+
|
|
21
|
+
- **`TextInput` primitive.** Cross-platform controlled text field — web maps to `<input type="text">` (or `<textarea>` when `multiline` is true), native maps to `react-native` `TextInput`. Props: `value`, `onChangeText`, `placeholder`, `multiline`, `rows`, `disabled`, `style`. Fixes the gap that forced widgets to fall back to raw `<textarea>` (web-only). The AI Widget Agent's system prompt now documents this primitive.
|
|
22
|
+
- **`useDatastoreMutation(...).update(id, partial)` is wired.** Hits `PATCH /api/v1/tables/:tableId/records/:id`, which now exists on the backend (REQ-DML-PATCH). Partial-update semantics — only the supplied columns are mutated, the rest of the row is left intact. MANY_TO_MANY relations follow replace-set semantics when supplied; omitting the column leaves the existing links alone. Constraint enforcement (REQ-CONS-04) runs against the merged row, excluding self so a re-affirm doesn't trip its own UNIQUE.
|
|
23
|
+
|
|
24
|
+
### What was in 0.4.1
|
|
25
|
+
|
|
26
|
+
- **`useDatastoreQuery` returns a stable `refetch` identity.** The hook no longer rebinds the underlying callback when the host's `WidgetContext` value (a fresh object identity on every render in Studio + PageRenderer) changes. Widgets that put `refetch` in a `useEffect` dep array no longer loop.
|
|
27
|
+
|
|
28
|
+
### What's new in 0.4.0
|
|
29
|
+
|
|
30
|
+
- **`CONTRACT` is now a named export.** A frozen object literal describing the SDK surface every consumer (LLM system prompt, static analyzer, host builder, contract tests) derives from instead of declaring its own copy. See `docs/design/ai-widget-contract.md` for the full design and `src/contract.js` for the source. The contract carries:
|
|
31
|
+
|
|
32
|
+
- `hooks` — name, signature, return shape, required `WidgetContext` slices, manifest scopes.
|
|
33
|
+
- `primitives` — the cross-platform primitive list (names + the React Native component each one backs).
|
|
34
|
+
- `manifestSchema` — the authoritative `WidgetManifest` field list (with `id` and `minAppStudioVersion`, not the legacy `manifestId` / `minAppStudioVer`).
|
|
35
|
+
- `themeTokens` — the default `useTheme()` payload.
|
|
36
|
+
- `widgetContextShape` — what the host must populate.
|
|
37
|
+
- `bundleExportContract` — the two default-export shapes the loader accepts.
|
|
38
|
+
- `bannedApis` — the global allowlist gate.
|
|
39
|
+
- `allowedBareImports` — `react`, `@colixsystems/widget-sdk`.
|
|
40
|
+
|
|
41
|
+
- **`useDatastoreQuery` is now stateful.** Returns `{ data, loading, error, refetch }`. Previously the hook returned the raw `list(...)` promise; widgets that called `.map(...)` synchronously on the result threw on first render. Migration: replace `const rows = useDatastoreQuery(...)` with `const { data, loading, error } = useDatastoreQuery(...)`. `data` is always an array (empty when the table is unbound or loading).
|
|
42
|
+
|
|
43
|
+
- **`DatastoreError` is now a named export.** Mutations throw a structured `DatastoreError` with `.code` (`VALIDATION` / `CONSTRAINT_VIOLATION` / `FORBIDDEN` / `NOT_FOUND` / `INTERNAL`) and an optional `.fieldErrors` map populated from the host's 422 payload. Widgets can branch on `err.code` without parsing axios messages.
|
|
44
|
+
|
|
45
|
+
- **`useI18n().t` now honours `fallback`.** `t(key, fallback)` returns `fallback` when the host's translation table has no entry for `key`.
|
|
46
|
+
|
|
47
|
+
### What was in 0.3.0
|
|
48
|
+
|
|
49
|
+
Additive: `WidgetManifest` carries an optional `datastoreTemplate` field. When a tenant installs a widget that declares one, the table set is seeded into their workspace alongside the install. Tables follow the existing built-in template semantics (auto-suffixed naming, REQ-ACL-05 creator-grants, REQ-TEMPLATES-ACL public grants, REQ-ACL-RELINHERIT cross-relation inheritance) and persist when the widget is later uninstalled. See `WidgetDatastoreTemplate` in `src/index.d.ts` for the structural constraints — at most 8 tables per widget, 24 columns per table, RELATION columns address siblings by `suffix`.
|
|
14
50
|
|
|
15
51
|
## Public API
|
|
16
52
|
|
|
@@ -21,7 +57,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
21
57
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
22
58
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
23
59
|
- `useDatastoreQuery`, `useDatastoreMutation`, `useWidgetEvent`, `useTheme`, `useI18n` — hooks that read from the host-provided `WidgetContext`.
|
|
24
|
-
- `Text`, `View`, `Pressable`, `Image`, `ScrollView` —
|
|
60
|
+
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet` — re-exported from `react-native`. The web build aliases `react-native` to `react-native-web` so widgets render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. See https://reactnative.dev/docs/ for per-component props.
|
|
25
61
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
26
62
|
|
|
27
63
|
## Linter
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Re-export of the default theme tokens from the canonical contract data.
|
|
2
|
+
// Exists so the frontend's `widgetTheme.js` can keep its existing
|
|
3
|
+
// `DEFAULT_THEME_TOKENS` named export without reaching into the contract
|
|
4
|
+
// path itself. The values are the same Object.freeze'd reference as
|
|
5
|
+
// CONTRACT.themeTokens — assertion #6 in the contract test relies on
|
|
6
|
+
// referential identity.
|
|
7
|
+
|
|
8
|
+
import { CONTRACT } from "./contract.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_THEME_TOKENS = CONTRACT.themeTokens;
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// CommonJS-flavoured copy of the contract data so backend services (CJS
|
|
2
|
+
// runtime) can `require()` it directly without dynamic import gymnastics.
|
|
3
|
+
//
|
|
4
|
+
// The ESM `contract.js` re-exports the SAME object as the `CONTRACT` named
|
|
5
|
+
// export. The contract test asserts the two are identical (deep-equal) so
|
|
6
|
+
// drift between this file and the ESM source-of-truth is caught in CI.
|
|
7
|
+
//
|
|
8
|
+
// Keep edits in lockstep between `contract.cjs` and `contract.js`. The
|
|
9
|
+
// SDK build script copies both into `dist/`.
|
|
10
|
+
|
|
11
|
+
const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
12
|
+
colors: Object.freeze({
|
|
13
|
+
primary: "#ff6b5b",
|
|
14
|
+
onPrimary: "#ffffff",
|
|
15
|
+
surface: "#ffffff",
|
|
16
|
+
onSurface: "#111827",
|
|
17
|
+
surfaceMuted: "#f8fafc",
|
|
18
|
+
onSurfaceMuted: "#475569",
|
|
19
|
+
border: "#e2e8f0",
|
|
20
|
+
danger: "#dc2626",
|
|
21
|
+
success: "#059669",
|
|
22
|
+
warning: "#d97706",
|
|
23
|
+
info: "#0284c7",
|
|
24
|
+
}),
|
|
25
|
+
spacing: Object.freeze({ xs: 4, sm: 8, md: 16, lg: 24, xl: 32 }),
|
|
26
|
+
radii: Object.freeze({ sm: 4, md: 8, lg: 16, pill: 9999 }),
|
|
27
|
+
typography: Object.freeze({
|
|
28
|
+
fontFamily:
|
|
29
|
+
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
30
|
+
sizes: Object.freeze({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, xxl: 32 }),
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const HOOKS = [
|
|
35
|
+
{
|
|
36
|
+
name: "useTheme",
|
|
37
|
+
signature: "useTheme()",
|
|
38
|
+
returnShape: {
|
|
39
|
+
colors:
|
|
40
|
+
"{ primary, onPrimary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }",
|
|
41
|
+
spacing: "{ xs, sm, md, lg, xl }",
|
|
42
|
+
radii: "{ sm, md, lg, pill }",
|
|
43
|
+
typography: "{ fontFamily, sizes: { xs, sm, md, lg, xl, xxl } }",
|
|
44
|
+
},
|
|
45
|
+
requiredContextSlice: ["workspace.theme"],
|
|
46
|
+
scopes: null,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "useI18n",
|
|
50
|
+
signature: "useI18n()",
|
|
51
|
+
returnShape: {
|
|
52
|
+
t: "(key: string, fallback?: string) => string",
|
|
53
|
+
locale: "string",
|
|
54
|
+
},
|
|
55
|
+
requiredContextSlice: ["i18n.t", "i18n.locale"],
|
|
56
|
+
scopes: null,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "useDatastoreQuery",
|
|
60
|
+
signature: "useDatastoreQuery(tableId, options?)",
|
|
61
|
+
returnShape: {
|
|
62
|
+
data: "Record[]",
|
|
63
|
+
loading: "boolean",
|
|
64
|
+
error: "DatastoreError | null",
|
|
65
|
+
refetch: "() => Promise<void>",
|
|
66
|
+
},
|
|
67
|
+
requiredContextSlice: ["datastore.records"],
|
|
68
|
+
scopes: ["datastore.read:*"],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "useDatastoreMutation",
|
|
72
|
+
signature: "useDatastoreMutation(tableId)",
|
|
73
|
+
returnShape: {
|
|
74
|
+
create: "(record) => Promise<Record> // rejects with DatastoreError",
|
|
75
|
+
update: "(id, partial) => Promise<Record> // rejects with DatastoreError",
|
|
76
|
+
delete: "(id) => Promise<void> // rejects with DatastoreError",
|
|
77
|
+
},
|
|
78
|
+
requiredContextSlice: ["datastore.records"],
|
|
79
|
+
scopes: ["datastore.write:*"],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "useWidgetEvent",
|
|
83
|
+
signature: "useWidgetEvent(eventName)",
|
|
84
|
+
returnShape: { emit: "(payload?) => void" },
|
|
85
|
+
requiredContextSlice: ["events.emit"],
|
|
86
|
+
scopes: null,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
91
|
+
// (backed by react-native-web on the browser, by react-native on the
|
|
92
|
+
// exported Expo app). The contract describes the *cross-platform
|
|
93
|
+
// subset* the SDK re-exports; the per-prop reference for each
|
|
94
|
+
// component is whatever React Native documents. Adding a new primitive
|
|
95
|
+
// = add the name to `./primitives.js` + `./primitives.native.js` (both
|
|
96
|
+
// one-line re-exports) and append it to this list.
|
|
97
|
+
const PRIMITIVES = [
|
|
98
|
+
{
|
|
99
|
+
name: "Text",
|
|
100
|
+
description:
|
|
101
|
+
"Text node. Backed by react-native Text (web: react-native-web maps to a styled <div>). See https://reactnative.dev/docs/text.",
|
|
102
|
+
rnComponent: "Text",
|
|
103
|
+
docsUrl: "https://reactnative.dev/docs/text",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "View",
|
|
107
|
+
description:
|
|
108
|
+
"Block container. Backed by react-native View. The cross-platform equivalent of a <div>. See https://reactnative.dev/docs/view.",
|
|
109
|
+
rnComponent: "View",
|
|
110
|
+
docsUrl: "https://reactnative.dev/docs/view",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "Pressable",
|
|
114
|
+
description:
|
|
115
|
+
"Tappable / clickable wrapper. Use `onPress` (NOT `onClick`). See https://reactnative.dev/docs/pressable.",
|
|
116
|
+
rnComponent: "Pressable",
|
|
117
|
+
docsUrl: "https://reactnative.dev/docs/pressable",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "Image",
|
|
121
|
+
description:
|
|
122
|
+
"Image. `source` accepts `{ uri }` or a string URL. See https://reactnative.dev/docs/image.",
|
|
123
|
+
rnComponent: "Image",
|
|
124
|
+
docsUrl: "https://reactnative.dev/docs/image",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "ScrollView",
|
|
128
|
+
description:
|
|
129
|
+
"Scrollable container. Pass `horizontal` to scroll on the x-axis. See https://reactnative.dev/docs/scrollview.",
|
|
130
|
+
rnComponent: "ScrollView",
|
|
131
|
+
docsUrl: "https://reactnative.dev/docs/scrollview",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "TextInput",
|
|
135
|
+
description:
|
|
136
|
+
"Single-line or multi-line text field. Controlled — pass `value` + `onChangeText`. Set `multiline` for a textarea-style field (`numberOfLines` controls visible rows). See https://reactnative.dev/docs/textinput.",
|
|
137
|
+
rnComponent: "TextInput",
|
|
138
|
+
docsUrl: "https://reactnative.dev/docs/textinput",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "FlatList",
|
|
142
|
+
description:
|
|
143
|
+
"Virtualised list. Render large arrays without paying for every row up front. Required props: `data`, `renderItem`, `keyExtractor`. See https://reactnative.dev/docs/flatlist.",
|
|
144
|
+
rnComponent: "FlatList",
|
|
145
|
+
docsUrl: "https://reactnative.dev/docs/flatlist",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "SectionList",
|
|
149
|
+
description:
|
|
150
|
+
"Like FlatList but grouped by section header. See https://reactnative.dev/docs/sectionlist.",
|
|
151
|
+
rnComponent: "SectionList",
|
|
152
|
+
docsUrl: "https://reactnative.dev/docs/sectionlist",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "ActivityIndicator",
|
|
156
|
+
description:
|
|
157
|
+
"Loading spinner. See https://reactnative.dev/docs/activityindicator.",
|
|
158
|
+
rnComponent: "ActivityIndicator",
|
|
159
|
+
docsUrl: "https://reactnative.dev/docs/activityindicator",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "Switch",
|
|
163
|
+
description:
|
|
164
|
+
"Toggle. Controlled — `value` + `onValueChange`. See https://reactnative.dev/docs/switch.",
|
|
165
|
+
rnComponent: "Switch",
|
|
166
|
+
docsUrl: "https://reactnative.dev/docs/switch",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "StyleSheet",
|
|
170
|
+
description:
|
|
171
|
+
"Helper for building style objects. `StyleSheet.create({ ... })` returns the same shape you can pass to `style={...}`. See https://reactnative.dev/docs/stylesheet.",
|
|
172
|
+
rnComponent: "StyleSheet",
|
|
173
|
+
docsUrl: "https://reactnative.dev/docs/stylesheet",
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const CATEGORIES = [
|
|
178
|
+
"INPUT",
|
|
179
|
+
"DISPLAY",
|
|
180
|
+
"LAYOUT",
|
|
181
|
+
"DATA",
|
|
182
|
+
"MEDIA",
|
|
183
|
+
"COMMUNICATION",
|
|
184
|
+
"CUSTOM",
|
|
185
|
+
];
|
|
186
|
+
const PLATFORMS = ["web", "native"];
|
|
187
|
+
|
|
188
|
+
// Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
|
|
189
|
+
// more labels, lowercase alnum + hyphen, label starts with a letter. The
|
|
190
|
+
// analyzer + the SDK validator both read this from the contract so a
|
|
191
|
+
// rename / relaxation can only happen in one place.
|
|
192
|
+
const MANIFEST_ID_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;
|
|
193
|
+
|
|
194
|
+
const MANIFEST_SCHEMA = {
|
|
195
|
+
id: {
|
|
196
|
+
type: "string",
|
|
197
|
+
required: true,
|
|
198
|
+
description: "Reverse-DNS identifier, e.g. com.acme.hello.",
|
|
199
|
+
example: "com.acme.hello",
|
|
200
|
+
pattern: MANIFEST_ID_PATTERN,
|
|
201
|
+
},
|
|
202
|
+
name: {
|
|
203
|
+
type: "string",
|
|
204
|
+
required: true,
|
|
205
|
+
description: "Human-readable widget name.",
|
|
206
|
+
},
|
|
207
|
+
version: {
|
|
208
|
+
type: "string",
|
|
209
|
+
required: true,
|
|
210
|
+
description: "Semver. Patch bump per publish unless author overrides.",
|
|
211
|
+
example: "1.0.0",
|
|
212
|
+
},
|
|
213
|
+
category: {
|
|
214
|
+
type: "enum",
|
|
215
|
+
required: true,
|
|
216
|
+
description:
|
|
217
|
+
"One of " + CATEGORIES.join(", ") + ". Drives the palette section.",
|
|
218
|
+
values: CATEGORIES,
|
|
219
|
+
},
|
|
220
|
+
icon: {
|
|
221
|
+
type: "string",
|
|
222
|
+
required: true,
|
|
223
|
+
description: "Icon identifier, e.g. lucide:sparkles.",
|
|
224
|
+
default: "lucide:sparkles",
|
|
225
|
+
},
|
|
226
|
+
description: {
|
|
227
|
+
type: "string",
|
|
228
|
+
required: true,
|
|
229
|
+
description: "1-2 sentence storefront description.",
|
|
230
|
+
},
|
|
231
|
+
author: {
|
|
232
|
+
type: "object",
|
|
233
|
+
required: true,
|
|
234
|
+
description: "{ name: string, url?: string, email?: string }",
|
|
235
|
+
},
|
|
236
|
+
supportedPlatforms: {
|
|
237
|
+
type: "string[]",
|
|
238
|
+
required: true,
|
|
239
|
+
description: "Subset of " + PLATFORMS.join(", ") + ".",
|
|
240
|
+
default: ["web"],
|
|
241
|
+
},
|
|
242
|
+
minAppStudioVersion: {
|
|
243
|
+
type: "string",
|
|
244
|
+
required: true,
|
|
245
|
+
description: "Semver range, e.g. >=2.4.0.",
|
|
246
|
+
default: ">=0.1.0",
|
|
247
|
+
},
|
|
248
|
+
requestedScopes: {
|
|
249
|
+
type: "string[]",
|
|
250
|
+
required: true,
|
|
251
|
+
description:
|
|
252
|
+
"Permission scopes; use [] unless the widget reads/writes data.",
|
|
253
|
+
default: [],
|
|
254
|
+
},
|
|
255
|
+
propertySchema: {
|
|
256
|
+
type: "object",
|
|
257
|
+
required: true,
|
|
258
|
+
description: "Map of prop name to property definition.",
|
|
259
|
+
default: {},
|
|
260
|
+
},
|
|
261
|
+
events: {
|
|
262
|
+
type: "object[]",
|
|
263
|
+
required: true,
|
|
264
|
+
description: "Declared events. Each entry { name, payloadSchema? }.",
|
|
265
|
+
default: [],
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const WIDGET_CONTEXT_SHAPE = {
|
|
270
|
+
props: {
|
|
271
|
+
description:
|
|
272
|
+
"Resolved property values, shape per manifest.propertySchema.",
|
|
273
|
+
required: true,
|
|
274
|
+
fields: {},
|
|
275
|
+
},
|
|
276
|
+
widget: {
|
|
277
|
+
description:
|
|
278
|
+
"Identity of the currently rendered widget instance ({ id, version }).",
|
|
279
|
+
required: true,
|
|
280
|
+
fields: { id: "manifest.id", version: "manifest.version" },
|
|
281
|
+
},
|
|
282
|
+
user: {
|
|
283
|
+
description: "Signed-in user ({ id, email, displayName }).",
|
|
284
|
+
required: true,
|
|
285
|
+
fields: { id: "string", email: "string", displayName: "string" },
|
|
286
|
+
},
|
|
287
|
+
workspace: {
|
|
288
|
+
description:
|
|
289
|
+
"Workspace identity + resolved theme ({ id, slug, theme, locale }).",
|
|
290
|
+
required: true,
|
|
291
|
+
fields: {
|
|
292
|
+
id: "string",
|
|
293
|
+
slug: "string",
|
|
294
|
+
theme: "ThemeTokens",
|
|
295
|
+
locale: "string",
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
navigation: {
|
|
299
|
+
description: "{ push(path), replace(path), back() }.",
|
|
300
|
+
required: true,
|
|
301
|
+
fields: { push: "function", replace: "function", back: "function" },
|
|
302
|
+
},
|
|
303
|
+
datastore: {
|
|
304
|
+
description:
|
|
305
|
+
"{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) } }.",
|
|
306
|
+
required: true,
|
|
307
|
+
fields: { records: "function" },
|
|
308
|
+
},
|
|
309
|
+
events: {
|
|
310
|
+
description: "{ emit(name, payload) }.",
|
|
311
|
+
required: true,
|
|
312
|
+
fields: { emit: "function" },
|
|
313
|
+
},
|
|
314
|
+
i18n: {
|
|
315
|
+
description: "{ t(key, fallback?), locale }.",
|
|
316
|
+
required: true,
|
|
317
|
+
fields: { t: "function", locale: "string" },
|
|
318
|
+
},
|
|
319
|
+
logger: {
|
|
320
|
+
description:
|
|
321
|
+
"{ debug, info, warn, error } — host-routed; never use console.*.",
|
|
322
|
+
required: true,
|
|
323
|
+
fields: {
|
|
324
|
+
debug: "function",
|
|
325
|
+
info: "function",
|
|
326
|
+
warn: "function",
|
|
327
|
+
error: "function",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const BUNDLE_EXPORT_CONTRACT = [
|
|
333
|
+
{
|
|
334
|
+
name: "wrapped",
|
|
335
|
+
description:
|
|
336
|
+
"default export = defineWidget({ manifest, component }) → resolves to { manifest, component, _kind: 'widget' }. Public marketplace widgets.",
|
|
337
|
+
predicate:
|
|
338
|
+
"typeof mod.default === 'object' && typeof mod.default.component === 'function' && typeof mod.default.manifest === 'object'",
|
|
339
|
+
manifestSource: "mod.default.manifest",
|
|
340
|
+
audience: "public-marketplace",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "bare-component",
|
|
344
|
+
description:
|
|
345
|
+
"default export = function MyWidget(props) {...} → host pairs it with the installation's pinnedVersion.manifestJson. AI-agent widgets.",
|
|
346
|
+
predicate: "typeof mod.default === 'function'",
|
|
347
|
+
manifestSource: "installation.pinnedVersion.manifestJson",
|
|
348
|
+
audience: "ai-agent",
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
const BANNED_APIS = [
|
|
353
|
+
{ identifier: "eval", reason: "Arbitrary code evaluation." },
|
|
354
|
+
{
|
|
355
|
+
identifier: "Function",
|
|
356
|
+
reason: "Function() constructor evaluates strings.",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
identifier: "new Function",
|
|
360
|
+
reason: "Function() constructor evaluates strings.",
|
|
361
|
+
},
|
|
362
|
+
{ identifier: "window", reason: "Host environment escape." },
|
|
363
|
+
{
|
|
364
|
+
identifier: "document",
|
|
365
|
+
reason: "Host environment escape; use SDK primitives.",
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
identifier: "process",
|
|
369
|
+
reason: "Node global; unavailable in the browser, banned for parity.",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
identifier: "localStorage",
|
|
373
|
+
reason: "Bypasses host data model; not portable to native.",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
identifier: "sessionStorage",
|
|
377
|
+
reason: "Same reason as localStorage.",
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
identifier: "fetch",
|
|
381
|
+
reason:
|
|
382
|
+
"Direct network calls bypass the datastore client + tenant auth.",
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
identifier: "XMLHttpRequest",
|
|
386
|
+
reason:
|
|
387
|
+
"Direct network calls bypass the datastore client + tenant auth.",
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
identifier: "import(",
|
|
391
|
+
reason: "Dynamic import bypasses the loader's allowlist.",
|
|
392
|
+
},
|
|
393
|
+
{ identifier: "globalThis", reason: "Host environment escape." },
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
const ALLOWED_BARE_IMPORTS = ["react", "@colixsystems/widget-sdk"];
|
|
397
|
+
|
|
398
|
+
function deepFreeze(value) {
|
|
399
|
+
if (value === null || typeof value !== "object") return value;
|
|
400
|
+
if (Object.isFrozen(value)) return value;
|
|
401
|
+
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
402
|
+
return Object.freeze(value);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const CONTRACT = deepFreeze({
|
|
406
|
+
version: "1.0.0",
|
|
407
|
+
hooks: HOOKS,
|
|
408
|
+
primitives: PRIMITIVES,
|
|
409
|
+
manifestSchema: MANIFEST_SCHEMA,
|
|
410
|
+
manifestCategories: CATEGORIES,
|
|
411
|
+
manifestPlatforms: PLATFORMS,
|
|
412
|
+
themeTokens: DEFAULT_THEME_TOKENS,
|
|
413
|
+
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
414
|
+
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
|
415
|
+
bannedApis: BANNED_APIS,
|
|
416
|
+
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
function isHookAllowed(name) {
|
|
420
|
+
return CONTRACT.hooks.some((h) => h.name === name);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function requiredContextKeys() {
|
|
424
|
+
const keys = new Set();
|
|
425
|
+
for (const hook of CONTRACT.hooks) {
|
|
426
|
+
for (const dotPath of hook.requiredContextSlice) {
|
|
427
|
+
keys.add(dotPath.split(".")[0]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return [...keys];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = { CONTRACT, isHookAllowed, requiredContextKeys };
|