@colixsystems/widget-sdk 0.93.0 → 0.94.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 +37 -0
- package/dist/contract.cjs +40 -2
- package/dist/contract.js +40 -2
- package/dist/hooks.js +112 -0
- package/dist/index.d.ts +49 -1
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +17 -0
- package/dist/linter.js +17 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
49
49
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
50
50
|
| **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (edits 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). |
|
|
@@ -876,6 +877,42 @@ The matching manifest declares the scopes:
|
|
|
876
877
|
|
|
877
878
|
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
879
|
|
|
880
|
+
## Listing pending invitations from a widget
|
|
881
|
+
|
|
882
|
+
`useInvites(query?)` lists the workspace's invites and resends / revokes them:
|
|
883
|
+
|
|
884
|
+
```jsx
|
|
885
|
+
import { useInvites, View, Text, Pressable } from '@colixsystems/widget-sdk';
|
|
886
|
+
|
|
887
|
+
// Manifest: requestedScopes: ['users.read:*', 'users.write:*']
|
|
888
|
+
function PendingInvites() {
|
|
889
|
+
const { invites, loading, error, resend, revoke } = useInvites({ status: 'pending' });
|
|
890
|
+
if (loading) return null;
|
|
891
|
+
if (error) return <Text>{error.message}</Text>; // code === 'FORBIDDEN' when the capability is missing
|
|
892
|
+
return (
|
|
893
|
+
<View>
|
|
894
|
+
{invites.map((i) => (
|
|
895
|
+
<View key={i.id}>
|
|
896
|
+
<Text>{i.email} · {i.status}</Text>
|
|
897
|
+
<Pressable onPress={() => resend(i.id).catch(() => {})}><Text>Resend</Text></Pressable>
|
|
898
|
+
<Pressable onPress={() => revoke(i.id).catch(() => {})}><Text>Revoke</Text></Pressable>
|
|
899
|
+
</View>
|
|
900
|
+
))}
|
|
901
|
+
</View>
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
Rows are snake_case: `{ id, email, name, group_ids, status, expires_at, accepted_at,
|
|
907
|
+
revoked_at, created_at }`. Trust the server-computed `status` rather than comparing
|
|
908
|
+
`expires_at` against the device clock.
|
|
909
|
+
|
|
910
|
+
**The gate is `users.write`, not `users.read`.** A pending invite exposes the email of
|
|
911
|
+
someone who is not a member yet, so the backend gates the entire invite surface —
|
|
912
|
+
listing included — on the `users.write` capability plus a signed `users.write:*`
|
|
913
|
+
scope. The `invites.read:*` / `invites.write:*` scope names mint but no route
|
|
914
|
+
enforces them, so declaring only those yields `FORBIDDEN`. Both mutations refetch
|
|
915
|
+
the list on success.
|
|
879
916
|
## Managing per-record permissions from a widget
|
|
880
917
|
|
|
881
918
|
`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
|
@@ -1023,6 +1023,44 @@ const HOOKS = [
|
|
|
1023
1023
|
requiredContextSlice: ["directory.groups"],
|
|
1024
1024
|
scopes: ["groups.read:*"],
|
|
1025
1025
|
},
|
|
1026
|
+
// sc-5097 — pending AppUser invite administration. Returns
|
|
1027
|
+
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1028
|
+
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1029
|
+
// caller's `users.write` SystemAcl capability.
|
|
1030
|
+
{
|
|
1031
|
+
name: "useInvites",
|
|
1032
|
+
signature: "useInvites(query?)",
|
|
1033
|
+
description:
|
|
1034
|
+
"Pending AppUser invite administration via the injected " +
|
|
1035
|
+
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1036
|
+
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1037
|
+
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1038
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1039
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1040
|
+
"value. query is { status?, limit?, offset? } passed verbatim; the " +
|
|
1041
|
+
"endpoint defaults to status 'all', so pass { status: 'pending' } " +
|
|
1042
|
+
"for the usual outstanding-invite list. The WHOLE invite surface — " +
|
|
1043
|
+
"list included — is gated on users.write:* plus the users.write " +
|
|
1044
|
+
"SystemAcl capability, because a pending invite exposes the email of " +
|
|
1045
|
+
"someone who is not a member yet; a users.read-only caller cannot see " +
|
|
1046
|
+
"it. (invites.read:* / invites.write:* mint but gate nothing today.) " +
|
|
1047
|
+
"Without the capability every call rejects with DirectoryError code " +
|
|
1048
|
+
"FORBIDDEN — surface that, do not hide the tab. Both mutations " +
|
|
1049
|
+
"refetch on success.",
|
|
1050
|
+
returnShape: {
|
|
1051
|
+
invites:
|
|
1052
|
+
"Array<{ id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1053
|
+
loading: "boolean",
|
|
1054
|
+
error: "DirectoryError | null // { code, message, retryable }",
|
|
1055
|
+
refetch: "() => Promise<void>",
|
|
1056
|
+
resend:
|
|
1057
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1058
|
+
revoke:
|
|
1059
|
+
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1060
|
+
},
|
|
1061
|
+
requiredContextSlice: ["directory.invites"],
|
|
1062
|
+
scopes: ["users.write:*"],
|
|
1063
|
+
},
|
|
1026
1064
|
// REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
|
|
1027
1065
|
// app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.js.
|
|
1028
1066
|
{
|
|
@@ -1677,9 +1715,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1677
1715
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
1678
1716
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
1679
1717
|
"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:*).",
|
|
1718
|
+
"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
1719
|
required: true,
|
|
1682
|
-
fields: { users: "object", groups: "object", bankid: "object" },
|
|
1720
|
+
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
1683
1721
|
},
|
|
1684
1722
|
assets: {
|
|
1685
1723
|
description:
|
package/dist/contract.js
CHANGED
|
@@ -1023,6 +1023,44 @@ const HOOKS = [
|
|
|
1023
1023
|
requiredContextSlice: ["directory.groups"],
|
|
1024
1024
|
scopes: ["groups.read:*"],
|
|
1025
1025
|
},
|
|
1026
|
+
// sc-5097 — pending AppUser invite administration. Returns
|
|
1027
|
+
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1028
|
+
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1029
|
+
// caller's `users.write` SystemAcl capability.
|
|
1030
|
+
{
|
|
1031
|
+
name: "useInvites",
|
|
1032
|
+
signature: "useInvites(query?)",
|
|
1033
|
+
description:
|
|
1034
|
+
"Pending AppUser invite administration via the injected " +
|
|
1035
|
+
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1036
|
+
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1037
|
+
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1038
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1039
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1040
|
+
"value. query is { status?, limit?, offset? } passed verbatim; the " +
|
|
1041
|
+
"endpoint defaults to status 'all', so pass { status: 'pending' } " +
|
|
1042
|
+
"for the usual outstanding-invite list. The WHOLE invite surface — " +
|
|
1043
|
+
"list included — is gated on users.write:* plus the users.write " +
|
|
1044
|
+
"SystemAcl capability, because a pending invite exposes the email of " +
|
|
1045
|
+
"someone who is not a member yet; a users.read-only caller cannot see " +
|
|
1046
|
+
"it. (invites.read:* / invites.write:* mint but gate nothing today.) " +
|
|
1047
|
+
"Without the capability every call rejects with DirectoryError code " +
|
|
1048
|
+
"FORBIDDEN — surface that, do not hide the tab. Both mutations " +
|
|
1049
|
+
"refetch on success.",
|
|
1050
|
+
returnShape: {
|
|
1051
|
+
invites:
|
|
1052
|
+
"Array<{ id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1053
|
+
loading: "boolean",
|
|
1054
|
+
error: "DirectoryError | null // { code, message, retryable }",
|
|
1055
|
+
refetch: "() => Promise<void>",
|
|
1056
|
+
resend:
|
|
1057
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1058
|
+
revoke:
|
|
1059
|
+
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1060
|
+
},
|
|
1061
|
+
requiredContextSlice: ["directory.invites"],
|
|
1062
|
+
scopes: ["users.write:*"],
|
|
1063
|
+
},
|
|
1026
1064
|
// REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in
|
|
1027
1065
|
// app-user. Self-service + JWT-gated (no widget scope). Mirror of contract.cjs.
|
|
1028
1066
|
{
|
|
@@ -1677,9 +1715,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1677
1715
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
1678
1716
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
1679
1717
|
"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:*).",
|
|
1718
|
+
"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
1719
|
required: true,
|
|
1682
|
-
fields: { users: "object", groups: "object", bankid: "object" },
|
|
1720
|
+
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
1683
1721
|
},
|
|
1684
1722
|
assets: {
|
|
1685
1723
|
description:
|
package/dist/hooks.js
CHANGED
|
@@ -3467,6 +3467,118 @@ export function useGroups(query) {
|
|
|
3467
3467
|
return { groups, loading, error, refetch, create, remove, addMember, removeMember };
|
|
3468
3468
|
}
|
|
3469
3469
|
|
|
3470
|
+
/**
|
|
3471
|
+
* sc-5097 — pending AppUser invite administration.
|
|
3472
|
+
*
|
|
3473
|
+
* Returns `{ invites, loading, error, refetch, resend, revoke }`. Reads the
|
|
3474
|
+
* injected `@colixsystems/directory-client` at
|
|
3475
|
+
* `ctx.directory.invites.{list, resend, revoke}` — `list` resolves to the
|
|
3476
|
+
* `{ data, meta }` envelope VERBATIM, so we unwrap `res.data` (default `[]`).
|
|
3477
|
+
*
|
|
3478
|
+
* Rows are snake_case exactly as the backend projects them: `{ id, tenant_id,
|
|
3479
|
+
* email, name, group_ids, status, expires_at, accepted_at, revoked_at,
|
|
3480
|
+
* created_at, invited_by_studio_user_id }`. `status` is the server-computed
|
|
3481
|
+
* lifecycle value (`"pending" | "accepted" | "revoked" | "expired"`) — prefer
|
|
3482
|
+
* it over re-deriving expiry on the client, whose clock may disagree.
|
|
3483
|
+
*
|
|
3484
|
+
* `query` is an optional `{ status?, limit?, offset? }` passed through
|
|
3485
|
+
* verbatim; `status` accepts `"pending" | "accepted" | "revoked" | "expired" |
|
|
3486
|
+
* "all"` and the endpoint defaults to `"all"`, so pass `{ status: "pending" }`
|
|
3487
|
+
* for the usual "who hasn't accepted yet" list.
|
|
3488
|
+
*
|
|
3489
|
+
* The WHOLE invite surface — list included — is gated on the `users.write`
|
|
3490
|
+
* capability plus a signed `users.write:*` scope: a pending invite exposes
|
|
3491
|
+
* the email of someone who is not a member yet, so a `users.read`-only
|
|
3492
|
+
* caller cannot see it. The `invites.read:*` / `invites.write:*` scope names
|
|
3493
|
+
* mint but gate nothing today, so declaring them alone yields FORBIDDEN.
|
|
3494
|
+
* Every call rejects with a `DirectoryError` carrying `code: "FORBIDDEN"`
|
|
3495
|
+
* when the capability is missing — surface it rather than hiding the tab.
|
|
3496
|
+
*/
|
|
3497
|
+
export function useInvites(query) {
|
|
3498
|
+
const ctx = useWidgetContextOrThrow("useInvites");
|
|
3499
|
+
if (
|
|
3500
|
+
!ctx.directory ||
|
|
3501
|
+
!ctx.directory.invites ||
|
|
3502
|
+
typeof ctx.directory.invites.list !== "function"
|
|
3503
|
+
) {
|
|
3504
|
+
throw new Error(
|
|
3505
|
+
"useInvites: host did not inject a directory client (ctx.directory.invites)",
|
|
3506
|
+
);
|
|
3507
|
+
}
|
|
3508
|
+
const [invites, setInvites] = useState([]);
|
|
3509
|
+
const [loading, setLoading] = useState(true);
|
|
3510
|
+
const [error, setError] = useState(null);
|
|
3511
|
+
|
|
3512
|
+
const queryRef = useRef(query);
|
|
3513
|
+
const invitesRef = useRef(ctx.directory.invites);
|
|
3514
|
+
queryRef.current = query;
|
|
3515
|
+
invitesRef.current = ctx.directory.invites;
|
|
3516
|
+
|
|
3517
|
+
const runRef = useRef(0);
|
|
3518
|
+
|
|
3519
|
+
const doFetch = useCallback(async () => {
|
|
3520
|
+
const myRun = ++runRef.current;
|
|
3521
|
+
setLoading(true);
|
|
3522
|
+
setError(null);
|
|
3523
|
+
try {
|
|
3524
|
+
const res = await invitesRef.current.list(queryRef.current);
|
|
3525
|
+
// Directory invites.list returns the { data, meta } envelope verbatim.
|
|
3526
|
+
const rows = res && Array.isArray(res.data) ? res.data : [];
|
|
3527
|
+
if (runRef.current !== myRun) return;
|
|
3528
|
+
setInvites(rows);
|
|
3529
|
+
setLoading(false);
|
|
3530
|
+
} catch (err) {
|
|
3531
|
+
if (runRef.current !== myRun) return;
|
|
3532
|
+
setError(toDirectoryError(err));
|
|
3533
|
+
setLoading(false);
|
|
3534
|
+
}
|
|
3535
|
+
}, []);
|
|
3536
|
+
|
|
3537
|
+
const queryKey = (() => {
|
|
3538
|
+
try {
|
|
3539
|
+
return JSON.stringify(query);
|
|
3540
|
+
} catch (_e) {
|
|
3541
|
+
return null;
|
|
3542
|
+
}
|
|
3543
|
+
})();
|
|
3544
|
+
useEffect(() => {
|
|
3545
|
+
doFetch();
|
|
3546
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3547
|
+
}, [queryKey]);
|
|
3548
|
+
|
|
3549
|
+
const refetch = useCallback(async () => {
|
|
3550
|
+
await doFetch();
|
|
3551
|
+
}, [doFetch]);
|
|
3552
|
+
|
|
3553
|
+
// Both mutations re-list on success: revoking drops the row and resending
|
|
3554
|
+
// moves `expires_at`, so a stale list would show the operator the old state.
|
|
3555
|
+
const resend = useCallback(
|
|
3556
|
+
async (inviteId) => {
|
|
3557
|
+
try {
|
|
3558
|
+
const row = await invitesRef.current.resend(inviteId);
|
|
3559
|
+
await doFetch();
|
|
3560
|
+
return row;
|
|
3561
|
+
} catch (err) {
|
|
3562
|
+
throw toDirectoryError(err);
|
|
3563
|
+
}
|
|
3564
|
+
},
|
|
3565
|
+
[doFetch],
|
|
3566
|
+
);
|
|
3567
|
+
const revoke = useCallback(
|
|
3568
|
+
async (inviteId) => {
|
|
3569
|
+
try {
|
|
3570
|
+
await invitesRef.current.revoke(inviteId);
|
|
3571
|
+
await doFetch();
|
|
3572
|
+
} catch (err) {
|
|
3573
|
+
throw toDirectoryError(err);
|
|
3574
|
+
}
|
|
3575
|
+
},
|
|
3576
|
+
[doFetch],
|
|
3577
|
+
);
|
|
3578
|
+
|
|
3579
|
+
return { invites, loading, error, refetch, resend, revoke };
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3470
3582
|
/**
|
|
3471
3583
|
* REQ-BANKID-AUTH — link / unlink a BankID identity to the signed-in app-user,
|
|
3472
3584
|
* 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
|
@@ -447,6 +447,7 @@ function _scopeRules(source, manifest) {
|
|
|
447
447
|
);
|
|
448
448
|
const usesUsersHook = /\buseUsers\s*\(/.test(source);
|
|
449
449
|
const usesGroupsHook = /\buseGroups\s*\(/.test(source);
|
|
450
|
+
const usesInvitesHook = /\buseInvites\s*\(/.test(source);
|
|
450
451
|
|
|
451
452
|
if (usesUsersHook) {
|
|
452
453
|
const reads = declared.has("users.read:*") || declared.has("users.read");
|
|
@@ -473,6 +474,22 @@ function _scopeRules(source, manifest) {
|
|
|
473
474
|
}
|
|
474
475
|
}
|
|
475
476
|
|
|
477
|
+
// sc-5097 — the whole invite surface (list, resend, revoke) is gated on
|
|
478
|
+
// the "users.write" capability, and the scope gate accepts only a signed
|
|
479
|
+
// scope matching that key. `invites.*` scopes mint but gate nothing.
|
|
480
|
+
if (usesInvitesHook) {
|
|
481
|
+
const writes =
|
|
482
|
+
declared.has("users.write:*") || declared.has("users.write");
|
|
483
|
+
if (!writes) {
|
|
484
|
+
findings.push({
|
|
485
|
+
rule: "scope-required-for-useInvites",
|
|
486
|
+
label:
|
|
487
|
+
"useInvites() requires `users.write:*` in manifest.requestedScopes",
|
|
488
|
+
line: 0,
|
|
489
|
+
snippet: "",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
476
493
|
const lines = source.split(/\r?\n/);
|
|
477
494
|
let firedUsersWrite = false;
|
|
478
495
|
let firedUsersDelete = false;
|
package/dist/linter.js
CHANGED
|
@@ -522,6 +522,7 @@ function _scopeRules(source, manifest) {
|
|
|
522
522
|
);
|
|
523
523
|
const usesUsersHook = /\buseUsers\s*\(/.test(source);
|
|
524
524
|
const usesGroupsHook = /\buseGroups\s*\(/.test(source);
|
|
525
|
+
const usesInvitesHook = /\buseInvites\s*\(/.test(source);
|
|
525
526
|
|
|
526
527
|
if (usesUsersHook) {
|
|
527
528
|
const reads = declared.has("users.read:*") || declared.has("users.read");
|
|
@@ -547,6 +548,22 @@ function _scopeRules(source, manifest) {
|
|
|
547
548
|
}
|
|
548
549
|
}
|
|
549
550
|
|
|
551
|
+
// sc-5097 — the whole invite surface (list, resend, revoke) is gated on
|
|
552
|
+
// the "users.write" capability, and the scope gate accepts only a signed
|
|
553
|
+
// scope matching that key. `invites.*` scopes mint but gate nothing.
|
|
554
|
+
if (usesInvitesHook) {
|
|
555
|
+
const writes =
|
|
556
|
+
declared.has("users.write:*") || declared.has("users.write");
|
|
557
|
+
if (!writes) {
|
|
558
|
+
findings.push({
|
|
559
|
+
rule: "scope-required-for-useInvites",
|
|
560
|
+
label:
|
|
561
|
+
"useInvites() requires `users.write:*` in manifest.requestedScopes",
|
|
562
|
+
line: 0,
|
|
563
|
+
snippet: "",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
550
567
|
const lines = source.split(/\r?\n/);
|
|
551
568
|
let firedUsersWrite = false;
|
|
552
569
|
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.94.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"
|