@colixsystems/widget-sdk 0.14.1 → 0.15.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 +20 -4
- package/dist/clipboard.js +88 -0
- package/dist/clipboard.native.js +64 -0
- package/dist/contract.cjs +216 -11
- package/dist/contract.js +178 -9
- package/dist/datetimepicker.js +102 -0
- package/dist/hooks.js +3 -1
- package/dist/icon.js +29 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +7 -0
- package/dist/index.native.js +5 -0
- package/dist/linter.cjs +148 -9
- package/dist/linter.js +193 -13
- package/dist/primitives.js +8 -0
- package/dist/primitives.native.js +9 -0
- package/dist/toast.js +73 -0
- package/dist/toast.native.js +46 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,25 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
`v0.
|
|
9
|
+
`v0.15.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.15.0
|
|
12
|
+
|
|
13
|
+
REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. See [`docs/design/req-widget-sdk-cross-platform-primitives.md`](../../docs/design/req-widget-sdk-cross-platform-primitives.md) for the full design and rationale.
|
|
14
|
+
|
|
15
|
+
- **`CONTRACT.vettedImports` (new).** A curated allowlist of bare specifiers a widget may import — `react`, `@colixsystems/widget-sdk`, `react-native`, `axios`, `date-fns`, `react-native-svg`, `lucide-react-native`, `react-native-maps`, `leaflet`, `react-leaflet`, `expo-av`, `@react-native-community/datetimepicker`, `expo-clipboard`, `expo-haptics`. Each entry carries `platforms` (one or both of `"web"` / `"native"`) and a `category` so the linter and the marketplace listing can render honest platform badges. `CONTRACT.allowedBareImports` (the existing field) is now derived from `vettedImports` and stays a plain `string[]` for back-compat.
|
|
16
|
+
- **`fetch` and `XMLHttpRequest` come off `CONTRACT.bannedApis`.** Widgets may call third-party APIs directly. Calls to the host's own `/api/*` surface will 401 because the JWT token is never shared with widget code; the linter emits a soft `no-host-api-url` warning when it sees host-URL substrings so authors learn the rule statically. Use SDK hooks (`useDatastoreQuery`, `useUsers`, `useFile`, …) for workspace data; use `axios` / `fetch` for third-party APIs.
|
|
17
|
+
- **`import-not-vetted` linter rule (new).** Every bare `import` specifier is validated against `CONTRACT.vettedImports`. Relative imports inside the bundle (`./shared.js`) are allowed so split-impl widgets can share helpers; `../` and absolute paths are rejected.
|
|
18
|
+
- **`import-platform-mismatch` linter rule (new).** A single-source widget that imports a native-only package while `manifest.supportedPlatforms` includes `"web"` fails the lint. The author either drops the platform from the manifest OR ships a `widget.web.jsx` + `widget.native.jsx` pair where the platform-specific import lives in the file that targets its platform.
|
|
19
|
+
- **Lint findings carry `severity`.** `"error"` (default) blocks publish; `"warning"` (currently only `no-host-api-url`) surfaces to reviewers without blocking. The `lintSource(...)` return shape stays `{ ok, findings }` — `ok` is true iff no error-severity findings exist.
|
|
20
|
+
- **Four Tier A SDK additions:**
|
|
21
|
+
- `<Icon>` primitive — `<Icon name="check" size={16} color={theme.colors.primary} />`. Wraps `lucide-react-native`; works on both platforms.
|
|
22
|
+
- `<DateTimePicker>` primitive — `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. Wraps `@react-native-community/datetimepicker` and normalizes the value to ISO 8601 strings (the datastore wire format).
|
|
23
|
+
- `useClipboard()` hook — `{ copy, paste, hasContent }`. Web via `navigator.clipboard`; native via `expo-clipboard`. Rejections are a structured `ClipboardError` with `.code` in `PERMISSION_DENIED | INTERNAL`.
|
|
24
|
+
- `useToast()` hook — `{ showToast }`. The host installs a workspace-themed renderer at `WidgetContext.toast.showToast`; if omitted, the web variant dispatches an `appstudio:widget-toast` CustomEvent and native logs to the console.
|
|
25
|
+
- **`CONTRACT.version` → `1.5.0`** (additive: two new contract fields — `vettedImports`, `hostApiUrlPatterns` — two banned APIs removed, two primitives + two hooks added, one optional `widgetContextShape.toast` slot). No existing export changed signature.
|
|
26
|
+
|
|
27
|
+
### What was in 0.14.1
|
|
12
28
|
|
|
13
29
|
- **`groupRef` property type (REQ-USERMGMT M4 / §4.8).** Authors can now declare `{ type: 'groupRef', label: 'Group' }` in their `propertySchema` to render a Group picker in the Studio Properties Panel. The widget receives a bare `AppUserGroup` UUID — REQ-GEN-07 compliant, so tenant-copy walks the value transparently. Used by the built-in `appstudio.user-management` widget for its `defaultGroupId` prop and available to third-party widgets that need to anchor behaviour on a specific group.
|
|
14
30
|
- **Patch bump** — additive enumeration entry, no exported function signature changed. `CONTRACT.version` stays `1.4.0`.
|
|
@@ -101,9 +117,9 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
101
117
|
|
|
102
118
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
103
119
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
104
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer` — hooks that read from the host-provided `WidgetContext
|
|
120
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (`id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useFile(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
|
|
105
121
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
106
|
-
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet` — re-exported from `react-native
|
|
122
|
+
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`, `DateTimePicker` wraps `@react-native-community/datetimepicker`). 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. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
107
123
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
108
124
|
|
|
109
125
|
## Managing app users from a widget
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — `useClipboard()` hook (web implementation).
|
|
2
|
+
//
|
|
3
|
+
// Wraps the browser's `navigator.clipboard` API. Returns a stable object:
|
|
4
|
+
//
|
|
5
|
+
// const { copy, paste, hasContent } = useClipboard();
|
|
6
|
+
//
|
|
7
|
+
// - `copy(text)` → Promise<void>. Writes a string. Rejects with a
|
|
8
|
+
// `ClipboardError` on failure (most common code:
|
|
9
|
+
// "PERMISSION_DENIED" — the page lacks the
|
|
10
|
+
// clipboard-write permission, often because the
|
|
11
|
+
// call wasn't triggered by a user gesture).
|
|
12
|
+
// - `paste()` → Promise<string>. Reads the current clipboard
|
|
13
|
+
// text. Returns "" when empty, rejects with
|
|
14
|
+
// `ClipboardError` (code "PERMISSION_DENIED" on
|
|
15
|
+
// sites that haven't asked for clipboard-read
|
|
16
|
+
// permission).
|
|
17
|
+
// - `hasContent()` → Promise<boolean>. Best-effort: reads the
|
|
18
|
+
// clipboard and reports whether it had a
|
|
19
|
+
// non-empty string. Same permission caveats as
|
|
20
|
+
// `paste()`.
|
|
21
|
+
//
|
|
22
|
+
// The native build uses ./clipboard.native.js (Metro picks via the
|
|
23
|
+
// package.json `react-native` resolution path through index.native.js).
|
|
24
|
+
|
|
25
|
+
export class ClipboardError extends Error {
|
|
26
|
+
constructor(code, message, opts) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "ClipboardError";
|
|
29
|
+
this.code = code;
|
|
30
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _isPermissionError(err) {
|
|
35
|
+
if (!err) return false;
|
|
36
|
+
if (err.name === "NotAllowedError" || err.name === "SecurityError") return true;
|
|
37
|
+
const msg = typeof err.message === "string" ? err.message.toLowerCase() : "";
|
|
38
|
+
return msg.includes("permission") || msg.includes("not allowed");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _wrap(err, label) {
|
|
42
|
+
if (err instanceof ClipboardError) return err;
|
|
43
|
+
const code = _isPermissionError(err) ? "PERMISSION_DENIED" : "INTERNAL";
|
|
44
|
+
return new ClipboardError(code, `${label}: ${err && err.message ? err.message : err}`, {
|
|
45
|
+
cause: err,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useClipboard() {
|
|
50
|
+
// Each method is a fresh function but the returned identity is stable
|
|
51
|
+
// per call site — callers passing one into useEffect dep arrays should
|
|
52
|
+
// pin the method, not the whole object.
|
|
53
|
+
return {
|
|
54
|
+
async copy(text) {
|
|
55
|
+
const value = text == null ? "" : String(text);
|
|
56
|
+
try {
|
|
57
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
58
|
+
throw new ClipboardError("INTERNAL", "Clipboard API unavailable.");
|
|
59
|
+
}
|
|
60
|
+
await navigator.clipboard.writeText(value);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
throw _wrap(err, "clipboard.copy");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async paste() {
|
|
66
|
+
try {
|
|
67
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
68
|
+
throw new ClipboardError("INTERNAL", "Clipboard API unavailable.");
|
|
69
|
+
}
|
|
70
|
+
const text = await navigator.clipboard.readText();
|
|
71
|
+
return typeof text === "string" ? text : "";
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw _wrap(err, "clipboard.paste");
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async hasContent() {
|
|
77
|
+
try {
|
|
78
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) return false;
|
|
79
|
+
const text = await navigator.clipboard.readText();
|
|
80
|
+
return typeof text === "string" && text.length > 0;
|
|
81
|
+
} catch {
|
|
82
|
+
// Permission-denied reads return false instead of throwing —
|
|
83
|
+
// hasContent() is the "best-effort" probe, not a write gate.
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — `useClipboard()` hook (native implementation).
|
|
2
|
+
//
|
|
3
|
+
// Wraps expo-clipboard. Same return shape as the web counterpart
|
|
4
|
+
// (./clipboard.js) so widget code is byte-for-byte identical across
|
|
5
|
+
// platforms. The two implementations only differ in how `copy` / `paste`
|
|
6
|
+
// reach the system clipboard.
|
|
7
|
+
//
|
|
8
|
+
// Errors are normalized to a structured `ClipboardError` with a stable
|
|
9
|
+
// `.code`. Native clipboard reads cannot fail on permission grounds
|
|
10
|
+
// (Android doesn't prompt; iOS reveals a paste banner but still
|
|
11
|
+
// resolves), so "PERMISSION_DENIED" is web-only.
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
14
|
+
import * as Clipboard from "expo-clipboard";
|
|
15
|
+
|
|
16
|
+
export class ClipboardError extends Error {
|
|
17
|
+
constructor(code, message, opts) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "ClipboardError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function _wrap(err, label) {
|
|
26
|
+
if (err instanceof ClipboardError) return err;
|
|
27
|
+
return new ClipboardError(
|
|
28
|
+
"INTERNAL",
|
|
29
|
+
`${label}: ${err && err.message ? err.message : err}`,
|
|
30
|
+
{ cause: err },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useClipboard() {
|
|
35
|
+
return {
|
|
36
|
+
async copy(text) {
|
|
37
|
+
const value = text == null ? "" : String(text);
|
|
38
|
+
try {
|
|
39
|
+
await Clipboard.setStringAsync(value);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw _wrap(err, "clipboard.copy");
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async paste() {
|
|
45
|
+
try {
|
|
46
|
+
const text = await Clipboard.getStringAsync();
|
|
47
|
+
return typeof text === "string" ? text : "";
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw _wrap(err, "clipboard.paste");
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async hasContent() {
|
|
53
|
+
try {
|
|
54
|
+
if (typeof Clipboard.hasStringAsync === "function") {
|
|
55
|
+
return !!(await Clipboard.hasStringAsync());
|
|
56
|
+
}
|
|
57
|
+
const text = await Clipboard.getStringAsync();
|
|
58
|
+
return typeof text === "string" && text.length > 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/contract.cjs
CHANGED
|
@@ -240,6 +240,42 @@ const HOOKS = [
|
|
|
240
240
|
],
|
|
241
241
|
scopes: ["groups.read:*"],
|
|
242
242
|
},
|
|
243
|
+
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
244
|
+
{
|
|
245
|
+
name: "useClipboard",
|
|
246
|
+
signature: "useClipboard()",
|
|
247
|
+
description:
|
|
248
|
+
"Cross-platform clipboard access. Returns { copy, paste, hasContent }. " +
|
|
249
|
+
"All methods return Promises; rejections surface a structured ClipboardError " +
|
|
250
|
+
"with .code in PERMISSION_DENIED | INTERNAL. On web the browser may " +
|
|
251
|
+
"require a user gesture for read access — surface the error to the " +
|
|
252
|
+
"user as a 'try again after clicking' prompt when code === PERMISSION_DENIED.",
|
|
253
|
+
returnShape: {
|
|
254
|
+
copy:
|
|
255
|
+
"(text: string) => Promise<void> // rejects with ClipboardError",
|
|
256
|
+
paste:
|
|
257
|
+
"() => Promise<string> // rejects with ClipboardError",
|
|
258
|
+
hasContent: "() => Promise<boolean> // best-effort, never throws",
|
|
259
|
+
},
|
|
260
|
+
requiredContextSlice: [],
|
|
261
|
+
scopes: null,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "useToast",
|
|
265
|
+
signature: "useToast()",
|
|
266
|
+
description:
|
|
267
|
+
"Surfaces a short auto-dismissing notification. Returns { showToast }. " +
|
|
268
|
+
"showToast({ kind: 'success' | 'error' | 'info' | 'warning', message }) " +
|
|
269
|
+
"asks the host to render a workspace-themed toast. If the host hasn't " +
|
|
270
|
+
"wired a renderer, the web variant dispatches an 'appstudio:widget-toast' " +
|
|
271
|
+
"CustomEvent on window; native logs to the console. The widget never " +
|
|
272
|
+
"owns the toast UI — that's the host's responsibility.",
|
|
273
|
+
returnShape: {
|
|
274
|
+
showToast: "({ kind, message }) => void",
|
|
275
|
+
},
|
|
276
|
+
requiredContextSlice: ["toast.showToast"],
|
|
277
|
+
scopes: null,
|
|
278
|
+
},
|
|
243
279
|
];
|
|
244
280
|
|
|
245
281
|
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
@@ -334,6 +370,22 @@ const PRIMITIVES = [
|
|
|
334
370
|
rnComponent: "Linking",
|
|
335
371
|
docsUrl: "https://reactnative.dev/docs/linking",
|
|
336
372
|
},
|
|
373
|
+
// REQ-WSDK-PLATFORM §6 — Tier A SDK primitive.
|
|
374
|
+
{
|
|
375
|
+
name: "Icon",
|
|
376
|
+
description:
|
|
377
|
+
'Lucide icon. `<Icon name="check" size={16} color="..." />`. Unknown names render the Square fallback so the canvas always shows something visible. Names are the lucide icon ids (`https://lucide.dev/icons`). Works on both web and native.',
|
|
378
|
+
rnComponent: "lucide-react-native",
|
|
379
|
+
docsUrl: "https://lucide.dev/icons",
|
|
380
|
+
},
|
|
381
|
+
// REQ-WSDK-PLATFORM §6 — Tier A SDK primitive.
|
|
382
|
+
{
|
|
383
|
+
name: "DateTimePicker",
|
|
384
|
+
description:
|
|
385
|
+
'Cross-platform date / time / datetime picker. `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. The value prop and the onChange callback both speak ISO 8601 strings (the datastore wire format) — authors never round-trip through `new Date()`. Web renders the browser\'s native input via react-native-web; native uses @react-native-community/datetimepicker.',
|
|
386
|
+
rnComponent: "@react-native-community/datetimepicker",
|
|
387
|
+
docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
|
|
388
|
+
},
|
|
337
389
|
];
|
|
338
390
|
|
|
339
391
|
const CATEGORIES = [
|
|
@@ -551,6 +603,17 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
551
603
|
error: "function",
|
|
552
604
|
},
|
|
553
605
|
},
|
|
606
|
+
// REQ-WSDK-PLATFORM §6 — backs useToast(). Mirror of contract.js.
|
|
607
|
+
toast: {
|
|
608
|
+
description:
|
|
609
|
+
"Optional host toast slot. { showToast({ kind, message }): void }. " +
|
|
610
|
+
"The host populates this to render workspace-themed notifications " +
|
|
611
|
+
"from any widget that calls useToast(). When omitted the SDK falls " +
|
|
612
|
+
"back to dispatching an 'appstudio:widget-toast' CustomEvent on web " +
|
|
613
|
+
"and console.log on native.",
|
|
614
|
+
required: false,
|
|
615
|
+
fields: { showToast: "function" },
|
|
616
|
+
},
|
|
554
617
|
};
|
|
555
618
|
|
|
556
619
|
const BUNDLE_EXPORT_CONTRACT = [
|
|
@@ -573,6 +636,15 @@ const BUNDLE_EXPORT_CONTRACT = [
|
|
|
573
636
|
},
|
|
574
637
|
];
|
|
575
638
|
|
|
639
|
+
// REQ-WSDK-PLATFORM (docs/design/req-widget-sdk-cross-platform-primitives.md
|
|
640
|
+
// §3.5, §8): `fetch` and `XMLHttpRequest` are NOT banned. Widgets may call
|
|
641
|
+
// third-party APIs directly. Same-origin requests to the host's own
|
|
642
|
+
// `/api/*` surface are rejected at runtime by the WidgetContextProvider's
|
|
643
|
+
// network gate (`no host-api access from widgets`) — the JWT token is
|
|
644
|
+
// never shared with widget code, so the call would 401 anyway; the runtime
|
|
645
|
+
// gate makes the failure mode "blocked" instead of "401 noise". A soft
|
|
646
|
+
// linter warning (`no-host-api-url`) flags obvious host-URL substrings at
|
|
647
|
+
// submission so authors learn the rule statically.
|
|
576
648
|
const BANNED_APIS = [
|
|
577
649
|
{ identifier: "eval", reason: "Arbitrary code evaluation." },
|
|
578
650
|
{
|
|
@@ -601,23 +673,144 @@ const BANNED_APIS = [
|
|
|
601
673
|
reason: "Same reason as localStorage.",
|
|
602
674
|
},
|
|
603
675
|
{
|
|
604
|
-
identifier: "
|
|
605
|
-
reason:
|
|
606
|
-
|
|
676
|
+
identifier: "import(",
|
|
677
|
+
reason: "Dynamic import bypasses the loader's allowlist.",
|
|
678
|
+
},
|
|
679
|
+
{ identifier: "globalThis", reason: "Host environment escape." },
|
|
680
|
+
];
|
|
681
|
+
|
|
682
|
+
// REQ-WSDK-PLATFORM §3.4, §5: vetted package allowlist. Each entry is a
|
|
683
|
+
// specifier the widget bundle may import as a bare module specifier. The
|
|
684
|
+
// linter validates every `import ... from "<spec>"` line against this
|
|
685
|
+
// list; specifiers not on the list fail the lint. The compiler reads it
|
|
686
|
+
// to know which packages to add to the exported Expo app's package.json.
|
|
687
|
+
//
|
|
688
|
+
// `platforms` is one or both of "web" / "native". A widget that imports a
|
|
689
|
+
// native-only package implicitly drops "web" from the package's effective
|
|
690
|
+
// `supportedPlatforms` (and vice versa) — the marketplace upload pipeline
|
|
691
|
+
// surfaces the derived set so the listing shows honest badges.
|
|
692
|
+
//
|
|
693
|
+
// Adding a package is a CONTRACT change. Review burden: confirm the
|
|
694
|
+
// package does what it claims, has a credible maintainer, and the
|
|
695
|
+
// import shape (named exports, default export) is stable. After that,
|
|
696
|
+
// every widget using the package inherits the review — the marketplace
|
|
697
|
+
// reviewer only spot-checks usage, not the package source.
|
|
698
|
+
const VETTED_IMPORTS = [
|
|
699
|
+
{
|
|
700
|
+
specifier: "react",
|
|
701
|
+
platforms: ["web", "native"],
|
|
702
|
+
category: "core",
|
|
703
|
+
description: "React. Hooks, JSX, lifecycle. Unchanged.",
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
specifier: "@colixsystems/widget-sdk",
|
|
707
|
+
platforms: ["web", "native"],
|
|
708
|
+
category: "core",
|
|
709
|
+
description:
|
|
710
|
+
"The AppStudio widget SDK — primitives, hooks, manifest helpers. Unchanged.",
|
|
607
711
|
},
|
|
608
712
|
{
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
713
|
+
specifier: "react-native",
|
|
714
|
+
platforms: ["web", "native"],
|
|
715
|
+
category: "primitive",
|
|
716
|
+
description:
|
|
717
|
+
"Direct RN imports for APIs the SDK hasn't re-exported yet. On web the host bundler aliases this to react-native-web; on native Metro resolves the real library.",
|
|
612
718
|
},
|
|
613
719
|
{
|
|
614
|
-
|
|
615
|
-
|
|
720
|
+
specifier: "axios",
|
|
721
|
+
platforms: ["web", "native"],
|
|
722
|
+
category: "network",
|
|
723
|
+
description:
|
|
724
|
+
"HTTP client for third-party APIs. Calls to the host's /api/* surface are blocked at runtime — widgets get no JWT token, so use SDK hooks for workspace data.",
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
specifier: "date-fns",
|
|
728
|
+
platforms: ["web", "native"],
|
|
729
|
+
category: "utility",
|
|
730
|
+
description: "Pure-JS date math. Works on both platforms unchanged.",
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
specifier: "react-native-svg",
|
|
734
|
+
platforms: ["web", "native"],
|
|
735
|
+
category: "drawing",
|
|
736
|
+
description:
|
|
737
|
+
"Cross-platform SVG drawing primitives. Used by the built-in Chart widget; works on both platforms.",
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
specifier: "lucide-react-native",
|
|
741
|
+
platforms: ["web", "native"],
|
|
742
|
+
category: "iconography",
|
|
743
|
+
description:
|
|
744
|
+
"Lucide icon set as React components. Used by the built-in Icon widget; works on both platforms.",
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
specifier: "react-native-maps",
|
|
748
|
+
platforms: ["native"],
|
|
749
|
+
category: "geo",
|
|
750
|
+
description:
|
|
751
|
+
"Native map view + markers. Native-only; pair with leaflet/react-leaflet in widget.web.jsx for a web variant.",
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
specifier: "leaflet",
|
|
755
|
+
platforms: ["web"],
|
|
756
|
+
category: "geo",
|
|
757
|
+
description:
|
|
758
|
+
"Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps.",
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
specifier: "react-leaflet",
|
|
762
|
+
platforms: ["web"],
|
|
763
|
+
category: "geo",
|
|
764
|
+
description: "React bindings for leaflet. Web-only.",
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
specifier: "expo-av",
|
|
768
|
+
platforms: ["native"],
|
|
769
|
+
category: "media",
|
|
770
|
+
description:
|
|
771
|
+
"Native audio + video playback. Native-only; pair with browser <audio>/<video> in widget.web.jsx.",
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
specifier: "@react-native-community/datetimepicker",
|
|
775
|
+
platforms: ["native"],
|
|
776
|
+
category: "input",
|
|
777
|
+
description:
|
|
778
|
+
"Native date/time picker. The SDK's <DateTimePicker> primitive already wraps this; reach for it directly only if you need RN-specific options.",
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
specifier: "expo-clipboard",
|
|
782
|
+
platforms: ["native"],
|
|
783
|
+
category: "system",
|
|
784
|
+
description:
|
|
785
|
+
"Native clipboard. The SDK's useClipboard() hook already wraps this; reach for it directly only if you need RN-specific options.",
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
specifier: "expo-haptics",
|
|
789
|
+
platforms: ["native"],
|
|
790
|
+
category: "system",
|
|
791
|
+
description:
|
|
792
|
+
"Native haptic feedback. Pair with navigator.vibrate in widget.web.jsx.",
|
|
616
793
|
},
|
|
617
|
-
{ identifier: "globalThis", reason: "Host environment escape." },
|
|
618
794
|
];
|
|
619
795
|
|
|
620
|
-
|
|
796
|
+
// Back-compat shape — every existing consumer (widgetLoader, the static
|
|
797
|
+
// analyzer's DEPENDENCY_ALLOWLIST, the AI prompt's "Allowed imports" line)
|
|
798
|
+
// reads a plain `string[]` from `CONTRACT.allowedBareImports`. Derive it
|
|
799
|
+
// from the rich list so a single edit in VETTED_IMPORTS propagates to
|
|
800
|
+
// every surface.
|
|
801
|
+
const ALLOWED_BARE_IMPORTS = VETTED_IMPORTS.map((v) => v.specifier);
|
|
802
|
+
|
|
803
|
+
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
804
|
+
// soft warning. None of these block the lint by themselves — they prompt
|
|
805
|
+
// the marketplace reviewer to spot-check, and they help authors learn the
|
|
806
|
+
// rule statically. Strings appear as literal substring matches (the URL
|
|
807
|
+
// is reachable in widget code by composing it at runtime, so this is
|
|
808
|
+
// best-effort).
|
|
809
|
+
const HOST_API_URL_PATTERNS = [
|
|
810
|
+
"/api/v1",
|
|
811
|
+
"/uploads/",
|
|
812
|
+
"Authorization: Bearer",
|
|
813
|
+
];
|
|
621
814
|
|
|
622
815
|
function deepFreeze(value) {
|
|
623
816
|
if (value === null || typeof value !== "object") return value;
|
|
@@ -627,7 +820,17 @@ function deepFreeze(value) {
|
|
|
627
820
|
}
|
|
628
821
|
|
|
629
822
|
const CONTRACT = deepFreeze({
|
|
630
|
-
|
|
823
|
+
// REQ-WSDK-PLATFORM bump:
|
|
824
|
+
// - `vettedImports` is a new field (rich allowlist with platforms +
|
|
825
|
+
// category + description).
|
|
826
|
+
// - `hostApiUrlPatterns` is a new field (soft-warning substrings).
|
|
827
|
+
// - `bannedApis` no longer lists `fetch` / `XMLHttpRequest`.
|
|
828
|
+
// - `allowedBareImports` is now derived from `vettedImports` (same
|
|
829
|
+
// shape; same contents grow with each vetted addition).
|
|
830
|
+
// Permissive-direction change: minor bump on the contract's own
|
|
831
|
+
// versioning (per CLAUDE.md §4, pre-1.0 minor is the breaking channel —
|
|
832
|
+
// the package.json version bumps accordingly).
|
|
833
|
+
version: "1.5.0",
|
|
631
834
|
hooks: HOOKS,
|
|
632
835
|
primitives: PRIMITIVES,
|
|
633
836
|
manifestSchema: MANIFEST_SCHEMA,
|
|
@@ -637,7 +840,9 @@ const CONTRACT = deepFreeze({
|
|
|
637
840
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
638
841
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
|
639
842
|
bannedApis: BANNED_APIS,
|
|
843
|
+
vettedImports: VETTED_IMPORTS,
|
|
640
844
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
845
|
+
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
641
846
|
});
|
|
642
847
|
|
|
643
848
|
function isHookAllowed(name) {
|