@uniweb/api 0.6.9 → 0.7.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
@@ -1,10 +1,10 @@
1
1
  # @uniweb/api
2
2
 
3
- The client a foundation uses to talk to **its site's `api` service** — sign-in, the
3
+ The client a foundation uses to talk to **its site's `backend` service** — sign-in, the
4
4
  site's members, and the content they create.
5
5
 
6
6
  You import it the way you import `@uniweb/kit`. It is bundled into your foundation,
7
- tree-shaken, and **inert on a site that has no `api` service**: nothing throws, no request
7
+ tree-shaken, and **inert on a site that has no `backend` service**: nothing throws, no request
8
8
  leaves, and your components render the version of themselves that does not need one.
9
9
 
10
10
  ```bash
@@ -17,9 +17,9 @@ Ask before you draw. This is a synchronous read of the site's own configuration,
17
17
  a probe — there is nothing to await.
18
18
 
19
19
  ```jsx
20
- import { isApiEnabled } from '@uniweb/kit'
20
+ import { isBackendEnabled } from '@uniweb/kit'
21
21
 
22
- if (!isApiEnabled()) return <StaticVersion />
22
+ if (!isBackendEnabled()) return <StaticVersion />
23
23
  ```
24
24
 
25
25
  ⛔ **And when the answer is no, draw nothing** — not a disabled button, and not an
@@ -79,7 +79,7 @@ const { status, records, hasMore } = useRecords({ schema: '@acme/session', scope
79
79
  ```
80
80
 
81
81
  ⭐ **`absent` and an empty `ready` are different answers.** `absent` means there is no
82
- live source — no `api` service, or nobody signed in — so render your site's own content.
82
+ live source — no `backend` service, or nobody signed in — so render your site's own content.
83
83
  `ready` with `records: []` means the service answered and there is nothing there.
84
84
  Showing "nothing yet" for the first case tells a visitor their content is missing
85
85
  when it is simply not being asked for.
@@ -170,8 +170,9 @@ Building against a live service is slow and puts a shared database behind your
170
170
  experiments. Name a local one in `site.yml`:
171
171
 
172
172
  ```yaml
173
- api: /_api # where the service answers — the same in production
174
- $devApi: ./mock/api.js # what answers it locally; never published
173
+ services:
174
+ backend: true # ask your host for it, on a site you publish
175
+ $devBackend: ./mock/api.js # what answers it in `uniweb dev`; never published
175
176
  ```
176
177
 
177
178
  ```js
@@ -203,8 +204,9 @@ export default createMockBackend({
203
204
  }).fetch
204
205
  ```
205
206
 
206
- `uniweb dev` mounts it at your `api:` address — same origin, so cookies behave as they
207
- will in production, and your site's configuration is identical either way.
207
+ `uniweb dev` mounts it at an address of its own on the dev server, and hands your site that
208
+ address the way a host hands over its own — same origin, so cookies behave as they will in
209
+ production, and nothing in your site's configuration changes between the two.
208
210
 
209
211
  ⭐ **The mock answers what the backend answers** — the same statuses, bodies and
210
212
  refusals, for every route this package uses. It enforces what your schemas declare —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/api",
3
- "version": "0.6.9",
3
+ "version": "0.7.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.36.0"
37
+ "@uniweb/core": "^0.38.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": "^19.0.0"
package/src/client.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @uniweb/api — the client half. No React in this module.
3
3
  *
4
4
  * A foundation's client for the site's own backend — the one a host declares
5
- * as the site service `api`. Absent that declaration the site has no such
5
+ * as the site service `backend`. Absent that declaration the site has no such
6
6
  * backend and everything here is inert: no request leaves, and a component
7
7
  * renders for that state rather than retrying it.
8
8
  *
@@ -18,8 +18,16 @@ import { composeUrl, isCrossOrigin, readBody, UNSAFE } from './http.js'
18
18
  import { AUTH, ROUTES, PARAM, FIELD, LIST, OP, GUARDED_OPS, CREATE, READ, SCHEMA, PAGE, TOTP } from './wire.js'
19
19
  import { Ledger } from './ledger.js'
20
20
 
21
- /** The site service this package reads its base from — the only name it owns. */
22
- export const SERVICE_NAME = 'api'
21
+ /**
22
+ * The site service this package reads its base from — the only name it owns.
23
+ *
24
+ * ⛔ **It was `api` until 2026-10-07.** A service is named for what it gives the
25
+ * site — `search`, `records` — and this one is the site's own backend; `/api` is
26
+ * only the interface it answers on, which is what this package is named for. A
27
+ * foundation built before then asks for `api`, which no host offers now, and draws
28
+ * no sign-in until it is rebuilt.
29
+ */
30
+ export const SERVICE_NAME = 'backend'
23
31
 
24
32
  /**
25
33
  * The shape of the shared instance, as a contract between copies of this
@@ -31,10 +39,10 @@ export const CONTRACT = 1
31
39
  /**
32
40
  * Where the site's backend is, if it has one.
33
41
  *
34
- * `resolveService` answers with the host's offer first (`config.services.api`),
35
- * then the site's own declaration (`api:` in `site.yml`), and `null` when neither
36
- * names an address. Absence is the ordinary state of a site with no backend,
37
- * not an error.
42
+ * `resolveService` answers with the host's offer first (`config.services.backend`),
43
+ * then the site's own address (`backend:` under `services:` in `site.yml`), and
44
+ * `null` when neither names one. Absence is the ordinary state of a site with no
45
+ * backend, not an error.
38
46
  *
39
47
  * @param {object} website - the active Website, or anything shaped `{ config, basePath }`
40
48
  * @returns {string|null} the base every request is made against, or null
@@ -44,34 +52,35 @@ export function resolveBase(website) {
44
52
  }
45
53
 
46
54
  /**
47
- * Does this site have an app backend — the `api` service?
55
+ * Does this site have an app backend — the `backend` service?
48
56
  *
49
57
  * The question to ask before drawing a sign-in affordance or any control only a
50
58
  * backend can answer. False means: draw nothing, or the static alternative the
51
59
  * site already carries.
52
60
  *
53
61
  * ⭐ **The website argument is OPTIONAL and defaults to the active one**, so this
54
- * is the same call as `@uniweb/kit`'s `isApiEnabled()` — one predicate per
62
+ * is the same call as `@uniweb/kit`'s `isBackendEnabled()` — one predicate per
55
63
  * service, no arguments, the website resolved for you. Pass one explicitly only
56
64
  * when you already hold it, or are working outside a render.
57
65
  *
58
66
  * ⛔ **THE DEFAULT IS NOT A CONVENIENCE, IT CLOSES A TRAP.** For one commit this
59
67
  * took a REQUIRED website while every doc and template showed the no-argument
60
- * spelling — so `import { isApiEnabled } from '@uniweb/api'` followed by
61
- * `isApiEnabled()` resolved `undefined`, returned `false` forever, and drew no
62
- * sign-in UI on a site that had a backend. Silent, and indistinguishable from a
63
- * site with no backend: exactly the invisible absence this predicate exists to
64
- * prevent. Caught in review the same day; pinned by `tests/enabled.test.js`.
68
+ * spelling — so the documented call resolved `undefined`, returned `false`
69
+ * forever, and drew no sign-in UI on a site that had a backend. Silent, and
70
+ * indistinguishable from a site with no backend: exactly the invisible absence
71
+ * this predicate exists to prevent. Caught in review the same day; pinned by
72
+ * `tests/enabled.test.js`.
65
73
  *
66
- * ⛔ **Renamed from `isEnabled` (2026-09-10).** The old name said nothing about
67
- * its subject and collided with three unrelated `isEnabled`s in the framework —
68
- * including the field `useSearch()` returns, which made `uniweb doctor` read a
69
- * search control as gated when it was gated on this instead.
74
+ * ⛔ **Renamed twice.** From `isEnabled` (2026-09-10): the old name said nothing
75
+ * about its subject and collided with three unrelated `isEnabled`s in the
76
+ * framework — including the field `useSearch()` returns, which made `uniweb
77
+ * doctor` read a search control as gated when it was gated on this instead. Then
78
+ * from `isApiEnabled` (2026-10-07), with the service it asks about (`SERVICE_NAME`).
70
79
  *
71
80
  * @param {object} [website] - defaults to the active website
72
81
  * @returns {boolean}
73
82
  */
74
- export function isApiEnabled(website = getUniweb()?.activeWebsite ?? null) {
83
+ export function isBackendEnabled(website = getUniweb()?.activeWebsite ?? null) {
75
84
  return resolveBase(website) !== null
76
85
  }
77
86
 
@@ -18,7 +18,7 @@ const DISABLED = Object.freeze({ status: 'absent', records: NONE, matched: 0, ha
18
18
  * ## ⭐ `absent` and an empty `ready` are DIFFERENT, and conflating them is the bug
19
19
  *
20
20
  * `absent` means **there is no live source for this viewer** — a site with no
21
- * `api` service, which is the ordinary standalone case, or nobody signed in, since
21
+ * `backend` service, which is the ordinary standalone case, or nobody signed in, since
22
22
  * the backend lists nothing to an anonymous caller (it answers `401`, so no request
23
23
  * is sent once the session is known to be anonymous). `ready` with `records: []`
24
24
  * means **the source answered, and there is nothing there.**
package/src/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  export {
14
14
  SERVICE_NAME,
15
15
  resolveBase,
16
- isApiEnabled,
16
+ isBackendEnabled,
17
17
  probeSession,
18
18
  signIn,
19
19
  completeChallenge,
package/src/mock/index.js CHANGED
@@ -3,7 +3,7 @@ import { MockStore, isUuid, refuse } from './store.js'
3
3
  import { DEFAULT_SEED } from './seed.js'
4
4
 
5
5
  /**
6
- * A mock of a site's `api` service, for local development.
6
+ * A mock of a site's `backend` service, for local development.
7
7
  *
8
8
  * ```js
9
9
  * import { createMockBackend } from '@uniweb/api/mock'
@@ -29,7 +29,7 @@ import { DEFAULT_SEED } from './seed.js'
29
29
  * ## Where it answers
30
30
  *
31
31
  * Its routes are the API's own — `/auth/me`, `/entities` — under whatever the
32
- * site's `api` address is. It accepts a request with the site's `/_api` prefix
32
+ * site's `backend` address is. It accepts a request with the site's `/_api` prefix
33
33
  * still on, and one with an extra `/api` after it (how clients before 0.4
34
34
  * composed URLs), so it answers the same whether or not the server mounting it
35
35
  * strips its path.
package/src/mock/node.js CHANGED
@@ -40,7 +40,7 @@ async function send(response, res) {
40
40
 
41
41
  /**
42
42
  * Connect-style middleware — what Vite's `configureServer` takes. Mount it at the
43
- * site's `api` address (`/_api`), which is the base the client composes routes
43
+ * site's `backend` address (`/_api`), which is the base the client composes routes
44
44
  * onto: `/_api/auth/me`, `/_api/entities`.
45
45
  *
46
46
  * ```js
@@ -76,7 +76,7 @@ export function middleware(mock, { prefix = '/_api' } = {}) {
76
76
 
77
77
  /**
78
78
  * A standalone server. Resolves once listening; call `close()` to stop. Its routes
79
- * answer at the root — a site's `api` address for it is the server's URL
79
+ * answer at the root — a site's `backend` address for it is the server's URL
80
80
  * (`http://localhost:8787`), plus `prefix` when one is given.
81
81
  *
82
82
  * @param {{ fetch: Function }} mock
package/src/mock/store.js CHANGED
@@ -6,7 +6,7 @@ import { checkItemWrite, OUTCOME } from './schema-shape.js'
6
6
  * entities whose content is items.
7
7
  *
8
8
  * ⭐ **It stores what the backend stores, the way the backend stores it** —
9
- * measured 2026-09-18 against a real one set up as a site's `api` service:
9
+ * measured 2026-09-18 against a real one set up as a site's `backend` service:
10
10
  *
11
11
  * - **An entity's content is items, each in a section of its Model.** There is no
12
12
  * entity-level data. A `single` section holds one item; its `brief` section's item
package/src/wire.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * ## ⭐ MEASURED 2026-09-18 — every shape below, in literal responses
23
23
  *
24
24
  * The whole surface this package touches was exercised against a real backend set
25
- * up the way a site's `api` service is: one operator account, members who sign up
25
+ * up the way a site's `backend` service is: one operator account, members who sign up
26
26
  * themselves, and the site's own Models. Where this module quotes a shape it was
27
27
  * copied from a response, not described. That pass corrected the package in these
28
28
  * places, each now fixed and pinned by a test:
@@ -47,7 +47,7 @@
47
47
  * The lane. ⭐ **RULED** *(2026-09-01)*: this package reads and writes
48
48
  * **entities**, reads the definitions of their Models, and touches nothing under
49
49
  * `/sites/*`. Those routes create sites, which is the app's job, not a
50
- * foundation's, and a site's own `api` service has no site to address.
50
+ * foundation's, and a site's own `backend` service has no site to address.
51
51
  *
52
52
  * ⛔ Do not add a route that is not under `/entities`, `/models` or `/auth`.
53
53
  */
@@ -59,7 +59,7 @@ export const MODELS = '/models'
59
59
  /**
60
60
  * The base, and how a route joins it. **MEASURED.**
61
61
  *
62
- * The base the site is handed for its `api` service is the address of the
62
+ * The base the site is handed for its `backend` service is the address of the
63
63
  * backend's API route space, and every route here is relative to it:
64
64
  * `${base}/auth/me`, `${base}/entities`. On a hosted site that is `/_api` on the
65
65
  * site's own origin, which reaches the backend's `/api/…`; wherever a page reaches
@@ -138,7 +138,7 @@ export const TOTP = {
138
138
  * `unit_admin`, `content_editor`, `user`. An ordinary member holds none: `[]`.
139
139
  *
140
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
141
+ * package's requests name none, and on a site's `backend` service such a request lands in
142
142
  * the service's **home org**, `handle: 'home'`, in which every account is enrolled.
143
143
  * Both `null` there means an account that is not a member — a service account, a
144
144
  * revoked membership. (On a backend with no home org, naming none is personal: both