@colixsystems/widget-sdk 0.18.0 → 0.19.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/dist/contract.cjs CHANGED
@@ -167,12 +167,12 @@ const HOOKS = [
167
167
  name: "useDirectory",
168
168
  signature: "useDirectory(query?)",
169
169
  returnShape: {
170
- users: "Array<{ id, name, role }>",
170
+ users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
171
171
  loading: "boolean",
172
172
  error: "DatastoreError | null",
173
173
  refetch: "() => Promise<void>",
174
174
  },
175
- requiredContextSlice: ["directory.listUsers"],
175
+ requiredContextSlice: ["directory.users"],
176
176
  scopes: ["directory.read:users"],
177
177
  },
178
178
  {
@@ -206,28 +206,26 @@ const HOOKS = [
206
206
  name: "useUsers",
207
207
  signature: "useUsers(query?)",
208
208
  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).",
209
+ "AppUser administration via the injected directory-client at " +
210
+ "ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
211
+ "{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
212
+ "list returns the { data, meta } envelope verbatim the hook unwraps " +
213
+ "res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
214
+ "scope; mutations need users.write:*. The `invite` call accepts " +
215
+ "{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
216
+ "(the email is sent by the host).",
213
217
  returnShape: {
214
- users: "Array<{ id, name, email?, role, isActive }>",
218
+ users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
215
219
  loading: "boolean",
216
220
  error: "DirectoryError | null",
217
221
  refetch: "() => Promise<void>",
218
222
  invite:
219
- "({ email, name, groupIds? }) => Promise<Invite> // rejects with DirectoryError",
223
+ "({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
220
224
  deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
221
225
  reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
222
226
  remove: "(userId) => Promise<void> // rejects with DirectoryError",
223
227
  },
224
- requiredContextSlice: [
225
- "users.listUsers",
226
- "users.invite",
227
- "users.deactivate",
228
- "users.reactivate",
229
- "users.remove",
230
- ],
228
+ requiredContextSlice: ["directory.users"],
231
229
  scopes: ["users.read:*"],
232
230
  },
233
231
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration. Returns
@@ -239,11 +237,14 @@ const HOOKS = [
239
237
  name: "useGroups",
240
238
  signature: "useGroups(query?)",
241
239
  description:
242
- "AppUserGroup administration. Returns { groups, loading, error, refetch, " +
243
- "create, remove, addMember, removeMember }. Reads need groups.read:*; " +
240
+ "AppUserGroup administration via the injected directory-client at " +
241
+ "ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
242
+ "Returns { groups, loading, error, refetch, create, remove, addMember, " +
243
+ "removeMember }. list returns the { data, meta } envelope verbatim — the " +
244
+ "hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
244
245
  "mutations need groups.write:*.",
245
246
  returnShape: {
246
- groups: "Array<{ id, name, memberCount }>",
247
+ groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
247
248
  loading: "boolean",
248
249
  error: "DirectoryError | null",
249
250
  refetch: "() => Promise<void>",
@@ -255,13 +256,7 @@ const HOOKS = [
255
256
  removeMember:
256
257
  "(groupId, userId) => Promise<void> // rejects with DirectoryError",
257
258
  },
258
- requiredContextSlice: [
259
- "groups.listGroups",
260
- "groups.create",
261
- "groups.remove",
262
- "groups.addMember",
263
- "groups.removeMember",
264
- ],
259
+ requiredContextSlice: ["directory.groups"],
265
260
  scopes: ["groups.read:*"],
266
261
  },
267
262
  // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
@@ -270,33 +265,33 @@ const HOOKS = [
270
265
  name: "useRecordPermissions",
271
266
  signature: "useRecordPermissions(tableId, recordId)",
272
267
  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.",
268
+ "Manage per-record VirtualPermission grants on a single record via the " +
269
+ "injected datastore-client at " +
270
+ "ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}. " +
271
+ "Returns { permissions, loading, error, grant, revoke, update, refetch } " +
272
+ "where permissions is Array<{ id, user_id, group_id, can_read, can_write, " +
273
+ "can_delete, can_grant }> (snake_case rows verbatim; list() returns the " +
274
+ "{ data, meta } envelope and the hook unwraps res.data). grant/update " +
275
+ "bodies are snake_case verbatim ({ user_id | group_id, can_read, " +
276
+ "can_write, can_delete, can_grant }). Mutating requires acl.write:records " +
277
+ "scope AND can_grant on the target record (REQ-ACL-RELINHERIT-05: " +
278
+ "APP_USER actors with can_grant are accepted, not only Studio owners). " +
279
+ "When tableId or recordId is null/empty the hook collapses to an empty " +
280
+ "no-op result without a network round-trip.",
281
281
  returnShape: {
282
282
  permissions:
283
- "Array<{ id, principalType: 'USER' | 'GROUP' | 'PUBLIC', principalId, canRead, canWrite, canDelete, canGrant }>",
283
+ "Array<{ id, user_id, group_id, can_read, can_write, can_delete, can_grant }> // snake_case rows; unwrapped from { data, meta }",
284
284
  loading: "boolean",
285
285
  error: "PermissionError | null",
286
286
  grant:
287
- "({ principalType, principalId?, canRead?, canWrite?, canDelete?, canGrant? }) => Promise<RecordPermission> // rejects with PermissionError",
287
+ "({ user_id?, group_id?, can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
288
288
  revoke:
289
289
  "(permissionId) => Promise<void> // rejects with PermissionError",
290
290
  update:
291
- "(permissionId, { canRead?, canWrite?, canDelete?, canGrant? }) => Promise<RecordPermission> // rejects with PermissionError",
291
+ "(permissionId, { can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
292
292
  refetch: "() => Promise<void>",
293
293
  },
294
- requiredContextSlice: [
295
- "recordPermissions.list",
296
- "recordPermissions.grant",
297
- "recordPermissions.revoke",
298
- "recordPermissions.update",
299
- ],
294
+ requiredContextSlice: ["datastore.records"],
300
295
  scopes: ["acl.write:records"],
301
296
  },
302
297
  // REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
@@ -454,10 +449,40 @@ const CATEGORIES = [
454
449
  "DATA",
455
450
  "MEDIA",
456
451
  "COMMUNICATION",
452
+ // REQ-USERMGMT-06: app-administration widgets (User Management, …) the
453
+ // published app embeds for its own member management — its own palette
454
+ // section, distinct from COMMUNICATION.
455
+ "ADMINISTRATION",
457
456
  "CUSTOM",
458
457
  ];
459
458
  const PLATFORMS = ["web", "native"];
460
459
 
460
+ // REQ-WIDGET-ACTION — server-side actions a widget may declare in its
461
+ // manifest. Each runs in the shared isolated-vm action runner (see backend
462
+ // action-runner.service.js) on a cron schedule or in response to a record
463
+ // CRUD event — NEVER in the rendered app, so they never affect Player ↔
464
+ // export parity. The trigger vocabulary mirrors the backend Action model.
465
+ const ACTION_TRIGGER_TYPES = [
466
+ "schedule",
467
+ "record_created",
468
+ "record_updated",
469
+ "record_deleted",
470
+ ];
471
+ // Globals the action script runs against (the runner's surface) — distinct
472
+ // from the React/SDK widget surface, so the component import/banned-API
473
+ // linter does NOT scan action scripts.
474
+ const ACTION_SCRIPT_GLOBALS = [
475
+ "datastore",
476
+ "fetch",
477
+ "console",
478
+ "record",
479
+ "tenantId",
480
+ "triggerType",
481
+ "triggerTableId",
482
+ ];
483
+ // Mirrors action.service.js SCRIPT_MAX_BYTES.
484
+ const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
485
+
461
486
  // Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
462
487
  // more labels, lowercase alnum + hyphen, label starts with a letter. The
463
488
  // analyzer + the SDK validator both read this from the contract so a
@@ -543,6 +568,17 @@ const MANIFEST_SCHEMA = {
543
568
  description:
544
569
  "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
570
  },
571
+ actions: {
572
+ type: "object[]",
573
+ required: false,
574
+ description:
575
+ "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 " +
576
+ ACTION_TRIGGER_TYPES.join(", ") +
577
+ "), scheduleCron? (required iff triggerType=='schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
578
+ ACTION_SCRIPT_GLOBALS.join(", ") +
579
+ " — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
580
+ default: [],
581
+ },
546
582
  };
547
583
 
548
584
  const WIDGET_CONTEXT_SHAPE = {
@@ -559,9 +595,10 @@ const WIDGET_CONTEXT_SHAPE = {
559
595
  fields: { id: "manifest.id", version: "manifest.version" },
560
596
  },
561
597
  user: {
562
- description: "Signed-in user ({ id, email, displayName }).",
598
+ description:
599
+ "Signed-in user, host-provided VERBATIM (snake_case: { id, email, display_name, roles, group_ids }). Not a data-client.",
563
600
  required: true,
564
- fields: { id: "string", email: "string", displayName: "string" },
601
+ fields: { id: "string", email: "string", display_name: "string" },
565
602
  },
566
603
  workspace: {
567
604
  description:
@@ -581,19 +618,30 @@ const WIDGET_CONTEXT_SHAPE = {
581
618
  },
582
619
  datastore: {
583
620
  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).",
621
+ "Injected @colixsystems/datastore-client instance. " +
622
+ "{ tables: { list(), get(idOrName) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
623
+ "records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
624
+ "permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
625
+ "`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(). " +
626
+ "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
627
  required: true,
586
- fields: { records: "function", schema: "function" },
628
+ fields: { records: "function", schema: "function", tables: "object" },
587
629
  },
588
630
  directory: {
589
631
  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.",
632
+ "Injected @colixsystems/directory-client instance. " +
633
+ "{ me(), users: { list(query?) -> Promise<{ data, meta }>, get(id), invite(body), deactivate(id), reactivate(id) }, " +
634
+ "groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
635
+ "invites: { list(), revoke(id), resend(id) } }. " +
636
+ "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
637
  required: true,
592
- fields: { listUsers: "function" },
638
+ fields: { users: "object", groups: "object" },
593
639
  },
594
640
  files: {
595
641
  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.",
642
+ "Injected @colixsystems/files-client instance, FLATTENED so file ops are top-level. " +
643
+ "{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData), folders: { list, create }, shares: { list, create, remove } }. " +
644
+ "Backs useFile(); the returned file already carries an absolute url the widget can drop into an <Image source>.",
597
645
  required: true,
598
646
  fields: { get: "function" },
599
647
  },
@@ -610,62 +658,17 @@ const WIDGET_CONTEXT_SHAPE = {
610
658
  },
611
659
  payments: {
612
660
  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.",
661
+ "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
662
  required: true,
615
663
  fields: { requestPayment: "function", getPayment: "function" },
616
664
  },
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
- },
665
+ // REQ-WSDK-DOMAIN-CLIENTSthe AppUser administration, AppUserGroup
666
+ // administration, and per-record VirtualPermission facades that used to
667
+ // live here (`users`, `groups`, `recordPermissions`) were folded into the
668
+ // injected domain-client instances: useUsers()/useGroups() read
669
+ // ctx.directory.{users,groups}; useRecordPermissions() reads
670
+ // ctx.datastore.records(table).permissions(record). See the `directory`
671
+ // and `datastore` slices above.
669
672
  i18n: {
670
673
  description: "{ t(key, fallback?), locale }.",
671
674
  required: true,
@@ -921,19 +924,43 @@ const CONTRACT = deepFreeze({
921
924
  // runtime (Form Builder renders inputs by column type). Reads the existing
922
925
  // ACL-gated `GET /tables/:id` — structure only, no row data.
923
926
  //
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",
927
+ // 1.8.0: two additive features.
928
+ // - REQ-WIDGET-ACTION manifests may declare an optional `actions` array.
929
+ // Each entry is a server-side action (cron- or record-triggered JS) the
930
+ // operator enables per tenant; it runs in the existing isolated-vm action
931
+ // runner, never in the rendered app. New contract fields
932
+ // `actionTriggerTypes`, `actionScriptGlobals`, and `actionScriptMaxBytes`
933
+ // let the docs + agent prompt + validator derive the grammar from one
934
+ // source. New `ADMINISTRATION` manifest category (REQ-USERMGMT-06).
935
+ // - REQ-ACL-06 — new `useRecordPermissions(tableId, recordId)` hook + the
936
+ // `recordPermissions` host-context slice it reads + the `acl.write:records`
937
+ // scope it gates on. Hits the existing REQ-ACL-06
938
+ // /api/v1/tables/:tableId/records/:recordId/permissions REST surface; the
939
+ // host normalises the wire shape into a single principalType+principalId
940
+ // pair widgets branch on. Caller still needs canGrant on the target
941
+ // record (REQ-ACL-RELINHERIT-05).
942
+ //
943
+ // 1.9.0: host-contract change (REQ-WSDK-DOMAIN-CLIENTS) — the host now
944
+ // injects domain-client INSTANCES on the WidgetContext rather than
945
+ // bespoke per-hook facades. ctx.datastore is a @colixsystems/datastore-client
946
+ // (records(t).list now returns the { data, meta } envelope verbatim;
947
+ // records(t).permissions(r) replaces the deleted ctx.recordPermissions),
948
+ // ctx.directory is a @colixsystems/directory-client (users/groups
949
+ // namespaces replace the deleted ctx.users / ctx.groups; list returns
950
+ // envelopes), ctx.files is a flattened @colixsystems/files-client,
951
+ // ctx.payments is a @colixsystems/payments-client. All rows/bodies are
952
+ // snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
953
+ // SDK stays duck-typed — it imports none of the four data SDKs. Minor
954
+ // bump on the contract's pre-1.0 versioning (the breaking channel).
955
+ version: "1.9.0",
932
956
  hooks: HOOKS,
933
957
  primitives: PRIMITIVES,
934
958
  manifestSchema: MANIFEST_SCHEMA,
935
959
  manifestCategories: CATEGORIES,
936
960
  manifestPlatforms: PLATFORMS,
961
+ actionTriggerTypes: ACTION_TRIGGER_TYPES,
962
+ actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
963
+ actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
937
964
  themeTokens: DEFAULT_THEME_TOKENS,
938
965
  widgetContextShape: WIDGET_CONTEXT_SHAPE,
939
966
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,