@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 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 \| not_sent \| open \| all` (endpoint default `all`; `open` = `pending`, `not_sent` or `expired`); `q` matches the invite's name or email, `app_user_id` returns the invites reserving that user id. `resend(inviteId)` sends the invitation — the first send of a `not_sent` one included. |
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.193.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**.
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
- **A widget can prepare an invitation now and send it later.** `useUsers().invite()` always emailed the invitation the moment it was created, so a widget could not line people up ahead of a launch day or let a second person check the list first. `invite()` now takes an optional `send_invitation`:
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 sends / revokes them:
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, sent_at,
2330
- expires_at, accepted_at, revoked_at, created_at }`. `status` is one of `pending`,
2331
- `accepted`, `revoked`, `expired` or `not_sent`; trust the server-computed value rather
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`, `not_sent` or `expired`); a re-invite keeps the id, so without it earlier invites match too.
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, group_ids?, send_invitation? }` and returns
1753
- // the resulting AppUserInvite row (the host sends the email, unless
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?, send_invitation? } and returns the resulting " +
1773
- "AppUserInvite row { id, email, name, expires_at, sent_at, status }. " +
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?, send_invitation? }) => Promise<Invite> // send_invitation: false stores it unsent (status 'not_sent'); rejects with DirectoryError",
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. sc-9714: `resend` is also
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, sent_at, expires_at, …) " +
1848
- "and status is the server-computed pending/accepted/revoked/expired/" +
1849
- "not_sent value. not_sent is an invitation created with " +
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 and " +
1861
- "{ status: 'not_sent' } for the prepared ones; " +
1862
- "status 'open' is every invite neither accepted nor revoked (pending, " +
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, sent_at, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; status is pending | accepted | revoked | expired | not_sent; unwrapped from { data, meta }",
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> // sends it (first send of a not_sent invite too); refetches; rejects with DirectoryError",
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 — resend also sends one created with send_invitation: false — 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:*).",
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
- version: "1.154.0",
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, group_ids?, send_invitation? }` and returns
1753
- // the resulting AppUserInvite row (the host sends the email, unless
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?, send_invitation? } and returns the resulting " +
1773
- "AppUserInvite row { id, email, name, expires_at, sent_at, status }. " +
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?, send_invitation? }) => Promise<Invite> // send_invitation: false stores it unsent (status 'not_sent'); rejects with DirectoryError",
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. sc-9714: `resend` is also
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, sent_at, expires_at, …) " +
1848
- "and status is the server-computed pending/accepted/revoked/expired/" +
1849
- "not_sent value. not_sent is an invitation created with " +
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 and " +
1861
- "{ status: 'not_sent' } for the prepared ones; " +
1862
- "status 'open' is every invite neither accepted nor revoked (pending, " +
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, sent_at, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; status is pending | accepted | revoked | expired | not_sent; unwrapped from { data, meta }",
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> // sends it (first send of a not_sent invite too); refetches; rejects with DirectoryError",
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 — resend also sends one created with send_invitation: false — 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:*).",
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
- version: "1.154.0",
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?, send_invitation? }`) pass through verbatim. `query` is an
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, sent_at, expires_at, accepted_at,
5780
- * revoked_at, created_at, invited_by_studio_user_id }`. `status` is the
5781
- * server-computed lifecycle value (`"pending" | "accepted" | "revoked" |
5782
- * "expired" | "not_sent"`) — prefer it over re-deriving expiry on the client,
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
- * "not_sent" | "all"` and the endpoint defaults to `"all"`, so pass
5792
- * `{ status: "pending" }` for the usual "who hasn't accepted yet" list.
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` (and a first send's status), so a stale list would
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
- /** When the invitation email last left; `null` until it is first sent. */
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 and
2550
- * "not_sent" for the ones prepared but not emailed yet.
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.193.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, group_ids?, send_invitation? }` and returns
1753
- // the resulting AppUserInvite row (the host sends the email, unless
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?, send_invitation? } and returns the resulting " +
1773
- "AppUserInvite row { id, email, name, expires_at, sent_at, status }. " +
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?, send_invitation? }) => Promise<Invite> // send_invitation: false stores it unsent (status 'not_sent'); rejects with DirectoryError",
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. sc-9714: `resend` is also
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, sent_at, expires_at, …) " +
1848
- "and status is the server-computed pending/accepted/revoked/expired/" +
1849
- "not_sent value. not_sent is an invitation created with " +
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 and " +
1861
- "{ status: 'not_sent' } for the prepared ones; " +
1862
- "status 'open' is every invite neither accepted nor revoked (pending, " +
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, sent_at, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; status is pending | accepted | revoked | expired | not_sent; unwrapped from { data, meta }",
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> // sends it (first send of a not_sent invite too); refetches; rejects with DirectoryError",
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 — resend also sends one created with send_invitation: false — 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:*).",
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
- version: "1.154.0",
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,