@colixsystems/widget-sdk 0.30.0 → 0.31.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
@@ -46,27 +46,28 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
46
46
 
47
47
  ## Status
48
48
 
49
- `v0.30.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**.
49
+ `v0.31.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**.
50
50
 
51
- ### What's new in 0.30.0
52
-
53
- **New `folderRef` propertySchema type (REQ-WDG-FOLDERREF).** A Filestore folder picker that stores a bare folder UUID (or `null` = space root) and renders the Asset/Filestore `FolderSelector` in the Studio Properties Panel, scoped by a sibling space field named via `ui.spaceTypeField` (its value `"project"` / `"personal"` selects the PROJECT / PERSONAL space). Changing the space drops a now-out-of-scope selection. Filestore spaces are per-tenant end-user data and are not copied, so tenant-copy leaves the id as-is. Additive — no existing export or type changed.
54
-
55
- ### What's new in 0.29.0
51
+ ### What's new in 0.31.0
56
52
 
57
- **New `assetList` propertySchema type + `ui.showWhen` `neq`/`in` + `ui.step` (REQ-WDG-ASSET).** `assetList` is the multi-pick form of `asset` an ordered array of bare File UUIDs rendered with the Asset Manager `MultiFileSelector` (selection order preserved; narrow with `ui.mimeFilter`). tenant-copy remaps each id. `ui.showWhen` now accepts `{ neq }` and `{ in: [...] }` alongside `{ eq }`; `ui.step` sets the numeric input/slider step. Additive — no existing export or type changed.
53
+ **Already-signed file state (REQ-SIGN).** So a file browser can show which files are signed (and not re-sign blindly):
54
+ - `useFileSignatures(fileIds)` → `{ signaturesByFileId, loading, error, refetch }` — a batch "is this file signed?" lookup. `signaturesByFileId` maps each signed, accessible file id to its latest `{ signature_id, signer_name, signed_at }`; unsigned files are absent. One request, no N+1. Reads `ctx.filestore.signatures.list` (new `@colixsystems/filestore-client` **0.3.0** method).
55
+ - `useFileSignature(fileId, existingSignatureId?)` gained an optional second arg: pass an already-signed file's signature id to seed the `complete` state and `verify()` it **without** opening a new order. Re-signing then requires an explicit `initiate()`.
58
56
 
59
- ### What's new in 0.28.0
57
+ `CONTRACT.version` `1.21.0` (additive — the new arg is optional, no existing hook changed).
60
58
 
61
- **New `asset` propertySchema type + `ui.showWhen` (REQ-WDG-ASSET).** `asset` is a file/asset picker that stores a bare File UUID and renders the Asset Manager `FileSelector` in the Studio Properties Panel (narrow it with `ui.mimeFilter`, e.g. `"image"`). `ui.showWhen: { field, eq }` conditionally hides a field unless a sibling field equals a value (e.g. show the asset picker only when `imageSource === "asset"`). tenant-copy remaps an `asset` File UUID to the copied file. Additive — no existing export or type changed.
59
+ ### What's new in 0.30.0
62
60
 
63
- ### What's new in 0.27.0
61
+ **Filestore browsing + BankID file signing for widgets (REQ-FS / REQ-SIGN).** Three new hooks read a newly-injected `ctx.filestore` (the `@colixsystems/filestore-client`, now constructed by both the web and native hosts):
62
+ - `useFilestoreFiles({ spaceType, folderId?, q?, type? })` → `{ files, loading, error, refetch }` — browses the end-user's Filestore space. The hook resolves `owner_id` from the host context (tenant for a project space, the app user for a personal space), so the widget only picks the space.
63
+ - `useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })` → `{ folders, loading, error, refetch }` — the folder-navigation companion to `useFilestoreFiles`; pass `enabled:false` to suspend fetching.
64
+ - `useFileSignature(fileId)` → `{ status, qr, signerName, verdict, initiate, refresh, cancel, verify, … }` — drives a BankID signing flow for a file (the backend hashes the bytes server-side, binds the digest into the signature, and verifies the proof offline).
64
65
 
65
- **New `richText` propertySchema type (REQ-WDG-RICHTEXT).** A rich-text (HTML) string edited via a Tiptap editor in the Studio Properties Panel; the widget renders the sanitised HTML. Stored as a plain string (no tenant-copy remap). Additive — no existing export or type changed.
66
+ `CONTRACT.version` `1.20.0` (additive — no existing hook changed).
66
67
 
67
- ### What's new in 0.26.0
68
+ ### What's new in 0.29.0
68
69
 
69
- **New `pageRef` propertySchema type (REQ-WDG-PAGEREF).** A page picker: the Studio Properties Panel renders a dropdown of the app's pages and stores the chosen page's bare UUID. A widget reads the prop and navigates with `useNavigation().goTo(pageId, params)` the cross-platform replacement for the old per-widget "navigate on press" event wiring. tenant-copy remaps the stored page id to the copied page. Additive: no existing export or type changed signature.
70
+ **The SIGNATURE column type and its hook are retired (REQ-SIGN).** `useSignature(tableId, recordId)` and the datastore-client `sign(recordId)` namespace are **removed** — signing a single record/column was the wrong granularity. BankID signing is being rebuilt around a standalone, polymorphic **Signature subject model** (sign a file first, then whole records and policy text), where the signature is cryptographically bound to the exact bytes signed (the file's SHA-256 embedded in the BankID signature) and verified offline against the pinned `BankID Root CA v1`. New SDK hooks for that model will land as the backend ships. `CONTRACT.version` `1.19.0` (pre-1.0 breaking removal of unreleased hooks; kept monotonic).
70
71
 
71
72
  ### What's new in 0.25.0
72
73
 
@@ -103,7 +104,7 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
103
104
  - **`expo-linear-gradient`** (`web`/`native`) — the cross-platform gradient.
104
105
  - **`lottie-react-native`** (`native`) + **`lottie-react`** (`web`) — Lottie animations, split-impl.
105
106
  - **`react-native-webview`** (`native`) — embedded web content (pair with an `<iframe>` on web).
106
- - **Parity (CLAUDE.md §3):** the compiler now pins the native-module members in the exported Expo app's `package.json` and emits the **`react-native-reanimated/plugin`** in `babel.config.js` **unconditionally** (previously only for the sidebar-drawer shell), so a baked widget that imports any of these resolves and bundles on native exactly as it renders in the web Player.
107
+ - **Parity (widget-parity skill):** the compiler now pins the native-module members in the exported Expo app's `package.json` and emits the **`react-native-reanimated/plugin`** in `babel.config.js` **unconditionally** (previously only for the sidebar-drawer shell), so a baked widget that imports any of these resolves and bundles on native exactly as it renders in the web Player.
107
108
  - **`CONTRACT.version` → `1.13.0`** (additive: new vetted import entries only). No existing export, type, manifest field, hook, or banned-API list changed.
108
109
 
109
110
  ### What's new in 0.22.0
package/dist/contract.cjs CHANGED
@@ -99,7 +99,7 @@ const HOOKS = [
99
99
  'style (flex: 1 / height: "100%"); widgets with no useful filled form ' +
100
100
  "may ignore it. Defaults to false wherever the host has not opted the " +
101
101
  "widget into filling, so calling it is always safe. The SAME value is " +
102
- "injected by the web Player and the native export (CLAUDE.md §3), so a " +
102
+ "injected by the web Player and the native export (widget-parity skill), so a " +
103
103
  "widget's fill behaviour is identical on both platforms.",
104
104
  returnShape: {
105
105
  "(returns)": "boolean",
@@ -155,6 +155,87 @@ const HOOKS = [
155
155
  requiredContextSlice: ["files.get"],
156
156
  scopes: null,
157
157
  },
158
+ {
159
+ name: "useFilestoreFiles",
160
+ signature: "useFilestoreFiles({ spaceType, folderId?, q?, type? })",
161
+ description:
162
+ "Browse the end-user's Filestore files in a project or personal space. " +
163
+ "The hook resolves owner_id from the host context (tenant for project, " +
164
+ "app user for personal) — the widget only chooses the space. Reads " +
165
+ "ctx.filestore.files.list and unwraps { data, meta } to the files array.",
166
+ returnShape: {
167
+ files: "FilestoreFile[]",
168
+ loading: "boolean",
169
+ error: "Error | null",
170
+ refetch: "() => Promise<void>",
171
+ },
172
+ requiredContextSlice: ["filestore.files"],
173
+ scopes: ["files.read:*"],
174
+ },
175
+ {
176
+ name: "useFilestoreFolders",
177
+ signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
178
+ description:
179
+ "Browse the end-user's Filestore folders in a project or personal space, " +
180
+ "mirroring useFilestoreFiles for subfolder navigation. The hook resolves " +
181
+ "owner_id from the host context; pass enabled:false to suspend fetching. " +
182
+ "Reads ctx.filestore.folders.list and unwraps { data, meta } to the array.",
183
+ returnShape: {
184
+ folders: "FilestoreFolder[]",
185
+ loading: "boolean",
186
+ error: "Error | null",
187
+ refetch: "() => Promise<void>",
188
+ },
189
+ requiredContextSlice: ["filestore.folders"],
190
+ scopes: ["files.read:*"],
191
+ },
192
+ {
193
+ name: "useFileSignature",
194
+ signature: "useFileSignature(fileId, existingSignatureId?)",
195
+ description:
196
+ "Drive a BankID signing flow for one Filestore file. initiate() opens a " +
197
+ "sign order (the backend hashes the bytes + binds the digest), refresh() " +
198
+ "polls (fresh QR while pending — render with the Image primitive), " +
199
+ "cancel() aborts, verify() returns the offline verdict. Pass " +
200
+ "existingSignatureId for an already-signed file to seed the complete " +
201
+ "state + verify() without re-signing. Reads " +
202
+ "ctx.filestore.signatures.{initiate,status,cancel,verify}.",
203
+ returnShape: {
204
+ status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
205
+ qr: "string | null // PNG data-URL of the animated BankID QR",
206
+ autoStartToken: "string | null",
207
+ message: "string | null",
208
+ signerName: "string | null",
209
+ signedAt: "string | null",
210
+ verdict: "{ valid, checks, content_status, ... } | null",
211
+ loading: "boolean",
212
+ error: "PermissionError | null",
213
+ initiate: "() => Promise<{ signature_id, qr, auto_start_token, status }>",
214
+ refresh: "() => Promise<void>",
215
+ cancel: "() => Promise<void>",
216
+ verify: "() => Promise<verdict>",
217
+ },
218
+ requiredContextSlice: ["filestore.signatures"],
219
+ scopes: ["files.write:*"],
220
+ },
221
+ {
222
+ name: "useFileSignatures",
223
+ signature: "useFileSignatures(fileIds)",
224
+ description:
225
+ "Batch 'is this file signed?' lookup for a set of file ids. Returns a map " +
226
+ "of each signed, accessible file id to its latest signature; unsigned " +
227
+ "files are absent. Lets a file browser mark already-signed files in one " +
228
+ "request. Reads ctx.filestore.signatures.list and unwraps { data, meta }.",
229
+ returnShape: {
230
+ signaturesByFileId:
231
+ "{ [fileId]: { signature_id, signer_name, signed_at } }",
232
+ loading: "boolean",
233
+ error: "Error | null",
234
+ refetch: "() => Promise<void>",
235
+ },
236
+ requiredContextSlice: ["filestore.signatures"],
237
+ scopes: ["files.read:*"],
238
+ },
158
239
  {
159
240
  name: "useDatastoreQuery",
160
241
  signature: "useDatastoreQuery(tableId, options?)",
@@ -741,6 +822,16 @@ const WIDGET_CONTEXT_SHAPE = {
741
822
  required: true,
742
823
  fields: { get: "function" },
743
824
  },
825
+ filestore: {
826
+ description:
827
+ "Injected @colixsystems/filestore-client instance — the end-user file archive (project / personal spaces) + BankID file signing. " +
828
+ "{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), remove(id), preview(id) }, " +
829
+ "folders: { list, create, update, remove }, shares: { ... }, trash: { ... }, " +
830
+ "signatures: { initiate(fileId), status(id), cancel(id), verify(id) }, objectUrl(token), fetchObject(token) }. " +
831
+ "Backs useFilestoreFiles() + useFilestoreFolders() + useFileSignature(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
832
+ required: false,
833
+ fields: { files: "object", folders: "object", signatures: "object" },
834
+ },
744
835
  renderer: {
745
836
  description:
746
837
  "Host child-node renderer. { renderNode(node) -> ReactElement }. Backs WidgetTree / useChildRenderer; lets a container widget (Tabs, Card, …) render arbitrary author-authored child nodes without importing the host renderer. The closure pre-binds breakpoint + page ctx + parent so the widget passes only the child node.",
@@ -973,7 +1064,7 @@ const VETTED_IMPORTS = [
973
1064
  // below runs on web AND native (directly, or via the documented platform-split
974
1065
  // counterpart). Native-module packages additionally carry a pinned version in
975
1066
  // the compiler's generatePackageJson so a baked marketplace widget resolves in
976
- // the Expo export (CLAUDE.md §3 parity). See the `adding-vetted-packages` skill
1067
+ // the Expo export (widget-parity skill parity). See the `adding-vetted-packages` skill
977
1068
  // for the full add checklist (contract ×2, version bump, compiler pin, docs).
978
1069
  {
979
1070
  specifier: "react-native-reanimated",
@@ -1103,7 +1194,7 @@ const CONTRACT = deepFreeze({
1103
1194
  // - `allowedBareImports` is now derived from `vettedImports` (same
1104
1195
  // shape; same contents grow with each vetted addition).
1105
1196
  // Permissive-direction change: minor bump on the contract's own
1106
- // versioning (per CLAUDE.md §4, pre-1.0 minor is the breaking channel —
1197
+ // versioning (per sdk-contract-lockstep skill, pre-1.0 minor is the breaking channel —
1107
1198
  // the package.json version bumps accordingly).
1108
1199
  //
1109
1200
  // 1.6.0: additive — `themeTokens.colors` gains `secondary` + `onSecondary`
@@ -1191,7 +1282,7 @@ const CONTRACT = deepFreeze({
1191
1282
  // existing entry changed shape and no other contract field moved, so this
1192
1283
  // is additive — minor bump on the pre-1.0 channel. The compiler pins the
1193
1284
  // native-module members in the exported Expo app's package.json and now
1194
- // always emits the react-native-reanimated/plugin (CLAUDE.md §3 parity).
1285
+ // always emits the react-native-reanimated/plugin (widget-parity skill parity).
1195
1286
  //
1196
1287
  // 1.14.0: additive (REQ-RT-07) — new `useDatastoreSubscription(tableId,
1197
1288
  // handlers, options?)` hook reading ctx.datastore.records(t).subscribe.
@@ -1209,7 +1300,30 @@ const CONTRACT = deepFreeze({
1209
1300
  // the widget applies them itself (the host never auto-styles). New
1210
1301
  // `useWidgetStyle()` hook returns props.style. No existing field, hook,
1211
1302
  // primitive, or token changed shape — minor bump on the pre-1.0 channel.
1212
- version: "1.15.0",
1303
+ // 1.16.0: additive (REQ-SIGN) — new `useSignature(tableId, recordId)` hook
1304
+ // reading ctx.datastore.records(tableId).sign(recordId).{initiate,status,
1305
+ // cancel}. Drives a BankID e-signing flow (QR data-URL + autostart token +
1306
+ // poll). No existing hook, primitive, or field changed — minor bump on the
1307
+ // pre-1.0 channel.
1308
+ // 1.18.0: REQ-SIGN — removed the record-less inline-form signing hook
1309
+ // `useSignatureColumn` (added in 1.17.0) and the datastore-client
1310
+ // `signColumn` namespace it read.
1311
+ // 1.19.0: REQ-SIGN — removed `useSignature` (record/column-scoped signing,
1312
+ // added in 1.16.0) and the datastore-client `sign(recordId)` namespace.
1313
+ // The SIGNATURE column type is retired; signing moves to a standalone
1314
+ // Signature subject model (files first). Pre-1.0 breaking removal of
1315
+ // unreleased hooks — version kept monotonic.
1316
+ // 1.20.0: additive (REQ-SIGN / REQ-FS) — new `useFilestoreFiles` +
1317
+ // `useFilestoreFolders` (browse the end-user's Filestore space) and
1318
+ // `useFileSignature(fileId)` (BankID file signing), all reading the
1319
+ // newly-injected `ctx.filestore` (@colixsystems/filestore-client). No
1320
+ // existing hook changed.
1321
+ // 1.21.0: additive (REQ-SIGN) — new `useFileSignatures(fileIds)` (batch
1322
+ // "is this file signed?" map, reads ctx.filestore.signatures.list) and a
1323
+ // new optional `existingSignatureId` arg on `useFileSignature` so an
1324
+ // already-signed file shows its state + verify()s without re-signing.
1325
+ // Backwards-compatible — the added arg is optional.
1326
+ version: "1.21.0",
1213
1327
  hooks: HOOKS,
1214
1328
  primitives: PRIMITIVES,
1215
1329
  manifestSchema: MANIFEST_SCHEMA,
package/dist/contract.js CHANGED
@@ -99,7 +99,7 @@ const HOOKS = [
99
99
  'style (flex: 1 / height: "100%"); widgets with no useful filled form ' +
100
100
  "may ignore it. Defaults to false wherever the host has not opted the " +
101
101
  "widget into filling, so calling it is always safe. The SAME value is " +
102
- "injected by the web Player and the native export (CLAUDE.md §3), so a " +
102
+ "injected by the web Player and the native export (widget-parity skill), so a " +
103
103
  "widget's fill behaviour is identical on both platforms.",
104
104
  returnShape: {
105
105
  "(returns)": "boolean",
@@ -155,6 +155,87 @@ const HOOKS = [
155
155
  requiredContextSlice: ["files.get"],
156
156
  scopes: null,
157
157
  },
158
+ {
159
+ name: "useFilestoreFiles",
160
+ signature: "useFilestoreFiles({ spaceType, folderId?, q?, type? })",
161
+ description:
162
+ "Browse the end-user's Filestore files in a project or personal space. " +
163
+ "The hook resolves owner_id from the host context (tenant for project, " +
164
+ "app user for personal) — the widget only chooses the space. Reads " +
165
+ "ctx.filestore.files.list and unwraps { data, meta } to the files array.",
166
+ returnShape: {
167
+ files: "FilestoreFile[]",
168
+ loading: "boolean",
169
+ error: "Error | null",
170
+ refetch: "() => Promise<void>",
171
+ },
172
+ requiredContextSlice: ["filestore.files"],
173
+ scopes: ["files.read:*"],
174
+ },
175
+ {
176
+ name: "useFilestoreFolders",
177
+ signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
178
+ description:
179
+ "Browse the end-user's Filestore folders in a project or personal space, " +
180
+ "mirroring useFilestoreFiles for subfolder navigation. The hook resolves " +
181
+ "owner_id from the host context; pass enabled:false to suspend fetching. " +
182
+ "Reads ctx.filestore.folders.list and unwraps { data, meta } to the array.",
183
+ returnShape: {
184
+ folders: "FilestoreFolder[]",
185
+ loading: "boolean",
186
+ error: "Error | null",
187
+ refetch: "() => Promise<void>",
188
+ },
189
+ requiredContextSlice: ["filestore.folders"],
190
+ scopes: ["files.read:*"],
191
+ },
192
+ {
193
+ name: "useFileSignature",
194
+ signature: "useFileSignature(fileId, existingSignatureId?)",
195
+ description:
196
+ "Drive a BankID signing flow for one Filestore file. initiate() opens a " +
197
+ "sign order (the backend hashes the bytes + binds the digest), refresh() " +
198
+ "polls (fresh QR while pending — render with the Image primitive), " +
199
+ "cancel() aborts, verify() returns the offline verdict. Pass " +
200
+ "existingSignatureId for an already-signed file to seed the complete " +
201
+ "state + verify() without re-signing. Reads " +
202
+ "ctx.filestore.signatures.{initiate,status,cancel,verify}.",
203
+ returnShape: {
204
+ status: "'pending' | 'complete' | 'failed' | 'cancelled' | null",
205
+ qr: "string | null // PNG data-URL of the animated BankID QR",
206
+ autoStartToken: "string | null",
207
+ message: "string | null",
208
+ signerName: "string | null",
209
+ signedAt: "string | null",
210
+ verdict: "{ valid, checks, content_status, ... } | null",
211
+ loading: "boolean",
212
+ error: "PermissionError | null",
213
+ initiate: "() => Promise<{ signature_id, qr, auto_start_token, status }>",
214
+ refresh: "() => Promise<void>",
215
+ cancel: "() => Promise<void>",
216
+ verify: "() => Promise<verdict>",
217
+ },
218
+ requiredContextSlice: ["filestore.signatures"],
219
+ scopes: ["files.write:*"],
220
+ },
221
+ {
222
+ name: "useFileSignatures",
223
+ signature: "useFileSignatures(fileIds)",
224
+ description:
225
+ "Batch 'is this file signed?' lookup for a set of file ids. Returns a map " +
226
+ "of each signed, accessible file id to its latest signature; unsigned " +
227
+ "files are absent. Lets a file browser mark already-signed files in one " +
228
+ "request. Reads ctx.filestore.signatures.list and unwraps { data, meta }.",
229
+ returnShape: {
230
+ signaturesByFileId:
231
+ "{ [fileId]: { signature_id, signer_name, signed_at } }",
232
+ loading: "boolean",
233
+ error: "Error | null",
234
+ refetch: "() => Promise<void>",
235
+ },
236
+ requiredContextSlice: ["filestore.signatures"],
237
+ scopes: ["files.read:*"],
238
+ },
158
239
  {
159
240
  name: "useDatastoreQuery",
160
241
  signature: "useDatastoreQuery(tableId, options?)",
@@ -741,6 +822,16 @@ const WIDGET_CONTEXT_SHAPE = {
741
822
  required: true,
742
823
  fields: { get: "function" },
743
824
  },
825
+ filestore: {
826
+ description:
827
+ "Injected @colixsystems/filestore-client instance — the end-user file archive (project / personal spaces) + BankID file signing. " +
828
+ "{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), remove(id), preview(id) }, " +
829
+ "folders: { list, create, update, remove }, shares: { ... }, trash: { ... }, " +
830
+ "signatures: { initiate(fileId), status(id), cancel(id), verify(id) }, objectUrl(token), fetchObject(token) }. " +
831
+ "Backs useFilestoreFiles() + useFilestoreFolders() + useFileSignature(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
832
+ required: false,
833
+ fields: { files: "object", folders: "object", signatures: "object" },
834
+ },
744
835
  renderer: {
745
836
  description:
746
837
  "Host child-node renderer. { renderNode(node) -> ReactElement }. Backs WidgetTree / useChildRenderer; lets a container widget (Tabs, Card, …) render arbitrary author-authored child nodes without importing the host renderer. The closure pre-binds breakpoint + page ctx + parent so the widget passes only the child node.",
@@ -973,7 +1064,7 @@ const VETTED_IMPORTS = [
973
1064
  // below runs on web AND native (directly, or via the documented platform-split
974
1065
  // counterpart). Native-module packages additionally carry a pinned version in
975
1066
  // the compiler's generatePackageJson so a baked marketplace widget resolves in
976
- // the Expo export (CLAUDE.md §3 parity). See the `adding-vetted-packages` skill
1067
+ // the Expo export (widget-parity skill parity). See the `adding-vetted-packages` skill
977
1068
  // for the full add checklist (contract ×2, version bump, compiler pin, docs).
978
1069
  {
979
1070
  specifier: "react-native-reanimated",
@@ -1103,7 +1194,7 @@ const CONTRACT = deepFreeze({
1103
1194
  // - `allowedBareImports` is now derived from `vettedImports` (same
1104
1195
  // shape; same contents grow with each vetted addition).
1105
1196
  // Permissive-direction change: minor bump on the contract's own
1106
- // versioning (per CLAUDE.md §4, pre-1.0 minor is the breaking channel —
1197
+ // versioning (per sdk-contract-lockstep skill, pre-1.0 minor is the breaking channel —
1107
1198
  // the package.json version bumps accordingly).
1108
1199
  //
1109
1200
  // 1.6.0: additive — `themeTokens.colors` gains `secondary` + `onSecondary`
@@ -1191,7 +1282,7 @@ const CONTRACT = deepFreeze({
1191
1282
  // existing entry changed shape and no other contract field moved, so this
1192
1283
  // is additive — minor bump on the pre-1.0 channel. The compiler pins the
1193
1284
  // native-module members in the exported Expo app's package.json and now
1194
- // always emits the react-native-reanimated/plugin (CLAUDE.md §3 parity).
1285
+ // always emits the react-native-reanimated/plugin (widget-parity skill parity).
1195
1286
  //
1196
1287
  // 1.14.0: additive (REQ-RT-07) — new `useDatastoreSubscription(tableId,
1197
1288
  // handlers, options?)` hook reading ctx.datastore.records(t).subscribe.
@@ -1209,7 +1300,30 @@ const CONTRACT = deepFreeze({
1209
1300
  // the widget applies them itself (the host never auto-styles). New
1210
1301
  // `useWidgetStyle()` hook returns props.style. No existing field, hook,
1211
1302
  // primitive, or token changed shape — minor bump on the pre-1.0 channel.
1212
- version: "1.15.0",
1303
+ // 1.16.0: additive (REQ-SIGN) — new `useSignature(tableId, recordId)` hook
1304
+ // reading ctx.datastore.records(tableId).sign(recordId).{initiate,status,
1305
+ // cancel}. Drives a BankID e-signing flow (QR data-URL + autostart token +
1306
+ // poll). No existing hook, primitive, or field changed — minor bump on the
1307
+ // pre-1.0 channel.
1308
+ // 1.18.0: REQ-SIGN — removed the record-less inline-form signing hook
1309
+ // `useSignatureColumn` (added in 1.17.0) and the datastore-client
1310
+ // `signColumn` namespace it read.
1311
+ // 1.19.0: REQ-SIGN — removed `useSignature` (record/column-scoped signing,
1312
+ // added in 1.16.0) and the datastore-client `sign(recordId)` namespace.
1313
+ // The SIGNATURE column type is retired; signing moves to a standalone
1314
+ // Signature subject model (files first). Pre-1.0 breaking removal of
1315
+ // unreleased hooks — version kept monotonic.
1316
+ // 1.20.0: additive (REQ-SIGN / REQ-FS) — new `useFilestoreFiles` +
1317
+ // `useFilestoreFolders` (browse the end-user's Filestore space) and
1318
+ // `useFileSignature(fileId)` (BankID file signing), all reading the
1319
+ // newly-injected `ctx.filestore` (@colixsystems/filestore-client). No
1320
+ // existing hook changed.
1321
+ // 1.21.0: additive (REQ-SIGN) — new `useFileSignatures(fileIds)` (batch
1322
+ // "is this file signed?" map, reads ctx.filestore.signatures.list) and a
1323
+ // new optional `existingSignatureId` arg on `useFileSignature` so an
1324
+ // already-signed file shows its state + verify()s without re-signing.
1325
+ // Backwards-compatible — the added arg is optional.
1326
+ version: "1.21.0",
1213
1327
  hooks: HOOKS,
1214
1328
  primitives: PRIMITIVES,
1215
1329
  manifestSchema: MANIFEST_SCHEMA,
package/dist/hooks.js CHANGED
@@ -132,7 +132,7 @@ export function useUser() {
132
132
  * opted the widget into filling, so calling it is always safe.
133
133
  *
134
134
  * The SAME value is injected by the web Player host and the native export
135
- * host (CLAUDE.md §3), so a widget's fill behaviour is identical on both
135
+ * host (widget-parity skill), so a widget's fill behaviour is identical on both
136
136
  * platforms — there is one source file and one `fill` flag driving it.
137
137
  */
138
138
  export function useFill() {
@@ -961,7 +961,7 @@ export function useRecordPermissions(tableId, recordId) {
961
961
  * expose `subscribe` (an older host, or a runtime with no WebSocket), the hook
962
962
  * resolves to `{ status: "fallback" }` WITHOUT throwing, so a widget that
963
963
  * subscribes degrades to polling on both the web Player and the native export
964
- * rather than crashing at render (CLAUDE.md §11).
964
+ * rather than crashing at render (widget-parity skill).
965
965
  *
966
966
  * @param {string} table Bound table id (falsy → no subscription, status "fallback").
967
967
  * @param {{ onCreated?, onUpdated?, onDeleted? }} [handlers] Per-event callbacks; each receives the snake_case record.
@@ -1106,6 +1106,403 @@ export function useFile(fileId) {
1106
1106
  return { url, file, loading, error, refetch };
1107
1107
  }
1108
1108
 
1109
+ /* ============================================================================
1110
+ * FILESTORE CLIENT — ctx.filestore (@colixsystems/filestore-client)
1111
+ *
1112
+ * The end-user file archive (project / personal spaces) + BankID file signing.
1113
+ * Covers: useFilestoreFiles, useFileSignature.
1114
+ * ==========================================================================*/
1115
+
1116
+ // Resolve the owner_id a filestore query needs from the host context: a PROJECT
1117
+ // space is owned by the tenant (ctx.workspace.id); a PERSONAL space is owned by
1118
+ // the signed-in app user (ctx.user.id). The widget only chooses the space.
1119
+ function _filestoreOwnerId(ctx, spaceType) {
1120
+ const space = String(spaceType || "project").toUpperCase();
1121
+ if (space === "PERSONAL") return (ctx.user && ctx.user.id) || null;
1122
+ return (ctx.workspace && ctx.workspace.id) || null;
1123
+ }
1124
+
1125
+ /**
1126
+ * Browse the end-user's Filestore files. Returns { files, loading, error,
1127
+ * refetch }. The widget passes only the SPACE (`{ spaceType, folderId, q,
1128
+ * type }`); the hook resolves owner_id from the host context (tenant for
1129
+ * project, app user for personal) and calls
1130
+ * `ctx.filestore.files.list({ space_type, owner_id, folder_id, q, type })`,
1131
+ * unwrapping the `{ data, meta }` envelope to the files array. When the owner
1132
+ * can't be resolved (no signed-in user for a personal space) it collapses to an
1133
+ * empty result without a network round-trip.
1134
+ */
1135
+ export function useFilestoreFiles(options) {
1136
+ const ctx = useWidgetContextOrThrow("useFilestoreFiles");
1137
+ if (
1138
+ !ctx.filestore ||
1139
+ !ctx.filestore.files ||
1140
+ typeof ctx.filestore.files.list !== "function"
1141
+ ) {
1142
+ throw new Error(
1143
+ "useFilestoreFiles: host did not inject a filestore client (ctx.filestore.files.list)",
1144
+ );
1145
+ }
1146
+ const { spaceType = "project", folderId = null, q, type } = options || {};
1147
+ const ownerId = _filestoreOwnerId(ctx, spaceType);
1148
+
1149
+ const [files, setFiles] = useState([]);
1150
+ const [loading, setLoading] = useState(true);
1151
+ const [error, setError] = useState(null);
1152
+
1153
+ const filesRef = useRef(ctx.filestore.files);
1154
+ filesRef.current = ctx.filestore.files;
1155
+ const argsRef = useRef({});
1156
+ argsRef.current = { spaceType, ownerId, folderId, q, type };
1157
+ const runRef = useRef(0);
1158
+
1159
+ const doFetch = useCallback(async () => {
1160
+ const myRun = ++runRef.current;
1161
+ const { spaceType: sp, ownerId: oid, folderId: fid, q: qq, type: tt } = argsRef.current;
1162
+ if (!oid) {
1163
+ setLoading(false);
1164
+ setError(null);
1165
+ setFiles([]);
1166
+ return;
1167
+ }
1168
+ setLoading(true);
1169
+ setError(null);
1170
+ try {
1171
+ const res = await filesRef.current.list({
1172
+ space_type: String(sp || "project").toUpperCase(),
1173
+ owner_id: oid,
1174
+ folder_id: fid || undefined,
1175
+ q: qq || undefined,
1176
+ type: tt || undefined,
1177
+ });
1178
+ const rows = res && Array.isArray(res.data) ? res.data : [];
1179
+ if (runRef.current !== myRun) return;
1180
+ setFiles(rows);
1181
+ setLoading(false);
1182
+ } catch (err) {
1183
+ if (runRef.current !== myRun) return;
1184
+ setError(err);
1185
+ setLoading(false);
1186
+ }
1187
+ }, []);
1188
+
1189
+ const key = `${spaceType}|${ownerId}|${folderId}|${q || ""}|${type || ""}`;
1190
+ useEffect(() => {
1191
+ doFetch();
1192
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1193
+ }, [key]);
1194
+
1195
+ const refetch = useCallback(async () => {
1196
+ await doFetch();
1197
+ }, [doFetch]);
1198
+
1199
+ return { files, loading, error, refetch };
1200
+ }
1201
+
1202
+ /**
1203
+ * Browse the end-user's Filestore folders. Returns { folders, loading, error,
1204
+ * refetch }. Mirrors useFilestoreFiles for subfolder navigation: the widget
1205
+ * passes only the SPACE (`{ spaceType, parentFolderId, q, enabled }`); the hook
1206
+ * resolves owner_id from the host context and calls
1207
+ * `ctx.filestore.folders.list({ space_type, owner_id, parent_folder_id, q })`,
1208
+ * unwrapping the `{ data, meta }` envelope. Pass `enabled: false` to suspend
1209
+ * fetching (e.g. when the widget hides subfolders) or when the owner can't be
1210
+ * resolved — the hook then resolves to an empty list without a network call.
1211
+ */
1212
+ export function useFilestoreFolders(options) {
1213
+ const ctx = useWidgetContextOrThrow("useFilestoreFolders");
1214
+ if (
1215
+ !ctx.filestore ||
1216
+ !ctx.filestore.folders ||
1217
+ typeof ctx.filestore.folders.list !== "function"
1218
+ ) {
1219
+ throw new Error(
1220
+ "useFilestoreFolders: host did not inject a filestore client (ctx.filestore.folders.list)",
1221
+ );
1222
+ }
1223
+ const {
1224
+ spaceType = "project",
1225
+ parentFolderId = null,
1226
+ q,
1227
+ enabled = true,
1228
+ } = options || {};
1229
+ const ownerId = _filestoreOwnerId(ctx, spaceType);
1230
+
1231
+ const [folders, setFolders] = useState([]);
1232
+ const [loading, setLoading] = useState(true);
1233
+ const [error, setError] = useState(null);
1234
+
1235
+ const foldersApiRef = useRef(ctx.filestore.folders);
1236
+ foldersApiRef.current = ctx.filestore.folders;
1237
+ const argsRef = useRef({});
1238
+ argsRef.current = { spaceType, ownerId, parentFolderId, q, enabled };
1239
+ const runRef = useRef(0);
1240
+
1241
+ const doFetch = useCallback(async () => {
1242
+ const myRun = ++runRef.current;
1243
+ const {
1244
+ spaceType: sp,
1245
+ ownerId: oid,
1246
+ parentFolderId: pid,
1247
+ q: qq,
1248
+ enabled: en,
1249
+ } = argsRef.current;
1250
+ if (!en || !oid) {
1251
+ setLoading(false);
1252
+ setError(null);
1253
+ setFolders([]);
1254
+ return;
1255
+ }
1256
+ setLoading(true);
1257
+ setError(null);
1258
+ try {
1259
+ const res = await foldersApiRef.current.list({
1260
+ space_type: String(sp || "project").toUpperCase(),
1261
+ owner_id: oid,
1262
+ parent_folder_id: pid || undefined,
1263
+ q: qq || undefined,
1264
+ });
1265
+ const rows = res && Array.isArray(res.data) ? res.data : [];
1266
+ if (runRef.current !== myRun) return;
1267
+ setFolders(rows);
1268
+ setLoading(false);
1269
+ } catch (err) {
1270
+ if (runRef.current !== myRun) return;
1271
+ setError(err);
1272
+ setLoading(false);
1273
+ }
1274
+ }, []);
1275
+
1276
+ const key = `${enabled ? 1 : 0}|${spaceType}|${ownerId}|${parentFolderId}|${q || ""}`;
1277
+ useEffect(() => {
1278
+ doFetch();
1279
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1280
+ }, [key]);
1281
+
1282
+ const refetch = useCallback(async () => {
1283
+ await doFetch();
1284
+ }, [doFetch]);
1285
+
1286
+ return { folders, loading, error, refetch };
1287
+ }
1288
+
1289
+ /**
1290
+ * Drive a BankID signing flow for one Filestore file. Returns
1291
+ * `{ status, qr, autoStartToken, message, signerName, signedAt, verdict,
1292
+ * loading, error, initiate, refresh, cancel, verify }`.
1293
+ *
1294
+ * `initiate()` opens a sign order against the file (the backend hashes its
1295
+ * bytes SERVER-SIDE and binds the digest into the BankID signature); `refresh()`
1296
+ * polls the order (surfacing a fresh animated-QR frame while pending — render
1297
+ * it with the `Image` primitive); `cancel()` aborts it; `verify()` re-runs the
1298
+ * offline verification and returns the verdict (`{ valid, content_status, … }`).
1299
+ * Reads `ctx.filestore.signatures.{initiate,status,cancel,verify}`. When
1300
+ * `fileId` is null/empty the hook collapses to a no-op.
1301
+ *
1302
+ * Pass `existingSignatureId` for a file that is ALREADY signed: the hook seeds
1303
+ * that order id (status `complete`) so the widget can show the signed state and
1304
+ * `verify()` it WITHOUT opening a new order — re-signing then requires an
1305
+ * explicit `initiate()`.
1306
+ */
1307
+ export function useFileSignature(fileId, existingSignatureId = null) {
1308
+ const ctx = useWidgetContextOrThrow("useFileSignature");
1309
+ if (!ctx.filestore || !ctx.filestore.signatures) {
1310
+ throw new Error(
1311
+ "useFileSignature: host did not inject a filestore client (ctx.filestore.signatures)",
1312
+ );
1313
+ }
1314
+ const ready = Boolean(fileId);
1315
+ const sigRef = useRef(ctx.filestore.signatures);
1316
+ sigRef.current = ctx.filestore.signatures;
1317
+ const fileIdRef = useRef(fileId);
1318
+ fileIdRef.current = fileId;
1319
+ // Seed the order from an already-completed signature so verify() works
1320
+ // without initiating; initiate() (an explicit re-sign) overwrites it.
1321
+ const orderRef = useRef(existingSignatureId || null);
1322
+
1323
+ const [status, setStatus] = useState(existingSignatureId ? "complete" : null);
1324
+ const [qr, setQr] = useState(null);
1325
+ const [autoStartToken, setAutoStartToken] = useState(null);
1326
+ const [message, setMessage] = useState(null);
1327
+ const [signerName, setSignerName] = useState(null);
1328
+ const [signedAt, setSignedAt] = useState(null);
1329
+ const [verdict, setVerdict] = useState(null);
1330
+ const [loading, setLoading] = useState(false);
1331
+ const [error, setError] = useState(null);
1332
+
1333
+ const initiate = useCallback(async () => {
1334
+ if (!fileIdRef.current) return null;
1335
+ setLoading(true);
1336
+ setError(null);
1337
+ setVerdict(null);
1338
+ try {
1339
+ const res = await sigRef.current.initiate(fileIdRef.current);
1340
+ orderRef.current = res && res.signature_id ? res.signature_id : null;
1341
+ setStatus(res && res.status ? res.status : "pending");
1342
+ setQr(res && res.qr ? res.qr : null);
1343
+ setAutoStartToken(res && res.auto_start_token ? res.auto_start_token : null);
1344
+ setSignerName(null);
1345
+ setSignedAt(null);
1346
+ setMessage(null);
1347
+ setLoading(false);
1348
+ return res;
1349
+ } catch (err) {
1350
+ setError(toPermissionError(err));
1351
+ setLoading(false);
1352
+ throw toPermissionError(err);
1353
+ }
1354
+ }, []);
1355
+
1356
+ const refresh = useCallback(async () => {
1357
+ const id = orderRef.current;
1358
+ if (!id) return;
1359
+ try {
1360
+ const res = await sigRef.current.status(id);
1361
+ if (res) {
1362
+ if (res.status != null) setStatus(res.status);
1363
+ if (res.qr !== undefined) setQr(res.qr || null);
1364
+ if (res.message !== undefined) setMessage(res.message || null);
1365
+ if (res.signer_name !== undefined) setSignerName(res.signer_name || null);
1366
+ if (res.signed_at !== undefined) setSignedAt(res.signed_at || null);
1367
+ }
1368
+ } catch (err) {
1369
+ setError(toPermissionError(err));
1370
+ }
1371
+ }, []);
1372
+
1373
+ const cancel = useCallback(async () => {
1374
+ const id = orderRef.current;
1375
+ if (!id) return;
1376
+ try {
1377
+ await sigRef.current.cancel(id);
1378
+ setStatus("cancelled");
1379
+ setQr(null);
1380
+ } catch (err) {
1381
+ throw toPermissionError(err);
1382
+ }
1383
+ }, []);
1384
+
1385
+ const verify = useCallback(async () => {
1386
+ const id = orderRef.current;
1387
+ if (!id) return null;
1388
+ try {
1389
+ const v = await sigRef.current.verify(id);
1390
+ setVerdict(v);
1391
+ return v;
1392
+ } catch (err) {
1393
+ setError(toPermissionError(err));
1394
+ throw toPermissionError(err);
1395
+ }
1396
+ }, []);
1397
+
1398
+ if (!ready) {
1399
+ return {
1400
+ status: null,
1401
+ qr: null,
1402
+ autoStartToken: null,
1403
+ message: null,
1404
+ signerName: null,
1405
+ signedAt: null,
1406
+ verdict: null,
1407
+ loading: false,
1408
+ error: null,
1409
+ initiate: async () => null,
1410
+ refresh: async () => undefined,
1411
+ cancel: async () => undefined,
1412
+ verify: async () => null,
1413
+ };
1414
+ }
1415
+ return {
1416
+ status,
1417
+ qr,
1418
+ autoStartToken,
1419
+ message,
1420
+ signerName,
1421
+ signedAt,
1422
+ verdict,
1423
+ loading,
1424
+ error,
1425
+ initiate,
1426
+ refresh,
1427
+ cancel,
1428
+ verify,
1429
+ };
1430
+ }
1431
+
1432
+ /**
1433
+ * Batch "is this file signed?" lookup for a set of file ids. Returns
1434
+ * `{ signaturesByFileId, loading, error, refetch }` where `signaturesByFileId`
1435
+ * maps each signed, accessible file id to its latest signature
1436
+ * (`{ signature_id, signer_name, signed_at }`); unsigned files are simply
1437
+ * absent. Lets a file browser mark already-signed files in one request (no
1438
+ * N+1). Reads `ctx.filestore.signatures.list(fileIds)` and unwraps the
1439
+ * `{ data, meta }` envelope. An empty/falsy id list resolves to an empty map
1440
+ * without a network round-trip.
1441
+ */
1442
+ export function useFileSignatures(fileIds) {
1443
+ const ctx = useWidgetContextOrThrow("useFileSignatures");
1444
+ if (
1445
+ !ctx.filestore ||
1446
+ !ctx.filestore.signatures ||
1447
+ typeof ctx.filestore.signatures.list !== "function"
1448
+ ) {
1449
+ throw new Error(
1450
+ "useFileSignatures: host did not inject a filestore client (ctx.filestore.signatures.list)",
1451
+ );
1452
+ }
1453
+ const ids = Array.isArray(fileIds) ? fileIds.filter(Boolean) : [];
1454
+ const key = ids.slice().sort().join(",");
1455
+
1456
+ const [signaturesByFileId, setSignaturesByFileId] = useState({});
1457
+ const [loading, setLoading] = useState(ids.length > 0);
1458
+ const [error, setError] = useState(null);
1459
+
1460
+ const sigRef = useRef(ctx.filestore.signatures);
1461
+ sigRef.current = ctx.filestore.signatures;
1462
+ const keyRef = useRef(key);
1463
+ keyRef.current = key;
1464
+ const runRef = useRef(0);
1465
+
1466
+ const doFetch = useCallback(async () => {
1467
+ const myRun = ++runRef.current;
1468
+ const currentIds = keyRef.current ? keyRef.current.split(",") : [];
1469
+ if (currentIds.length === 0) {
1470
+ setLoading(false);
1471
+ setError(null);
1472
+ setSignaturesByFileId({});
1473
+ return;
1474
+ }
1475
+ setLoading(true);
1476
+ setError(null);
1477
+ try {
1478
+ const res = await sigRef.current.list(currentIds);
1479
+ const rows = res && Array.isArray(res.data) ? res.data : [];
1480
+ if (runRef.current !== myRun) return;
1481
+ const map = {};
1482
+ for (const row of rows) {
1483
+ if (row && row.subject_file_id) map[row.subject_file_id] = row;
1484
+ }
1485
+ setSignaturesByFileId(map);
1486
+ setLoading(false);
1487
+ } catch (err) {
1488
+ if (runRef.current !== myRun) return;
1489
+ setError(err);
1490
+ setLoading(false);
1491
+ }
1492
+ }, []);
1493
+
1494
+ useEffect(() => {
1495
+ doFetch();
1496
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1497
+ }, [key]);
1498
+
1499
+ const refetch = useCallback(async () => {
1500
+ await doFetch();
1501
+ }, [doFetch]);
1502
+
1503
+ return { signaturesByFileId, loading, error, refetch };
1504
+ }
1505
+
1109
1506
  /* ============================================================================
1110
1507
  * DIRECTORY CLIENT — ctx.directory (@colixsystems/directory-client)
1111
1508
  *
package/dist/index.js CHANGED
@@ -15,6 +15,10 @@ export {
15
15
  useDatastoreRecord,
16
16
  useDatastoreSchema,
17
17
  useFile,
18
+ useFilestoreFiles,
19
+ useFilestoreFolders,
20
+ useFileSignature,
21
+ useFileSignatures,
18
22
  useDatastoreMutation,
19
23
  useDirectory,
20
24
  useUsers,
@@ -15,6 +15,10 @@ export {
15
15
  useDatastoreRecord,
16
16
  useDatastoreSchema,
17
17
  useFile,
18
+ useFilestoreFiles,
19
+ useFilestoreFolders,
20
+ useFileSignature,
21
+ useFileSignatures,
18
22
  useDatastoreMutation,
19
23
  useDirectory,
20
24
  useUsers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",