@uniweb/api 0.4.1 → 0.5.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
@@ -53,8 +53,9 @@ function Account() {
53
53
 
54
54
  `viewer` is flat — `viewer.uuid`, `viewer.username`, `viewer.handle`, and `viewer.roles`,
55
55
  a list of `{ role, scope_unit_id }`. A member holds no roles (`[]`); the site's operator
56
- holds `system_admin`. ⚠️ `viewer.actingUnitId` is the same for every member of a site, so
57
- it cannot tell an operator from a member. To decide whether to show an edit control for
56
+ holds `system_admin`. ⚠️ `viewer.workspace` — the site service's home org,
57
+ `{ handle: 'home', … }` — is the same for every member, so it cannot tell an operator
58
+ from a member. To decide whether to show an edit control for
58
59
  a particular record, read that record: `entity.can_edit` is the backend's own answer.
59
60
 
60
61
  **Signing up** — `useSignUp().signUp({ username, email, password })` answers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/api",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "The client for a Uniweb site's own backend — session, records, entities — imported by a foundation the way it imports @uniweb/kit.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,7 +34,7 @@
34
34
  "node": ">=20.19"
35
35
  },
36
36
  "dependencies": {
37
- "@uniweb/core": "^0.29.4"
37
+ "@uniweb/core": "^0.29.8"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": "^19.0.0"
package/src/client.js CHANGED
@@ -93,6 +93,16 @@ const LOADING = Object.freeze({ status: 'loading', viewer: null, error: null })
93
93
  * from `website.config` on each call, so the editor's `Website.rebuild()`
94
94
  * needs no hook here.
95
95
  */
96
+
97
+ /**
98
+ * `/auth/me`'s `workspace` — `{ unit_uuid, handle }`, both null when none — as the
99
+ * viewer carries it: `{ unitUuid, handle }`, or `null` for none (`VIEWER`).
100
+ */
101
+ function viewerWorkspace(ws) {
102
+ if (!ws || typeof ws !== 'object' || (ws.unit_uuid == null && ws.handle == null)) return null
103
+ return Object.freeze({ unitUuid: ws.unit_uuid ?? null, handle: ws.handle ?? null })
104
+ }
105
+
96
106
  export class ApiClient {
97
107
  /**
98
108
  * @param {object} uniweb - the page's `Uniweb` singleton
@@ -307,7 +317,7 @@ export class ApiClient {
307
317
  const viewer = Object.freeze({
308
318
  ...account,
309
319
  roles: Array.isArray(me?.roles) ? me.roles : [],
310
- actingUnitId: me?.acting_unit_id ?? null,
320
+ workspace: viewerWorkspace(me?.workspace),
311
321
  })
312
322
  if (this._session.viewer && this._session.viewer.uuid !== viewer.uuid) this.forgetViewer()
313
323
  return this.setSession({ status: 'authenticated', viewer, error: null })
package/src/mock/store.js CHANGED
@@ -32,9 +32,20 @@ import { checkItemWrite, OUTCOME } from './schema-shape.js'
32
32
  * same read), nested sections, second factors, reference fields.
33
33
  */
34
34
 
35
- /** The unit every account of a site acts in — `acting_unit_id` on `/auth/me`. */
35
+ /** The unit every account of a site acts in — an entity's `unit_id`. */
36
36
  export const SITE_UNIT = 1
37
37
 
38
+ /**
39
+ * The service's home org — `workspace` on `/auth/me`. A site's service is founded with
40
+ * one, handle `home`, and enrolls every account in it, the owner included, so a request
41
+ * that names no workspace lands there. Its own value: it is minted in the service's
42
+ * database, and has nothing to do with the workspace the site was authored in.
43
+ */
44
+ export const HOME_ORG = Object.freeze({
45
+ unit_uuid: '01926d5e-0000-7000-8000-0000000000a0',
46
+ handle: 'home',
47
+ })
48
+
38
49
  /** The gap between neighbouring items' order numbers, as the backend spaces them. */
39
50
  const GAP = 1_000_000
40
51
 
@@ -169,7 +180,9 @@ export class MockStore {
169
180
  return {
170
181
  account: this.identity(account),
171
182
  roles: account.operator ? [{ role: 'system_admin', scope_unit_id: null }] : [],
172
- acting_unit_id: SITE_UNIT,
183
+ // A request that names no workspace — as this package's never do — lands in the
184
+ // home org, which every account belongs to. Both null would mean a non-member.
185
+ workspace: { ...HOME_ORG },
173
186
  }
174
187
  }
175
188
 
package/src/wire.js CHANGED
@@ -38,6 +38,7 @@
38
38
  * | a `409` was rebased onto "the first op with an item id" | only the item the stale `409` names (`item_id`, since 2026-09-18); an older backend names none, and a batch is then not rebased — never by guesswork |
39
39
  * | a sign-up answered with the account | it answers `202 { status: 'verification_required', email }`; the account cannot sign in until verified (`403 Email Not Verified`) |
40
40
  * | `acting_unit_id` was a membership signal | every signed-in member of a site acts in the same unit — it cannot tell an operator from a member; `roles` can |
41
+ * | the viewer carried `actingUnitId` | it carries `workspace` — where the request landed: on a site's service, its home org (2026-09-23; `acting_unit_id` leaves the wire) |
41
42
  *
42
43
  * @module @uniweb/api/wire
43
44
  */
@@ -113,7 +114,7 @@ export const AUTH = {
113
114
  * | reset request | `{ email }` | `202 { status: 'reset_requested' }` whether or not the address is known |
114
115
  * | reset confirm | `{ token, new_password, code? }` | `200 { reset: true }`; a bad token is `400 Validation`; `code` is required when a second factor is enrolled |
115
116
  * | logout | none | `204`; with no session to end, `401` |
116
- * | me | none | `200 { account: { uuid, username, handle }, roles, acting_unit_id }` · `401` |
117
+ * | me | none | `200 { account: { uuid, username, handle }, roles, workspace: { unit_uuid, handle } }` · `401` |
117
118
  */
118
119
  export const AUTH_BODY = {
119
120
  login: ['username', 'password'],
@@ -136,16 +137,24 @@ export const TOTP = {
136
137
  * `roles` is a list of `{ role, scope_unit_id }` — `role` one of `system_admin`,
137
138
  * `unit_admin`, `content_editor`, `user`. An ordinary member holds none: `[]`.
138
139
  *
139
- * ⛔ **`acting_unit_id` is NOT a membership or operator signal.** Every signed-in
140
- * member of a site acts in the same unit, so it reads the same for the operator
141
- * and for someone who signed up a minute ago. The operator of a site's `api`
142
- * service holds `system_admin`. Better than either: ask about the thing — a
143
- * single-entity read carries `can_edit`, the write gate's own answer.
140
+ * `workspace` is `{ unit_uuid, handle }` — **the workspace the request works in**. This
141
+ * package's requests name none, and on a site's `api` service such a request lands in
142
+ * the service's **home org**, `handle: 'home'`, in which every account is enrolled.
143
+ * Both `null` there means an account that is not a member — a service account, a
144
+ * revoked membership. (On a backend with no home org, naming none is personal: both
145
+ * `null`.) The viewer carries it as `{ unitUuid, handle }`, or `null` when both are
146
+ * null. *(It replaced `acting_unit_id` on 2026-09-23. MEASURED by backend at the
147
+ * function the route serializes, 2026-09-23.)*
148
+ *
149
+ * ⛔ **Neither is an operator signal.** Every member of a site's service works in the
150
+ * same home org, so it reads the same for the operator and for someone who signed up a
151
+ * minute ago. The operator holds `system_admin`. Better than either: ask about the
152
+ * thing — a single-entity read carries `can_edit`, the write gate's own answer.
144
153
  */
145
154
  export const VIEWER = {
146
155
  account: 'account',
147
156
  roles: 'roles',
148
- actingUnit: 'acting_unit_id',
157
+ workspace: 'workspace',
149
158
  }
150
159
 
151
160
  /**