@colixsystems/widget-sdk 0.94.0 → 0.95.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 +13 -1
- package/dist/contract.cjs +25 -8
- package/dist/contract.js +25 -8
- package/dist/hooks.js +30 -1
- package/dist/linter.cjs +8 -1
- package/dist/linter.js +10 -2
- package/package.json +1 -1
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,18 @@ 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.95.0 (contract 1.67.0)
|
|
70
|
+
|
|
71
|
+
**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.
|
|
72
|
+
|
|
73
|
+
`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.
|
|
74
|
+
|
|
75
|
+
- **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.
|
|
76
|
+
- **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.
|
|
77
|
+
- Issuing a link supersedes any outstanding one for that user, and the send is rate-limited per acting admin.
|
|
78
|
+
- The built-in **User Management** widget gains the row action and a new `onPasswordResetSent` event.
|
|
79
|
+
- **`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`.
|
|
80
|
+
|
|
69
81
|
### What's new in 0.93.0 (contract 1.66.0)
|
|
70
82
|
|
|
71
83
|
**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
|
|
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}.
|
|
973
|
-
"{ users, loading, error, refetch, invite, deactivate,
|
|
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
|
|
977
|
-
"users.write:*, and the destructive remove()
|
|
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:*"],
|
|
@@ -2922,7 +2932,14 @@ const CONTRACT = deepFreeze({
|
|
|
2922
2932
|
// REFUSED by the validator (sc-4879). Server-side automation ships as its own
|
|
2923
2933
|
// marketplace deliverable (`@colixsystems/action-sdk`), which a workspace
|
|
2924
2934
|
// installs and configures separately. A widget renders; it does not automate.
|
|
2925
|
-
|
|
2935
|
+
// 1.67.0: additive (sc-5335) — `useUsers()` gains
|
|
2936
|
+
// `sendPasswordReset(userId)`, which mails the standard self-serve
|
|
2937
|
+
// password-reset link to that user's own registered address so an
|
|
2938
|
+
// app-admin can unblock a locked-out member from inside the app. It
|
|
2939
|
+
// resolves `{ sent, email_masked }` and never returns the token or the
|
|
2940
|
+
// link, so it is not an account-takeover primitive; it gates on the same
|
|
2941
|
+
// `users.write:*` scope as invite / deactivate.
|
|
2942
|
+
version: "1.67.0",
|
|
2926
2943
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2927
2944
|
hooks: HOOKS,
|
|
2928
2945
|
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
|
|
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}.
|
|
973
|
-
"{ users, loading, error, refetch, invite, deactivate,
|
|
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
|
|
977
|
-
"users.write:*, and the destructive remove()
|
|
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:*"],
|
|
@@ -2922,7 +2932,14 @@ const CONTRACT = deepFreeze({
|
|
|
2922
2932
|
// REFUSED by the validator (sc-4879). Server-side automation ships as its own
|
|
2923
2933
|
// marketplace deliverable (`@colixsystems/action-sdk`), which a workspace
|
|
2924
2934
|
// installs and configures separately. A widget renders; it does not automate.
|
|
2925
|
-
|
|
2935
|
+
// 1.67.0: additive (sc-5335) — `useUsers()` gains
|
|
2936
|
+
// `sendPasswordReset(userId)`, which mails the standard self-serve
|
|
2937
|
+
// password-reset link to that user's own registered address so an
|
|
2938
|
+
// app-admin can unblock a locked-out member from inside the app. It
|
|
2939
|
+
// resolves `{ sent, email_masked }` and never returns the token or the
|
|
2940
|
+
// link, so it is not an account-takeover primitive; it gates on the same
|
|
2941
|
+
// `users.write:*` scope as invite / deactivate.
|
|
2942
|
+
version: "1.67.0",
|
|
2926
2943
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2927
2944
|
hooks: HOOKS,
|
|
2928
2945
|
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 {
|
|
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/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 = [
|
|
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
|
|
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 = [
|
|
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.
|
|
3
|
+
"version": "0.95.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",
|