@colixsystems/widget-sdk 0.141.0 → 0.142.1

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
@@ -70,7 +70,30 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
70
70
 
71
71
  ## Status
72
72
 
73
- `v0.141.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**.
73
+ `v0.142.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**.
74
+
75
+ ### What's new in 0.142.1
76
+
77
+ **A native file upload no longer crashes the app (sc-7906).** Making the part a `File` let the upload through, and then the app died on any real file: Expo's `fetch` builds a multipart body by reading the WHOLE file into the JS heap — `convertFormDataAsync` awaits `part.bytes()` and concatenates — so a send costs about three times the file's size on the JS thread. The native host now streams the file off disk through expo-file-system's upload task instead. `<FilePicker>`, `useCamera()` and `useImageEditor()` are unchanged for a widget author — the picked file stays where the picker put it, so `asset.uri` still renders, and the name the user chose travels to the backend as an explicit `file_name` field.
78
+
79
+ ### What's new in 0.142.0 (contract 1.110.0)
80
+
81
+ **An action script can finally tell WHO started the run — `triggeredBy` joins `CONTRACT.actionScriptGlobals` (sc-7885).** The identity was already captured (the run history has recorded an `actorId` since REQ-ACTION-RUNLOG) but never reached the sandbox, so a script could do the work and not attribute it: no "X submitted this" notification back to the submitter, no audit row naming the actor. Authors worked around it by writing the user id into a record first and switching to a `record_*` trigger — plumbing in the app's data model, and not available to a plain button press at all.
82
+
83
+ ```js
84
+ if (triggeredBy && triggeredBy.type === "app_user") {
85
+ await notifications.notifyUser(triggeredBy.id, {
86
+ title: "Request received",
87
+ body: "Thanks " + triggeredBy.name + ", we have started on it.",
88
+ });
89
+ }
90
+ ```
91
+
92
+ - The shape is `{ type, id, name, email, role }`, frozen, or **`null` when nothing initiated the run** — a `schedule` tick or a `record_*` event. `null` is the normal case for the automation triggers, so guard before reading it.
93
+ - **`type` is load-bearing, not decoration.** It is `"app_user"` on an `app` button press and an `http_post` webhook, and `"studio_user"` on an operator's Run now. One id column, two directories behind it: check `type` before handing the id to `notifications.notifyUser` or a USER column, or a studio user's id will silently resolve to nobody. `role` carries the app-user vocabulary (`USER` / `INTEGRATION`) and is `null` for a studio user, whose workspace role is a different axis entirely. `email` is likewise `null` for a studio user: nothing in a script can address an operator, so shipping their real address to an installed third-party Action would be PII with no consumer.
94
+ - **It grants nothing.** The script still runs as the Action's bound integration identity, exactly as before; `triggeredBy` describes the caller and does not widen the data the script can reach. Resolution is host-side and tenant-scoped, so a stale or foreign id resolves to `null` rather than leaking a row across workspaces.
95
+ - Actions never render, so this has **no effect on Player ↔ export parity** (CLAUDE.md §8).
96
+ - Also in this release: **`properties` was added to `CONTRACT.actionScriptGlobals`**, which the `@colixsystems/action-sdk` copy of the list had carried since 1.50.0 while this one had not — the two hand-mirrored lists had drifted. A parity test now pins them together. `CONTRACT.version` → `1.110.0`. Additive for every existing manifest and script.
74
97
 
75
98
  ### What's new in 0.141.0 (contract 1.109.0)
76
99
 
@@ -507,7 +530,7 @@ When you take the `useCanWrite`-only route, omit the USER column for a guest (`i
507
530
 
508
531
  **Dismissing the picker resolves `null`, not an error.** Backing out is the most common outcome, so it is deliberately not a rejection — your happy path needs no `try/catch`. A genuine failure (permission refused, no host broker) rejects with a structured `CameraError` (new named export) carrying a stable `.code` (`PERMISSION_DENIED` / `UNSUPPORTED` / `INTERNAL`).
509
532
 
510
- **One upload path on both hosts.** The resolved asset is normalised to `{ uri, name, mimeType, width, height, size, file }`, where `uri` is directly displayable (`<Image source={{ uri }} />`) and `file` is already the right upload part for the platform — a Blob on both hosts: a browser `File` on web, expo-file-system's `File` on native. (It must be a Blob: Expo's fetch implements the web FormData spec and rejects React Native's legacy `{ uri, name, type }` part.) So the same three lines work everywhere:
533
+ **One upload path on both hosts.** The resolved asset is normalised to `{ uri, name, mimeType, width, height, size, file }`, where `uri` is directly displayable (`<Image source={{ uri }} />`) and `file` is already the right upload part for the platform — a browser `File` on web, expo-file-system's `File` on native. (Never React Native's legacy `{ uri, name, type }` part: the native host streams the file off disk, and Expo's fetch rejects that part outright anyway. Do not branch on the part's type — an expo-file-system `File` is NOT `instanceof Blob` at runtime.) So the same three lines work everywhere:
511
534
 
512
535
  ```js
513
536
  const fd = new FormData();
package/dist/contract.cjs CHANGED
@@ -1990,12 +1990,23 @@ const ACTION_TRIGGER_TYPES = [
1990
1990
  // Globals the action script runs against (the runner's surface) — distinct
1991
1991
  // from the React/SDK widget surface, so the component import/banned-API
1992
1992
  // linter does NOT scan action scripts.
1993
+ // sc-7877 deliberately does NOT add `directory` here. That global is
1994
+ // reachable only by a MARKETPLACE action whose install consented to a
1995
+ // `directory:` scope; a widget-declared action has no install, so
1996
+ // resolveForActionRow grants it nothing and the global is never defined.
1993
1997
  const ACTION_SCRIPT_GLOBALS = [
1994
1998
  "datastore",
1995
1999
  "secrets",
2000
+ // Was missing here while action-sdk listed it — the two hand-mirrored lists
2001
+ // had drifted. action-script-globals-parity.test.cjs now pins them together.
2002
+ "properties",
1996
2003
  "fetch",
1997
2004
  "connectors",
1998
2005
  "notifications",
2006
+ // Conditionally defined — only when the install consented to a `directory:`
2007
+ // scope. Added to the WIDGET-sdk copy here: sc-7891 put it in the action-sdk
2008
+ // list alone, the same drift this commit's parity test now catches.
2009
+ "directory",
1999
2010
  "console",
2000
2011
  "record",
2001
2012
  // sc-5366 — `{ body }` on a run fired by the inbound `http_post` trigger,
@@ -2004,6 +2015,8 @@ const ACTION_SCRIPT_GLOBALS = [
2004
2015
  "tenantId",
2005
2016
  "triggerType",
2006
2017
  "triggerTableId",
2018
+ // sc-7885 — who initiated the run; null on `schedule` / `record_*`.
2019
+ "triggeredBy",
2007
2020
  ];
2008
2021
  // Mirrors action.service.js SCRIPT_MAX_BYTES.
2009
2022
  const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
@@ -3970,7 +3983,16 @@ const CONTRACT = deepFreeze({
3970
3983
  // and the sc-7352 backfill deletes the stored key (a bottom-tabs app's
3971
3984
  // strip becomes the sidebar's quick bar) BEFORE this ships.
3972
3985
  // `menuItemCap` / `quickBarCap` keep their signatures.
3973
- version: "1.109.0",
3986
+ // 1.110.0: additive (sc-7885) — `triggeredBy` joins `actionScriptGlobals`:
3987
+ // WHO initiated this run, as `{ type, id, name, email, role }`, or `null`
3988
+ // on `schedule` / `record_*` where nothing did. `type` is `"app_user"` (an
3989
+ // `app` button press or an `http_post` webhook) or `"studio_user"` (an
3990
+ // operator's Run now) — load-bearing, because those are two different
3991
+ // directories behind one id. The script still runs as the Action's bound
3992
+ // integration identity; this describes the caller, it does not grant
3993
+ // anything. The same commit adds `properties` to this list, which the
3994
+ // action-sdk copy had carried since 1.50.0 while this one had not.
3995
+ version: "1.110.0",
3974
3996
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3975
3997
  hooks: HOOKS,
3976
3998
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -1993,9 +1993,16 @@ const ACTION_TRIGGER_TYPES = [
1993
1993
  const ACTION_SCRIPT_GLOBALS = [
1994
1994
  "datastore",
1995
1995
  "secrets",
1996
+ // Was missing here while action-sdk listed it — the two hand-mirrored lists
1997
+ // had drifted. action-script-globals-parity.test.cjs now pins them together.
1998
+ "properties",
1996
1999
  "fetch",
1997
2000
  "connectors",
1998
2001
  "notifications",
2002
+ // Conditionally defined — only when the install consented to a `directory:`
2003
+ // scope. Added to the WIDGET-sdk copy here: sc-7891 put it in the action-sdk
2004
+ // list alone, the same drift this commit's parity test now catches.
2005
+ "directory",
1999
2006
  "console",
2000
2007
  "record",
2001
2008
  // sc-5366 — `{ body }` on a run fired by the inbound `http_post` trigger,
@@ -2004,6 +2011,8 @@ const ACTION_SCRIPT_GLOBALS = [
2004
2011
  "tenantId",
2005
2012
  "triggerType",
2006
2013
  "triggerTableId",
2014
+ // sc-7885 — who initiated the run; null on `schedule` / `record_*`.
2015
+ "triggeredBy",
2007
2016
  ];
2008
2017
  // Mirrors action.service.js SCRIPT_MAX_BYTES.
2009
2018
  const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
@@ -3970,7 +3979,16 @@ const CONTRACT = deepFreeze({
3970
3979
  // and the sc-7352 backfill deletes the stored key (a bottom-tabs app's
3971
3980
  // strip becomes the sidebar's quick bar) BEFORE this ships.
3972
3981
  // `menuItemCap` / `quickBarCap` keep their signatures.
3973
- version: "1.109.0",
3982
+ // 1.110.0: additive (sc-7885) — `triggeredBy` joins `actionScriptGlobals`:
3983
+ // WHO initiated this run, as `{ type, id, name, email, role }`, or `null`
3984
+ // on `schedule` / `record_*` where nothing did. `type` is `"app_user"` (an
3985
+ // `app` button press or an `http_post` webhook) or `"studio_user"` (an
3986
+ // operator's Run now) — load-bearing, because those are two different
3987
+ // directories behind one id. The script still runs as the Action's bound
3988
+ // integration identity; this describes the caller, it does not grant
3989
+ // anything. The same commit adds `properties` to this list, which the
3990
+ // action-sdk copy had carried since 1.50.0 while this one had not.
3991
+ version: "1.110.0",
3974
3992
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3975
3993
  hooks: HOOKS,
3976
3994
  primitives: PRIMITIVES,
@@ -10,11 +10,11 @@
10
10
  //
11
11
  // `onPick` receives the same shape on both platforms:
12
12
  // - web → the browser's `File` object (a `Blob` with name + type).
13
- // - native → expo-file-system's `File` — ALSO a `Blob`, and carrying the
14
- // same `uri` / `name` / `type` / `size` the old React Native
15
- // `{ uri, name, type, size }` part did, so a widget reading those is
16
- // unaffected. It must be a Blob: Expo's fetch implements the WEB
17
- // FormData spec and rejects the RN triple outright ("Unsupported
13
+ // - native → expo-file-system's `File`, carrying the same `uri` / `name` /
14
+ // `type` / `size` the old React Native `{ uri, name, type, size }` part
15
+ // did, so a widget reading those is unaffected. It must be a file on
16
+ // disk: the host uploads it by streaming it off disk (sc-7906), and
17
+ // Expo's fetch rejects the RN triple outright anyway ("Unsupported
18
18
  // FormDataPart implementation"), which failed every native upload.
19
19
  // `useFilestoreUpload` appends whichever verbatim into the multipart body.
20
20
 
@@ -49,10 +49,9 @@ function _mapAccept(accept) {
49
49
  // keeping it as a fallback would only defer the failure.
50
50
  function _asset(picked) {
51
51
  const file = new FsFile(picked.uri);
52
- // Both are read off the PART when the multipart is serialised (the third
53
- // `append` argument is ignored for a non-Blob), and both are derived from
54
- // the cache URI — which may be renamed, percent-encoded, or extensionless.
55
- // The picker's own values are the ones the user and the backend expect.
52
+ // The File derives both from the cache URI, which may be renamed,
53
+ // percent-encoded or extensionless. The upload sends the part's `name` as
54
+ // the stored name (sc-7906), so the picker's values are the ones that win.
56
55
  _shadow(file, "name", picked.name || "upload");
57
56
  _shadow(file, "type", picked.mimeType || "application/octet-stream");
58
57
  return file;
package/dist/hooks.js CHANGED
@@ -3571,14 +3571,13 @@ export function useFilestoreFile(fileId) {
3571
3571
  * same way the read hooks do (tenant for PROJECT, app user for PERSONAL)
3572
3572
  * and posts a multipart FormData to `ctx.filestore.files.upload`.
3573
3573
  *
3574
- * `upload(file, { folderId?, fileName? })` takes a BLOB on both hosts — a
3575
- * browser `File` on the web Player, expo-file-system's `File` (also a Blob)
3576
- * on the Expo export, as `<FilePicker>`, `useCamera()` and `useImageEditor()`
3577
- * all hand back. It must be a Blob: Expo's fetch implements the web FormData
3578
- * spec and rejects React Native's legacy `{ uri, name, type }` part with
3579
- * "Unsupported FormDataPart implementation", which failed every native
3580
- * upload. `fileName` overrides the stored name, which otherwise comes from
3581
- * the Blob's own `.name`.
3574
+ * `upload(file, { folderId?, fileName? })` takes a browser `File` on the web
3575
+ * Player and expo-file-system's `File` on the Expo export, as `<FilePicker>`,
3576
+ * `useCamera()` and `useImageEditor()` all hand back. Never React Native's
3577
+ * legacy `{ uri, name, type }` part: the native host streams the file off
3578
+ * disk (sc-7906), and Expo's fetch rejects that part outright anyway with
3579
+ * "Unsupported FormDataPart implementation". `fileName` overrides the stored
3580
+ * name, which otherwise comes from the part's own `.name`.
3582
3581
  *
3583
3582
  * The destination can refuse the add two ways (sc-6977). BRANCH ON
3584
3583
  * `err.status`: 403 means the caller may know why — most often a public space
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.141.0",
3
+ "version": "0.142.1",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",