@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 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.14.1` — 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**.
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.14.1
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`. `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).
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`. 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.
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: "fetch",
605
- reason:
606
- "Direct network calls bypass the datastore client + tenant auth.",
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
- identifier: "XMLHttpRequest",
610
- reason:
611
- "Direct network calls bypass the datastore client + tenant auth.",
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
- identifier: "import(",
615
- reason: "Dynamic import bypasses the loader's allowlist.",
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
- const ALLOWED_BARE_IMPORTS = ["react", "@colixsystems/widget-sdk"];
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
- version: "1.4.0",
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) {