@colixsystems/widget-sdk 0.18.0 → 0.21.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/dist/contract.cjs CHANGED
@@ -10,9 +10,9 @@
10
10
 
11
11
  const DEFAULT_THEME_TOKENS = Object.freeze({
12
12
  colors: Object.freeze({
13
- primary: "#ff6b5b",
13
+ primary: "#3b82f6",
14
14
  onPrimary: "#ffffff",
15
- secondary: "#475569",
15
+ secondary: "#10b981",
16
16
  onSecondary: "#ffffff",
17
17
  surface: "#ffffff",
18
18
  onSurface: "#111827",
@@ -70,6 +70,26 @@ const HOOKS = [
70
70
  requiredContextSlice: ["user"],
71
71
  scopes: null,
72
72
  },
73
+ {
74
+ name: "useFill",
75
+ signature: "useFill()",
76
+ description:
77
+ "Returns true when the host has sized this widget to fill its layout " +
78
+ 'slot\'s available height — a page-grid tile whose author chose "Fill ' +
79
+ 'tile height", or a widget type that fills by default (containers and ' +
80
+ "media). When true, a widget that has a meaningful filled form (Image, " +
81
+ "Chart, Map, Video) should switch from its intrinsic height to a stretch " +
82
+ 'style (flex: 1 / height: "100%"); widgets with no useful filled form ' +
83
+ "may ignore it. Defaults to false wherever the host has not opted the " +
84
+ "widget into filling, so calling it is always safe. The SAME value is " +
85
+ "injected by the web Player and the native export (CLAUDE.md §3), so a " +
86
+ "widget's fill behaviour is identical on both platforms.",
87
+ returnShape: {
88
+ "(returns)": "boolean",
89
+ },
90
+ requiredContextSlice: ["fill"],
91
+ scopes: null,
92
+ },
73
93
  {
74
94
  name: "useChildRenderer",
75
95
  signature: "useChildRenderer()",
@@ -167,12 +187,12 @@ const HOOKS = [
167
187
  name: "useDirectory",
168
188
  signature: "useDirectory(query?)",
169
189
  returnShape: {
170
- users: "Array<{ id, name, role }>",
190
+ users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
171
191
  loading: "boolean",
172
192
  error: "DatastoreError | null",
173
193
  refetch: "() => Promise<void>",
174
194
  },
175
- requiredContextSlice: ["directory.listUsers"],
195
+ requiredContextSlice: ["directory.users"],
176
196
  scopes: ["directory.read:users"],
177
197
  },
178
198
  {
@@ -206,28 +226,26 @@ const HOOKS = [
206
226
  name: "useUsers",
207
227
  signature: "useUsers(query?)",
208
228
  description:
209
- "AppUser administration. Returns { users, loading, error, refetch, invite, " +
210
- "deactivate, reactivate, remove }. Reads need users.read:* scope; mutations " +
211
- "need users.write:*. The `invite` call accepts { email, name, groupIds? } " +
212
- "and returns the resulting AppUserInvite row (the email is sent by the host).",
229
+ "AppUser administration via the injected directory-client at " +
230
+ "ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
231
+ "{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
232
+ "list returns the { data, meta } envelope verbatim the hook unwraps " +
233
+ "res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
234
+ "scope; mutations need users.write:*. The `invite` call accepts " +
235
+ "{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
236
+ "(the email is sent by the host).",
213
237
  returnShape: {
214
- users: "Array<{ id, name, email?, role, isActive }>",
238
+ users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
215
239
  loading: "boolean",
216
240
  error: "DirectoryError | null",
217
241
  refetch: "() => Promise<void>",
218
242
  invite:
219
- "({ email, name, groupIds? }) => Promise<Invite> // rejects with DirectoryError",
243
+ "({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
220
244
  deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
221
245
  reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
222
246
  remove: "(userId) => Promise<void> // rejects with DirectoryError",
223
247
  },
224
- requiredContextSlice: [
225
- "users.listUsers",
226
- "users.invite",
227
- "users.deactivate",
228
- "users.reactivate",
229
- "users.remove",
230
- ],
248
+ requiredContextSlice: ["directory.users"],
231
249
  scopes: ["users.read:*"],
232
250
  },
233
251
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration. Returns
@@ -239,11 +257,14 @@ const HOOKS = [
239
257
  name: "useGroups",
240
258
  signature: "useGroups(query?)",
241
259
  description:
242
- "AppUserGroup administration. Returns { groups, loading, error, refetch, " +
243
- "create, remove, addMember, removeMember }. Reads need groups.read:*; " +
260
+ "AppUserGroup administration via the injected directory-client at " +
261
+ "ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
262
+ "Returns { groups, loading, error, refetch, create, remove, addMember, " +
263
+ "removeMember }. list returns the { data, meta } envelope verbatim — the " +
264
+ "hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
244
265
  "mutations need groups.write:*.",
245
266
  returnShape: {
246
- groups: "Array<{ id, name, memberCount }>",
267
+ groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
247
268
  loading: "boolean",
248
269
  error: "DirectoryError | null",
249
270
  refetch: "() => Promise<void>",
@@ -255,13 +276,7 @@ const HOOKS = [
255
276
  removeMember:
256
277
  "(groupId, userId) => Promise<void> // rejects with DirectoryError",
257
278
  },
258
- requiredContextSlice: [
259
- "groups.listGroups",
260
- "groups.create",
261
- "groups.remove",
262
- "groups.addMember",
263
- "groups.removeMember",
264
- ],
279
+ requiredContextSlice: ["directory.groups"],
265
280
  scopes: ["groups.read:*"],
266
281
  },
267
282
  // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
@@ -270,33 +285,33 @@ const HOOKS = [
270
285
  name: "useRecordPermissions",
271
286
  signature: "useRecordPermissions(tableId, recordId)",
272
287
  description:
273
- "Manage per-record VirtualPermission grants on a single record. Returns " +
274
- "{ permissions, loading, error, grant, revoke, update, refetch } where " +
275
- "permissions is Array<{ id, principalType: 'USER' | 'GROUP' | 'PUBLIC', " +
276
- "principalId, canRead, canWrite, canDelete, canGrant }>. Mutating " +
277
- "requires acl.write:records scope AND canGrant on the target record " +
278
- "(REQ-ACL-RELINHERIT-05: APP_USER actors with canGrant are accepted, " +
279
- "not only Studio owners). When tableId or recordId is null/empty the " +
280
- "hook collapses to an empty no-op result without a network round-trip.",
288
+ "Manage per-record VirtualPermission grants on a single record via the " +
289
+ "injected datastore-client at " +
290
+ "ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}. " +
291
+ "Returns { permissions, loading, error, grant, revoke, update, refetch } " +
292
+ "where permissions is Array<{ id, user_id, group_id, can_read, can_write, " +
293
+ "can_delete, can_grant }> (snake_case rows verbatim; list() returns the " +
294
+ "{ data, meta } envelope and the hook unwraps res.data). grant/update " +
295
+ "bodies are snake_case verbatim ({ user_id | group_id, can_read, " +
296
+ "can_write, can_delete, can_grant }). Mutating requires acl.write:records " +
297
+ "scope AND can_grant on the target record (REQ-ACL-RELINHERIT-05: " +
298
+ "APP_USER actors with can_grant are accepted, not only Studio owners). " +
299
+ "When tableId or recordId is null/empty the hook collapses to an empty " +
300
+ "no-op result without a network round-trip.",
281
301
  returnShape: {
282
302
  permissions:
283
- "Array<{ id, principalType: 'USER' | 'GROUP' | 'PUBLIC', principalId, canRead, canWrite, canDelete, canGrant }>",
303
+ "Array<{ id, user_id, group_id, can_read, can_write, can_delete, can_grant }> // snake_case rows; unwrapped from { data, meta }",
284
304
  loading: "boolean",
285
305
  error: "PermissionError | null",
286
306
  grant:
287
- "({ principalType, principalId?, canRead?, canWrite?, canDelete?, canGrant? }) => Promise<RecordPermission> // rejects with PermissionError",
307
+ "({ user_id?, group_id?, can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
288
308
  revoke:
289
309
  "(permissionId) => Promise<void> // rejects with PermissionError",
290
310
  update:
291
- "(permissionId, { canRead?, canWrite?, canDelete?, canGrant? }) => Promise<RecordPermission> // rejects with PermissionError",
311
+ "(permissionId, { can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
292
312
  refetch: "() => Promise<void>",
293
313
  },
294
- requiredContextSlice: [
295
- "recordPermissions.list",
296
- "recordPermissions.grant",
297
- "recordPermissions.revoke",
298
- "recordPermissions.update",
299
- ],
314
+ requiredContextSlice: ["datastore.records"],
300
315
  scopes: ["acl.write:records"],
301
316
  },
302
317
  // REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
@@ -454,10 +469,40 @@ const CATEGORIES = [
454
469
  "DATA",
455
470
  "MEDIA",
456
471
  "COMMUNICATION",
472
+ // REQ-USERMGMT-06: app-administration widgets (User Management, …) the
473
+ // published app embeds for its own member management — its own palette
474
+ // section, distinct from COMMUNICATION.
475
+ "ADMINISTRATION",
457
476
  "CUSTOM",
458
477
  ];
459
478
  const PLATFORMS = ["web", "native"];
460
479
 
480
+ // REQ-WIDGET-ACTION — server-side actions a widget may declare in its
481
+ // manifest. Each runs in the shared isolated-vm action runner (see backend
482
+ // action-runner.service.js) on a cron schedule or in response to a record
483
+ // CRUD event — NEVER in the rendered app, so they never affect Player ↔
484
+ // export parity. The trigger vocabulary mirrors the backend Action model.
485
+ const ACTION_TRIGGER_TYPES = [
486
+ "schedule",
487
+ "record_created",
488
+ "record_updated",
489
+ "record_deleted",
490
+ ];
491
+ // Globals the action script runs against (the runner's surface) — distinct
492
+ // from the React/SDK widget surface, so the component import/banned-API
493
+ // linter does NOT scan action scripts.
494
+ const ACTION_SCRIPT_GLOBALS = [
495
+ "datastore",
496
+ "fetch",
497
+ "console",
498
+ "record",
499
+ "tenantId",
500
+ "triggerType",
501
+ "triggerTableId",
502
+ ];
503
+ // Mirrors action.service.js SCRIPT_MAX_BYTES.
504
+ const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
505
+
461
506
  // Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
462
507
  // more labels, lowercase alnum + hyphen, label starts with a letter. The
463
508
  // analyzer + the SDK validator both read this from the contract so a
@@ -543,6 +588,24 @@ const MANIFEST_SCHEMA = {
543
588
  description:
544
589
  "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.",
545
590
  },
591
+ actions: {
592
+ type: "object[]",
593
+ required: false,
594
+ description:
595
+ "Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?, triggerType (one of " +
596
+ ACTION_TRIGGER_TYPES.join(", ") +
597
+ "), scheduleCron? (required iff triggerType=='schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
598
+ ACTION_SCRIPT_GLOBALS.join(", ") +
599
+ " — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
600
+ default: [],
601
+ },
602
+ translations: {
603
+ type: "object",
604
+ required: false,
605
+ description:
606
+ "Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤100 keys; each key matches /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/; each value ≤1 KB; the namespaced key must fit the dictionary key cap (128 chars).",
607
+ default: {},
608
+ },
546
609
  };
547
610
 
548
611
  const WIDGET_CONTEXT_SHAPE = {
@@ -558,10 +621,24 @@ const WIDGET_CONTEXT_SHAPE = {
558
621
  required: true,
559
622
  fields: { id: "manifest.id", version: "manifest.version" },
560
623
  },
624
+ // REQ-LAY-08 — optional host layout hint backing useFill(). True when the
625
+ // host sized this widget to fill its layout slot's available height (a
626
+ // page-grid tile set to "Fill tile height", or a default-fill widget type).
627
+ // Optional: a host that never fills a widget simply omits it and useFill()
628
+ // returns false, so existing hosts need no change.
629
+ fill: {
630
+ description:
631
+ "Optional host layout hint (boolean). True when the host sized this " +
632
+ "widget to fill its layout slot's available height (page-grid tile " +
633
+ "fill). Backs useFill(); defaults to false when absent.",
634
+ required: false,
635
+ fields: {},
636
+ },
561
637
  user: {
562
- description: "Signed-in user ({ id, email, displayName }).",
638
+ description:
639
+ "Signed-in user, host-provided VERBATIM (snake_case: { id, email, display_name, roles, group_ids }). Not a data-client.",
563
640
  required: true,
564
- fields: { id: "string", email: "string", displayName: "string" },
641
+ fields: { id: "string", email: "string", display_name: "string" },
565
642
  },
566
643
  workspace: {
567
644
  description:
@@ -581,19 +658,30 @@ const WIDGET_CONTEXT_SHAPE = {
581
658
  },
582
659
  datastore: {
583
660
  description:
584
- "{ records(table) -> { list(query?), get(id), create(values), update(id, values), delete(id) }, schema(table) -> Promise<{ id, name, columns: [...] }> }. `records` backs the query/record/mutation hooks; `schema` backs useDatastoreSchema() and resolves a table's column structure (no row data).",
661
+ "Injected @colixsystems/datastore-client instance. " +
662
+ "{ tables: { list(), get(idOrName) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
663
+ "records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
664
+ "permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
665
+ "`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(). " +
666
+ "List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
585
667
  required: true,
586
- fields: { records: "function", schema: "function" },
668
+ fields: { records: "function", schema: "function", tables: "object" },
587
669
  },
588
670
  directory: {
589
671
  description:
590
- "Read-only user directory. { listUsers(query?) -> Promise<Array<{ id, name, role }>> }. Backs useDirectory(); for chat people-lists / @-mention pickers / author-id resolution. Requires the directory.read:users scope.",
672
+ "Injected @colixsystems/directory-client instance. " +
673
+ "{ me(), users: { list(query?) -> Promise<{ data, meta }>, get(id), invite(body), deactivate(id), reactivate(id) }, " +
674
+ "groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
675
+ "invites: { list(), revoke(id), resend(id) } }. " +
676
+ "users backs useDirectory() + useUsers(); groups backs useGroups(). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:*.",
591
677
  required: true,
592
- fields: { listUsers: "function" },
678
+ fields: { users: "object", groups: "object" },
593
679
  },
594
680
  files: {
595
681
  description:
596
- "Read-only asset resolver. { get(fileId) -> Promise<{ id, url, storedFilename, mimeType, sizeBytes, ... }> }. Backs useFile(); resolves an asset id to an absolute URL the widget can drop into an <Image source>. The url field is always an absolute URL composed against the host's API base.",
682
+ "Injected @colixsystems/files-client instance, FLATTENED so file ops are top-level. " +
683
+ "{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData), folders: { list, create }, shares: { list, create, remove } }. " +
684
+ "Backs useFile(); the returned file already carries an absolute url the widget can drop into an <Image source>.",
597
685
  required: true,
598
686
  fields: { get: "function" },
599
687
  },
@@ -610,62 +698,17 @@ const WIDGET_CONTEXT_SHAPE = {
610
698
  },
611
699
  payments: {
612
700
  description:
613
- "Incoming app-user payments (REQ-BILL-07-WIDGETPAY). { requestPayment({ amountCents, currency?, description, metadata? }) -> Promise<{ id, status, checkoutUrl? }>, getPayment(id) -> Promise<payment> }. Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
701
+ "Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status, checkoutUrl? }>, getPayment(id) -> Promise<payment> }. Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
614
702
  required: true,
615
703
  fields: { requestPayment: "function", getPayment: "function" },
616
704
  },
617
- // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration facade backing
618
- // useUsers(). Reads gated by `users.read:*`; mutations by `users.write:*`.
619
- // The host signs an `X-Widget-Scopes` header against JWT_SECRET so an
620
- // APP_USER cannot forge scope claims, and the request is additionally
621
- // gated by a SystemAcl `users.read` / `users.write` capability grant.
622
- users: {
623
- description:
624
- "AppUser administration. { listUsers(query?) -> Promise<User[]>, invite({ email, name, groupIds? }) -> Promise<Invite>, deactivate(userId) -> Promise<User>, reactivate(userId) -> Promise<User>, remove(userId) -> Promise<void> }. Backs useUsers(); reads require users.read:*, mutations require users.write:*.",
625
- required: true,
626
- fields: {
627
- listUsers: "function",
628
- invite: "function",
629
- deactivate: "function",
630
- reactivate: "function",
631
- remove: "function",
632
- },
633
- },
634
- // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
635
- // management facade backing useRecordPermissions(). Mirror of
636
- // contract.js.
637
- recordPermissions: {
638
- description:
639
- "Per-record VirtualPermission management. " +
640
- "{ list(tableId, recordId) -> Promise<RecordPermission[]>, " +
641
- "grant(tableId, recordId, body) -> Promise<RecordPermission>, " +
642
- "revoke(tableId, recordId, permissionId) -> Promise<void>, " +
643
- "update(tableId, recordId, permissionId, body) -> Promise<RecordPermission> }. " +
644
- "Backs useRecordPermissions(); requires acl.write:records scope AND " +
645
- "canGrant on the target record.",
646
- required: true,
647
- fields: {
648
- list: "function",
649
- grant: "function",
650
- revoke: "function",
651
- update: "function",
652
- },
653
- },
654
- // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration facade
655
- // backing useGroups(). Reads gated by `groups.read:*`; mutations by
656
- // `groups.write:*`. Same X-Widget-Scopes + SystemAcl gating as users.
657
- groups: {
658
- description:
659
- "AppUserGroup administration. { listGroups(query?) -> Promise<Group[]>, create({ name }) -> Promise<Group>, remove(groupId) -> Promise<void>, addMember(groupId, userId) -> Promise<void>, removeMember(groupId, userId) -> Promise<void> }. Backs useGroups(); reads require groups.read:*, mutations require groups.write:*.",
660
- required: true,
661
- fields: {
662
- listGroups: "function",
663
- create: "function",
664
- remove: "function",
665
- addMember: "function",
666
- removeMember: "function",
667
- },
668
- },
705
+ // REQ-WSDK-DOMAIN-CLIENTSthe AppUser administration, AppUserGroup
706
+ // administration, and per-record VirtualPermission facades that used to
707
+ // live here (`users`, `groups`, `recordPermissions`) were folded into the
708
+ // injected domain-client instances: useUsers()/useGroups() read
709
+ // ctx.directory.{users,groups}; useRecordPermissions() reads
710
+ // ctx.datastore.records(table).permissions(record). See the `directory`
711
+ // and `datastore` slices above.
669
712
  i18n: {
670
713
  description: "{ t(key, fallback?), locale }.",
671
714
  required: true,
@@ -898,6 +941,20 @@ function deepFreeze(value) {
898
941
  return Object.freeze(value);
899
942
  }
900
943
 
944
+ // REQ-L10N-WIDGET — a widget's manifest `translations` are merged into the
945
+ // tenant dictionary under a per-widget namespace so they never collide with
946
+ // app keys or another widget's keys. The prefix is DERIVED from the widget id
947
+ // (it "follows the widget"), so authors call `t("greeting")` and the host
948
+ // resolves `widget.<id>.greeting`. This is the ONE definition of the key
949
+ // format: the SDK `useI18n` hook (lookup) and the backend seeder (write) both
950
+ // call it, so the wire key and the lookup key can never drift.
951
+ function widgetTranslationPrefix(id) {
952
+ return `widget.${id}.`;
953
+ }
954
+ function widgetTranslationKey(id, key) {
955
+ return `widget.${id}.${key}`;
956
+ }
957
+
901
958
  const CONTRACT = deepFreeze({
902
959
  // REQ-WSDK-PLATFORM bump:
903
960
  // - `vettedImports` is a new field (rich allowlist with platforms +
@@ -921,19 +978,71 @@ const CONTRACT = deepFreeze({
921
978
  // runtime (Form Builder renders inputs by column type). Reads the existing
922
979
  // ACL-gated `GET /tables/:id` — structure only, no row data.
923
980
  //
924
- // 1.8.0: additive — new `useRecordPermissions(tableId, recordId)` hook +
925
- // the `recordPermissions` host-context slice it reads + the
926
- // `acl.write:records` scope it gates on. Hits the existing REQ-ACL-06
927
- // /api/v1/tables/:tableId/records/:recordId/permissions REST surface; the
928
- // host normalises the wire shape into a single principalType+principalId
929
- // pair widgets branch on. Caller still needs canGrant on the target
930
- // record (REQ-ACL-RELINHERIT-05).
931
- version: "1.8.0",
981
+ // 1.8.0: two additive features.
982
+ // - REQ-WIDGET-ACTION manifests may declare an optional `actions` array.
983
+ // Each entry is a server-side action (cron- or record-triggered JS) the
984
+ // operator enables per tenant; it runs in the existing isolated-vm action
985
+ // runner, never in the rendered app. New contract fields
986
+ // `actionTriggerTypes`, `actionScriptGlobals`, and `actionScriptMaxBytes`
987
+ // let the docs + agent prompt + validator derive the grammar from one
988
+ // source. New `ADMINISTRATION` manifest category (REQ-USERMGMT-06).
989
+ // - REQ-ACL-06 — new `useRecordPermissions(tableId, recordId)` hook + the
990
+ // `recordPermissions` host-context slice it reads + the `acl.write:records`
991
+ // scope it gates on. Hits the existing REQ-ACL-06
992
+ // /api/v1/tables/:tableId/records/:recordId/permissions REST surface; the
993
+ // host normalises the wire shape into a single principalType+principalId
994
+ // pair widgets branch on. Caller still needs canGrant on the target
995
+ // record (REQ-ACL-RELINHERIT-05).
996
+ //
997
+ // 1.9.0: host-contract change (REQ-WSDK-DOMAIN-CLIENTS) — the host now
998
+ // injects domain-client INSTANCES on the WidgetContext rather than
999
+ // bespoke per-hook facades. ctx.datastore is a @colixsystems/datastore-client
1000
+ // (records(t).list now returns the { data, meta } envelope verbatim;
1001
+ // records(t).permissions(r) replaces the deleted ctx.recordPermissions),
1002
+ // ctx.directory is a @colixsystems/directory-client (users/groups
1003
+ // namespaces replace the deleted ctx.users / ctx.groups; list returns
1004
+ // envelopes), ctx.files is a flattened @colixsystems/files-client,
1005
+ // ctx.payments is a @colixsystems/payments-client. All rows/bodies are
1006
+ // snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
1007
+ // SDK stays duck-typed — it imports none of the four data SDKs. Minor
1008
+ // bump on the contract's pre-1.0 versioning (the breaking channel).
1009
+ //
1010
+ // 1.10.0: additive (REQ-L10N-WIDGET) — manifests may declare an optional
1011
+ // `translations` map ({ <key>: { en, <locale>? } }, en required). The
1012
+ // host seeds them into the tenant l10n dictionary under a per-widget
1013
+ // namespace at install; `useI18n().t(key)` now auto-prefixes the lookup
1014
+ // with `widget.<ctx.widget.id>.` (then falls back to the raw key, so
1015
+ // existing widgets and shared app keys keep resolving). New exported
1016
+ // helpers widgetTranslationPrefix / widgetTranslationKey are the single
1017
+ // source of the key format, shared by the hook and the backend seeder.
1018
+ //
1019
+ // 1.11.0: additive (REQ-LAY-08) — new useFill() hook + the optional `fill`
1020
+ // host-context slice it reads. The host sets ctx.fill=true when it has
1021
+ // sized a widget to fill its layout slot's available height (a page-grid
1022
+ // tile whose author chose "Fill tile height", or a default-fill widget
1023
+ // type — containers + media). Widgets that can stretch (Image, Chart,
1024
+ // Map, Video) switch to flex:1/height:"100%" when useFill() is true; all
1025
+ // others ignore it. The slice is OPTIONAL (defaults false) so existing
1026
+ // hosts need no change, and the same value is injected on web and native
1027
+ // so fill behaviour cannot diverge between the Player and the export.
1028
+ //
1029
+ // 1.11.1: fix — the canonical default `themeTokens.colors` now match the
1030
+ // product's advertised default brand (primary #3b82f6 blue, secondary
1031
+ // #10b981 green) instead of the stale coral/slate that no other surface
1032
+ // used. The Theme Settings tab and Player chrome already defaulted to
1033
+ // these values, so a tenant that never customised its theme rendered
1034
+ // widgets in coral until a first save persisted the blue — a divergence
1035
+ // between the unsaved and saved-defaults render. Aligning the canonical
1036
+ // tokens removes it (no shape/signature change — default-value fix only).
1037
+ version: "1.11.1",
932
1038
  hooks: HOOKS,
933
1039
  primitives: PRIMITIVES,
934
1040
  manifestSchema: MANIFEST_SCHEMA,
935
1041
  manifestCategories: CATEGORIES,
936
1042
  manifestPlatforms: PLATFORMS,
1043
+ actionTriggerTypes: ACTION_TRIGGER_TYPES,
1044
+ actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
1045
+ actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
937
1046
  themeTokens: DEFAULT_THEME_TOKENS,
938
1047
  widgetContextShape: WIDGET_CONTEXT_SHAPE,
939
1048
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
@@ -957,4 +1066,10 @@ function requiredContextKeys() {
957
1066
  return [...keys];
958
1067
  }
959
1068
 
960
- module.exports = { CONTRACT, isHookAllowed, requiredContextKeys };
1069
+ module.exports = {
1070
+ CONTRACT,
1071
+ isHookAllowed,
1072
+ requiredContextKeys,
1073
+ widgetTranslationPrefix,
1074
+ widgetTranslationKey,
1075
+ };