@colixsystems/widget-sdk 0.193.0 → 0.194.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 -37
- package/dist/contract.cjs +19 -33
- package/dist/contract.js +19 -33
- package/dist/hooks.js +8 -14
- package/dist/index.d.ts +5 -25
- package/package.json +1 -1
- package/src/contract.js +19 -33
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
62
62
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query)` (optional: query) | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
63
63
|
| **DIRECTORY** | `useUsers(query)` (optional: query) | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove, sendPasswordReset }` | `directory.users.*` — `users.read:*` plus the `users.read` (or `users.write`) capability, else `FORBIDDEN` (edits, incl. `sendPasswordReset()`, also `users.write:*`; `remove()` also `users.delete:*`) |
|
|
64
64
|
| **DIRECTORY** | `useGroups(query)` (optional: query) | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
65
|
-
| **DIRECTORY** | `useInvites(query)` (optional: 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?, q?, app_user_id?, limit?, offset? }` with `status` ∈ `pending \| accepted \| revoked \| expired \|
|
|
65
|
+
| **DIRECTORY** | `useInvites(query)` (optional: 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?, q?, app_user_id?, limit?, offset? }` with `status` ∈ `pending \| accepted \| revoked \| expired \| open \| all` (endpoint default `all`; `open` = `pending` or `expired`); `q` matches the invite's name or email, `app_user_id` returns the invites reserving that user id. |
|
|
66
66
|
| **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
|
|
67
67
|
| **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId })` (optional: spaceType, folderId) | `{ openPdf, opening, canOpenPdf, exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.renderPdf` + `ctx.device.pdfViewer`, `ctx.filestore.files.exportPdf` — `files.write:*`. `openPdf(html, { fileName? })` renders the HTML to a PDF server-side and OPENS it (web: PDF tab; native: print / share sheet) without saving. `exportToPdf(html, { fileName?, folderId? })` renders it and saves it as a file (`application/pdf`). Same server-side renderer on web + native. |
|
|
68
68
|
| **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). |
|
|
@@ -77,7 +77,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
77
77
|
|
|
78
78
|
## Status
|
|
79
79
|
|
|
80
|
-
`v0.
|
|
80
|
+
`v0.194.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**.
|
|
81
|
+
|
|
82
|
+
### What's new in 0.194.0 (contract 1.155.0)
|
|
83
|
+
|
|
84
|
+
**Send-later invitations are withdrawn.** 0.183.0 let `useUsers().invite()` take `send_invitation: false` to store an invitation without emailing it. That argument, the `not_sent` invite status and the `sent_at` field are removed: `invite()` emails the invitation at once, as it did before, and `useInvites().resend(inviteId)` re-sends a pending one. A widget that wants to line people up first keeps its own list and calls `invite()` when it is ready.
|
|
81
85
|
|
|
82
86
|
### What's new in 0.193.0 (contract 1.154.0)
|
|
83
87
|
|
|
@@ -250,24 +254,7 @@ An installed widget is not rebuilt or republished for this. The web Player hands
|
|
|
250
254
|
|
|
251
255
|
### What's new in 0.183.0 (contract 1.144.0)
|
|
252
256
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
```js
|
|
256
|
-
const { invite } = useUsers();
|
|
257
|
-
const { invites, resend } = useInvites({ status: "not_sent" });
|
|
258
|
-
|
|
259
|
-
const prepared = await invite({ email: "bo@example.com", name: "Bo", send_invitation: false });
|
|
260
|
-
// prepared.status === "not_sent", prepared.sent_at === null — nobody was emailed.
|
|
261
|
-
|
|
262
|
-
await resend(prepared.id); // emails it; the row is now "pending" with a fresh 7-day expiry
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
- **`send_invitation`** defaults to `true`, so an existing `invite({ email, name, group_ids })` call emails at once exactly as before. `false` stores the invitation and emails nobody. A value that is not a boolean rejects with a `DirectoryError` coded `VALIDATION`.
|
|
266
|
-
- **`not_sent` joins the invite `status` values**, and every invite row carries **`sent_at`** (`null` until the first send). A `not_sent` invitation cannot be accepted yet and its `expires_at` means nothing until it is sent, so show no expiry for it. `useInvites({ status: "not_sent" })` lists just the prepared ones. Where several states apply the first of accepted, revoked, not_sent, expired, pending wins.
|
|
267
|
-
- **`useInvites().resend(inviteId)` is the one call that sends**, whether or not the invitation was sent before. There is no separate `send` method: on a `not_sent` row it is the first send, on a `pending` or `expired` one it sends again, and either way it stamps `sent_at`, restarts the expiry and resolves the row as `pending`. Label the button to match the row — "Send invitation" when `status === "not_sent"`, "Resend" otherwise.
|
|
268
|
-
- **A widget that switches on `status` should handle the new value.** A `not_sent` row falls through an `if (status === "pending")` check, so a list that offers its actions only to pending rows would leave a prepared invitation with no way to send it.
|
|
269
|
-
|
|
270
|
-
The built-in User Management widget offers the choice on its invite form ("Send the invitation later", off by default) and a "Send invitation" action on each row that has not been sent. No new scope: both calls sit under `users.write:*`, as before.
|
|
257
|
+
Send-later invitations (`send_invitation`, the `not_sent` status, `sent_at`). Withdrawn in 0.194.0.
|
|
271
258
|
|
|
272
259
|
### What's new in 0.182.0 (contract 1.143.0)
|
|
273
260
|
|
|
@@ -2294,13 +2281,11 @@ The matching manifest declares the scopes:
|
|
|
2294
2281
|
}
|
|
2295
2282
|
```
|
|
2296
2283
|
|
|
2297
|
-
`invite()` emails the invitation at once. Pass `send_invitation: false` to store it without emailing anyone — the row comes back with `status: "not_sent"` and `sent_at: null` — and send it later with `useInvites().resend(inviteId)`.
|
|
2298
|
-
|
|
2299
2284
|
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.
|
|
2300
2285
|
|
|
2301
2286
|
## Listing pending invitations from a widget
|
|
2302
2287
|
|
|
2303
|
-
`useInvites(query?)` lists the workspace's invites and
|
|
2288
|
+
`useInvites(query?)` lists the workspace's invites and resends / revokes them:
|
|
2304
2289
|
|
|
2305
2290
|
```jsx
|
|
2306
2291
|
import { useInvites, View, Text, Pressable } from '@colixsystems/widget-sdk';
|
|
@@ -2315,9 +2300,7 @@ function PendingInvites() {
|
|
|
2315
2300
|
{invites.map((i) => (
|
|
2316
2301
|
<View key={i.id}>
|
|
2317
2302
|
<Text>{i.email} · {i.status}</Text>
|
|
2318
|
-
<Pressable onPress={() => resend(i.id).catch(() => {})}>
|
|
2319
|
-
<Text>{i.status === 'not_sent' ? 'Send invitation' : 'Resend'}</Text>
|
|
2320
|
-
</Pressable>
|
|
2303
|
+
<Pressable onPress={() => resend(i.id).catch(() => {})}><Text>Resend</Text></Pressable>
|
|
2321
2304
|
<Pressable onPress={() => revoke(i.id).catch(() => {})}><Text>Revoke</Text></Pressable>
|
|
2322
2305
|
</View>
|
|
2323
2306
|
))}
|
|
@@ -2326,16 +2309,9 @@ function PendingInvites() {
|
|
|
2326
2309
|
}
|
|
2327
2310
|
```
|
|
2328
2311
|
|
|
2329
|
-
Rows are snake_case: `{ id, app_user_id, email, name, group_ids, status,
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
than comparing `expires_at` against the device clock.
|
|
2333
|
-
|
|
2334
|
-
A `not_sent` row is an invitation created with `send_invitation: false`: nobody has
|
|
2335
|
-
been emailed and `sent_at` is `null`. `resend(inviteId)` is the one call that sends
|
|
2336
|
-
an invitation, a first send and a repeat send alike — it emails the invitee, stamps
|
|
2337
|
-
`sent_at`, restarts the 7-day expiry and resolves the row as `pending`. Pass
|
|
2338
|
-
`{ status: 'not_sent' }` to list only the prepared ones.
|
|
2312
|
+
Rows are snake_case: `{ id, app_user_id, email, name, group_ids, status, expires_at,
|
|
2313
|
+
accepted_at, revoked_at, created_at }`. Trust the server-computed `status` rather than
|
|
2314
|
+
comparing `expires_at` against the device clock.
|
|
2339
2315
|
|
|
2340
2316
|
`query` also takes `q` (a case-insensitive substring match on the invite's name or
|
|
2341
2317
|
email) and `app_user_id`. Each row's `app_user_id` is the id the invitee's user account
|
|
@@ -2343,7 +2319,7 @@ will have once they accept. A `USER` column may hold that id before the person h
|
|
|
2343
2319
|
joined — it then reads back as `{ id, name, invited: true }`, with no `email` — and
|
|
2344
2320
|
`useInvites({ app_user_id: value.id, status: 'open', limit: 1 })` returns the invite
|
|
2345
2321
|
behind it. `status: 'open'` is every invite that is neither accepted nor revoked
|
|
2346
|
-
(`pending
|
|
2322
|
+
(`pending` or `expired`); a re-invite keeps the id, so without it earlier invites match too.
|
|
2347
2323
|
|
|
2348
2324
|
**The gate is `users.write`, not `users.read`.** A pending invite exposes the email of
|
|
2349
2325
|
someone who is not a member yet, so the backend gates the entire invite surface —
|
package/dist/contract.cjs
CHANGED
|
@@ -1749,9 +1749,8 @@ const HOOKS = [
|
|
|
1749
1749
|
// deactivate / reactivate / sendPasswordReset) additionally need
|
|
1750
1750
|
// `users.write:*`, and the
|
|
1751
1751
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
1752
|
-
// call accepts `{ email, name,
|
|
1753
|
-
//
|
|
1754
|
-
// `send_invitation` is false). Mutating users from
|
|
1752
|
+
// call accepts `{ email, name, groupIds? }` and returns the resulting
|
|
1753
|
+
// AppUserInvite row (the email is sent by the host). Mutating users from
|
|
1755
1754
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
1756
1755
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
1757
1756
|
// that only call read methods need only `users.read:*`.
|
|
@@ -1769,13 +1768,8 @@ const HOOKS = [
|
|
|
1769
1768
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1770
1769
|
"sendPasswordReset) need users.write:*, and the destructive remove() " +
|
|
1771
1770
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
1772
|
-
"{ email, name, group_ids
|
|
1773
|
-
"
|
|
1774
|
-
"The host emails the invitation at once unless send_invitation is " +
|
|
1775
|
-
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
1776
|
-
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
1777
|
-
"later. A send_invitation that is not a boolean rejects with " +
|
|
1778
|
-
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
1771
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
1772
|
+
"(the email is sent by the host). sendPasswordReset(userId) mails the " +
|
|
1779
1773
|
"standard self-serve reset link to that user's own registered address " +
|
|
1780
1774
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
1781
1775
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -1788,7 +1782,7 @@ const HOOKS = [
|
|
|
1788
1782
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1789
1783
|
refetch: "() => Promise<void>",
|
|
1790
1784
|
invite:
|
|
1791
|
-
"({ email, name, group_ids
|
|
1785
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
1792
1786
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1793
1787
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1794
1788
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -1833,8 +1827,7 @@ const HOOKS = [
|
|
|
1833
1827
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
1834
1828
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1835
1829
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1836
|
-
// caller's `users.write` SystemAcl capability.
|
|
1837
|
-
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
1830
|
+
// caller's `users.write` SystemAcl capability.
|
|
1838
1831
|
{
|
|
1839
1832
|
name: "useInvites",
|
|
1840
1833
|
signature: "useInvites(query)",
|
|
@@ -1844,23 +1837,13 @@ const HOOKS = [
|
|
|
1844
1837
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1845
1838
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1846
1839
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1847
|
-
"res.data; rows are snake_case (email, status,
|
|
1848
|
-
"
|
|
1849
|
-
"
|
|
1850
|
-
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
1851
|
-
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
1852
|
-
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
1853
|
-
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
1854
|
-
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
1855
|
-
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
1856
|
-
"'pending'. There is no separate send method; label the button " +
|
|
1857
|
-
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
1858
|
-
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1840
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1841
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1842
|
+
"value. query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1859
1843
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
1860
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
1861
|
-
"
|
|
1862
|
-
"
|
|
1863
|
-
"not_sent or expired). q is a " +
|
|
1844
|
+
"{ status: 'pending' } for the usual outstanding-invite list; " +
|
|
1845
|
+
"status 'open' is every invite neither accepted nor revoked (pending " +
|
|
1846
|
+
"or expired). q is a " +
|
|
1864
1847
|
"case-insensitive substring match on the invite's name or email. " +
|
|
1865
1848
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
1866
1849
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -1877,12 +1860,12 @@ const HOOKS = [
|
|
|
1877
1860
|
"refetch on success.",
|
|
1878
1861
|
returnShape: {
|
|
1879
1862
|
invites:
|
|
1880
|
-
"Array<{ id, app_user_id, email, name, group_ids, status,
|
|
1863
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1881
1864
|
loading: "boolean",
|
|
1882
1865
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1883
1866
|
refetch: "() => Promise<void>",
|
|
1884
1867
|
resend:
|
|
1885
|
-
"(inviteId) => Promise<Invite> //
|
|
1868
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1886
1869
|
revoke:
|
|
1887
1870
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1888
1871
|
},
|
|
@@ -3049,7 +3032,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3049
3032
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
3050
3033
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
3051
3034
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
3052
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites
|
|
3035
|
+
"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:*).",
|
|
3053
3036
|
required: true,
|
|
3054
3037
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
3055
3038
|
},
|
|
@@ -5081,7 +5064,10 @@ const CONTRACT = deepFreeze({
|
|
|
5081
5064
|
// `scope: { kind: "granted" }` (the rows the signed-in user reads through
|
|
5082
5065
|
// a user or group grant) and the `onAccessGained` / `onAccessLost`
|
|
5083
5066
|
// handlers that report rows entering and leaving that set.
|
|
5084
|
-
|
|
5067
|
+
// 1.155.0: narrowing (sc-10566) — withdraws 1.144.0: `useUsers().invite`
|
|
5068
|
+
// takes no `send_invitation` and always emails, invite rows carry no
|
|
5069
|
+
// `sent_at`, and `not_sent` is no longer a status.
|
|
5070
|
+
version: "1.155.0",
|
|
5085
5071
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5086
5072
|
hooks: HOOKS,
|
|
5087
5073
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1749,9 +1749,8 @@ const HOOKS = [
|
|
|
1749
1749
|
// deactivate / reactivate / sendPasswordReset) additionally need
|
|
1750
1750
|
// `users.write:*`, and the
|
|
1751
1751
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
1752
|
-
// call accepts `{ email, name,
|
|
1753
|
-
//
|
|
1754
|
-
// `send_invitation` is false). Mutating users from
|
|
1752
|
+
// call accepts `{ email, name, groupIds? }` and returns the resulting
|
|
1753
|
+
// AppUserInvite row (the email is sent by the host). Mutating users from
|
|
1755
1754
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
1756
1755
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
1757
1756
|
// that only call read methods need only `users.read:*`.
|
|
@@ -1769,13 +1768,8 @@ const HOOKS = [
|
|
|
1769
1768
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1770
1769
|
"sendPasswordReset) need users.write:*, and the destructive remove() " +
|
|
1771
1770
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
1772
|
-
"{ email, name, group_ids
|
|
1773
|
-
"
|
|
1774
|
-
"The host emails the invitation at once unless send_invitation is " +
|
|
1775
|
-
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
1776
|
-
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
1777
|
-
"later. A send_invitation that is not a boolean rejects with " +
|
|
1778
|
-
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
1771
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
1772
|
+
"(the email is sent by the host). sendPasswordReset(userId) mails the " +
|
|
1779
1773
|
"standard self-serve reset link to that user's own registered address " +
|
|
1780
1774
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
1781
1775
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -1788,7 +1782,7 @@ const HOOKS = [
|
|
|
1788
1782
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1789
1783
|
refetch: "() => Promise<void>",
|
|
1790
1784
|
invite:
|
|
1791
|
-
"({ email, name, group_ids
|
|
1785
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
1792
1786
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1793
1787
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1794
1788
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -1833,8 +1827,7 @@ const HOOKS = [
|
|
|
1833
1827
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
1834
1828
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1835
1829
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1836
|
-
// caller's `users.write` SystemAcl capability.
|
|
1837
|
-
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
1830
|
+
// caller's `users.write` SystemAcl capability.
|
|
1838
1831
|
{
|
|
1839
1832
|
name: "useInvites",
|
|
1840
1833
|
signature: "useInvites(query)",
|
|
@@ -1844,23 +1837,13 @@ const HOOKS = [
|
|
|
1844
1837
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1845
1838
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1846
1839
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1847
|
-
"res.data; rows are snake_case (email, status,
|
|
1848
|
-
"
|
|
1849
|
-
"
|
|
1850
|
-
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
1851
|
-
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
1852
|
-
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
1853
|
-
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
1854
|
-
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
1855
|
-
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
1856
|
-
"'pending'. There is no separate send method; label the button " +
|
|
1857
|
-
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
1858
|
-
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1840
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1841
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1842
|
+
"value. query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1859
1843
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
1860
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
1861
|
-
"
|
|
1862
|
-
"
|
|
1863
|
-
"not_sent or expired). q is a " +
|
|
1844
|
+
"{ status: 'pending' } for the usual outstanding-invite list; " +
|
|
1845
|
+
"status 'open' is every invite neither accepted nor revoked (pending " +
|
|
1846
|
+
"or expired). q is a " +
|
|
1864
1847
|
"case-insensitive substring match on the invite's name or email. " +
|
|
1865
1848
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
1866
1849
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -1877,12 +1860,12 @@ const HOOKS = [
|
|
|
1877
1860
|
"refetch on success.",
|
|
1878
1861
|
returnShape: {
|
|
1879
1862
|
invites:
|
|
1880
|
-
"Array<{ id, app_user_id, email, name, group_ids, status,
|
|
1863
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1881
1864
|
loading: "boolean",
|
|
1882
1865
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1883
1866
|
refetch: "() => Promise<void>",
|
|
1884
1867
|
resend:
|
|
1885
|
-
"(inviteId) => Promise<Invite> //
|
|
1868
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1886
1869
|
revoke:
|
|
1887
1870
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1888
1871
|
},
|
|
@@ -3045,7 +3028,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3045
3028
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
3046
3029
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
3047
3030
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
3048
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites
|
|
3031
|
+
"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:*).",
|
|
3049
3032
|
required: true,
|
|
3050
3033
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
3051
3034
|
},
|
|
@@ -5077,7 +5060,10 @@ const CONTRACT = deepFreeze({
|
|
|
5077
5060
|
// `scope: { kind: "granted" }` (the rows the signed-in user reads through
|
|
5078
5061
|
// a user or group grant) and the `onAccessGained` / `onAccessLost`
|
|
5079
5062
|
// handlers that report rows entering and leaving that set.
|
|
5080
|
-
|
|
5063
|
+
// 1.155.0: narrowing (sc-10566) — withdraws 1.144.0: `useUsers().invite`
|
|
5064
|
+
// takes no `send_invitation` and always emails, invite rows carry no
|
|
5065
|
+
// `sent_at`, and `not_sent` is no longer a status.
|
|
5066
|
+
version: "1.155.0",
|
|
5081
5067
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5082
5068
|
hooks: HOOKS,
|
|
5083
5069
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -5539,7 +5539,7 @@ export function useDirectory(query) {
|
|
|
5539
5539
|
* invite, deactivate, reactivate}` — `list` resolves to the `{ data, meta }`
|
|
5540
5540
|
* envelope VERBATIM, so we unwrap `res.data` (default `[]`). User rows are
|
|
5541
5541
|
* snake_case (`id`, `name`, `email`, `role`, `is_active`, …) and bodies
|
|
5542
|
-
* (e.g. `{ email, name, group_ids
|
|
5542
|
+
* (e.g. `{ email, name, group_ids? }`) pass through verbatim. `query` is an
|
|
5543
5543
|
* optional `{ q?, role?, is_active?, group_id?, limit?, offset? }`; `group_id`
|
|
5544
5544
|
* (the `id` of a `useGroups()` row) narrows the list to that group's members.
|
|
5545
5545
|
* The list refetches whenever `JSON.stringify(query)` changes; the imperative methods
|
|
@@ -5776,20 +5776,15 @@ export function useGroups(query) {
|
|
|
5776
5776
|
* `{ data, meta }` envelope VERBATIM, so we unwrap `res.data` (default `[]`).
|
|
5777
5777
|
*
|
|
5778
5778
|
* Rows are snake_case exactly as the backend projects them: `{ id, tenant_id,
|
|
5779
|
-
* email, name, group_ids, status,
|
|
5780
|
-
*
|
|
5781
|
-
*
|
|
5782
|
-
*
|
|
5783
|
-
* whose clock may disagree.
|
|
5784
|
-
*
|
|
5785
|
-
* `not_sent` is an invite created with `send_invitation: false`: `sent_at` is
|
|
5786
|
-
* null and nothing was emailed. `resend(inviteId)` is the one call that sends
|
|
5787
|
-
* — a first send and a repeat send alike — and resolves the row as `pending`.
|
|
5779
|
+
* email, name, group_ids, status, expires_at, accepted_at, revoked_at,
|
|
5780
|
+
* created_at, invited_by_studio_user_id }`. `status` is the server-computed
|
|
5781
|
+
* lifecycle value (`"pending" | "accepted" | "revoked" | "expired"`) — prefer
|
|
5782
|
+
* it over re-deriving expiry on the client, whose clock may disagree.
|
|
5788
5783
|
*
|
|
5789
5784
|
* `query` is an optional `{ status?, limit?, offset? }` passed through
|
|
5790
5785
|
* verbatim; `status` accepts `"pending" | "accepted" | "revoked" | "expired" |
|
|
5791
|
-
* "
|
|
5792
|
-
*
|
|
5786
|
+
* "all"` and the endpoint defaults to `"all"`, so pass `{ status: "pending" }`
|
|
5787
|
+
* for the usual "who hasn't accepted yet" list.
|
|
5793
5788
|
*
|
|
5794
5789
|
* The WHOLE invite surface — list included — is gated on the `users.write`
|
|
5795
5790
|
* capability plus a signed `users.write:*` scope: a pending invite exposes
|
|
@@ -5856,8 +5851,7 @@ export function useInvites(query) {
|
|
|
5856
5851
|
}, [doFetch]);
|
|
5857
5852
|
|
|
5858
5853
|
// Both mutations re-list on success: revoking drops the row and resending
|
|
5859
|
-
// moves `expires_at
|
|
5860
|
-
// show the operator the old state.
|
|
5854
|
+
// moves `expires_at`, so a stale list would show the operator the old state.
|
|
5861
5855
|
const resend = useCallback(
|
|
5862
5856
|
async (inviteId) => {
|
|
5863
5857
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -2442,25 +2442,13 @@ export interface InviteArgs {
|
|
|
2442
2442
|
email: string;
|
|
2443
2443
|
name?: string;
|
|
2444
2444
|
group_ids?: string[];
|
|
2445
|
-
/**
|
|
2446
|
-
* Defaults to `true`: the invitation is emailed at once. `false` stores it
|
|
2447
|
-
* without emailing anyone (`status: "not_sent"`); send it later with
|
|
2448
|
-
* `useInvites().resend(inviteId)`.
|
|
2449
|
-
*/
|
|
2450
|
-
send_invitation?: boolean;
|
|
2451
2445
|
}
|
|
2452
2446
|
|
|
2453
|
-
/**
|
|
2454
|
-
* When several apply the first of accepted, revoked, not_sent, expired,
|
|
2455
|
-
* pending wins. `not_sent` is an invitation created with
|
|
2456
|
-
* `send_invitation: false` that has not been emailed yet.
|
|
2457
|
-
*/
|
|
2458
2447
|
export type AppUserInviteStatus =
|
|
2459
2448
|
| "pending"
|
|
2460
2449
|
| "accepted"
|
|
2461
2450
|
| "revoked"
|
|
2462
|
-
| "expired"
|
|
2463
|
-
| "not_sent";
|
|
2451
|
+
| "expired";
|
|
2464
2452
|
|
|
2465
2453
|
export interface AppUserInviteRow {
|
|
2466
2454
|
id: string;
|
|
@@ -2475,9 +2463,7 @@ export interface AppUserInviteRow {
|
|
|
2475
2463
|
status: AppUserInviteStatus;
|
|
2476
2464
|
name?: string;
|
|
2477
2465
|
group_ids?: string[];
|
|
2478
|
-
|
|
2479
|
-
sent_at: string | null;
|
|
2480
|
-
/** Meaningless while `status` is `"not_sent"` — it restarts on the send. */
|
|
2466
|
+
invited_at?: string;
|
|
2481
2467
|
expires_at?: string;
|
|
2482
2468
|
accepted_at?: string | null;
|
|
2483
2469
|
revoked_at?: string | null;
|
|
@@ -2546,9 +2532,8 @@ export function useGroups(query?: GroupsQuery): GroupsApi;
|
|
|
2546
2532
|
|
|
2547
2533
|
export interface InvitesQuery {
|
|
2548
2534
|
/**
|
|
2549
|
-
* Defaults to "all" server-side; pass "pending" for outstanding invites
|
|
2550
|
-
* "
|
|
2551
|
-
* "open" is "pending", "not_sent" or "expired": neither accepted nor revoked.
|
|
2535
|
+
* Defaults to "all" server-side; pass "pending" for outstanding invites.
|
|
2536
|
+
* "open" is "pending" or "expired": neither accepted nor revoked.
|
|
2552
2537
|
*/
|
|
2553
2538
|
status?: AppUserInviteStatus | "open" | "all";
|
|
2554
2539
|
/** Case-insensitive substring match on the invite's name or email. */
|
|
@@ -2569,12 +2554,7 @@ export interface InvitesApi {
|
|
|
2569
2554
|
loading: boolean;
|
|
2570
2555
|
error: DirectoryError | null;
|
|
2571
2556
|
refetch(): Promise<void>;
|
|
2572
|
-
/**
|
|
2573
|
-
* Email the invitation and restart its 7-day expiry. The one call that
|
|
2574
|
-
* sends: the first send of a `not_sent` invitation and a repeat send of a
|
|
2575
|
-
* sent one alike. Resolves the row with `status: "pending"`; refetches on
|
|
2576
|
-
* success.
|
|
2577
|
-
*/
|
|
2557
|
+
/** Re-send the invitation email. Refetches on success. */
|
|
2578
2558
|
resend(inviteId: string): Promise<AppUserInviteRow>;
|
|
2579
2559
|
/** Cancel a pending invitation. Refetches on success. */
|
|
2580
2560
|
revoke(inviteId: string): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.194.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -1749,9 +1749,8 @@ const HOOKS = [
|
|
|
1749
1749
|
// deactivate / reactivate / sendPasswordReset) additionally need
|
|
1750
1750
|
// `users.write:*`, and the
|
|
1751
1751
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
1752
|
-
// call accepts `{ email, name,
|
|
1753
|
-
//
|
|
1754
|
-
// `send_invitation` is false). Mutating users from
|
|
1752
|
+
// call accepts `{ email, name, groupIds? }` and returns the resulting
|
|
1753
|
+
// AppUserInvite row (the email is sent by the host). Mutating users from
|
|
1755
1754
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
1756
1755
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
1757
1756
|
// that only call read methods need only `users.read:*`.
|
|
@@ -1769,13 +1768,8 @@ const HOOKS = [
|
|
|
1769
1768
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1770
1769
|
"sendPasswordReset) need users.write:*, and the destructive remove() " +
|
|
1771
1770
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
1772
|
-
"{ email, name, group_ids
|
|
1773
|
-
"
|
|
1774
|
-
"The host emails the invitation at once unless send_invitation is " +
|
|
1775
|
-
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
1776
|
-
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
1777
|
-
"later. A send_invitation that is not a boolean rejects with " +
|
|
1778
|
-
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
1771
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
1772
|
+
"(the email is sent by the host). sendPasswordReset(userId) mails the " +
|
|
1779
1773
|
"standard self-serve reset link to that user's own registered address " +
|
|
1780
1774
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
1781
1775
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -1788,7 +1782,7 @@ const HOOKS = [
|
|
|
1788
1782
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1789
1783
|
refetch: "() => Promise<void>",
|
|
1790
1784
|
invite:
|
|
1791
|
-
"({ email, name, group_ids
|
|
1785
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
1792
1786
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1793
1787
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1794
1788
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -1833,8 +1827,7 @@ const HOOKS = [
|
|
|
1833
1827
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
1834
1828
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1835
1829
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1836
|
-
// caller's `users.write` SystemAcl capability.
|
|
1837
|
-
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
1830
|
+
// caller's `users.write` SystemAcl capability.
|
|
1838
1831
|
{
|
|
1839
1832
|
name: "useInvites",
|
|
1840
1833
|
signature: "useInvites(query)",
|
|
@@ -1844,23 +1837,13 @@ const HOOKS = [
|
|
|
1844
1837
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1845
1838
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1846
1839
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1847
|
-
"res.data; rows are snake_case (email, status,
|
|
1848
|
-
"
|
|
1849
|
-
"
|
|
1850
|
-
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
1851
|
-
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
1852
|
-
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
1853
|
-
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
1854
|
-
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
1855
|
-
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
1856
|
-
"'pending'. There is no separate send method; label the button " +
|
|
1857
|
-
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
1858
|
-
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1840
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1841
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1842
|
+
"value. query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1859
1843
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
1860
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
1861
|
-
"
|
|
1862
|
-
"
|
|
1863
|
-
"not_sent or expired). q is a " +
|
|
1844
|
+
"{ status: 'pending' } for the usual outstanding-invite list; " +
|
|
1845
|
+
"status 'open' is every invite neither accepted nor revoked (pending " +
|
|
1846
|
+
"or expired). q is a " +
|
|
1864
1847
|
"case-insensitive substring match on the invite's name or email. " +
|
|
1865
1848
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
1866
1849
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -1877,12 +1860,12 @@ const HOOKS = [
|
|
|
1877
1860
|
"refetch on success.",
|
|
1878
1861
|
returnShape: {
|
|
1879
1862
|
invites:
|
|
1880
|
-
"Array<{ id, app_user_id, email, name, group_ids, status,
|
|
1863
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1881
1864
|
loading: "boolean",
|
|
1882
1865
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1883
1866
|
refetch: "() => Promise<void>",
|
|
1884
1867
|
resend:
|
|
1885
|
-
"(inviteId) => Promise<Invite> //
|
|
1868
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1886
1869
|
revoke:
|
|
1887
1870
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1888
1871
|
},
|
|
@@ -3045,7 +3028,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3045
3028
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
3046
3029
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
3047
3030
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
3048
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites
|
|
3031
|
+
"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:*).",
|
|
3049
3032
|
required: true,
|
|
3050
3033
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
3051
3034
|
},
|
|
@@ -5077,7 +5060,10 @@ const CONTRACT = deepFreeze({
|
|
|
5077
5060
|
// `scope: { kind: "granted" }` (the rows the signed-in user reads through
|
|
5078
5061
|
// a user or group grant) and the `onAccessGained` / `onAccessLost`
|
|
5079
5062
|
// handlers that report rows entering and leaving that set.
|
|
5080
|
-
|
|
5063
|
+
// 1.155.0: narrowing (sc-10566) — withdraws 1.144.0: `useUsers().invite`
|
|
5064
|
+
// takes no `send_invitation` and always emails, invite rows carry no
|
|
5065
|
+
// `sent_at`, and `not_sent` is no longer a status.
|
|
5066
|
+
version: "1.155.0",
|
|
5081
5067
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5082
5068
|
hooks: HOOKS,
|
|
5083
5069
|
primitives: PRIMITIVES,
|