@colixsystems/widget-sdk 0.17.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.js CHANGED
@@ -168,12 +168,12 @@ const HOOKS = [
168
168
  name: "useDirectory",
169
169
  signature: "useDirectory(query?)",
170
170
  returnShape: {
171
- users: "Array<{ id, name, role }>",
171
+ users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
172
172
  loading: "boolean",
173
173
  error: "DatastoreError | null",
174
174
  refetch: "() => Promise<void>",
175
175
  },
176
- requiredContextSlice: ["directory.listUsers"],
176
+ requiredContextSlice: ["directory.users"],
177
177
  scopes: ["directory.read:users"],
178
178
  },
179
179
  {
@@ -207,28 +207,26 @@ const HOOKS = [
207
207
  name: "useUsers",
208
208
  signature: "useUsers(query?)",
209
209
  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).",
210
+ "AppUser administration via the injected directory-client at " +
211
+ "ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
212
+ "{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
213
+ "list returns the { data, meta } envelope verbatim the hook unwraps " +
214
+ "res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
215
+ "scope; mutations need users.write:*. The `invite` call accepts " +
216
+ "{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
217
+ "(the email is sent by the host).",
214
218
  returnShape: {
215
- users: "Array<{ id, name, email?, role, isActive }>",
219
+ users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
216
220
  loading: "boolean",
217
221
  error: "DirectoryError | null",
218
222
  refetch: "() => Promise<void>",
219
223
  invite:
220
- "({ email, name, groupIds? }) => Promise<Invite> // rejects with DirectoryError",
224
+ "({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
221
225
  deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
222
226
  reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
223
227
  remove: "(userId) => Promise<void> // rejects with DirectoryError",
224
228
  },
225
- requiredContextSlice: [
226
- "users.listUsers",
227
- "users.invite",
228
- "users.deactivate",
229
- "users.reactivate",
230
- "users.remove",
231
- ],
229
+ requiredContextSlice: ["directory.users"],
232
230
  scopes: ["users.read:*"],
233
231
  },
234
232
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration. Returns
@@ -240,11 +238,14 @@ const HOOKS = [
240
238
  name: "useGroups",
241
239
  signature: "useGroups(query?)",
242
240
  description:
243
- "AppUserGroup administration. Returns { groups, loading, error, refetch, " +
244
- "create, remove, addMember, removeMember }. Reads need groups.read:*; " +
241
+ "AppUserGroup administration via the injected directory-client at " +
242
+ "ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
243
+ "Returns { groups, loading, error, refetch, create, remove, addMember, " +
244
+ "removeMember }. list returns the { data, meta } envelope verbatim — the " +
245
+ "hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
245
246
  "mutations need groups.write:*.",
246
247
  returnShape: {
247
- groups: "Array<{ id, name, memberCount }>",
248
+ groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
248
249
  loading: "boolean",
249
250
  error: "DirectoryError | null",
250
251
  refetch: "() => Promise<void>",
@@ -256,15 +257,50 @@ const HOOKS = [
256
257
  removeMember:
257
258
  "(groupId, userId) => Promise<void> // rejects with DirectoryError",
258
259
  },
259
- requiredContextSlice: [
260
- "groups.listGroups",
261
- "groups.create",
262
- "groups.remove",
263
- "groups.addMember",
264
- "groups.removeMember",
265
- ],
260
+ requiredContextSlice: ["directory.groups"],
266
261
  scopes: ["groups.read:*"],
267
262
  },
263
+ // REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
264
+ // management for a single record. Reads the injected datastore-client at
265
+ // ctx.datastore.records(tableId).permissions(recordId) (the recordPermissions
266
+ // facade was folded into the datastore-client). The backend gates the call
267
+ // on `can_grant` for the target record (Studio owners short-circuit;
268
+ // APP_USER actors must hold `can_grant` via REQ-ACL-05 / REQ-ACL-06). A
269
+ // widget that declares the scope but whose caller lacks the grant receives
270
+ // `PermissionError { code: 'FORBIDDEN' }`.
271
+ {
272
+ name: "useRecordPermissions",
273
+ signature: "useRecordPermissions(tableId, recordId)",
274
+ description:
275
+ "Manage per-record VirtualPermission grants on a single record via the " +
276
+ "injected datastore-client at " +
277
+ "ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}. " +
278
+ "Returns { permissions, loading, error, grant, revoke, update, refetch } " +
279
+ "where permissions is Array<{ id, user_id, group_id, can_read, can_write, " +
280
+ "can_delete, can_grant }> (snake_case rows verbatim; list() returns the " +
281
+ "{ data, meta } envelope and the hook unwraps res.data). grant/update " +
282
+ "bodies are snake_case verbatim ({ user_id | group_id, can_read, " +
283
+ "can_write, can_delete, can_grant }). Mutating requires acl.write:records " +
284
+ "scope AND can_grant on the target record (REQ-ACL-RELINHERIT-05: " +
285
+ "APP_USER actors with can_grant are accepted, not only Studio owners). " +
286
+ "When tableId or recordId is null/empty the hook collapses to an empty " +
287
+ "no-op result without a network round-trip.",
288
+ returnShape: {
289
+ permissions:
290
+ "Array<{ id, user_id, group_id, can_read, can_write, can_delete, can_grant }> // snake_case rows; unwrapped from { data, meta }",
291
+ loading: "boolean",
292
+ error: "PermissionError | null",
293
+ grant:
294
+ "({ user_id?, group_id?, can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
295
+ revoke:
296
+ "(permissionId) => Promise<void> // rejects with PermissionError",
297
+ update:
298
+ "(permissionId, { can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
299
+ refetch: "() => Promise<void>",
300
+ },
301
+ requiredContextSlice: ["datastore.records"],
302
+ scopes: ["acl.write:records"],
303
+ },
268
304
  // REQ-WSDK-PLATFORM §6 — Tier A SDK hooks.
269
305
  {
270
306
  name: "useClipboard",
@@ -414,10 +450,40 @@ const CATEGORIES = [
414
450
  "DATA",
415
451
  "MEDIA",
416
452
  "COMMUNICATION",
453
+ // REQ-USERMGMT-06: app-administration widgets (User Management, …) the
454
+ // published app embeds for its own member management — its own palette
455
+ // section, distinct from COMMUNICATION.
456
+ "ADMINISTRATION",
417
457
  "CUSTOM",
418
458
  ];
419
459
  const PLATFORMS = ["web", "native"];
420
460
 
461
+ // REQ-WIDGET-ACTION — server-side actions a widget may declare in its
462
+ // manifest. Each runs in the shared isolated-vm action runner (see backend
463
+ // action-runner.service.js) on a cron schedule or in response to a record
464
+ // CRUD event — NEVER in the rendered app, so they never affect Player ↔
465
+ // export parity. The trigger vocabulary mirrors the backend Action model.
466
+ const ACTION_TRIGGER_TYPES = [
467
+ "schedule",
468
+ "record_created",
469
+ "record_updated",
470
+ "record_deleted",
471
+ ];
472
+ // Globals the action script runs against (the runner's surface) — distinct
473
+ // from the React/SDK widget surface, so the component import/banned-API
474
+ // linter does NOT scan action scripts.
475
+ const ACTION_SCRIPT_GLOBALS = [
476
+ "datastore",
477
+ "fetch",
478
+ "console",
479
+ "record",
480
+ "tenantId",
481
+ "triggerType",
482
+ "triggerTableId",
483
+ ];
484
+ // Mirrors action.service.js SCRIPT_MAX_BYTES.
485
+ const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
486
+
421
487
  // Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
422
488
  // more labels, lowercase alnum + hyphen, label starts with a letter. The
423
489
  // analyzer + the SDK validator both read this from the contract so a
@@ -503,6 +569,17 @@ const MANIFEST_SCHEMA = {
503
569
  description:
504
570
  "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.",
505
571
  },
572
+ actions: {
573
+ type: "object[]",
574
+ required: false,
575
+ description:
576
+ "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 " +
577
+ ACTION_TRIGGER_TYPES.join(", ") +
578
+ "), scheduleCron? (required iff triggerType=='schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
579
+ ACTION_SCRIPT_GLOBALS.join(", ") +
580
+ " — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
581
+ default: [],
582
+ },
506
583
  };
507
584
 
508
585
  const WIDGET_CONTEXT_SHAPE = {
@@ -518,9 +595,10 @@ const WIDGET_CONTEXT_SHAPE = {
518
595
  fields: { id: "manifest.id", version: "manifest.version" },
519
596
  },
520
597
  user: {
521
- 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.",
522
600
  required: true,
523
- fields: { id: "string", email: "string", displayName: "string" },
601
+ fields: { id: "string", email: "string", display_name: "string" },
524
602
  },
525
603
  workspace: {
526
604
  description:
@@ -540,19 +618,30 @@ const WIDGET_CONTEXT_SHAPE = {
540
618
  },
541
619
  datastore: {
542
620
  description:
543
- "{ 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).",
544
627
  required: true,
545
- fields: { records: "function", schema: "function" },
628
+ fields: { records: "function", schema: "function", tables: "object" },
546
629
  },
547
630
  directory: {
548
631
  description:
549
- "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:*.",
550
637
  required: true,
551
- fields: { listUsers: "function" },
638
+ fields: { users: "object", groups: "object" },
552
639
  },
553
640
  files: {
554
641
  description:
555
- "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>.",
556
645
  required: true,
557
646
  fields: { get: "function" },
558
647
  },
@@ -569,42 +658,17 @@ const WIDGET_CONTEXT_SHAPE = {
569
658
  },
570
659
  payments: {
571
660
  description:
572
- "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.",
573
662
  required: true,
574
663
  fields: { requestPayment: "function", getPayment: "function" },
575
664
  },
576
- // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration facade backing
577
- // useUsers(). Reads gated by `users.read:*`; mutations by `users.write:*`.
578
- // The host signs an `X-Widget-Scopes` header against JWT_SECRET so an
579
- // APP_USER cannot forge scope claims, and the request is additionally
580
- // gated by a SystemAcl `users.read` / `users.write` capability grant.
581
- users: {
582
- description:
583
- "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:*.",
584
- required: true,
585
- fields: {
586
- listUsers: "function",
587
- invite: "function",
588
- deactivate: "function",
589
- reactivate: "function",
590
- remove: "function",
591
- },
592
- },
593
- // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration facade
594
- // backing useGroups(). Reads gated by `groups.read:*`; mutations by
595
- // `groups.write:*`. Same X-Widget-Scopes + SystemAcl gating as users.
596
- groups: {
597
- description:
598
- "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:*.",
599
- required: true,
600
- fields: {
601
- listGroups: "function",
602
- create: "function",
603
- remove: "function",
604
- addMember: "function",
605
- removeMember: "function",
606
- },
607
- },
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.
608
672
  i18n: {
609
673
  description: "{ t(key, fallback?), locale }.",
610
674
  required: true,
@@ -813,12 +877,43 @@ const CONTRACT = deepFreeze({
813
877
  // 1.7.0: additive — new useDatastoreSchema(tableId) hook + the
814
878
  // datastore.schema host-context slice it reads (resolves a table's column
815
879
  // structure at runtime via the existing ACL-gated GET /tables/:id).
816
- version: "1.7.0",
880
+ //
881
+ // 1.8.0: two additive features.
882
+ // - REQ-WIDGET-ACTION — manifests may declare an optional `actions` array:
883
+ // server-side cron/record-triggered scripts the operator enables per
884
+ // tenant, run in the existing isolated-vm action runner (never in the
885
+ // rendered app). New fields actionTriggerTypes / actionScriptGlobals /
886
+ // actionScriptMaxBytes feed the docs + agent prompt. New ADMINISTRATION
887
+ // manifest category (REQ-USERMGMT-06).
888
+ // - REQ-ACL-06 — new useRecordPermissions(tableId, recordId) hook + the
889
+ // recordPermissions host-context slice it reads + the acl.write:records
890
+ // scope it gates on. Hits the existing REQ-ACL-06
891
+ // /api/v1/tables/:tableId/records/:recordId/permissions REST surface;
892
+ // the host normalises the wire shape into a single principalType+
893
+ // principalId pair widgets branch on. Caller still needs canGrant on
894
+ // the target record (REQ-ACL-RELINHERIT-05).
895
+ //
896
+ // 1.9.0: host-contract change (REQ-WSDK-DOMAIN-CLIENTS) — the host now
897
+ // injects domain-client INSTANCES on the WidgetContext rather than
898
+ // bespoke per-hook facades. ctx.datastore is a @colixsystems/datastore-client
899
+ // (records(t).list now returns the { data, meta } envelope verbatim;
900
+ // records(t).permissions(r) replaces the deleted ctx.recordPermissions),
901
+ // ctx.directory is a @colixsystems/directory-client (users/groups
902
+ // namespaces replace the deleted ctx.users / ctx.groups; list returns
903
+ // envelopes), ctx.files is a flattened @colixsystems/files-client,
904
+ // ctx.payments is a @colixsystems/payments-client. All rows/bodies are
905
+ // snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
906
+ // SDK stays duck-typed — it imports none of the four data SDKs. Minor
907
+ // bump on the contract's pre-1.0 versioning (the breaking channel).
908
+ version: "1.9.0",
817
909
  hooks: HOOKS,
818
910
  primitives: PRIMITIVES,
819
911
  manifestSchema: MANIFEST_SCHEMA,
820
912
  manifestCategories: CATEGORIES,
821
913
  manifestPlatforms: PLATFORMS,
914
+ actionTriggerTypes: ACTION_TRIGGER_TYPES,
915
+ actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
916
+ actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
822
917
  themeTokens: DEFAULT_THEME_TOKENS,
823
918
  widgetContextShape: WIDGET_CONTEXT_SHAPE,
824
919
  bundleExportContract: BUNDLE_EXPORT_CONTRACT,