@colixsystems/widget-sdk 0.94.0 → 0.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
47
47
  | **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
48
48
  | **FILES** | `useAssetsByTag(tag, { type? })` | `{ assets, loading, error, refetch }` | `ctx.assets.list` (unwraps `{ data, meta }` to `assets`) — no scope. `type` defaults to `"image"`; pass `"all"` / `"audio"` / `"video"` / `"document"` to widen. Falsy `tag` collapses to `assets: []` without a round-trip. |
49
49
  | **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
50
- | **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (edits also `users.write:*`; `remove()` also `users.delete:*`) |
50
+ | **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove, sendPasswordReset }` | `directory.users.*` — `users.read:*` (edits, incl. `sendPasswordReset()`, also `users.write:*`; `remove()` also `users.delete:*`) |
51
51
  | **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
52
52
  | **DIRECTORY** | `useInvites(query?)` | `{ invites, loading, error, refetch, resend, revoke }` | `directory.invites.*` — `users.write:*` + the SystemAcl `users.write` capability (the whole invite surface, list included). `query` is `{ status?, limit?, offset? }` with `status` ∈ `pending \| accepted \| revoked \| expired \| all` (endpoint default `all`). |
53
53
  | **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
@@ -66,6 +66,36 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
66
66
 
67
67
  `v0.91.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
68
68
 
69
+ ### What's new in 0.96.0 (contract 1.68.0)
70
+
71
+ **A manifest action can declare `manual`, and every action script gains a `request` global (sc-5366).** The backend Action model grew three ways of *reaching* a script to sit beside the ones that *fire* it: `manual` (nothing starts it — the workspace runs it on demand), `app` (a published app's button `onPress`) and `http_post` (an inbound webhook at `POST /api/v1/action-hooks/:actionId`, authenticated with one of the workspace's integration API keys). The same change retired the separate `appInvokable` boolean, so one column now answers "what starts this action?".
72
+
73
+ Only `manual` joins `CONTRACT.actionTriggerTypes`, and that is deliberate. `app` and `http_post` expose a script to a caller **outside** the Studio, which is the installing workspace's decision about running someone else's code — not the author's. A manifest that declares either is rejected by `validateManifest`, the CLI linter and the backend alike; the operator grants them in the Actions admin page after install, on top of whatever triggers your manifest declared. Nothing about an already-published manifest changes.
74
+
75
+ `CONTRACT.actionScriptGlobals` gains **`request`**: `{ body }` — the JSON an inbound webhook caller sent — on an `http_post` run, and `null` on every other trigger. Request *headers* are never passed through, because they carry the caller's API key. `triggerType` now also reports `"http_post"` alongside `"manual"` and `"app"`, so one script can tell a webhook apart from its nightly schedule:
76
+
77
+ ```js
78
+ if (triggerType === "http_post") {
79
+ const order = request?.body;
80
+ if (!order?.id) return; // never trust the caller's shape
81
+ await datastore.records("Orders").create({ externalId: order.id });
82
+ }
83
+ ```
84
+
85
+ `CONTRACT.version` → `1.68.0`. Additive for every existing manifest.
86
+
87
+ ### What's new in 0.95.0 (contract 1.67.0)
88
+
89
+ **An admin can mail a locked-out member a password-reset link — `useUsers().sendPasswordReset(userId)` (sc-5335).** An app user who forgot their password could only recover it themselves, from the app's own login screen. The admin they actually ask — the one already able to invite, deactivate and remove them — had no way to help, and the workaround in the field was to remove and re-invite the account, which discards its group memberships and history.
90
+
91
+ `sendPasswordReset(userId)` mails the **same** self-serve link `POST /auth/app/forgot-password` sends, to that user's **own registered address**, and resolves `{ sent, email_masked }`. It deliberately returns neither the token nor the link, so it is not an account-takeover primitive: an admin can start the recovery, only the user can finish it. `email_masked` (`ad**********@example.com`) is there because a widget caller reads the roster through the privacy-reduced directory projection, which omits email — the confirmation says where the mail went without becoming a new way to read addresses.
92
+
93
+ - **Scope**: `users.write:*`, alongside `invite` / `deactivate` / `reactivate` — the same grant, since mailing someone a link they must act on is strictly less powerful than deactivating them. The `scope-required-for-user-mutation` linter rule covers the new method, so calling it without the scope fails the lint.
94
+ - **Refusals are typed, not silent.** Rejects with a `DirectoryError` coded `USER_INACTIVE` (deactivated — reactivate first) or `NO_PASSWORD_CREDENTIAL` (an INTEGRATION service account authenticates by API key and holds no password). Branch on `code` and render `err.message`; don't offer the action on those rows at all.
95
+ - Issuing a link supersedes any outstanding one for that user, and the send is rate-limited per acting admin.
96
+ - The built-in **User Management** widget gains the row action and a new `onPasswordResetSent` event.
97
+ - **`CONTRACT.version` → `1.67.0`** (additive: one hook method). No existing signature changed, and both hosts get it from the same injected `@colixsystems/directory-client`.
98
+
69
99
  ### What's new in 0.93.0 (contract 1.66.0)
70
100
 
71
101
  **BREAKING: a widget no longer declares server-side actions.** `manifest.actions` is removed from the contract and **refused** by `validateManifest` — an author who declares it now fails the publish with a message naming the replacement, rather than shipping a widget that quietly carries no automation.
package/dist/contract.cjs CHANGED
@@ -957,7 +957,8 @@ const HOOKS = [
957
957
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration. Returns
958
958
  // `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }`.
959
959
  // Reads need `users.read:*` scope; edit-style mutations (invite /
960
- // deactivate / reactivate) additionally need `users.write:*`, and the
960
+ // deactivate / reactivate / sendPasswordReset) additionally need
961
+ // `users.write:*`, and the
961
962
  // destructive `remove` needs `users.delete:*` (SC-902). The `invite`
962
963
  // call accepts `{ email, name, groupIds? }` and returns the resulting
963
964
  // AppUserInvite row (the email is sent by the host). Mutating users from
@@ -969,15 +970,22 @@ const HOOKS = [
969
970
  signature: "useUsers(query?)",
970
971
  description:
971
972
  "AppUser administration via the injected directory-client at " +
972
- "ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
973
- "{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
973
+ "ctx.directory.users.{list,get,invite,deactivate,reactivate,sendPasswordReset}. " +
974
+ "Returns { users, loading, error, refetch, invite, deactivate, " +
975
+ "reactivate, remove, sendPasswordReset }. " +
974
976
  "list returns the { data, meta } envelope verbatim — the hook unwraps " +
975
977
  "res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
976
- "scope; edit-style mutations (invite/deactivate/reactivate) need " +
977
- "users.write:*, and the destructive remove() additionally needs " +
978
- "users.delete:* (SC-902). The `invite` call accepts " +
978
+ "scope; edit-style mutations (invite/deactivate/reactivate/" +
979
+ "sendPasswordReset) need users.write:*, and the destructive remove() " +
980
+ "additionally needs users.delete:* (SC-902). The `invite` call accepts " +
979
981
  "{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
980
- "(the email is sent by the host).",
982
+ "(the email is sent by the host). sendPasswordReset(userId) mails the " +
983
+ "standard self-serve reset link to that user's own registered address " +
984
+ "and resolves { sent, email_masked } — never the token or the link, so " +
985
+ "it cannot be used to sign in as them. It rejects with a DirectoryError " +
986
+ "coded USER_INACTIVE (deactivated) or NO_PASSWORD_CREDENTIAL (an " +
987
+ "INTEGRATION service account); surface those as an explanation rather " +
988
+ "than a generic failure.",
981
989
  returnShape: {
982
990
  users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
983
991
  loading: "boolean",
@@ -988,6 +996,8 @@ const HOOKS = [
988
996
  deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
989
997
  reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
990
998
  remove: "(userId) => Promise<void> // rejects with DirectoryError",
999
+ sendPasswordReset:
1000
+ "(userId) => Promise<{ sent, email_masked }> // rejects with DirectoryError",
991
1001
  },
992
1002
  requiredContextSlice: ["directory.users"],
993
1003
  scopes: ["users.read:*"],
@@ -1449,14 +1459,23 @@ const PLATFORMS = ["web", "native"];
1449
1459
 
1450
1460
  // REQ-WIDGET-ACTION — server-side actions a widget may declare in its
1451
1461
  // manifest. Each runs in the shared isolated-vm action runner (see backend
1452
- // action-runner.service.js) on a cron schedule or in response to a record
1453
- // CRUD event — NEVER in the rendered app, so they never affect Player ↔
1454
- // export parity. The trigger vocabulary mirrors the backend Action model.
1462
+ // action-runner.service.js) on a cron schedule, in response to a record CRUD
1463
+ // event, or on demand — NEVER in the rendered app, so they never affect
1464
+ // Player ↔ export parity.
1465
+ //
1466
+ // This is what a MANIFEST may declare. The backend Action model additionally
1467
+ // accepts 'app' and 'http_post' (sc-5366), which expose a script to a
1468
+ // published app's buttons or to a public webhook — those are the installing
1469
+ // workspace's decision about running someone else's code, so an operator
1470
+ // grants them and a manifest may not ask for them. `validateManifest` and the
1471
+ // backend's `_validateWidgetDeclared` both reject them, so the linter and the
1472
+ // server agree about what a manifest declared.
1455
1473
  const ACTION_TRIGGER_TYPES = [
1456
1474
  "schedule",
1457
1475
  "record_created",
1458
1476
  "record_updated",
1459
1477
  "record_deleted",
1478
+ "manual",
1460
1479
  ];
1461
1480
  // Globals the action script runs against (the runner's surface) — distinct
1462
1481
  // from the React/SDK widget surface, so the component import/banned-API
@@ -1469,6 +1488,9 @@ const ACTION_SCRIPT_GLOBALS = [
1469
1488
  "notifications",
1470
1489
  "console",
1471
1490
  "record",
1491
+ // sc-5366 — `{ body }` on a run fired by the inbound `http_post` trigger,
1492
+ // null on every other trigger.
1493
+ "request",
1472
1494
  "tenantId",
1473
1495
  "triggerType",
1474
1496
  "triggerTableId",
@@ -2922,7 +2944,19 @@ const CONTRACT = deepFreeze({
2922
2944
  // REFUSED by the validator (sc-4879). Server-side automation ships as its own
2923
2945
  // marketplace deliverable (`@colixsystems/action-sdk`), which a workspace
2924
2946
  // installs and configures separately. A widget renders; it does not automate.
2925
- version: "1.66.0",
2947
+ // 1.67.0: additive (sc-5335) — `useUsers()` gains
2948
+ // `sendPasswordReset(userId)`, which mails the standard self-serve
2949
+ // password-reset link to that user's own registered address so an
2950
+ // app-admin can unblock a locked-out member from inside the app. It
2951
+ // resolves `{ sent, email_masked }` and never returns the token or the
2952
+ // link, so it is not an account-takeover primitive; it gates on the same
2953
+ // `users.write:*` scope as invite / deactivate.
2954
+ // 1.68.0: additive (sc-5366) — a manifest action may declare the `manual`
2955
+ // trigger (nothing fires it; the workspace runs it on demand), and every
2956
+ // action script now sees a `request` global — `{ body }` on a run fired
2957
+ // by the workspace-granted `http_post` webhook trigger, null otherwise.
2958
+ // `app` and `http_post` stay operator-granted and are NOT declarable.
2959
+ version: "1.68.0",
2926
2960
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
2927
2961
  hooks: HOOKS,
2928
2962
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -957,7 +957,8 @@ const HOOKS = [
957
957
  // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration. Returns
958
958
  // `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }`.
959
959
  // Reads need `users.read:*` scope; edit-style mutations (invite /
960
- // deactivate / reactivate) additionally need `users.write:*`, and the
960
+ // deactivate / reactivate / sendPasswordReset) additionally need
961
+ // `users.write:*`, and the
961
962
  // destructive `remove` needs `users.delete:*` (SC-902). The `invite`
962
963
  // call accepts `{ email, name, groupIds? }` and returns the resulting
963
964
  // AppUserInvite row (the email is sent by the host). Mutating users from
@@ -969,15 +970,22 @@ const HOOKS = [
969
970
  signature: "useUsers(query?)",
970
971
  description:
971
972
  "AppUser administration via the injected directory-client at " +
972
- "ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
973
- "{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
973
+ "ctx.directory.users.{list,get,invite,deactivate,reactivate,sendPasswordReset}. " +
974
+ "Returns { users, loading, error, refetch, invite, deactivate, " +
975
+ "reactivate, remove, sendPasswordReset }. " +
974
976
  "list returns the { data, meta } envelope verbatim — the hook unwraps " +
975
977
  "res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
976
- "scope; edit-style mutations (invite/deactivate/reactivate) need " +
977
- "users.write:*, and the destructive remove() additionally needs " +
978
- "users.delete:* (SC-902). The `invite` call accepts " +
978
+ "scope; edit-style mutations (invite/deactivate/reactivate/" +
979
+ "sendPasswordReset) need users.write:*, and the destructive remove() " +
980
+ "additionally needs users.delete:* (SC-902). The `invite` call accepts " +
979
981
  "{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
980
- "(the email is sent by the host).",
982
+ "(the email is sent by the host). sendPasswordReset(userId) mails the " +
983
+ "standard self-serve reset link to that user's own registered address " +
984
+ "and resolves { sent, email_masked } — never the token or the link, so " +
985
+ "it cannot be used to sign in as them. It rejects with a DirectoryError " +
986
+ "coded USER_INACTIVE (deactivated) or NO_PASSWORD_CREDENTIAL (an " +
987
+ "INTEGRATION service account); surface those as an explanation rather " +
988
+ "than a generic failure.",
981
989
  returnShape: {
982
990
  users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
983
991
  loading: "boolean",
@@ -988,6 +996,8 @@ const HOOKS = [
988
996
  deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
989
997
  reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
990
998
  remove: "(userId) => Promise<void> // rejects with DirectoryError",
999
+ sendPasswordReset:
1000
+ "(userId) => Promise<{ sent, email_masked }> // rejects with DirectoryError",
991
1001
  },
992
1002
  requiredContextSlice: ["directory.users"],
993
1003
  scopes: ["users.read:*"],
@@ -1449,14 +1459,23 @@ const PLATFORMS = ["web", "native"];
1449
1459
 
1450
1460
  // REQ-WIDGET-ACTION — server-side actions a widget may declare in its
1451
1461
  // manifest. Each runs in the shared isolated-vm action runner (see backend
1452
- // action-runner.service.js) on a cron schedule or in response to a record
1453
- // CRUD event — NEVER in the rendered app, so they never affect Player ↔
1454
- // export parity. The trigger vocabulary mirrors the backend Action model.
1462
+ // action-runner.service.js) on a cron schedule, in response to a record CRUD
1463
+ // event, or on demand — NEVER in the rendered app, so they never affect
1464
+ // Player ↔ export parity.
1465
+ //
1466
+ // This is what a MANIFEST may declare. The backend Action model additionally
1467
+ // accepts 'app' and 'http_post' (sc-5366), which expose a script to a
1468
+ // published app's buttons or to a public webhook — those are the installing
1469
+ // workspace's decision about running someone else's code, so an operator
1470
+ // grants them and a manifest may not ask for them. `validateManifest` and the
1471
+ // backend's `_validateWidgetDeclared` both reject them, so the linter and the
1472
+ // server agree about what a manifest declared.
1455
1473
  const ACTION_TRIGGER_TYPES = [
1456
1474
  "schedule",
1457
1475
  "record_created",
1458
1476
  "record_updated",
1459
1477
  "record_deleted",
1478
+ "manual",
1460
1479
  ];
1461
1480
  // Globals the action script runs against (the runner's surface) — distinct
1462
1481
  // from the React/SDK widget surface, so the component import/banned-API
@@ -1469,6 +1488,9 @@ const ACTION_SCRIPT_GLOBALS = [
1469
1488
  "notifications",
1470
1489
  "console",
1471
1490
  "record",
1491
+ // sc-5366 — `{ body }` on a run fired by the inbound `http_post` trigger,
1492
+ // null on every other trigger.
1493
+ "request",
1472
1494
  "tenantId",
1473
1495
  "triggerType",
1474
1496
  "triggerTableId",
@@ -2922,7 +2944,19 @@ const CONTRACT = deepFreeze({
2922
2944
  // REFUSED by the validator (sc-4879). Server-side automation ships as its own
2923
2945
  // marketplace deliverable (`@colixsystems/action-sdk`), which a workspace
2924
2946
  // installs and configures separately. A widget renders; it does not automate.
2925
- version: "1.66.0",
2947
+ // 1.67.0: additive (sc-5335) — `useUsers()` gains
2948
+ // `sendPasswordReset(userId)`, which mails the standard self-serve
2949
+ // password-reset link to that user's own registered address so an
2950
+ // app-admin can unblock a locked-out member from inside the app. It
2951
+ // resolves `{ sent, email_masked }` and never returns the token or the
2952
+ // link, so it is not an account-takeover primitive; it gates on the same
2953
+ // `users.write:*` scope as invite / deactivate.
2954
+ // 1.68.0: additive (sc-5366) — a manifest action may declare the `manual`
2955
+ // trigger (nothing fires it; the workspace runs it on demand), and every
2956
+ // action script now sees a `request` global — `{ body }` on a run fired
2957
+ // by the workspace-granted `http_post` webhook trigger, null otherwise.
2958
+ // `app` and `http_post` stay operator-granted and are NOT declarable.
2959
+ version: "1.68.0",
2926
2960
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
2927
2961
  hooks: HOOKS,
2928
2962
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -3363,8 +3363,37 @@ export function useUsers(query) {
3363
3363
  throw toDirectoryError(err);
3364
3364
  }
3365
3365
  }, []);
3366
+ // sc-5335: mail the self-serve reset link to a user who has locked
3367
+ // themselves out. Resolves `{ sent, email_masked }`; the host never returns
3368
+ // the token, so this cannot be used to sign in as that user. Rejects with a
3369
+ // DirectoryError carrying `code` USER_INACTIVE or NO_PASSWORD_CREDENTIAL
3370
+ // when the target cannot receive one.
3371
+ const sendPasswordReset = useCallback(async (userId) => {
3372
+ if (typeof usersRef.current.sendPasswordReset !== "function") {
3373
+ throw toDirectoryError(
3374
+ new Error(
3375
+ "useUsers: this host's directory client predates sendPasswordReset",
3376
+ ),
3377
+ );
3378
+ }
3379
+ try {
3380
+ return await usersRef.current.sendPasswordReset(userId);
3381
+ } catch (err) {
3382
+ throw toDirectoryError(err);
3383
+ }
3384
+ }, []);
3366
3385
 
3367
- return { users, loading, error, refetch, invite, deactivate, reactivate, remove };
3386
+ return {
3387
+ users,
3388
+ loading,
3389
+ error,
3390
+ refetch,
3391
+ invite,
3392
+ deactivate,
3393
+ reactivate,
3394
+ remove,
3395
+ sendPasswordReset,
3396
+ };
3368
3397
  }
3369
3398
 
3370
3399
  /**
package/dist/index.d.ts CHANGED
@@ -229,17 +229,25 @@ export interface WidgetManifestAction {
229
229
  * that actually fired.
230
230
  */
231
231
  triggerTypes: Array<
232
- "schedule" | "record_created" | "record_updated" | "record_deleted"
232
+ | "schedule"
233
+ | "record_created"
234
+ | "record_updated"
235
+ | "record_deleted"
236
+ // sc-5366 — nothing fires it; the workspace runs it on demand. The
237
+ // `"app"` and `"http_post"` triggers are operator-granted, not declarable.
238
+ | "manual"
233
239
  >;
234
240
  /** Required iff `triggerTypes` contains `"schedule"`. node-cron syntax. */
235
241
  scheduleCron?: string;
236
242
  /** 100–300000. Defaults to 30000 on materialise. */
237
243
  timeoutMs?: number;
238
244
  /**
239
- * Runs against `datastore`, `fetch`, `console`, `record`, `tenantId`,
240
- * `triggerType`, `triggerTableId` — NOT the React/SDK surface. ≤ 200 KiB.
241
- * `triggerType` is the trigger that fired THIS run — one of the declared
242
- * `triggerTypes`, or `"manual"` / `"app"` for an operator or button run.
245
+ * Runs against `datastore`, `fetch`, `console`, `record`, `request`,
246
+ * `tenantId`, `triggerType`, `triggerTableId` — NOT the React/SDK surface.
247
+ * ≤ 200 KiB. `triggerType` is the trigger that fired THIS run — one of the
248
+ * declared `triggerTypes`, or `"manual"` / `"app"` / `"http_post"` for an
249
+ * operator, button or webhook run. `request` is `{ body }` on a webhook run
250
+ * and `null` otherwise (sc-5366).
243
251
  */
244
252
  scriptSource: string;
245
253
  }
package/dist/linter.cjs CHANGED
@@ -428,7 +428,14 @@ function _translationApiRules(source) {
428
428
  // REQ-USERMGMT / REQ-ACL-SYS M3 — scope-required-for-user-mutation. See
429
429
  // linter.js for the rationale comment. The two files must stay in
430
430
  // lockstep (the contract test asserts behaviour-equivalence).
431
- const USER_MUTATION_METHODS = ["invite", "deactivate", "reactivate"];
431
+ const USER_MUTATION_METHODS = [
432
+ "invite",
433
+ "deactivate",
434
+ "reactivate",
435
+ // sc-5335 — mailing a reset link is edit-style, so it rides `users.write`
436
+ // alongside deactivate rather than the destructive `users.delete`.
437
+ "sendPasswordReset",
438
+ ];
432
439
  // SC-902 — destructive user removal is gated on the dedicated
433
440
  // `users.delete` capability, NOT `users.write`. A widget that calls
434
441
  // useUsers().remove() must declare `users.delete:*` so the static contract
package/dist/linter.js CHANGED
@@ -485,7 +485,8 @@ function _translationApiRules(source) {
485
485
  // REQ-USERMGMT / REQ-ACL-SYS M3 — scope-required-for-user-mutation.
486
486
  //
487
487
  // A widget that calls `useUsers().invite()` / `.deactivate()` /
488
- // `.reactivate()` / `.remove()` MUST declare `users.write:*` in its
488
+ // `.reactivate()` / `.sendPasswordReset()` / `.remove()` MUST declare
489
+ // `users.write:*` in its
489
490
  // manifest's `requestedScopes`; similarly `useGroups()` mutation methods
490
491
  // require `groups.write:*`. The rule is enforced statically so a manifest
491
492
  // that drifts from the source (e.g. an author forgot to add the scope
@@ -498,7 +499,14 @@ function _translationApiRules(source) {
498
499
  // AST-free; an author whose code happened to spell `.invite(` for an
499
500
  // unrelated reason can opt out with a `// @appstudio-skip-scope-check`
500
501
  // trailing comment on the offending line.
501
- const USER_MUTATION_METHODS = ["invite", "deactivate", "reactivate"];
502
+ const USER_MUTATION_METHODS = [
503
+ "invite",
504
+ "deactivate",
505
+ "reactivate",
506
+ // sc-5335 — mailing a reset link is edit-style, so it rides `users.write`
507
+ // alongside deactivate rather than the destructive `users.delete`.
508
+ "sendPasswordReset",
509
+ ];
502
510
  // SC-902 — destructive user removal is gated on the dedicated
503
511
  // `users.delete` capability, NOT `users.write`. A widget that calls
504
512
  // useUsers().remove() must declare `users.delete:*` so the static contract
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.94.0",
3
+ "version": "0.96.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",