@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.js 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()",
@@ -168,12 +188,12 @@ const HOOKS = [
168
188
  name: "useDirectory",
169
189
  signature: "useDirectory(query?)",
170
190
  returnShape: {
171
- users: "Array<{ id, name, role }>",
191
+ users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
172
192
  loading: "boolean",
173
193
  error: "DatastoreError | null",
174
194
  refetch: "() => Promise<void>",
175
195
  },
176
- requiredContextSlice: ["directory.listUsers"],
196
+ requiredContextSlice: ["directory.users"],
177
197
  scopes: ["directory.read:users"],
178
198
  },
179
199
  {
@@ -207,28 +227,26 @@ const HOOKS = [
207
227
  name: "useUsers",
208
228
  signature: "useUsers(query?)",
209
229
  description:
210
- "AppUser administration. Returns { users, loading, error, refetch, invite, " +
211
- "deactivate, reactivate, remove }. Reads need users.read:* scope; mutations " +
212
- "need users.write:*. The `invite` call accepts { email, name, groupIds? } " +
213
- "and returns the resulting AppUserInvite row (the email is sent by the host).",
230
+ "AppUser administration via the injected directory-client at " +
231
+ "ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
232
+ "{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
233
+ "list returns the { data, meta } envelope verbatim the hook unwraps " +
234
+ "res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
235
+ "scope; mutations need users.write:*. The `invite` call accepts " +
236
+ "{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
237
+ "(the email is sent by the host).",
214
238
  returnShape: {
215
- users: "Array<{ id, name, email?, role, isActive }>",
239
+ users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
216
240
  loading: "boolean",
217
241
  error: "DirectoryError | null",
218
242
  refetch: "() => Promise<void>",
219
243
  invite:
220
- "({ email, name, groupIds? }) => Promise<Invite> // rejects with DirectoryError",
244
+ "({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
221
245
  deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
222
246
  reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
223
247
  remove: "(userId) => Promise<void> // rejects with DirectoryError",
224
248
  },
225
- requiredContextSlice: [
226
- "users.listUsers",
227
- "users.invite",
228
- "users.deactivate",
229
- "users.reactivate",
230
- "users.remove",
231
- ],
249
+ requiredContextSlice: ["directory.users"],
232
250
  scopes: ["users.read:*"],
233
251
  },
234
252
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration. Returns
@@ -240,11 +258,14 @@ const HOOKS = [
240
258
  name: "useGroups",
241
259
  signature: "useGroups(query?)",
242
260
  description:
243
- "AppUserGroup administration. Returns { groups, loading, error, refetch, " +
244
- "create, remove, addMember, removeMember }. Reads need groups.read:*; " +
261
+ "AppUserGroup administration via the injected directory-client at " +
262
+ "ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
263
+ "Returns { groups, loading, error, refetch, create, remove, addMember, " +
264
+ "removeMember }. list returns the { data, meta } envelope verbatim — the " +
265
+ "hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
245
266
  "mutations need groups.write:*.",
246
267
  returnShape: {
247
- groups: "Array<{ id, name, memberCount }>",
268
+ groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
248
269
  loading: "boolean",
249
270
  error: "DirectoryError | null",
250
271
  refetch: "() => Promise<void>",
@@ -256,54 +277,48 @@ const HOOKS = [
256
277
  removeMember:
257
278
  "(groupId, userId) => Promise<void> // rejects with DirectoryError",
258
279
  },
259
- requiredContextSlice: [
260
- "groups.listGroups",
261
- "groups.create",
262
- "groups.remove",
263
- "groups.addMember",
264
- "groups.removeMember",
265
- ],
280
+ requiredContextSlice: ["directory.groups"],
266
281
  scopes: ["groups.read:*"],
267
282
  },
268
283
  // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
269
- // management for a single record. Reads `ctx.recordPermissions` (the
270
- // host injects this on web through `widgetHostDatastore.js` and on
271
- // native through the compiled `WidgetHost` shell). The backend gates
272
- // the call on `canGrant` for the target record (Studio owners short-
273
- // circuit; APP_USER actors must hold `canGrant` via REQ-ACL-05 /
274
- // REQ-ACL-06). A widget that declares the scope but whose caller
275
- // lacks the grant receives `PermissionError { code: 'FORBIDDEN' }`.
284
+ // management for a single record. Reads the injected datastore-client at
285
+ // ctx.datastore.records(tableId).permissions(recordId) (the recordPermissions
286
+ // facade was folded into the datastore-client). The backend gates the call
287
+ // on `can_grant` for the target record (Studio owners short-circuit;
288
+ // APP_USER actors must hold `can_grant` via REQ-ACL-05 / REQ-ACL-06). A
289
+ // widget that declares the scope but whose caller lacks the grant receives
290
+ // `PermissionError { code: 'FORBIDDEN' }`.
276
291
  {
277
292
  name: "useRecordPermissions",
278
293
  signature: "useRecordPermissions(tableId, recordId)",
279
294
  description:
280
- "Manage per-record VirtualPermission grants on a single record. Returns " +
281
- "{ permissions, loading, error, grant, revoke, update, refetch } where " +
282
- "permissions is Array<{ id, principalType: 'USER' | 'GROUP' | 'PUBLIC', " +
283
- "principalId, canRead, canWrite, canDelete, canGrant }>. Mutating " +
284
- "requires acl.write:records scope AND canGrant on the target record " +
285
- "(REQ-ACL-RELINHERIT-05: APP_USER actors with canGrant are accepted, " +
286
- "not only Studio owners). When tableId or recordId is null/empty the " +
287
- "hook collapses to an empty no-op result without a network round-trip.",
295
+ "Manage per-record VirtualPermission grants on a single record via the " +
296
+ "injected datastore-client at " +
297
+ "ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}. " +
298
+ "Returns { permissions, loading, error, grant, revoke, update, refetch } " +
299
+ "where permissions is Array<{ id, user_id, group_id, can_read, can_write, " +
300
+ "can_delete, can_grant }> (snake_case rows verbatim; list() returns the " +
301
+ "{ data, meta } envelope and the hook unwraps res.data). grant/update " +
302
+ "bodies are snake_case verbatim ({ user_id | group_id, can_read, " +
303
+ "can_write, can_delete, can_grant }). Mutating requires acl.write:records " +
304
+ "scope AND can_grant on the target record (REQ-ACL-RELINHERIT-05: " +
305
+ "APP_USER actors with can_grant are accepted, not only Studio owners). " +
306
+ "When tableId or recordId is null/empty the hook collapses to an empty " +
307
+ "no-op result without a network round-trip.",
288
308
  returnShape: {
289
309
  permissions:
290
- "Array<{ id, principalType: 'USER' | 'GROUP' | 'PUBLIC', principalId, canRead, canWrite, canDelete, canGrant }>",
310
+ "Array<{ id, user_id, group_id, can_read, can_write, can_delete, can_grant }> // snake_case rows; unwrapped from { data, meta }",
291
311
  loading: "boolean",
292
312
  error: "PermissionError | null",
293
313
  grant:
294
- "({ principalType, principalId?, canRead?, canWrite?, canDelete?, canGrant? }) => Promise<RecordPermission> // rejects with PermissionError",
314
+ "({ user_id?, group_id?, can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
295
315
  revoke:
296
316
  "(permissionId) => Promise<void> // rejects with PermissionError",
297
317
  update:
298
- "(permissionId, { canRead?, canWrite?, canDelete?, canGrant? }) => Promise<RecordPermission> // rejects with PermissionError",
318
+ "(permissionId, { can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
299
319
  refetch: "() => Promise<void>",
300
320
  },
301
- requiredContextSlice: [
302
- "recordPermissions.list",
303
- "recordPermissions.grant",
304
- "recordPermissions.revoke",
305
- "recordPermissions.update",
306
- ],
321
+ requiredContextSlice: ["datastore.records"],
307
322
  scopes: ["acl.write:records"],
308
323
  },
309
324
  // REQ-WSDK-PLATFORM §6 — Tier A SDK hooks.
@@ -455,10 +470,40 @@ const CATEGORIES = [
455
470
  "DATA",
456
471
  "MEDIA",
457
472
  "COMMUNICATION",
473
+ // REQ-USERMGMT-06: app-administration widgets (User Management, …) the
474
+ // published app embeds for its own member management — its own palette
475
+ // section, distinct from COMMUNICATION.
476
+ "ADMINISTRATION",
458
477
  "CUSTOM",
459
478
  ];
460
479
  const PLATFORMS = ["web", "native"];
461
480
 
481
+ // REQ-WIDGET-ACTION — server-side actions a widget may declare in its
482
+ // manifest. Each runs in the shared isolated-vm action runner (see backend
483
+ // action-runner.service.js) on a cron schedule or in response to a record
484
+ // CRUD event — NEVER in the rendered app, so they never affect Player ↔
485
+ // export parity. The trigger vocabulary mirrors the backend Action model.
486
+ const ACTION_TRIGGER_TYPES = [
487
+ "schedule",
488
+ "record_created",
489
+ "record_updated",
490
+ "record_deleted",
491
+ ];
492
+ // Globals the action script runs against (the runner's surface) — distinct
493
+ // from the React/SDK widget surface, so the component import/banned-API
494
+ // linter does NOT scan action scripts.
495
+ const ACTION_SCRIPT_GLOBALS = [
496
+ "datastore",
497
+ "fetch",
498
+ "console",
499
+ "record",
500
+ "tenantId",
501
+ "triggerType",
502
+ "triggerTableId",
503
+ ];
504
+ // Mirrors action.service.js SCRIPT_MAX_BYTES.
505
+ const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
506
+
462
507
  // Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
463
508
  // more labels, lowercase alnum + hyphen, label starts with a letter. The
464
509
  // analyzer + the SDK validator both read this from the contract so a
@@ -544,6 +589,24 @@ const MANIFEST_SCHEMA = {
544
589
  description:
545
590
  "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.",
546
591
  },
592
+ actions: {
593
+ type: "object[]",
594
+ required: false,
595
+ description:
596
+ "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 " +
597
+ ACTION_TRIGGER_TYPES.join(", ") +
598
+ "), scheduleCron? (required iff triggerType=='schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
599
+ ACTION_SCRIPT_GLOBALS.join(", ") +
600
+ " — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
601
+ default: [],
602
+ },
603
+ translations: {
604
+ type: "object",
605
+ required: false,
606
+ description:
607
+ "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).",
608
+ default: {},
609
+ },
547
610
  };
548
611
 
549
612
  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,64 +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(). Hits the existing
636
- // /api/v1/tables/:tableId/records/:recordId/permissions REST surface;
637
- // the host normalises the wire shape (userId / groupId / both-null =
638
- // public) into the principalType+principalId pair widgets read.
639
- recordPermissions: {
640
- description:
641
- "Per-record VirtualPermission management. " +
642
- "{ list(tableId, recordId) -> Promise<RecordPermission[]>, " +
643
- "grant(tableId, recordId, body) -> Promise<RecordPermission>, " +
644
- "revoke(tableId, recordId, permissionId) -> Promise<void>, " +
645
- "update(tableId, recordId, permissionId, body) -> Promise<RecordPermission> }. " +
646
- "Backs useRecordPermissions(); requires acl.write:records scope AND " +
647
- "canGrant on the target record.",
648
- required: true,
649
- fields: {
650
- list: "function",
651
- grant: "function",
652
- revoke: "function",
653
- update: "function",
654
- },
655
- },
656
- // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration facade
657
- // backing useGroups(). Reads gated by `groups.read:*`; mutations by
658
- // `groups.write:*`. Same X-Widget-Scopes + SystemAcl gating as users.
659
- groups: {
660
- description:
661
- "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:*.",
662
- required: true,
663
- fields: {
664
- listGroups: "function",
665
- create: "function",
666
- remove: "function",
667
- addMember: "function",
668
- removeMember: "function",
669
- },
670
- },
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.
671
712
  i18n: {
672
713
  description: "{ t(key, fallback?), locale }.",
673
714
  required: true,
@@ -872,20 +913,89 @@ function deepFreeze(value) {
872
913
  return Object.freeze(value);
873
914
  }
874
915
 
916
+ // REQ-L10N-WIDGET — a widget's manifest `translations` are merged into the
917
+ // tenant dictionary under a per-widget namespace so they never collide with
918
+ // app keys or another widget's keys. The prefix is DERIVED from the widget id
919
+ // (it "follows the widget"), so authors call `t("greeting")` and the host
920
+ // resolves `widget.<id>.greeting`. This is the ONE definition of the key
921
+ // format: the SDK `useI18n` hook (lookup) and the backend seeder (write) both
922
+ // call it, so the wire key and the lookup key can never drift.
923
+ function widgetTranslationPrefix(id) {
924
+ return `widget.${id}.`;
925
+ }
926
+ function widgetTranslationKey(id, key) {
927
+ return `widget.${id}.${key}`;
928
+ }
929
+
875
930
  const CONTRACT = deepFreeze({
876
- // 1.8.0: additive — new useRecordPermissions(tableId, recordId) hook +
877
- // the recordPermissions host-context slice it reads + the
878
- // acl.write:records scope it gates on. Hits the existing REQ-ACL-06
879
- // /api/v1/tables/:tableId/records/:recordId/permissions REST surface;
880
- // the host normalises the wire shape into a single principalType+
881
- // principalId pair widgets branch on. Caller still needs canGrant on
882
- // the target record (REQ-ACL-RELINHERIT-05).
883
- version: "1.8.0",
931
+ // 1.7.0: additive — new useDatastoreSchema(tableId) hook + the
932
+ // datastore.schema host-context slice it reads (resolves a table's column
933
+ // structure at runtime via the existing ACL-gated GET /tables/:id).
934
+ //
935
+ // 1.8.0: two additive features.
936
+ // - REQ-WIDGET-ACTION manifests may declare an optional `actions` array:
937
+ // server-side cron/record-triggered scripts the operator enables per
938
+ // tenant, run in the existing isolated-vm action runner (never in the
939
+ // rendered app). New fields actionTriggerTypes / actionScriptGlobals /
940
+ // actionScriptMaxBytes feed the docs + agent prompt. New ADMINISTRATION
941
+ // manifest category (REQ-USERMGMT-06).
942
+ // - REQ-ACL-06 — new useRecordPermissions(tableId, recordId) hook + the
943
+ // recordPermissions host-context slice it reads + the acl.write:records
944
+ // scope it gates on. Hits the existing REQ-ACL-06
945
+ // /api/v1/tables/:tableId/records/:recordId/permissions REST surface;
946
+ // the host normalises the wire shape into a single principalType+
947
+ // principalId pair widgets branch on. Caller still needs canGrant on
948
+ // the target record (REQ-ACL-RELINHERIT-05).
949
+ //
950
+ // 1.9.0: host-contract change (REQ-WSDK-DOMAIN-CLIENTS) — the host now
951
+ // injects domain-client INSTANCES on the WidgetContext rather than
952
+ // bespoke per-hook facades. ctx.datastore is a @colixsystems/datastore-client
953
+ // (records(t).list now returns the { data, meta } envelope verbatim;
954
+ // records(t).permissions(r) replaces the deleted ctx.recordPermissions),
955
+ // ctx.directory is a @colixsystems/directory-client (users/groups
956
+ // namespaces replace the deleted ctx.users / ctx.groups; list returns
957
+ // envelopes), ctx.files is a flattened @colixsystems/files-client,
958
+ // ctx.payments is a @colixsystems/payments-client. All rows/bodies are
959
+ // snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
960
+ // SDK stays duck-typed — it imports none of the four data SDKs. Minor
961
+ // bump on the contract's pre-1.0 versioning (the breaking channel).
962
+ //
963
+ // 1.10.0: additive (REQ-L10N-WIDGET) — manifests may declare an optional
964
+ // `translations` map ({ <key>: { en, <locale>? } }, en required). The
965
+ // host seeds them into the tenant l10n dictionary under a per-widget
966
+ // namespace at install; `useI18n().t(key)` now auto-prefixes the lookup
967
+ // with `widget.<ctx.widget.id>.` (then falls back to the raw key, so
968
+ // existing widgets and shared app keys keep resolving). New exported
969
+ // helpers widgetTranslationPrefix / widgetTranslationKey are the single
970
+ // source of the key format, shared by the hook and the backend seeder.
971
+ //
972
+ // 1.11.0: additive (REQ-LAY-08) — new useFill() hook + the optional `fill`
973
+ // host-context slice it reads. The host sets ctx.fill=true when it has
974
+ // sized a widget to fill its layout slot's available height (a page-grid
975
+ // tile whose author chose "Fill tile height", or a default-fill widget
976
+ // type — containers + media). Widgets that can stretch (Image, Chart,
977
+ // Map, Video) switch to flex:1/height:"100%" when useFill() is true; all
978
+ // others ignore it. The slice is OPTIONAL (defaults false) so existing
979
+ // hosts need no change, and the same value is injected on web and native
980
+ // so fill behaviour cannot diverge between the Player and the export.
981
+ //
982
+ // 1.11.1: fix — the canonical default `themeTokens.colors` now match the
983
+ // product's advertised default brand (primary #3b82f6 blue, secondary
984
+ // #10b981 green) instead of the stale coral/slate that no other surface
985
+ // used. The Theme Settings tab and Player chrome already defaulted to
986
+ // these values, so a tenant that never customised its theme rendered
987
+ // widgets in coral until a first save persisted the blue — a divergence
988
+ // between the unsaved and saved-defaults render. Aligning the canonical
989
+ // tokens removes it (no shape/signature change — default-value fix only).
990
+ version: "1.11.1",
884
991
  hooks: HOOKS,
885
992
  primitives: PRIMITIVES,
886
993
  manifestSchema: MANIFEST_SCHEMA,
887
994
  manifestCategories: CATEGORIES,
888
995
  manifestPlatforms: PLATFORMS,
996
+ actionTriggerTypes: ACTION_TRIGGER_TYPES,
997
+ actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
998
+ actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
889
999
  themeTokens: DEFAULT_THEME_TOKENS,
890
1000
  widgetContextShape: WIDGET_CONTEXT_SHAPE,
891
1001
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,
@@ -909,4 +1019,10 @@ function requiredContextKeys() {
909
1019
  return [...keys];
910
1020
  }
911
1021
 
912
- export { CONTRACT, isHookAllowed, requiredContextKeys };
1022
+ export {
1023
+ CONTRACT,
1024
+ isHookAllowed,
1025
+ requiredContextKeys,
1026
+ widgetTranslationPrefix,
1027
+ widgetTranslationKey,
1028
+ };