@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 +11 -1
- package/dist/linter.cjs +15 -9
- package/dist/linter.js +15 -9
- package/package.json +1 -1
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 "
|
|
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
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
// a
|
|
935
|
-
//
|
|
936
|
-
//
|
|
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
|
|
965
|
-
|
|
966
|
-
`
|
|
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
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
// a
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
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
|
|
1114
|
-
|
|
1115
|
-
`
|
|
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.
|
|
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",
|