@colixsystems/widget-sdk 0.135.0 → 0.137.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
@@ -70,7 +70,65 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
70
70
 
71
71
  ## Status
72
72
 
73
- `v0.135.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.137.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.137.0 (contract 1.106.0)
76
+
77
+ **A `datastoreTemplate` column marked `encrypted` is now actually encrypted in the workspace that installs you (sc-7557).** The field was documented nowhere and honoured nowhere: install built its column rows one way, the republish/upgrade migration built them another way, and neither carried the flag — so a column you declared confidential was created as an ordinary plaintext column and nothing told you or your installer. Both paths now share one projection, so the flag lands on install and on upgrade alike.
78
+
79
+ ```js
80
+ datastoreTemplate: {
81
+ tables: [
82
+ {
83
+ suffix: "Patients",
84
+ columns: [
85
+ { name: "Name", dataType: "STRING", required: true },
86
+ { name: "Ssn", dataType: "STRING", required: true, encrypted: true },
87
+ ],
88
+ rows: [{ Name: "Ada Lovelace", Ssn: "600101-1234" }],
89
+ },
90
+ ],
91
+ }
92
+ ```
93
+
94
+ - **Your widget code does not change.** It reads and writes plaintext through `useDatastoreQuery` / `useDatastoreMutation` exactly as it does for any other column — the platform encrypts on write (AES-256-GCM under a per-workspace subkey) and decrypts for end users.
95
+ - **Studio users see `🔒`, not the value.** That is the point of the flag: the people authoring the app cannot read what their end users store. The column also cannot be searched, filtered or sorted on — ciphertext is opaque.
96
+ - **Sample `rows` are encrypted too.** They used to be written straight into the value table; a plaintext row under an encrypted column would have been read back as a corrupt envelope.
97
+ - **Not valid on `RELATION`.** Its value is a foreign key the backend has to resolve, so it can never be opaque — declaring it is now a publish error, as is a non-boolean `encrypted`.
98
+ - **An upgrade adds a new encrypted column, but never flips a live one.** Turning encryption on over existing plaintext (or off over existing ciphertext) would strand what is already stored, so a changed flag on a column that already exists is reported back to you rather than applied.
99
+
100
+ ### What's new in 0.136.0 (contract 1.105.0)
101
+
102
+ **A `datastoreTemplate` can now say WHICH AUDIENCE gets which access — not just the two anonymous ones (sc-7530).** `publicGrant` could only ever reach `everyone` and `authenticated`, because those are the two principals that need no id. Anything naming a real user group was unexpressible: a group id belongs to the workspace that installs your widget, so it could never travel in your manifest. The practical cost was that a permission model you had already set up correctly — say an admin group with full CRUD while ordinary signed-in users only read — shipped to your installers as nothing at all, and every one of them rebuilt it by hand across both the table grant and the record permissions.
103
+
104
+ Templates now name their audiences symbolically and let the installer bind them:
105
+
106
+ ```js
107
+ datastoreTemplate: {
108
+ roles: [
109
+ { key: "admin", label: "Administrators", description: "Manages orders end to end" },
110
+ ],
111
+ tables: [
112
+ {
113
+ suffix: "Orders",
114
+ columns: [...],
115
+ // the id-less audiences, unchanged
116
+ publicGrant: { canRead: true, canWrite: false, canDelete: false },
117
+ // the audience that needs a real group
118
+ roleGrants: [{ role: "admin", canRead: true, canWrite: true, canDelete: true }],
119
+ },
120
+ ],
121
+ }
122
+ ```
123
+
124
+ `roles` is declared ONCE per template and referenced by every table, so three widgets over two tables ask the installing workspace **one** question per audience rather than one per grant. At install they pick one of their own groups for each role, or have one created for them under the role's label.
125
+
126
+ - **`key`** is lowercase-kebab (`/^[a-z][a-z0-9-]*$/`), at most 8 roles per template. A `roleGrants` entry naming a role you did not declare is a publish error, as is granting the same role twice on one table or granting it no verb at all.
127
+ - **`canDelete` is row-level only.** A table-scope grant answers "may I create a record"; only a record permission answers a question about a row. Both halves are written for you from the one declaration.
128
+ - **An unbound role is an ordinary outcome, never a failed install.** If the installer skips a role, no grant is written and they are told which role is still unbound — they can bind it later from the table's permissions.
129
+ - **Purely additive.** A template declaring neither `roles` nor `roleGrants` behaves exactly as it does today.
130
+
131
+ Also tightened in this release: the `publicGrant` flags are now type-checked. They never were, so a stringy `canRead: "false"` read as truthy and silently opened the table to anonymous reads. If you have been passing anything but a real boolean there, publishing will now tell you.
74
132
 
75
133
  ### What's new in 0.135.0 (contract 1.104.0)
76
134
 
package/dist/contract.cjs CHANGED
@@ -2153,7 +2153,7 @@ const MANIFEST_SCHEMA = {
2153
2153
  type: "object",
2154
2154
  required: false,
2155
2155
  description:
2156
- "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. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for.",
2156
+ "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. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected.",
2157
2157
  },
2158
2158
  translations: {
2159
2159
  type: "object",
@@ -3913,7 +3913,20 @@ const CONTRACT = deepFreeze({
3913
3913
  // the web half is a widget.web.jsx typed-code or QR path. Pinned in the
3914
3914
  // export and gated behind the `nfc` export capability, because its iOS
3915
3915
  // entitlement obliges every App ID that carries it.
3916
- version: "1.104.0",
3916
+ // 1.105.0: additive (sc-7530) — a `datastoreTemplate` can declare the
3917
+ // AUDIENCES its tables grant access to, not just the two id-less ones.
3918
+ // `publicGrant` could only ever reach `everyone` and `authenticated`, so a
3919
+ // widget whose author had set up "an admin group does everything, signed-in
3920
+ // users only read" shipped that model to nobody: a group id is tenant-local
3921
+ // and cannot travel in a manifest. Templates now declare `roles:
3922
+ // [{ key, label, description? }]` once (lowercase-kebab key, max 8) and
3923
+ // per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`; the
3924
+ // installing workspace binds each role to one of ITS OWN user groups — or
3925
+ // has one created — during the install consent step. Purely additive: a
3926
+ // template declaring neither behaves exactly as before, and a role nobody
3927
+ // binds writes no grant rather than failing the install. Minor bump on the
3928
+ // pre-1.0 channel.
3929
+ version: "1.106.0",
3917
3930
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3918
3931
  hooks: HOOKS,
3919
3932
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -2153,7 +2153,7 @@ const MANIFEST_SCHEMA = {
2153
2153
  type: "object",
2154
2154
  required: false,
2155
2155
  description:
2156
- "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. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for.",
2156
+ "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. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected.",
2157
2157
  },
2158
2158
  translations: {
2159
2159
  type: "object",
@@ -3913,7 +3913,20 @@ const CONTRACT = deepFreeze({
3913
3913
  // the web half is a widget.web.jsx typed-code or QR path. Pinned in the
3914
3914
  // export and gated behind the `nfc` export capability, because its iOS
3915
3915
  // entitlement obliges every App ID that carries it.
3916
- version: "1.104.0",
3916
+ // 1.105.0: additive (sc-7530) — a `datastoreTemplate` can declare the
3917
+ // AUDIENCES its tables grant access to, not just the two id-less ones.
3918
+ // `publicGrant` could only ever reach `everyone` and `authenticated`, so a
3919
+ // widget whose author had set up "an admin group does everything, signed-in
3920
+ // users only read" shipped that model to nobody: a group id is tenant-local
3921
+ // and cannot travel in a manifest. Templates now declare `roles:
3922
+ // [{ key, label, description? }]` once (lowercase-kebab key, max 8) and
3923
+ // per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`; the
3924
+ // installing workspace binds each role to one of ITS OWN user groups — or
3925
+ // has one created — during the install consent step. Purely additive: a
3926
+ // template declaring neither behaves exactly as before, and a role nobody
3927
+ // binds writes no grant rather than failing the install. Minor bump on the
3928
+ // pre-1.0 channel.
3929
+ version: "1.106.0",
3917
3930
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
3918
3931
  hooks: HOOKS,
3919
3932
  primitives: PRIMITIVES,
package/dist/index.d.ts CHANGED
@@ -186,6 +186,15 @@ export interface WidgetDatastoreTemplateColumn {
186
186
  relationType?: "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_MANY";
187
187
  /** REQ-ACL-RELINHERIT: opt this RELATION column into row-level ACL inheritance. */
188
188
  inheritAcl?: boolean;
189
+ /**
190
+ * REQ-DDL-ENCRYPT: store this column's values encrypted at rest (AES-256-GCM
191
+ * under a per-workspace subkey). The widget still reads and writes plaintext
192
+ * — the platform encrypts on write and decrypts for end users — but studio
193
+ * users see `🔒` instead of the value, and the column cannot be searched,
194
+ * filtered or sorted on. Not valid on `RELATION` (its value is a foreign key
195
+ * the backend must resolve); publishing one is rejected.
196
+ */
197
+ encrypted?: boolean;
189
198
  }
190
199
 
191
200
  export interface WidgetDatastoreTemplateTable {
@@ -201,6 +210,14 @@ export interface WidgetDatastoreTemplateTable {
201
210
  * studio-owner only regardless.
202
211
  */
203
212
  publicGrant?: { canRead?: boolean; canWrite?: boolean; canDelete?: boolean };
213
+ /**
214
+ * sc-7530: group-scoped grants, named symbolically. Each entry's `role` must
215
+ * be a `key` declared on the template's `roles`. Unlike `publicGrant` — which
216
+ * can only reach the two synthetic principals (`everyone` / `authenticated`)
217
+ * — these express "this audience gets these verbs" and are bound to a real
218
+ * group by the workspace that installs the widget.
219
+ */
220
+ roleGrants?: WidgetDatastoreTemplateRoleGrant[];
204
221
  columns: WidgetDatastoreTemplateColumn[];
205
222
  /**
206
223
  * Optional sample rows seeded into the table at install time so the widget
@@ -213,8 +230,44 @@ export interface WidgetDatastoreTemplateTable {
213
230
  >;
214
231
  }
215
232
 
233
+ /**
234
+ * sc-7530: a role the template's `roleGrants` name. Declared once per template
235
+ * and referenced by every table, so several widgets over several tables ask the
236
+ * installing workspace ONE question per audience rather than one per grant.
237
+ *
238
+ * Symbolic on purpose. A group id is tenant-local, so it could only ever be
239
+ * wrong in the workspace that installs the widget — the installer binds each
240
+ * role to one of their OWN groups (or has one created) at install time.
241
+ */
242
+ export interface WidgetDatastoreTemplateRole {
243
+ /** Stable within the template; matches /^[a-z][a-z0-9-]*$/. */
244
+ key: string;
245
+ /** What the installing workspace sees in the binding step. */
246
+ label: string;
247
+ /** Why the role exists — shown under the label. */
248
+ description?: string;
249
+ }
250
+
251
+ /** sc-7530: the verbs one role holds over one table. */
252
+ export interface WidgetDatastoreTemplateRoleGrant {
253
+ /** A `key` from the template's `roles`. */
254
+ role: string;
255
+ /** Read the table and the rows in it. */
256
+ canRead?: boolean;
257
+ /** Create records, and edit existing ones. */
258
+ canWrite?: boolean;
259
+ /** Delete records. Row-level only — table scope has no delete verb. */
260
+ canDelete?: boolean;
261
+ }
262
+
216
263
  export interface WidgetDatastoreTemplate {
217
264
  tables: WidgetDatastoreTemplateTable[];
265
+ /**
266
+ * sc-7530: the audiences this template's tables grant access to. Bound to
267
+ * real `AppUserGroup`s by the installing workspace; an unbound role simply
268
+ * writes no grant.
269
+ */
270
+ roles?: WidgetDatastoreTemplateRole[];
218
271
  }
219
272
 
220
273
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.135.0",
3
+ "version": "0.137.0",
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",