@colixsystems/widget-sdk 0.114.0 → 0.115.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
@@ -49,7 +49,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
49
49
  | **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
50
50
  | **DATASTORE** | `useDatastoreSubscription(table, handlers, options?)` | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
51
51
  | **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) |
52
- | **DATASTORE** | `useCanWrite(tableId, options?)` | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "is this caller signed in AND permitted", reading the same table-ACL answer the write endpoint enforces. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
52
+ | **DATASTORE** | `useCanWrite(tableId, options?)` | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "may this caller write", reading the same table-ACL answer the write endpoint enforces — so a table granting Create to Everyone answers `true` for a logged-out visitor, and this hook alone is the right gate for a widget meant to work without signing in. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
53
53
  | **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
54
54
  | **FILES** | `useAssetsByTag(tag, { type? })` | `{ assets, loading, error, refetch }` | `ctx.assets.list` (unwraps `{ data, meta }` to `assets`) — no scope. `type` defaults to `"image"`; pass `"all"` / `"audio"` / `"video"` / `"document"` to widen. Falsy `tag` collapses to `assets: []` without a round-trip. |
55
55
  | **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
@@ -72,6 +72,16 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
72
72
 
73
73
  `v0.112.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
74
 
75
+ ### What's new in 0.115.0 (contract unchanged)
76
+
77
+ **`write-not-gated-on-user` now accepts a `useCanWrite()` gate — a widget may be opened to logged-out visitors (sc-6593).** The rule (added in 0.89.0, below) flagged any `useDatastoreMutation` write that carried no identity guard, and only a `.id` / `groupIds` / `roles` check counted as one. That encoded "a write needs a signed-in app user" as a platform fact, which it is not: a table whose permissions grant **Create** to *Everyone (anonymous + signed-in)* accepts a write from a logged-out visitor, and `useCanWrite(tableId)` answers `true` for them.
78
+
79
+ So a widget gated on `useCanWrite` alone — the correct shape for a public tally, a guest sign-up sheet, or an open feedback form — used to trip the warning that steers the AI widget agent's repair loop back to identity gating, making the sign-in requirement impossible for an author to remove. It is now recognised as a gate, and its finding label names it first.
80
+
81
+ Nothing else changes: a widget with **no** gate at all is still flagged, `useUser().id` read purely as a VALUE still does not satisfy the rule, and the severity is still `warning` (never publish-blocking). Identity gating remains the right default for almost every write — this only stops the linter from arguing against the one case where it isn't.
82
+
83
+ When you take the `useCanWrite`-only route, omit the USER column for a guest (`if (user.id) payload[byField] = user.id;`) — an anonymous row records no author, so per-person limits and "my entries" views cannot work for one.
84
+
75
85
  ### What's new in 0.113.0 (contract 1.86.0)
76
86
 
77
87
  **New `useCamera()` hook — take a photo or pick one from the device library.** A new CORE hook reading a new `camera` capability on the existing `ctx.device` slice. Returns `{ asset, loading, error, supported, capture, pick, reset }`. Capture is **imperative** — call `capture()` or `pick()` from a user gesture (a `Pressable.onPress`); the browser and the mobile OS gate the permission prompt on a gesture, so it NEVER opens on mount. `options` (`{ allowsEditing, quality }`) pass through to the host. It needs **no manifest scope** and **no `requestedScopes` entry**.
package/dist/linter.cjs CHANGED
@@ -928,12 +928,17 @@ const CURRENCY_LABEL_RES = [
928
928
  ];
929
929
 
930
930
  // sc-4985 — soft warning: a widget that writes must decide what a signed-OUT
931
- // visitor sees. A write needs a signed-in app user, so an anonymous visitor
932
- // handed a live "Save" / "Book" / "Delete" button can only tap it and fail —
933
- // the failure the gate exists to spare them. Satisfied by any identity guard:
934
- // a negated or compared `.id`, or a `groupIds` / `roles` check. Reading
935
- // `useUser().id` purely as a VALUE (the USER-column write pattern) is NOT a
936
- // guard, which is why an operator has to follow it.
931
+ // visitor sees. A visitor handed a live "Save" / "Book" / "Delete" button the
932
+ // table will refuse can only tap it and fail — the failure the gate exists to
933
+ // spare them. Satisfied by any identity guard: a negated or compared `.id`, or
934
+ // a `groupIds` / `roles` check. Reading `useUser().id` purely as a VALUE (the
935
+ // USER-column write pattern) is NOT a guard, which is why an operator follows.
936
+ //
937
+ // sc-6593 — `useCanWrite(tableId)` satisfies it too, and answers better: it
938
+ // reads the ACL the write endpoint enforces, so it goes live for whoever may
939
+ // write. Since a table granting Create to EVERYONE accepts an anonymous write
940
+ // (sc-5229), demanding identity here would flag the only correct way to build
941
+ // the logged-out-friendly widget an author explicitly asked for.
937
942
  //
938
943
  // Conservative on purpose: an unrelated `.id` comparison elsewhere in the
939
944
  // source silences the rule. A warning that occasionally stays quiet is far
@@ -952,6 +957,7 @@ function _writeGatedOnUserRules(source) {
952
957
  const code = _stripNonCode(source);
953
958
  const call = /\buseDatastoreMutation\s*\(/.exec(code);
954
959
  if (!call) return [];
960
+ if (/\buseCanWrite\s*\(/.test(code)) return [];
955
961
  if (_IDENTITY_GUARD_RES.some((re) => re.test(code))) return [];
956
962
  const line = code.slice(0, call.index).split(/\r?\n/).length;
957
963
  return [
@@ -961,9 +967,9 @@ function _writeGatedOnUserRules(source) {
961
967
  // Kept under ~210 chars: a finding is truncated at 300 downstream, and
962
968
  // the fix instruction is the half worth keeping.
963
969
  label:
964
- `writes with useDatastoreMutation() but never checks who is signed ` +
965
- `in - read useUser(), and when !user.id render the action inactive ` +
966
- `with a "sign in" line instead of a live button that can only fail.`,
970
+ `writes with useDatastoreMutation() but never checks who may write ` +
971
+ `- gate on useCanWrite(tableId), or useUser() when the action needs ` +
972
+ `a signed-in user, and render the control inactive, never live-but-doomed.`,
967
973
  line,
968
974
  snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
969
975
  },
package/dist/linter.js CHANGED
@@ -1077,12 +1077,17 @@ const CURRENCY_LABEL_RES = [
1077
1077
  ];
1078
1078
 
1079
1079
  // sc-4985 — soft warning: a widget that writes must decide what a signed-OUT
1080
- // visitor sees. A write needs a signed-in app user, so an anonymous visitor
1081
- // handed a live "Save" / "Book" / "Delete" button can only tap it and fail —
1082
- // the failure the gate exists to spare them. Satisfied by any identity guard:
1083
- // a negated or compared `.id`, or a `groupIds` / `roles` check. Reading
1084
- // `useUser().id` purely as a VALUE (the USER-column write pattern) is NOT a
1085
- // guard, which is why an operator has to follow it.
1080
+ // visitor sees. A visitor handed a live "Save" / "Book" / "Delete" button the
1081
+ // table will refuse can only tap it and fail — the failure the gate exists to
1082
+ // spare them. Satisfied by any identity guard: a negated or compared `.id`, or
1083
+ // a `groupIds` / `roles` check. Reading `useUser().id` purely as a VALUE (the
1084
+ // USER-column write pattern) is NOT a guard, which is why an operator follows.
1085
+ //
1086
+ // sc-6593 — `useCanWrite(tableId)` satisfies it too, and answers better: it
1087
+ // reads the ACL the write endpoint enforces, so it goes live for whoever may
1088
+ // write. Since a table granting Create to EVERYONE accepts an anonymous write
1089
+ // (sc-5229), demanding identity here would flag the only correct way to build
1090
+ // the logged-out-friendly widget an author explicitly asked for.
1086
1091
  //
1087
1092
  // Conservative on purpose: an unrelated `.id` comparison elsewhere in the
1088
1093
  // source silences the rule. A warning that occasionally stays quiet is far
@@ -1101,6 +1106,7 @@ function _writeGatedOnUserRules(source) {
1101
1106
  const code = _stripNonCode(source);
1102
1107
  const call = /\buseDatastoreMutation\s*\(/.exec(code);
1103
1108
  if (!call) return [];
1109
+ if (/\buseCanWrite\s*\(/.test(code)) return [];
1104
1110
  if (_IDENTITY_GUARD_RES.some((re) => re.test(code))) return [];
1105
1111
  const line = code.slice(0, call.index).split(/\r?\n/).length;
1106
1112
  return [
@@ -1110,9 +1116,9 @@ function _writeGatedOnUserRules(source) {
1110
1116
  // Kept under ~210 chars: a finding is truncated at 300 downstream, and
1111
1117
  // the fix instruction is the half worth keeping.
1112
1118
  label:
1113
- `writes with useDatastoreMutation() but never checks who is signed ` +
1114
- `in - read useUser(), and when !user.id render the action inactive ` +
1115
- `with a "sign in" line instead of a live button that can only fail.`,
1119
+ `writes with useDatastoreMutation() but never checks who may write ` +
1120
+ `- gate on useCanWrite(tableId), or useUser() when the action needs ` +
1121
+ `a signed-in user, and render the control inactive, never live-but-doomed.`,
1116
1122
  line,
1117
1123
  snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
1118
1124
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.114.0",
3
+ "version": "0.115.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",