@colixsystems/widget-sdk 0.93.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 +50 -1
- package/dist/contract.cjs +65 -10
- package/dist/contract.js +65 -10
- package/dist/hooks.js +142 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +25 -1
- package/dist/linter.js +27 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,8 +47,9 @@ 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
|
+
| **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`). |
|
|
52
53
|
| **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
|
|
53
54
|
| **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId? })` | `{ exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.exportPdf` — `files.write:*`. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF server-side and saves it as a file (`application/pdf`); same server-side renderer on web + native. |
|
|
54
55
|
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser`. Rejects with `PaymentError { code, message, retryable }`; when `retryable` is `false` show `message` and drop the retry. Charges are accepted ONLY in the currency the workspace sells in — omit `currency` and the platform applies it (a disagreeing literal is a publish-blocking `payment-currency` finding). |
|
|
@@ -65,6 +66,18 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
65
66
|
|
|
66
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**.
|
|
67
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
|
+
|
|
68
81
|
### What's new in 0.93.0 (contract 1.66.0)
|
|
69
82
|
|
|
70
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.
|
|
@@ -876,6 +889,42 @@ The matching manifest declares the scopes:
|
|
|
876
889
|
|
|
877
890
|
The host rejects calls whose scope is not declared in the manifest (the SDK linter catches this statically too). Declaring a write scope is also a consent prompt the Studio admin sees at install time — the wider the scope set, the more careful the admin is about granting the install.
|
|
878
891
|
|
|
892
|
+
## Listing pending invitations from a widget
|
|
893
|
+
|
|
894
|
+
`useInvites(query?)` lists the workspace's invites and resends / revokes them:
|
|
895
|
+
|
|
896
|
+
```jsx
|
|
897
|
+
import { useInvites, View, Text, Pressable } from '@colixsystems/widget-sdk';
|
|
898
|
+
|
|
899
|
+
// Manifest: requestedScopes: ['users.read:*', 'users.write:*']
|
|
900
|
+
function PendingInvites() {
|
|
901
|
+
const { invites, loading, error, resend, revoke } = useInvites({ status: 'pending' });
|
|
902
|
+
if (loading) return null;
|
|
903
|
+
if (error) return <Text>{error.message}</Text>; // code === 'FORBIDDEN' when the capability is missing
|
|
904
|
+
return (
|
|
905
|
+
<View>
|
|
906
|
+
{invites.map((i) => (
|
|
907
|
+
<View key={i.id}>
|
|
908
|
+
<Text>{i.email} · {i.status}</Text>
|
|
909
|
+
<Pressable onPress={() => resend(i.id).catch(() => {})}><Text>Resend</Text></Pressable>
|
|
910
|
+
<Pressable onPress={() => revoke(i.id).catch(() => {})}><Text>Revoke</Text></Pressable>
|
|
911
|
+
</View>
|
|
912
|
+
))}
|
|
913
|
+
</View>
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
Rows are snake_case: `{ id, email, name, group_ids, status, expires_at, accepted_at,
|
|
919
|
+
revoked_at, created_at }`. Trust the server-computed `status` rather than comparing
|
|
920
|
+
`expires_at` against the device clock.
|
|
921
|
+
|
|
922
|
+
**The gate is `users.write`, not `users.read`.** A pending invite exposes the email of
|
|
923
|
+
someone who is not a member yet, so the backend gates the entire invite surface —
|
|
924
|
+
listing included — on the `users.write` capability plus a signed `users.write:*`
|
|
925
|
+
scope. The `invites.read:*` / `invites.write:*` scope names mint but no route
|
|
926
|
+
enforces them, so declaring only those yields `FORBIDDEN`. Both mutations refetch
|
|
927
|
+
the list on success.
|
|
879
928
|
## Managing per-record permissions from a widget
|
|
880
929
|
|
|
881
930
|
`useRecordPermissions(tableId, recordId)` is the in-app surface for sharing a single record with another user or group. The chat widget uses it to invite members into a channel — the channel record's per-record grants ARE the membership list (messages inherit those grants). The hook also covers project-workspace, document-sharing, and team-roster widgets that grant access record-by-record.
|
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:*"],
|
|
@@ -1023,6 +1033,44 @@ const HOOKS = [
|
|
|
1023
1033
|
requiredContextSlice: ["directory.groups"],
|
|
1024
1034
|
scopes: ["groups.read:*"],
|
|
1025
1035
|
},
|
|
1036
|
+
// sc-5097 — pending AppUser invite administration. Returns
|
|
1037
|
+
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1038
|
+
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1039
|
+
// caller's `users.write` SystemAcl capability.
|
|
1040
|
+
{
|
|
1041
|
+
name: "useInvites",
|
|
1042
|
+
signature: "useInvites(query?)",
|
|
1043
|
+
description:
|
|
1044
|
+
"Pending AppUser invite administration via the injected " +
|
|
1045
|
+
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1046
|
+
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1047
|
+
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1048
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1049
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1050
|
+
"value. query is { status?, limit?, offset? } passed verbatim; the " +
|
|
1051
|
+
"endpoint defaults to status 'all', so pass { status: 'pending' } " +
|
|
1052
|
+
"for the usual outstanding-invite list. The WHOLE invite surface — " +
|
|
1053
|
+
"list included — is gated on users.write:* plus the users.write " +
|
|
1054
|
+
"SystemAcl capability, because a pending invite exposes the email of " +
|
|
1055
|
+
"someone who is not a member yet; a users.read-only caller cannot see " +
|
|
1056
|
+
"it. (invites.read:* / invites.write:* mint but gate nothing today.) " +
|
|
1057
|
+
"Without the capability every call rejects with DirectoryError code " +
|
|
1058
|
+
"FORBIDDEN — surface that, do not hide the tab. Both mutations " +
|
|
1059
|
+
"refetch on success.",
|
|
1060
|
+
returnShape: {
|
|
1061
|
+
invites:
|
|
1062
|
+
"Array<{ id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1063
|
+
loading: "boolean",
|
|
1064
|
+
error: "DirectoryError | null // { code, message, retryable }",
|
|
1065
|
+
refetch: "() => Promise<void>",
|
|
1066
|
+
resend:
|
|
1067
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1068
|
+
revoke:
|
|
1069
|
+
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1070
|
+
},
|
|
1071
|
+
requiredContextSlice: ["directory.invites"],
|
|
1072
|
+
scopes: ["users.write:*"],
|
|
1073
|
+
},
|
|
1026
1074
|
// REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
|
|
1027
1075
|
// app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.js.
|
|
1028
1076
|
{
|
|
@@ -1677,9 +1725,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1677
1725
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
1678
1726
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
1679
1727
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
1680
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). 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:* (destructive user removal by users.delete:*).",
|
|
1728
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke pending invites, gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). 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:* (destructive user removal by users.delete:*).",
|
|
1681
1729
|
required: true,
|
|
1682
|
-
fields: { users: "object", groups: "object", bankid: "object" },
|
|
1730
|
+
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
1683
1731
|
},
|
|
1684
1732
|
assets: {
|
|
1685
1733
|
description:
|
|
@@ -2884,7 +2932,14 @@ const CONTRACT = deepFreeze({
|
|
|
2884
2932
|
// REFUSED by the validator (sc-4879). Server-side automation ships as its own
|
|
2885
2933
|
// marketplace deliverable (`@colixsystems/action-sdk`), which a workspace
|
|
2886
2934
|
// installs and configures separately. A widget renders; it does not automate.
|
|
2887
|
-
|
|
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",
|
|
2888
2943
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2889
2944
|
hooks: HOOKS,
|
|
2890
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:*"],
|
|
@@ -1023,6 +1033,44 @@ const HOOKS = [
|
|
|
1023
1033
|
requiredContextSlice: ["directory.groups"],
|
|
1024
1034
|
scopes: ["groups.read:*"],
|
|
1025
1035
|
},
|
|
1036
|
+
// sc-5097 — pending AppUser invite administration. Returns
|
|
1037
|
+
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1038
|
+
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1039
|
+
// caller's `users.write` SystemAcl capability.
|
|
1040
|
+
{
|
|
1041
|
+
name: "useInvites",
|
|
1042
|
+
signature: "useInvites(query?)",
|
|
1043
|
+
description:
|
|
1044
|
+
"Pending AppUser invite administration via the injected " +
|
|
1045
|
+
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1046
|
+
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1047
|
+
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1048
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1049
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1050
|
+
"value. query is { status?, limit?, offset? } passed verbatim; the " +
|
|
1051
|
+
"endpoint defaults to status 'all', so pass { status: 'pending' } " +
|
|
1052
|
+
"for the usual outstanding-invite list. The WHOLE invite surface — " +
|
|
1053
|
+
"list included — is gated on users.write:* plus the users.write " +
|
|
1054
|
+
"SystemAcl capability, because a pending invite exposes the email of " +
|
|
1055
|
+
"someone who is not a member yet; a users.read-only caller cannot see " +
|
|
1056
|
+
"it. (invites.read:* / invites.write:* mint but gate nothing today.) " +
|
|
1057
|
+
"Without the capability every call rejects with DirectoryError code " +
|
|
1058
|
+
"FORBIDDEN — surface that, do not hide the tab. Both mutations " +
|
|
1059
|
+
"refetch on success.",
|
|
1060
|
+
returnShape: {
|
|
1061
|
+
invites:
|
|
1062
|
+
"Array<{ id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1063
|
+
loading: "boolean",
|
|
1064
|
+
error: "DirectoryError | null // { code, message, retryable }",
|
|
1065
|
+
refetch: "() => Promise<void>",
|
|
1066
|
+
resend:
|
|
1067
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1068
|
+
revoke:
|
|
1069
|
+
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1070
|
+
},
|
|
1071
|
+
requiredContextSlice: ["directory.invites"],
|
|
1072
|
+
scopes: ["users.write:*"],
|
|
1073
|
+
},
|
|
1026
1074
|
// REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
|
|
1027
1075
|
// app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.cjs.
|
|
1028
1076
|
{
|
|
@@ -1677,9 +1725,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1677
1725
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
1678
1726
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
1679
1727
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
1680
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). 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:* (destructive user removal by users.delete:*).",
|
|
1728
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke pending invites, gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). 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:* (destructive user removal by users.delete:*).",
|
|
1681
1729
|
required: true,
|
|
1682
|
-
fields: { users: "object", groups: "object", bankid: "object" },
|
|
1730
|
+
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
1683
1731
|
},
|
|
1684
1732
|
assets: {
|
|
1685
1733
|
description:
|
|
@@ -2884,7 +2932,14 @@ const CONTRACT = deepFreeze({
|
|
|
2884
2932
|
// REFUSED by the validator (sc-4879). Server-side automation ships as its own
|
|
2885
2933
|
// marketplace deliverable (`@colixsystems/action-sdk`), which a workspace
|
|
2886
2934
|
// installs and configures separately. A widget renders; it does not automate.
|
|
2887
|
-
|
|
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",
|
|
2888
2943
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2889
2944
|
hooks: HOOKS,
|
|
2890
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
|
/**
|
|
@@ -3467,6 +3496,118 @@ export function useGroups(query) {
|
|
|
3467
3496
|
return { groups, loading, error, refetch, create, remove, addMember, removeMember };
|
|
3468
3497
|
}
|
|
3469
3498
|
|
|
3499
|
+
/**
|
|
3500
|
+
* sc-5097 — pending AppUser invite administration.
|
|
3501
|
+
*
|
|
3502
|
+
* Returns `{ invites, loading, error, refetch, resend, revoke }`. Reads the
|
|
3503
|
+
* injected `@colixsystems/directory-client` at
|
|
3504
|
+
* `ctx.directory.invites.{list, resend, revoke}` — `list` resolves to the
|
|
3505
|
+
* `{ data, meta }` envelope VERBATIM, so we unwrap `res.data` (default `[]`).
|
|
3506
|
+
*
|
|
3507
|
+
* Rows are snake_case exactly as the backend projects them: `{ id, tenant_id,
|
|
3508
|
+
* email, name, group_ids, status, expires_at, accepted_at, revoked_at,
|
|
3509
|
+
* created_at, invited_by_studio_user_id }`. `status` is the server-computed
|
|
3510
|
+
* lifecycle value (`"pending" | "accepted" | "revoked" | "expired"`) — prefer
|
|
3511
|
+
* it over re-deriving expiry on the client, whose clock may disagree.
|
|
3512
|
+
*
|
|
3513
|
+
* `query` is an optional `{ status?, limit?, offset? }` passed through
|
|
3514
|
+
* verbatim; `status` accepts `"pending" | "accepted" | "revoked" | "expired" |
|
|
3515
|
+
* "all"` and the endpoint defaults to `"all"`, so pass `{ status: "pending" }`
|
|
3516
|
+
* for the usual "who hasn't accepted yet" list.
|
|
3517
|
+
*
|
|
3518
|
+
* The WHOLE invite surface — list included — is gated on the `users.write`
|
|
3519
|
+
* capability plus a signed `users.write:*` scope: a pending invite exposes
|
|
3520
|
+
* the email of someone who is not a member yet, so a `users.read`-only
|
|
3521
|
+
* caller cannot see it. The `invites.read:*` / `invites.write:*` scope names
|
|
3522
|
+
* mint but gate nothing today, so declaring them alone yields FORBIDDEN.
|
|
3523
|
+
* Every call rejects with a `DirectoryError` carrying `code: "FORBIDDEN"`
|
|
3524
|
+
* when the capability is missing — surface it rather than hiding the tab.
|
|
3525
|
+
*/
|
|
3526
|
+
export function useInvites(query) {
|
|
3527
|
+
const ctx = useWidgetContextOrThrow("useInvites");
|
|
3528
|
+
if (
|
|
3529
|
+
!ctx.directory ||
|
|
3530
|
+
!ctx.directory.invites ||
|
|
3531
|
+
typeof ctx.directory.invites.list !== "function"
|
|
3532
|
+
) {
|
|
3533
|
+
throw new Error(
|
|
3534
|
+
"useInvites: host did not inject a directory client (ctx.directory.invites)",
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
const [invites, setInvites] = useState([]);
|
|
3538
|
+
const [loading, setLoading] = useState(true);
|
|
3539
|
+
const [error, setError] = useState(null);
|
|
3540
|
+
|
|
3541
|
+
const queryRef = useRef(query);
|
|
3542
|
+
const invitesRef = useRef(ctx.directory.invites);
|
|
3543
|
+
queryRef.current = query;
|
|
3544
|
+
invitesRef.current = ctx.directory.invites;
|
|
3545
|
+
|
|
3546
|
+
const runRef = useRef(0);
|
|
3547
|
+
|
|
3548
|
+
const doFetch = useCallback(async () => {
|
|
3549
|
+
const myRun = ++runRef.current;
|
|
3550
|
+
setLoading(true);
|
|
3551
|
+
setError(null);
|
|
3552
|
+
try {
|
|
3553
|
+
const res = await invitesRef.current.list(queryRef.current);
|
|
3554
|
+
// Directory invites.list returns the { data, meta } envelope verbatim.
|
|
3555
|
+
const rows = res && Array.isArray(res.data) ? res.data : [];
|
|
3556
|
+
if (runRef.current !== myRun) return;
|
|
3557
|
+
setInvites(rows);
|
|
3558
|
+
setLoading(false);
|
|
3559
|
+
} catch (err) {
|
|
3560
|
+
if (runRef.current !== myRun) return;
|
|
3561
|
+
setError(toDirectoryError(err));
|
|
3562
|
+
setLoading(false);
|
|
3563
|
+
}
|
|
3564
|
+
}, []);
|
|
3565
|
+
|
|
3566
|
+
const queryKey = (() => {
|
|
3567
|
+
try {
|
|
3568
|
+
return JSON.stringify(query);
|
|
3569
|
+
} catch (_e) {
|
|
3570
|
+
return null;
|
|
3571
|
+
}
|
|
3572
|
+
})();
|
|
3573
|
+
useEffect(() => {
|
|
3574
|
+
doFetch();
|
|
3575
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3576
|
+
}, [queryKey]);
|
|
3577
|
+
|
|
3578
|
+
const refetch = useCallback(async () => {
|
|
3579
|
+
await doFetch();
|
|
3580
|
+
}, [doFetch]);
|
|
3581
|
+
|
|
3582
|
+
// Both mutations re-list on success: revoking drops the row and resending
|
|
3583
|
+
// moves `expires_at`, so a stale list would show the operator the old state.
|
|
3584
|
+
const resend = useCallback(
|
|
3585
|
+
async (inviteId) => {
|
|
3586
|
+
try {
|
|
3587
|
+
const row = await invitesRef.current.resend(inviteId);
|
|
3588
|
+
await doFetch();
|
|
3589
|
+
return row;
|
|
3590
|
+
} catch (err) {
|
|
3591
|
+
throw toDirectoryError(err);
|
|
3592
|
+
}
|
|
3593
|
+
},
|
|
3594
|
+
[doFetch],
|
|
3595
|
+
);
|
|
3596
|
+
const revoke = useCallback(
|
|
3597
|
+
async (inviteId) => {
|
|
3598
|
+
try {
|
|
3599
|
+
await invitesRef.current.revoke(inviteId);
|
|
3600
|
+
await doFetch();
|
|
3601
|
+
} catch (err) {
|
|
3602
|
+
throw toDirectoryError(err);
|
|
3603
|
+
}
|
|
3604
|
+
},
|
|
3605
|
+
[doFetch],
|
|
3606
|
+
);
|
|
3607
|
+
|
|
3608
|
+
return { invites, loading, error, refetch, resend, revoke };
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3470
3611
|
/**
|
|
3471
3612
|
* REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in app-user,
|
|
3472
3613
|
* and read whether BankID is available + already linked. Returns
|
package/dist/index.d.ts
CHANGED
|
@@ -1523,12 +1523,26 @@ export interface InviteArgs {
|
|
|
1523
1523
|
group_ids?: string[];
|
|
1524
1524
|
}
|
|
1525
1525
|
|
|
1526
|
+
export type AppUserInviteStatus =
|
|
1527
|
+
| "pending"
|
|
1528
|
+
| "accepted"
|
|
1529
|
+
| "revoked"
|
|
1530
|
+
| "expired";
|
|
1531
|
+
|
|
1526
1532
|
export interface AppUserInviteRow {
|
|
1527
1533
|
id: string;
|
|
1528
1534
|
email: string;
|
|
1529
|
-
|
|
1535
|
+
/** Server-computed lifecycle value — prefer it over re-deriving expiry. */
|
|
1536
|
+
status: AppUserInviteStatus;
|
|
1537
|
+
name?: string;
|
|
1538
|
+
group_ids?: string[];
|
|
1530
1539
|
invited_at?: string;
|
|
1531
1540
|
expires_at?: string;
|
|
1541
|
+
accepted_at?: string | null;
|
|
1542
|
+
revoked_at?: string | null;
|
|
1543
|
+
created_at?: string;
|
|
1544
|
+
tenant_id?: string;
|
|
1545
|
+
invited_by_studio_user_id?: string | null;
|
|
1532
1546
|
}
|
|
1533
1547
|
|
|
1534
1548
|
export interface UsersApi {
|
|
@@ -1585,6 +1599,40 @@ export interface GroupsApi {
|
|
|
1585
1599
|
*/
|
|
1586
1600
|
export function useGroups(query?: GroupsQuery): GroupsApi;
|
|
1587
1601
|
|
|
1602
|
+
// --------------------------------------------------------------- useInvites
|
|
1603
|
+
//
|
|
1604
|
+
// sc-5097 — pending AppUser invite administration.
|
|
1605
|
+
|
|
1606
|
+
export interface InvitesQuery {
|
|
1607
|
+
/** Defaults to "all" server-side; pass "pending" for outstanding invites. */
|
|
1608
|
+
status?: AppUserInviteStatus | "all";
|
|
1609
|
+
limit?: number;
|
|
1610
|
+
offset?: number;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
export interface InvitesApi {
|
|
1614
|
+
invites: AppUserInviteRow[];
|
|
1615
|
+
loading: boolean;
|
|
1616
|
+
error: DirectoryError | null;
|
|
1617
|
+
refetch(): Promise<void>;
|
|
1618
|
+
/** Re-send the invitation email. Refetches on success. */
|
|
1619
|
+
resend(inviteId: string): Promise<AppUserInviteRow>;
|
|
1620
|
+
/** Cancel a pending invitation. Refetches on success. */
|
|
1621
|
+
revoke(inviteId: string): Promise<void>;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Pending AppUser invite administration through the injected directory-client
|
|
1626
|
+
* at `ctx.directory.invites.{list,resend,revoke}`.
|
|
1627
|
+
*
|
|
1628
|
+
* The WHOLE surface — listing included — requires the `users.write:*` scope
|
|
1629
|
+
* AND the SystemAcl `users.write` capability, because a pending invite exposes
|
|
1630
|
+
* the email of someone who is not a member yet. The `invites.read:*` /
|
|
1631
|
+
* `invites.write:*` scope names mint but no route enforces them, so declaring
|
|
1632
|
+
* only those yields a `FORBIDDEN` DirectoryError.
|
|
1633
|
+
*/
|
|
1634
|
+
export function useInvites(query?: InvitesQuery): InvitesApi;
|
|
1635
|
+
|
|
1588
1636
|
// ----------------------------------------------------- useBankIdLink
|
|
1589
1637
|
//
|
|
1590
1638
|
// REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in app-user.
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
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
|
|
@@ -447,6 +454,7 @@ function _scopeRules(source, manifest) {
|
|
|
447
454
|
);
|
|
448
455
|
const usesUsersHook = /\buseUsers\s*\(/.test(source);
|
|
449
456
|
const usesGroupsHook = /\buseGroups\s*\(/.test(source);
|
|
457
|
+
const usesInvitesHook = /\buseInvites\s*\(/.test(source);
|
|
450
458
|
|
|
451
459
|
if (usesUsersHook) {
|
|
452
460
|
const reads = declared.has("users.read:*") || declared.has("users.read");
|
|
@@ -473,6 +481,22 @@ function _scopeRules(source, manifest) {
|
|
|
473
481
|
}
|
|
474
482
|
}
|
|
475
483
|
|
|
484
|
+
// sc-5097 — the whole invite surface (list, resend, revoke) is gated on
|
|
485
|
+
// the "users.write" capability, and the scope gate accepts only a signed
|
|
486
|
+
// scope matching that key. `invites.*` scopes mint but gate nothing.
|
|
487
|
+
if (usesInvitesHook) {
|
|
488
|
+
const writes =
|
|
489
|
+
declared.has("users.write:*") || declared.has("users.write");
|
|
490
|
+
if (!writes) {
|
|
491
|
+
findings.push({
|
|
492
|
+
rule: "scope-required-for-useInvites",
|
|
493
|
+
label:
|
|
494
|
+
"useInvites() requires `users.write:*` in manifest.requestedScopes",
|
|
495
|
+
line: 0,
|
|
496
|
+
snippet: "",
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
476
500
|
const lines = source.split(/\r?\n/);
|
|
477
501
|
let firedUsersWrite = false;
|
|
478
502
|
let firedUsersDelete = false;
|
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
|
|
@@ -522,6 +530,7 @@ function _scopeRules(source, manifest) {
|
|
|
522
530
|
);
|
|
523
531
|
const usesUsersHook = /\buseUsers\s*\(/.test(source);
|
|
524
532
|
const usesGroupsHook = /\buseGroups\s*\(/.test(source);
|
|
533
|
+
const usesInvitesHook = /\buseInvites\s*\(/.test(source);
|
|
525
534
|
|
|
526
535
|
if (usesUsersHook) {
|
|
527
536
|
const reads = declared.has("users.read:*") || declared.has("users.read");
|
|
@@ -547,6 +556,22 @@ function _scopeRules(source, manifest) {
|
|
|
547
556
|
}
|
|
548
557
|
}
|
|
549
558
|
|
|
559
|
+
// sc-5097 — the whole invite surface (list, resend, revoke) is gated on
|
|
560
|
+
// the "users.write" capability, and the scope gate accepts only a signed
|
|
561
|
+
// scope matching that key. `invites.*` scopes mint but gate nothing.
|
|
562
|
+
if (usesInvitesHook) {
|
|
563
|
+
const writes =
|
|
564
|
+
declared.has("users.write:*") || declared.has("users.write");
|
|
565
|
+
if (!writes) {
|
|
566
|
+
findings.push({
|
|
567
|
+
rule: "scope-required-for-useInvites",
|
|
568
|
+
label:
|
|
569
|
+
"useInvites() requires `users.write:*` in manifest.requestedScopes",
|
|
570
|
+
line: 0,
|
|
571
|
+
snippet: "",
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
550
575
|
const lines = source.split(/\r?\n/);
|
|
551
576
|
let firedUsersWrite = false;
|
|
552
577
|
let firedUsersDelete = false;
|
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",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js"
|
|
51
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|