@uniweb/api 0.2.12 → 0.3.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
@@ -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 { isEnabled } from '@uniweb/api'
20
+ import { isApiEnabled } from '@uniweb/kit'
21
21
 
22
- if (!isEnabled(website)) return <StaticVersion />
22
+ if (!isApiEnabled()) return <StaticVersion />
23
23
  ```
24
24
 
25
25
  ⛔ **And when the answer is no, draw nothing** — not a disabled button, and not an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/api",
3
- "version": "0.2.12",
3
+ "version": "0.3.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.24.2"
37
+ "@uniweb/core": "^0.24.3"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": "^19.0.0"
package/src/client.js CHANGED
@@ -44,16 +44,34 @@ export function resolveBase(website) {
44
44
  }
45
45
 
46
46
  /**
47
- * Does this site have a backend the package can talk to?
47
+ * Does this site have an app backend — the `api` service?
48
48
  *
49
49
  * The question to ask before drawing a sign-in affordance or any control only a
50
50
  * backend can answer. False means: draw nothing, or the static alternative the
51
51
  * site already carries.
52
52
  *
53
- * @param {object} website
53
+ * ⭐ **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
55
+ * service, no arguments, the website resolved for you. Pass one explicitly only
56
+ * when you already hold it, or are working outside a render.
57
+ *
58
+ * ⛔ **THE DEFAULT IS NOT A CONVENIENCE, IT CLOSES A TRAP.** For one commit this
59
+ * 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`.
65
+ *
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.
70
+ *
71
+ * @param {object} [website] - defaults to the active website
54
72
  * @returns {boolean}
55
73
  */
56
- export function isEnabled(website) {
74
+ export function isApiEnabled(website = getUniweb()?.activeWebsite ?? null) {
57
75
  return resolveBase(website) !== null
58
76
  }
59
77
 
package/src/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  export {
14
14
  SERVICE_NAME,
15
15
  resolveBase,
16
- isEnabled,
16
+ isApiEnabled,
17
17
  probeSession,
18
18
  signIn,
19
19
  completeChallenge,
package/src/mock/index.js CHANGED
@@ -38,11 +38,14 @@ import { DEFAULT_SEED } from './seed.js'
38
38
  *
39
39
  * @param {object} [options]
40
40
  * @param {object} [options.seed] - accounts, schemas and entities to start from
41
+ * @param {string} [options.signedInAs] - a seeded `username` to start signed in as, for a
42
+ * demo whose subject is the signed-in view. Development-only by construction, since
43
+ * this whole module is; throws if the username is not seeded.
41
44
  * @param {string} [options.prefix] - the path the API is mounted under (default `/api`)
42
45
  * @returns {{ fetch: (request: Request) => Promise<Response>, store: MockStore }}
43
46
  */
44
- export function createMockBackend({ seed = DEFAULT_SEED, prefix = '/api' } = {}) {
45
- const store = new MockStore(seed)
47
+ export function createMockBackend({ seed = DEFAULT_SEED, prefix = '/api', signedInAs = null } = {}) {
48
+ const store = new MockStore(seed, { signedInAs })
46
49
 
47
50
  const json = (status, body) =>
48
51
  new Response(body === undefined ? null : JSON.stringify(body), {
package/src/mock/store.js CHANGED
@@ -33,8 +33,10 @@ export class MockStore {
33
33
  * @param {object[]} [seed.accounts] - `{ username, password, handle, roles?, units? }`
34
34
  * @param {object} [seed.schemas] - `{ '@/session': { creatable_by?, append_only? } }`
35
35
  * @param {object[]} [seed.entities] - `{ uuid?, model, data?, items? }`
36
+ * @param {object} [options]
37
+ * @param {string} [options.signedInAs] - start with this account already signed in.
36
38
  */
37
- constructor(seed = {}) {
39
+ constructor(seed = {}, { signedInAs = null } = {}) {
38
40
  this.accounts = (seed.accounts || []).map((a) => ({
39
41
  uuid: a.uuid || nextId('acct'),
40
42
  username: a.username,
@@ -52,6 +54,26 @@ export class MockStore {
52
54
  /** The one session. A mock serves one developer, so one is the honest number. */
53
55
  this.session = null
54
56
  this.resets = new Map()
57
+
58
+ // ⭐ `signedInAs` — start already signed in, for a demo whose whole point is the
59
+ // signed-in view. Without it a visitor must type credentials before seeing
60
+ // anything, which for a lived-in demo is the experience itself.
61
+ //
62
+ // ⛔ THROWS on an unknown username rather than leaving the session null. A typo
63
+ // here produces "why am I not logged in?" — a question with no visible cause, in
64
+ // the one place where the answer is a string three lines away. The mock is
65
+ // development-only, so failing at construction costs nothing and a silent
66
+ // anonymous session costs an afternoon.
67
+ if (signedInAs) {
68
+ const account = this.accounts.find((a) => a.username === signedInAs)
69
+ if (!account) {
70
+ const known = this.accounts.map((a) => a.username).join(', ') || '(none)'
71
+ throw new Error(
72
+ `[uniweb/api mock] signedInAs: '${signedInAs}' is not a seeded account. Seeded: ${known}`
73
+ )
74
+ }
75
+ this.session = { account, at: now() }
76
+ }
55
77
  }
56
78
 
57
79
  seedEntity({ uuid, model, data = {}, items = [], owner = null }) {