@colixsystems/widget-sdk 0.17.0 → 0.19.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 +135 -17
- package/dist/contract.cjs +156 -66
- package/dist/contract.js +161 -66
- package/dist/hooks.js +776 -387
- package/dist/index.d.ts +292 -37
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/linter.cjs +56 -0
- package/dist/linter.js +57 -0
- package/dist/manifest.cjs +75 -2
- package/dist/manifest.js +75 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,23 +1,87 @@
|
|
|
1
1
|
# @colixsystems/widget-sdk
|
|
2
2
|
|
|
3
|
-
Common widget interface for [AppStudio](https://github.com/appstudio). This package implements the contract that every widget
|
|
3
|
+
Common widget interface for [AppStudio](https://github.com/appstudio). This package is **core only** — it implements the contract that every widget (built-in or third-party, web or native) speaks: a `WidgetManifest`, a `WidgetContext`, a property schema, the primitives + rendering surface, the helper hooks, events, theme/i18n, and the static linter that gates submissions. **It owns no HTTP and depends on none of the data SDK packages.**
|
|
4
|
+
|
|
5
|
+
The data layer lives in **four separate domain-client packages**, each instantiated by the host and **injected into `WidgetContext`**. Widgets never import those packages — they reach the data surface only through this SDK's hooks, which read the injected client instances:
|
|
6
|
+
|
|
7
|
+
| Injected at | Package | Surface (snake_case verbatim, `list` → `{ data, meta }`) |
|
|
8
|
+
| ----------- | ------- | -------- |
|
|
9
|
+
| `ctx.datastore` | `@colixsystems/datastore-client` | `tables.{list,get}`, `schema(tableId)`, `records(tableId).{ list(query), get(id), create(values), update(id,values) [PATCH], delete(id), aggregate(spec), permissions(recordId).{ list, grant, update, revoke } }` |
|
|
10
|
+
| `ctx.directory` | `@colixsystems/directory-client` | `me()`, `users.{list,get,invite,deactivate,reactivate}`, `groups.{list,create,remove,addMember,removeMember,listMine}`, `invites.{list,revoke,resend}` |
|
|
11
|
+
| `ctx.files` | `@colixsystems/files-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useFile()` resolves |
|
|
12
|
+
| `ctx.payments` | `@colixsystems/payments-client` | `requestPayment(body)`, `getPayment(id)` |
|
|
13
|
+
|
|
14
|
+
**Wire / casing: snake_case end to end.** The clients send and return snake_case **verbatim** (`created_at`, `group_ids`, `can_read`, `amount_cents`, `data_type`, `is_active`, …). There is **no client-side case transform anywhere** — the only transform left in the system is the backend's wire ↔ Prisma middleware. Author-defined record column values pass through verbatim. Every `list(...)` returns the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you.
|
|
15
|
+
|
|
16
|
+
**Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (18 hooks), grouped by the domain client each one reads. **CORE** hooks read host state directly off `WidgetContext` (no data client); the rest delegate to one of the four injected clients. The grouping mirrors the banner sections in [`src/hooks.js`](src/hooks.js).
|
|
17
|
+
|
|
18
|
+
| Group | Hook (signature) | Returns | Reads / scope |
|
|
19
|
+
| ----- | ---------------- | ------- | ------------- |
|
|
20
|
+
| **CORE** | `useTheme()` | `{ colors, spacing, radii, typography }` | `ctx.workspace.theme` — no scope |
|
|
21
|
+
| **CORE** | `useUser()` | `{ id, email, display_name, roles, group_ids }` | `ctx.user` (snake_case verbatim; `id` null when anonymous) — no scope |
|
|
22
|
+
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute }` | `ctx.navigation` — no scope (external URLs use the `Linking` primitive) |
|
|
23
|
+
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope |
|
|
24
|
+
| **CORE** | `useChildRenderer()` | `{ renderNode(node) }` | `ctx.renderer` — no scope (prefer the `WidgetTree` component) |
|
|
25
|
+
| **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
|
|
26
|
+
| **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
|
|
27
|
+
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope |
|
|
28
|
+
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
29
|
+
| **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
|
|
30
|
+
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
31
|
+
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
|
|
32
|
+
| **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
|
|
33
|
+
| **FILES** (`ctx.files`) | `useFile(id)` | `{ url, file, loading, error, refetch }` | `ctx.files.get` — no scope |
|
|
34
|
+
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
35
|
+
| **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (mutations also `users.write:*`) |
|
|
36
|
+
| **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
37
|
+
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser` |
|
|
38
|
+
|
|
39
|
+
All list calls return the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you. There is no `useWorkspace()` or `useLogger()` hook — read the theme via `useTheme()` and the locale via `useI18n()`; the host logger lives on `ctx.logger` (`{ debug, info, warn, error }`).
|
|
40
|
+
|
|
41
|
+
`ctx.recordPermissions`, `ctx.users`, and `ctx.groups` **no longer exist** — they were folded into `ctx.datastore.records(t).permissions` and `ctx.directory.{users,groups}` respectively.
|
|
4
42
|
|
|
5
43
|
See the design reference for the full architecture: [`docs/architecture/widget-marketplace.md`](../../docs/architecture/widget-marketplace.md), specifically section 3.1.
|
|
6
44
|
|
|
7
45
|
## Status
|
|
8
46
|
|
|
9
|
-
`v0.
|
|
47
|
+
`v0.19.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
48
|
|
|
11
|
-
### What's new in 0.
|
|
49
|
+
### What's new in 0.19.0
|
|
12
50
|
|
|
13
|
-
|
|
51
|
+
**The data layer splits into four injected domain clients; the SDK becomes core-only.**
|
|
14
52
|
|
|
15
|
-
-
|
|
53
|
+
- **The SDK no longer owns any data facade.** The bespoke per-hook facades that used to live on `WidgetContext` (`ctx.datastore` as an opaque host object, `ctx.directory.listUsers`, `ctx.users`, `ctx.groups`, `ctx.recordPermissions`, `ctx.files.get`) are replaced by four host-instantiated, host-injected domain clients: `ctx.datastore` (`@colixsystems/datastore-client`), `ctx.directory` (`@colixsystems/directory-client`), `ctx.files` (`@colixsystems/files-client`, flattened), `ctx.payments` (`@colixsystems/payments-client`). The SDK imports none of them and ships no HTTP.
|
|
54
|
+
- **`ctx.recordPermissions`, `ctx.users`, `ctx.groups` are removed.** Per-record permission management moved under `ctx.datastore.records(tableId).permissions(recordId)`; user / group administration moved under `ctx.directory.users` / `ctx.directory.groups`. The hooks (`useRecordPermissions`, `useUsers`, `useGroups`) keep the same names and signatures — only the client slice they read changed.
|
|
55
|
+
- **snake_case end to end, no client-side transform.** Clients send and return snake_case verbatim (`group_ids`, `can_read`, `is_active`, `amount_cents`, `data_type`, `created_at`, …). The SDK passes bodies straight through and unwraps the `{ data, meta }` list envelope without renaming a single field. Hook return rows and `useUser()` fields are therefore snake_case (`display_name`, `group_ids`, `is_active`, `member_count`, and `useRecordPermissions` rows carry `user_id` / `group_id` / `can_read` / `can_write` / `can_delete` / `can_grant`).
|
|
56
|
+
- **Companion package versions:** `datastore-client 0.5.0`, `files-client 0.3.0`, `directory-client 0.1.0`, `payments-client 0.1.0`.
|
|
57
|
+
- **`CONTRACT.version` → `1.9.0`.** Breaking for `WidgetContext` consumers (removed slices, renamed wire fields); the hook export surface is unchanged.
|
|
58
|
+
|
|
59
|
+
### What was in 0.18.0
|
|
60
|
+
|
|
61
|
+
Two additive features land in this version.
|
|
62
|
+
|
|
63
|
+
**A widget may declare server-side actions in its manifest.**
|
|
64
|
+
|
|
65
|
+
- **`WidgetManifest.actions` is now part of the public contract.** An optional array; each entry is `{ key, name, description?, triggerType, scheduleCron?, timeoutMs?, scriptSource }`. `triggerType` is one of `schedule`, `record_created`, `record_updated`, `record_deleted`; `scheduleCron` is required when `triggerType === "schedule"`. The `scriptSource` (≤ 200 KiB) runs in the **shared isolated-vm action runner** — against `datastore` / `fetch` / `console` / `record` / `tenantId` (the runner surface, **not** the React/SDK widget surface, so SDK imports and hooks are unavailable there and the component linter does not scan it). Actions never run in the rendered app, so they have **no effect on Player ↔ export parity**.
|
|
66
|
+
- **Operators enable actions per tenant** from the Properties Panel. An enabled action materialises a tenant `Action` row **DISABLED** until the operator binds an integration API key (and, for `record_*` triggers, a target table) in the Actions admin page — those bindings are tenant-local, so `triggerTableId` / `apiKeyId` must **not** appear in the manifest (the validator and linter reject them).
|
|
67
|
+
- **New contract fields** `CONTRACT.actionTriggerTypes`, `CONTRACT.actionScriptGlobals`, `CONTRACT.actionScriptMaxBytes` expose the grammar so the Developer page, the AI agent prompt, and `validateManifest` derive it from one source. `validateManifest` now structurally validates `actions`; the marketplace linter rejects malformed / oversized declarations.
|
|
68
|
+
- **New manifest category `ADMINISTRATION`** for app-administration widgets such as User Management. Added to `CONTRACT.manifestCategories`, `validateManifest`, the `WidgetCategory` type, the marketplace category list, and the master-DB `WidgetCategory` enum.
|
|
69
|
+
|
|
70
|
+
**Per-record permission management from inside a widget.**
|
|
71
|
+
|
|
72
|
+
- **`useRecordPermissions(tableId, recordId)` is wired** + the new `WidgetContext.recordPermissions` slice + the new `acl.write:records` scope. Returns `{ permissions, loading, error, grant, revoke, update, refetch }` where `permissions` is `Array<{ id, principalType: "USER" | "GROUP" | "PUBLIC", principalId, canRead, canWrite, canDelete, canGrant }>`. Rejections from `grant` / `revoke` / `update` surface as a structured `PermissionError` (named export) with `code` ∈ `FORBIDDEN | VALIDATION | NOT_FOUND | CONFLICT | INTERNAL`. When `tableId` or `recordId` is null/empty the hook collapses to a stable empty no-op result. Mutating requires `acl.write:records` AND `canGrant` on the target record — Studio owners pass automatically; an APP_USER holds `canGrant` as the record's creator or via a delegated grant. Backs the built-in Chat widget's "+ New channel" + DM-create flows. Additive.
|
|
73
|
+
- **`CONTRACT.version` → `1.8.0`** (additive: a new optional `actions` manifest field, three new contract fields, the `ADMINISTRATION` category, the `useRecordPermissions` hook, the `recordPermissions` context slice, the `acl.write:records` scope, and the `PermissionError` class). No existing export changed signature.
|
|
74
|
+
|
|
75
|
+
### What was in 0.17.0
|
|
76
|
+
|
|
77
|
+
A runtime schema resolver so widgets can render by column type.
|
|
78
|
+
|
|
79
|
+
- **`useDatastoreSchema(tableId)` is wired** + the new `WidgetContext.datastore.schema` slice. Returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] }` (`null` until loaded). Reads structure only, never row data, so a public-grant table resolves for anonymous visitors like a record read. Use it to resolve a stored `columnId` to its name / dataType / relation target at runtime. Reads need the `datastore.read:<table>` scope. Additive.
|
|
16
80
|
- **`CONTRACT.version` → `1.7.0`** (additive: one new hook, one new `datastore.schema` context field). No existing export changed signature.
|
|
17
81
|
|
|
18
82
|
### What's new in 0.16.0
|
|
19
83
|
|
|
20
|
-
|
|
84
|
+
The tenant's **Theme Settings** now flow all the way into `useTheme()`.
|
|
21
85
|
|
|
22
86
|
- **`themeTokens.colors` gains `secondary` + `onSecondary`.** `useTheme().colors.secondary` reflects the tenant's *Secondary Color* picker (with `onSecondary` as its readable contrast color), alongside the existing `primary` / `onPrimary`. Built-in widgets like Button use it for their secondary variant; third-party widgets can use it for a branded second accent. The full `colors` shape is now `{ primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }`.
|
|
23
87
|
- **`colors.primary` / `colors.secondary` / `typography.fontFamily` are tenant-resolved.** The host maps the Studio Theme Settings blob (Primary Color, Secondary Color, Global Font) onto the default tokens before handing them to `useTheme()`, on both the live Player and the exported app — so a widget that reads tokens re-themes automatically. (Custom Google fonts render in the Player today; the exported app falls back to the system face for non-system fonts until font bundling lands.)
|
|
@@ -25,7 +89,7 @@ REQ-THEME — the tenant's **Theme Settings** now flow all the way into `useThem
|
|
|
25
89
|
|
|
26
90
|
### What's new in 0.15.0
|
|
27
91
|
|
|
28
|
-
|
|
92
|
+
The "split-implementation + vetted package list" pivot.
|
|
29
93
|
|
|
30
94
|
- **`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.
|
|
31
95
|
- **`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.
|
|
@@ -41,12 +105,12 @@ REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. Se
|
|
|
41
105
|
|
|
42
106
|
### What was in 0.14.1
|
|
43
107
|
|
|
44
|
-
- **`groupRef` property type
|
|
108
|
+
- **`groupRef` property type.** Authors can 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, 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.
|
|
45
109
|
- **Patch bump** — additive enumeration entry, no exported function signature changed. `CONTRACT.version` stays `1.4.0`.
|
|
46
110
|
|
|
47
111
|
### What's new in 0.14.0
|
|
48
112
|
|
|
49
|
-
- **`useUsers()` + `useGroups()` — AppUser administration hooks
|
|
113
|
+
- **`useUsers()` + `useGroups()` — AppUser administration hooks.** A widget can invite, deactivate, reactivate, and remove members, and create / delete groups + add / remove members, from a published-app surface. Returns `{ users | groups, loading, error, refetch, ... }` plus imperative mutation methods. Reads gated by `users.read:*` / `groups.read:*`; mutations by `users.write:*` / `groups.write:*`. Rejections surface as a structured `DirectoryError` (new named export) with `code` ∈ `FORBIDDEN | VALIDATION | NOT_FOUND | INVITE_ONLY`. The host signs an `X-Widget-Scopes` header against `JWT_SECRET` so an APP_USER cannot forge a scope set, and the backend additionally gates the request behind a SystemAcl `users.*` / `groups.*` capability grant.
|
|
50
114
|
- **Managing app users from a widget — see the section below.**
|
|
51
115
|
- **New linter rule `no-scope-mismatch-useUsers` / `no-scope-mismatch-useGroups`.** Calling `useUsers().invite()` / `.deactivate()` / `.reactivate()` / `.remove()` without `users.write:*` in the manifest's `requestedScopes` fails the lint; calling `useGroups()` mutation methods without `groups.write:*` fails the lint. Keeps the manifest honest at submission time.
|
|
52
116
|
- **`CONTRACT.version` → `1.4.0`** (additive: two new hooks, two new context slices, six new scope verbs, one new error class). No existing export changed signature.
|
|
@@ -71,12 +135,12 @@ REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. Se
|
|
|
71
135
|
|
|
72
136
|
### What's new in 0.9.0
|
|
73
137
|
|
|
74
|
-
- **`usePayments()` — incoming app-user payments
|
|
138
|
+
- **`usePayments()` — incoming app-user payments.** Returns `{ requestPayment, getPayment }`. `requestPayment({ amountCents, currency?, description, metadata?, returnPath? })` triggers a one-time charge from the signed-in app user and resolves to `{ id, status, checkoutUrl? }`: when `checkoutUrl` is present the widget opens it (web: navigate; native: `expo-web-browser`) — the user pays in **hosted Stripe Checkout**; when absent (the platform's built-in **mock** provider, the default until Stripe is configured) the charge auto-confirms (`status: "PAID"`). `getPayment(id)` polls the terminal status. Backed by a new `WidgetContext.payments` slice and gated by the new `payments.charge:appUser` scope. **No card data ever touches the widget** — never collect card fields yourself. The charge settles to the workspace owner; the amount is bounded by a platform per-charge cap. Rejections are a structured `PaymentError` (also a new named export) with a stable `.code`.
|
|
75
139
|
- **`CONTRACT.version` → `1.2.0`** (additive: one new hook, one new context slice, one new scope, one new error class). No existing export changed signature.
|
|
76
140
|
|
|
77
141
|
### What was in 0.8.0
|
|
78
142
|
|
|
79
|
-
- **`useDirectory()` — read-only user directory hook
|
|
143
|
+
- **`useDirectory()` — read-only user directory hook.** Returns `{ users, loading, error, refetch }` where each user is `{ id, name, role }`. Gated by the new `directory.read:users` scope. Use it for a chat people-list, an @-mention picker, or to resolve an author id to a display name. Player callers get the reduced `{ id, name, role }` projection — email and other admin fields never leave the server for an app end-user. `query` is an optional `{ q, role, isActive, limit, offset }` (`q` substring-matches the display name; `role` is `"USER"` (default), `"INTEGRATION"`, or `"ALL"`). The directory is read-only.
|
|
80
144
|
- **`CONTRACT.version` → `1.1.0`** (additive: one new hook, one new context slice, one new scope). No existing export changed signature.
|
|
81
145
|
|
|
82
146
|
### What was in 0.7.0
|
|
@@ -95,7 +159,7 @@ REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. Se
|
|
|
95
159
|
### What was in 0.5.0
|
|
96
160
|
|
|
97
161
|
- **`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.
|
|
98
|
-
- **`useDatastoreMutation(...).update(id, partial)` is wired.**
|
|
162
|
+
- **`useDatastoreMutation(...).update(id, partial)` is wired.** 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 runs against the merged row, excluding self so a re-affirm doesn't trip its own UNIQUE.
|
|
99
163
|
|
|
100
164
|
### What was in 0.4.1
|
|
101
165
|
|
|
@@ -122,7 +186,7 @@ REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. Se
|
|
|
122
186
|
|
|
123
187
|
### What was in 0.3.0
|
|
124
188
|
|
|
125
|
-
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
|
|
189
|
+
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 built-in template semantics (auto-suffixed naming, creator-grants, public grants, cross-relation inheritance) and persist when the widget is later uninstalled. See `WidgetDatastoreTemplate` in `src/index.d.ts` for the constraints — at most 8 tables per widget, 24 columns per table, RELATION columns address siblings by `suffix`.
|
|
126
190
|
|
|
127
191
|
## Public API
|
|
128
192
|
|
|
@@ -132,14 +196,14 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
132
196
|
|
|
133
197
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
134
198
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
135
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `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,
|
|
199
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `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, display_name, roles, group_ids }` (snake_case verbatim; `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). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `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).
|
|
136
200
|
- `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.
|
|
137
201
|
- `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.
|
|
138
202
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
139
203
|
|
|
140
204
|
## Managing app users from a widget
|
|
141
205
|
|
|
142
|
-
|
|
206
|
+
`useUsers()` and `useGroups()` let a widget invite, deactivate, reactivate, and remove members, plus create / delete groups and add / remove their members. Two gates apply: the manifest must declare the scope (the static analyzer + the host's signed `X-Widget-Scopes` header agree), and the calling APP_USER must hold the matching `users.*` / `groups.*` capability (a SystemAcl grant the Studio admin issues via the Roles UI). A widget that declares `users.write:*` but whose caller lacks the grant gets a `DirectoryError` with `code: 'FORBIDDEN'` — surface that as a "you do not have permission" message.
|
|
143
207
|
|
|
144
208
|
```js
|
|
145
209
|
import { Text, View, Pressable, useUsers, useGroups } from "@colixsystems/widget-sdk";
|
|
@@ -152,7 +216,7 @@ export default function MemberManager() {
|
|
|
152
216
|
<View>
|
|
153
217
|
{users.map((u) => (
|
|
154
218
|
<View key={u.id}>
|
|
155
|
-
<Text>{u.name} — {u.
|
|
219
|
+
<Text>{u.name} — {u.is_active ? "active" : "inactive"}</Text>
|
|
156
220
|
<Pressable onPress={() => deactivate(u.id)}><Text>Deactivate</Text></Pressable>
|
|
157
221
|
<Pressable onPress={() => remove(u.id)}><Text>Remove</Text></Pressable>
|
|
158
222
|
</View>
|
|
@@ -160,7 +224,7 @@ export default function MemberManager() {
|
|
|
160
224
|
<Pressable
|
|
161
225
|
onPress={async () => {
|
|
162
226
|
try {
|
|
163
|
-
await invite({ email: "a@b.com", name: "New User",
|
|
227
|
+
await invite({ email: "a@b.com", name: "New User", group_ids: [groups[0]?.id].filter(Boolean) });
|
|
164
228
|
} catch (err) {
|
|
165
229
|
// err.code is one of FORBIDDEN | VALIDATION | NOT_FOUND | INVITE_ONLY
|
|
166
230
|
}
|
|
@@ -184,6 +248,60 @@ The matching manifest declares the scopes:
|
|
|
184
248
|
|
|
185
249
|
The host rejects calls whose scope is not declared in the manifest (the SDK linter catches this statically too). Declaring a write scope is also a consent prompt the Studio admin sees at install time — the wider the scope set, the more careful the admin is about granting the install.
|
|
186
250
|
|
|
251
|
+
## Managing per-record permissions from a widget
|
|
252
|
+
|
|
253
|
+
`useRecordPermissions(tableId, recordId)` is the in-app surface for sharing a single record with another user or group. The chat widget uses it to invite members into a channel — the channel record's per-record grants ARE the membership list (messages inherit those grants). The hook also covers project-workspace, document-sharing, and team-roster widgets that grant access record-by-record.
|
|
254
|
+
|
|
255
|
+
The rows and bodies are snake_case verbatim. A row carries `user_id` OR `group_id` (both null = a public grant) plus the `can_read` / `can_write` / `can_delete` / `can_grant` flags; the hook reads `ctx.datastore.records(tableId).permissions(recordId)`.
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
import { Text, View, Pressable, useRecordPermissions, PermissionError } from "@colixsystems/widget-sdk";
|
|
259
|
+
|
|
260
|
+
export default function ShareRecord({ tableId, recordId, partnerUserId }) {
|
|
261
|
+
const { permissions, loading, grant, revoke } = useRecordPermissions(tableId, recordId);
|
|
262
|
+
if (loading) return <Text>Loading members…</Text>;
|
|
263
|
+
return (
|
|
264
|
+
<View>
|
|
265
|
+
{permissions.map((p) => (
|
|
266
|
+
<View key={p.id}>
|
|
267
|
+
<Text>{p.user_id || p.group_id || "public"} — {p.can_write ? "writer" : "reader"}</Text>
|
|
268
|
+
<Pressable onPress={() => revoke(p.id)}><Text>Remove</Text></Pressable>
|
|
269
|
+
</View>
|
|
270
|
+
))}
|
|
271
|
+
<Pressable
|
|
272
|
+
onPress={async () => {
|
|
273
|
+
try {
|
|
274
|
+
await grant({
|
|
275
|
+
user_id: partnerUserId,
|
|
276
|
+
can_read: true,
|
|
277
|
+
can_write: true,
|
|
278
|
+
can_grant: true,
|
|
279
|
+
});
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (err instanceof PermissionError && err.code === "FORBIDDEN") {
|
|
282
|
+
// The signed-in user lacks can_grant on this record.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<Text>Invite partner</Text>
|
|
288
|
+
</Pressable>
|
|
289
|
+
</View>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The manifest declares the matching scope:
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
{
|
|
298
|
+
// ...
|
|
299
|
+
requestedScopes: ["acl.write:records"],
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The server-side gate is `canGrant` on the target record — Studio owners pass automatically; an APP_USER holds `canGrant` as the record's creator or via a delegated grant. A caller without `canGrant` receives `PermissionError { code: "FORBIDDEN" }`. The hook collapses to a stable no-op when `tableId` or `recordId` is null/empty — so a widget can render its picker first, then bind to the picked record without conditional hook tricks.
|
|
304
|
+
|
|
187
305
|
## Linter
|
|
188
306
|
|
|
189
307
|
```sh
|
package/dist/contract.cjs
CHANGED
|
@@ -167,12 +167,12 @@ const HOOKS = [
|
|
|
167
167
|
name: "useDirectory",
|
|
168
168
|
signature: "useDirectory(query?)",
|
|
169
169
|
returnShape: {
|
|
170
|
-
users: "Array<{ id, name, role }>",
|
|
170
|
+
users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
|
|
171
171
|
loading: "boolean",
|
|
172
172
|
error: "DatastoreError | null",
|
|
173
173
|
refetch: "() => Promise<void>",
|
|
174
174
|
},
|
|
175
|
-
requiredContextSlice: ["directory.
|
|
175
|
+
requiredContextSlice: ["directory.users"],
|
|
176
176
|
scopes: ["directory.read:users"],
|
|
177
177
|
},
|
|
178
178
|
{
|
|
@@ -206,28 +206,26 @@ const HOOKS = [
|
|
|
206
206
|
name: "useUsers",
|
|
207
207
|
signature: "useUsers(query?)",
|
|
208
208
|
description:
|
|
209
|
-
"AppUser administration
|
|
210
|
-
"deactivate,
|
|
211
|
-
"
|
|
212
|
-
"
|
|
209
|
+
"AppUser administration via the injected directory-client at " +
|
|
210
|
+
"ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
|
|
211
|
+
"{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
|
|
212
|
+
"list returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
213
|
+
"res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
|
|
214
|
+
"scope; mutations need users.write:*. The `invite` call accepts " +
|
|
215
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
216
|
+
"(the email is sent by the host).",
|
|
213
217
|
returnShape: {
|
|
214
|
-
users: "Array<{ id, name, email?, role,
|
|
218
|
+
users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
|
|
215
219
|
loading: "boolean",
|
|
216
220
|
error: "DirectoryError | null",
|
|
217
221
|
refetch: "() => Promise<void>",
|
|
218
222
|
invite:
|
|
219
|
-
"({ email, name,
|
|
223
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
220
224
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
221
225
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
222
226
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
223
227
|
},
|
|
224
|
-
requiredContextSlice: [
|
|
225
|
-
"users.listUsers",
|
|
226
|
-
"users.invite",
|
|
227
|
-
"users.deactivate",
|
|
228
|
-
"users.reactivate",
|
|
229
|
-
"users.remove",
|
|
230
|
-
],
|
|
228
|
+
requiredContextSlice: ["directory.users"],
|
|
231
229
|
scopes: ["users.read:*"],
|
|
232
230
|
},
|
|
233
231
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration. Returns
|
|
@@ -239,11 +237,14 @@ const HOOKS = [
|
|
|
239
237
|
name: "useGroups",
|
|
240
238
|
signature: "useGroups(query?)",
|
|
241
239
|
description:
|
|
242
|
-
"AppUserGroup administration
|
|
243
|
-
"create,
|
|
240
|
+
"AppUserGroup administration via the injected directory-client at " +
|
|
241
|
+
"ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
|
|
242
|
+
"Returns { groups, loading, error, refetch, create, remove, addMember, " +
|
|
243
|
+
"removeMember }. list returns the { data, meta } envelope verbatim — the " +
|
|
244
|
+
"hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
|
|
244
245
|
"mutations need groups.write:*.",
|
|
245
246
|
returnShape: {
|
|
246
|
-
groups: "Array<{ id, name,
|
|
247
|
+
groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
|
|
247
248
|
loading: "boolean",
|
|
248
249
|
error: "DirectoryError | null",
|
|
249
250
|
refetch: "() => Promise<void>",
|
|
@@ -255,15 +256,44 @@ const HOOKS = [
|
|
|
255
256
|
removeMember:
|
|
256
257
|
"(groupId, userId) => Promise<void> // rejects with DirectoryError",
|
|
257
258
|
},
|
|
258
|
-
requiredContextSlice: [
|
|
259
|
-
"groups.listGroups",
|
|
260
|
-
"groups.create",
|
|
261
|
-
"groups.remove",
|
|
262
|
-
"groups.addMember",
|
|
263
|
-
"groups.removeMember",
|
|
264
|
-
],
|
|
259
|
+
requiredContextSlice: ["directory.groups"],
|
|
265
260
|
scopes: ["groups.read:*"],
|
|
266
261
|
},
|
|
262
|
+
// REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
263
|
+
// management for a single record. Mirror of contract.js.
|
|
264
|
+
{
|
|
265
|
+
name: "useRecordPermissions",
|
|
266
|
+
signature: "useRecordPermissions(tableId, recordId)",
|
|
267
|
+
description:
|
|
268
|
+
"Manage per-record VirtualPermission grants on a single record via the " +
|
|
269
|
+
"injected datastore-client at " +
|
|
270
|
+
"ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}. " +
|
|
271
|
+
"Returns { permissions, loading, error, grant, revoke, update, refetch } " +
|
|
272
|
+
"where permissions is Array<{ id, user_id, group_id, can_read, can_write, " +
|
|
273
|
+
"can_delete, can_grant }> (snake_case rows verbatim; list() returns the " +
|
|
274
|
+
"{ data, meta } envelope and the hook unwraps res.data). grant/update " +
|
|
275
|
+
"bodies are snake_case verbatim ({ user_id | group_id, can_read, " +
|
|
276
|
+
"can_write, can_delete, can_grant }). Mutating requires acl.write:records " +
|
|
277
|
+
"scope AND can_grant on the target record (REQ-ACL-RELINHERIT-05: " +
|
|
278
|
+
"APP_USER actors with can_grant are accepted, not only Studio owners). " +
|
|
279
|
+
"When tableId or recordId is null/empty the hook collapses to an empty " +
|
|
280
|
+
"no-op result without a network round-trip.",
|
|
281
|
+
returnShape: {
|
|
282
|
+
permissions:
|
|
283
|
+
"Array<{ id, user_id, group_id, can_read, can_write, can_delete, can_grant }> // snake_case rows; unwrapped from { data, meta }",
|
|
284
|
+
loading: "boolean",
|
|
285
|
+
error: "PermissionError | null",
|
|
286
|
+
grant:
|
|
287
|
+
"({ user_id?, group_id?, can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
|
|
288
|
+
revoke:
|
|
289
|
+
"(permissionId) => Promise<void> // rejects with PermissionError",
|
|
290
|
+
update:
|
|
291
|
+
"(permissionId, { can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
|
|
292
|
+
refetch: "() => Promise<void>",
|
|
293
|
+
},
|
|
294
|
+
requiredContextSlice: ["datastore.records"],
|
|
295
|
+
scopes: ["acl.write:records"],
|
|
296
|
+
},
|
|
267
297
|
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
268
298
|
{
|
|
269
299
|
name: "useClipboard",
|
|
@@ -419,10 +449,40 @@ const CATEGORIES = [
|
|
|
419
449
|
"DATA",
|
|
420
450
|
"MEDIA",
|
|
421
451
|
"COMMUNICATION",
|
|
452
|
+
// REQ-USERMGMT-06: app-administration widgets (User Management, …) the
|
|
453
|
+
// published app embeds for its own member management — its own palette
|
|
454
|
+
// section, distinct from COMMUNICATION.
|
|
455
|
+
"ADMINISTRATION",
|
|
422
456
|
"CUSTOM",
|
|
423
457
|
];
|
|
424
458
|
const PLATFORMS = ["web", "native"];
|
|
425
459
|
|
|
460
|
+
// REQ-WIDGET-ACTION — server-side actions a widget may declare in its
|
|
461
|
+
// manifest. Each runs in the shared isolated-vm action runner (see backend
|
|
462
|
+
// action-runner.service.js) on a cron schedule or in response to a record
|
|
463
|
+
// CRUD event — NEVER in the rendered app, so they never affect Player ↔
|
|
464
|
+
// export parity. The trigger vocabulary mirrors the backend Action model.
|
|
465
|
+
const ACTION_TRIGGER_TYPES = [
|
|
466
|
+
"schedule",
|
|
467
|
+
"record_created",
|
|
468
|
+
"record_updated",
|
|
469
|
+
"record_deleted",
|
|
470
|
+
];
|
|
471
|
+
// Globals the action script runs against (the runner's surface) — distinct
|
|
472
|
+
// from the React/SDK widget surface, so the component import/banned-API
|
|
473
|
+
// linter does NOT scan action scripts.
|
|
474
|
+
const ACTION_SCRIPT_GLOBALS = [
|
|
475
|
+
"datastore",
|
|
476
|
+
"fetch",
|
|
477
|
+
"console",
|
|
478
|
+
"record",
|
|
479
|
+
"tenantId",
|
|
480
|
+
"triggerType",
|
|
481
|
+
"triggerTableId",
|
|
482
|
+
];
|
|
483
|
+
// Mirrors action.service.js SCRIPT_MAX_BYTES.
|
|
484
|
+
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
485
|
+
|
|
426
486
|
// Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
|
|
427
487
|
// more labels, lowercase alnum + hyphen, label starts with a letter. The
|
|
428
488
|
// analyzer + the SDK validator both read this from the contract so a
|
|
@@ -508,6 +568,17 @@ const MANIFEST_SCHEMA = {
|
|
|
508
568
|
description:
|
|
509
569
|
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (must be declared earlier in the array). Tables persist across uninstalls.",
|
|
510
570
|
},
|
|
571
|
+
actions: {
|
|
572
|
+
type: "object[]",
|
|
573
|
+
required: false,
|
|
574
|
+
description:
|
|
575
|
+
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?, triggerType (one of " +
|
|
576
|
+
ACTION_TRIGGER_TYPES.join(", ") +
|
|
577
|
+
"), scheduleCron? (required iff triggerType=='schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
|
|
578
|
+
ACTION_SCRIPT_GLOBALS.join(", ") +
|
|
579
|
+
" — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
|
|
580
|
+
default: [],
|
|
581
|
+
},
|
|
511
582
|
};
|
|
512
583
|
|
|
513
584
|
const WIDGET_CONTEXT_SHAPE = {
|
|
@@ -524,9 +595,10 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
524
595
|
fields: { id: "manifest.id", version: "manifest.version" },
|
|
525
596
|
},
|
|
526
597
|
user: {
|
|
527
|
-
description:
|
|
598
|
+
description:
|
|
599
|
+
"Signed-in user, host-provided VERBATIM (snake_case: { id, email, display_name, roles, group_ids }). Not a data-client.",
|
|
528
600
|
required: true,
|
|
529
|
-
fields: { id: "string", email: "string",
|
|
601
|
+
fields: { id: "string", email: "string", display_name: "string" },
|
|
530
602
|
},
|
|
531
603
|
workspace: {
|
|
532
604
|
description:
|
|
@@ -546,19 +618,30 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
546
618
|
},
|
|
547
619
|
datastore: {
|
|
548
620
|
description:
|
|
549
|
-
"
|
|
621
|
+
"Injected @colixsystems/datastore-client instance. " +
|
|
622
|
+
"{ tables: { list(), get(idOrName) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
623
|
+
"records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
|
|
624
|
+
"permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
|
|
625
|
+
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(). " +
|
|
626
|
+
"List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
|
|
550
627
|
required: true,
|
|
551
|
-
fields: { records: "function", schema: "function" },
|
|
628
|
+
fields: { records: "function", schema: "function", tables: "object" },
|
|
552
629
|
},
|
|
553
630
|
directory: {
|
|
554
631
|
description:
|
|
555
|
-
"
|
|
632
|
+
"Injected @colixsystems/directory-client instance. " +
|
|
633
|
+
"{ me(), users: { list(query?) -> Promise<{ data, meta }>, get(id), invite(body), deactivate(id), reactivate(id) }, " +
|
|
634
|
+
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
635
|
+
"invites: { list(), revoke(id), resend(id) } }. " +
|
|
636
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:*.",
|
|
556
637
|
required: true,
|
|
557
|
-
fields: {
|
|
638
|
+
fields: { users: "object", groups: "object" },
|
|
558
639
|
},
|
|
559
640
|
files: {
|
|
560
641
|
description:
|
|
561
|
-
"
|
|
642
|
+
"Injected @colixsystems/files-client instance, FLATTENED so file ops are top-level. " +
|
|
643
|
+
"{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData), folders: { list, create }, shares: { list, create, remove } }. " +
|
|
644
|
+
"Backs useFile(); the returned file already carries an absolute url the widget can drop into an <Image source>.",
|
|
562
645
|
required: true,
|
|
563
646
|
fields: { get: "function" },
|
|
564
647
|
},
|
|
@@ -575,42 +658,17 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
575
658
|
},
|
|
576
659
|
payments: {
|
|
577
660
|
description:
|
|
578
|
-
"
|
|
661
|
+
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status, checkoutUrl? }>, getPayment(id) -> Promise<payment> }. Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
|
|
579
662
|
required: true,
|
|
580
663
|
fields: { requestPayment: "function", getPayment: "function" },
|
|
581
664
|
},
|
|
582
|
-
// REQ-
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
"AppUser administration. { listUsers(query?) -> Promise<User[]>, invite({ email, name, groupIds? }) -> Promise<Invite>, deactivate(userId) -> Promise<User>, reactivate(userId) -> Promise<User>, remove(userId) -> Promise<void> }. Backs useUsers(); reads require users.read:*, mutations require users.write:*.",
|
|
590
|
-
required: true,
|
|
591
|
-
fields: {
|
|
592
|
-
listUsers: "function",
|
|
593
|
-
invite: "function",
|
|
594
|
-
deactivate: "function",
|
|
595
|
-
reactivate: "function",
|
|
596
|
-
remove: "function",
|
|
597
|
-
},
|
|
598
|
-
},
|
|
599
|
-
// REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration facade
|
|
600
|
-
// backing useGroups(). Reads gated by `groups.read:*`; mutations by
|
|
601
|
-
// `groups.write:*`. Same X-Widget-Scopes + SystemAcl gating as users.
|
|
602
|
-
groups: {
|
|
603
|
-
description:
|
|
604
|
-
"AppUserGroup administration. { listGroups(query?) -> Promise<Group[]>, create({ name }) -> Promise<Group>, remove(groupId) -> Promise<void>, addMember(groupId, userId) -> Promise<void>, removeMember(groupId, userId) -> Promise<void> }. Backs useGroups(); reads require groups.read:*, mutations require groups.write:*.",
|
|
605
|
-
required: true,
|
|
606
|
-
fields: {
|
|
607
|
-
listGroups: "function",
|
|
608
|
-
create: "function",
|
|
609
|
-
remove: "function",
|
|
610
|
-
addMember: "function",
|
|
611
|
-
removeMember: "function",
|
|
612
|
-
},
|
|
613
|
-
},
|
|
665
|
+
// REQ-WSDK-DOMAIN-CLIENTS — the AppUser administration, AppUserGroup
|
|
666
|
+
// administration, and per-record VirtualPermission facades that used to
|
|
667
|
+
// live here (`users`, `groups`, `recordPermissions`) were folded into the
|
|
668
|
+
// injected domain-client instances: useUsers()/useGroups() read
|
|
669
|
+
// ctx.directory.{users,groups}; useRecordPermissions() reads
|
|
670
|
+
// ctx.datastore.records(table).permissions(record). See the `directory`
|
|
671
|
+
// and `datastore` slices above.
|
|
614
672
|
i18n: {
|
|
615
673
|
description: "{ t(key, fallback?), locale }.",
|
|
616
674
|
required: true,
|
|
@@ -865,12 +923,44 @@ const CONTRACT = deepFreeze({
|
|
|
865
923
|
// stored columnId to its column name / dataType / relation target at
|
|
866
924
|
// runtime (Form Builder renders inputs by column type). Reads the existing
|
|
867
925
|
// ACL-gated `GET /tables/:id` — structure only, no row data.
|
|
868
|
-
|
|
926
|
+
//
|
|
927
|
+
// 1.8.0: two additive features.
|
|
928
|
+
// - REQ-WIDGET-ACTION — manifests may declare an optional `actions` array.
|
|
929
|
+
// Each entry is a server-side action (cron- or record-triggered JS) the
|
|
930
|
+
// operator enables per tenant; it runs in the existing isolated-vm action
|
|
931
|
+
// runner, never in the rendered app. New contract fields
|
|
932
|
+
// `actionTriggerTypes`, `actionScriptGlobals`, and `actionScriptMaxBytes`
|
|
933
|
+
// let the docs + agent prompt + validator derive the grammar from one
|
|
934
|
+
// source. New `ADMINISTRATION` manifest category (REQ-USERMGMT-06).
|
|
935
|
+
// - REQ-ACL-06 — new `useRecordPermissions(tableId, recordId)` hook + the
|
|
936
|
+
// `recordPermissions` host-context slice it reads + the `acl.write:records`
|
|
937
|
+
// scope it gates on. Hits the existing REQ-ACL-06
|
|
938
|
+
// /api/v1/tables/:tableId/records/:recordId/permissions REST surface; the
|
|
939
|
+
// host normalises the wire shape into a single principalType+principalId
|
|
940
|
+
// pair widgets branch on. Caller still needs canGrant on the target
|
|
941
|
+
// record (REQ-ACL-RELINHERIT-05).
|
|
942
|
+
//
|
|
943
|
+
// 1.9.0: host-contract change (REQ-WSDK-DOMAIN-CLIENTS) — the host now
|
|
944
|
+
// injects domain-client INSTANCES on the WidgetContext rather than
|
|
945
|
+
// bespoke per-hook facades. ctx.datastore is a @colixsystems/datastore-client
|
|
946
|
+
// (records(t).list now returns the { data, meta } envelope verbatim;
|
|
947
|
+
// records(t).permissions(r) replaces the deleted ctx.recordPermissions),
|
|
948
|
+
// ctx.directory is a @colixsystems/directory-client (users/groups
|
|
949
|
+
// namespaces replace the deleted ctx.users / ctx.groups; list returns
|
|
950
|
+
// envelopes), ctx.files is a flattened @colixsystems/files-client,
|
|
951
|
+
// ctx.payments is a @colixsystems/payments-client. All rows/bodies are
|
|
952
|
+
// snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
|
|
953
|
+
// SDK stays duck-typed — it imports none of the four data SDKs. Minor
|
|
954
|
+
// bump on the contract's pre-1.0 versioning (the breaking channel).
|
|
955
|
+
version: "1.9.0",
|
|
869
956
|
hooks: HOOKS,
|
|
870
957
|
primitives: PRIMITIVES,
|
|
871
958
|
manifestSchema: MANIFEST_SCHEMA,
|
|
872
959
|
manifestCategories: CATEGORIES,
|
|
873
960
|
manifestPlatforms: PLATFORMS,
|
|
961
|
+
actionTriggerTypes: ACTION_TRIGGER_TYPES,
|
|
962
|
+
actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
|
|
963
|
+
actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
|
|
874
964
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
875
965
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
876
966
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|