@owlmeans/web-auth 0.1.18 → 0.1.19

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
@@ -13,7 +13,7 @@ package by the `web-auth` skill.
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
- bun add @owlmeans/web-auth@^0.1.18
16
+ bun add @owlmeans/web-auth@^0.1.19
17
17
  ```
18
18
 
19
19
  `react` is a peer dependency.
@@ -166,7 +166,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
166
166
  your project's skill store (`.agents/skills/`):
167
167
 
168
168
  ```sh
169
- npx @owlmeans/agent-skills@^0.1.18
169
+ npx @owlmeans/agent-skills@^0.1.19
170
170
  ```
171
171
 
172
172
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 3,
3
3
  "package": "@owlmeans/web-auth",
4
- "version": "0.1.18",
5
- "generatedAt": "2026-10-28T00:31:34.730Z",
4
+ "version": "0.1.19",
5
+ "generatedAt": "2026-11-01T22:22:22.298Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/web-auth
9
9
 
10
10
  **Layer:** Web
11
- **Install:** `"@owlmeans/web-auth": "^0.1.18"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-auth": "^0.1.19"` in `dependencies`
12
12
 
13
13
  Web-side auth UI plugins registering into the shared `@owlmeans/client-auth/manager` plugin registry
14
14
  (mirroring `@owlmeans/web-oidc-rp`). It ships the **PK supervisor** login form.
@@ -57,10 +57,17 @@ On submit the form requests a fresh allowance, signs `buildSupervisorPayload(cha
57
57
  salt)` with the entered private key, packs `{ salt, signature }` as the credential, exchanges the
58
58
  resulting token through the client `AuthService`, and navigates via `landAfterLogin` +
59
59
  `landingUrl` (`@owlmeans/client-auth/login`) — a pending post-login step, then a screen suspended
60
- in `@owlmeans/client-flow` (an OAuth consent screen reached signed out, say), else `HOME`. It no
61
- longer imports `@owlmeans/client-flow` or `@owlmeans/web-client` directly; `landAfterLogin` is the
60
+ in `@owlmeans/client-flow` (an OAuth consent screen reached signed out, say), else `HOME`. The form
61
+ imports neither `@owlmeans/client-flow` nor `@owlmeans/web-client` for that; `landAfterLogin` is the
62
62
  whole decision.
63
63
 
64
+ A failed submit is the control's Error stage (`control.setError(e)`), rendered phrased at
65
+ `supervisor-error` through `loginFailureHelper` + `makeAuthTranslate` (`@owlmeans/client-auth/login`,
66
+ context-free — no i18n provider needed): a refusal of the account (an `AuthRefused` such as an
67
+ unknown or banned account, or a 403 with the access-denied marker) reads as a refusal and is logged
68
+ at `warn` (`auth.refused`), anything else as a failure with its incident reference, logged at
69
+ `error`. The next submit asks for a fresh allowance, which clears the stage.
70
+
64
71
  See the **supervisor-auth** skill for the whole feature — the server append, its options, the
65
72
  security boundary and the `@owlmeans/test-ui` helpers.
66
73
 
@@ -69,3 +76,4 @@ security boundary and the `@owlmeans/test-ui` helpers.
69
76
  - `@owlmeans/auth` — `AuthenticationType.Supervisor`, `buildSupervisorPayload`
70
77
  - `@owlmeans/auth-common` — the client `AuthService` type
71
78
  - `@owlmeans/basic-keys` (signing), `@owlmeans/basic-ids` (the salt), `@owlmeans/config`
79
+ - `@owlmeans/log` — the refused/failed sign-in lines
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../../src/auth/plugins/supervisor.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAA;AAUjF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,EAAE,oBA+EpC,CAAA"}
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../../src/auth/plugins/supervisor.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAA;AAajF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,EAAE,oBA8FpC,CAAA"}
@@ -1,11 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { AuthenticationType, buildSupervisorPayload } from '@owlmeans/auth';
3
+ import { AuthenticationStage, AuthenticationType, buildSupervisorPayload } from '@owlmeans/auth';
4
4
  import { DEFAULT_ALIAS as AUTH_SERVICE } from '@owlmeans/client-auth';
5
5
  import { useContext } from '@owlmeans/client';
6
- import { loginLandingOf } from '@owlmeans/client-auth/login';
6
+ import { loginFailureHelper, loginLandingOf, makeAuthTranslate } from '@owlmeans/client-auth/login';
7
+ import { logger } from '@owlmeans/log';
7
8
  import { makeKeyPairModel } from '@owlmeans/basic-keys';
8
9
  import { createIdOfLength } from '@owlmeans/basic-ids';
10
+ const log = logger('web-auth:supervisor');
9
11
  /**
10
12
  * PK-based supervisor login form (development-only). The operator (or an e2e
11
13
  * test) enters a target user id / email and one of the project's trusted private
@@ -24,15 +26,19 @@ export const supervisorClientPlugin = {
24
26
  // screen of a production application — the configuration has to name it. Last in the order and
25
27
  // rendered as a link, because it is a tool, not a way in.
26
28
  method: { order: 900, icon: 'key', emphasis: 'link', restricted: true },
27
- Implementation: () => ({ type, control }) => {
29
+ Implementation: () => ({ type, stage, control }) => {
28
30
  const context = useContext();
29
31
  const [userId, setUserId] = useState('');
30
32
  const [pk, setPk] = useState('');
31
- const [error, setError] = useState(null);
32
33
  const [busy, setBusy] = useState(false);
34
+ // A failed attempt is the control's Error stage (`control.setError`), phrased — never the raw
35
+ // message. The next submit asks for a new allowance, which leaves that stage and clears it.
36
+ const failure = stage === AuthenticationStage.Error && control.error != null
37
+ ? loginFailureHelper.failureOf(control.error) : null;
38
+ // Context-free: the form must not depend on the app mounting an i18n provider.
39
+ const phrase = failure != null ? loginFailureHelper.phrase(failure, makeAuthTranslate(context.cfg)) : null;
33
40
  const onSubmit = async (event) => {
34
41
  event.preventDefault();
35
- setError(null);
36
42
  setBusy(true);
37
43
  try {
38
44
  // Fetch a fresh server challenge right before signing so the allowance is
@@ -51,11 +57,20 @@ export const supervisorClientPlugin = {
51
57
  window.location.href = await landings.landingUrl(landing);
52
58
  }
53
59
  catch (e) {
54
- setError(e instanceof Error ? e.message : String(e));
60
+ const failed = loginFailureHelper.failureOf(e);
61
+ if (failed.refused) {
62
+ log.warn('Supervisor sign-in refused', { kind: failed.kind, incidentId: failed.incidentId }, { event: 'auth.refused' });
63
+ }
64
+ else {
65
+ log.error('Supervisor sign-in failed', e, { event: 'auth.refused' });
66
+ }
67
+ await control.setError(e);
55
68
  setBusy(false);
56
69
  }
57
70
  };
58
- return (_jsxs("form", { onSubmit: onSubmit, "data-testid": "supervisor-auth-form", style: { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 420, margin: '0 auto' }, children: [_jsx("h3", { children: "Supervisor sign-in (development)" }), _jsxs("label", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: ["User id / email", _jsx("input", { name: "userId", type: "text", autoComplete: "off", value: userId, "data-testid": "supervisor-user-id", onChange: e => setUserId(e.target.value), required: true })] }), _jsxs("label", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: ["Primary key", _jsx("input", { name: "pk", type: "password", autoComplete: "off", value: pk, "data-testid": "supervisor-pk", onChange: e => setPk(e.target.value), required: true })] }), _jsx("button", { type: "submit", "data-testid": "supervisor-submit", disabled: busy || userId === '' || pk === '', children: busy ? 'Signing in…' : 'Sign in as supervisor' }), error != null ? _jsx("p", { role: "alert", "data-testid": "supervisor-error", style: { color: 'crimson' }, children: error }) : null] }));
71
+ return (_jsxs("form", { onSubmit: onSubmit, "data-testid": "supervisor-auth-form", style: { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 420, margin: '0 auto' }, children: [_jsx("h3", { children: "Supervisor sign-in (development)" }), _jsxs("label", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: ["User id / email", _jsx("input", { name: "userId", type: "text", autoComplete: "off", value: userId, "data-testid": "supervisor-user-id", onChange: e => setUserId(e.target.value), required: true })] }), _jsxs("label", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: ["Primary key", _jsx("input", { name: "pk", type: "password", autoComplete: "off", value: pk, "data-testid": "supervisor-pk", onChange: e => setPk(e.target.value), required: true })] }), _jsx("button", { type: "submit", "data-testid": "supervisor-submit", disabled: busy || userId === '' || pk === '', children: busy ? 'Signing in…' : 'Sign in as supervisor' }), failure != null && phrase != null
72
+ ? _jsx("p", { role: "alert", "data-testid": "supervisor-error", "data-login-failure": failure.kind, style: { color: 'crimson' }, children: phrase.incident != null ? `${phrase.message} ${phrase.incident}` : phrase.message })
73
+ : null] }));
59
74
  },
60
75
  // Sign the supervisor payload with the entered private key and pack the salt +
61
76
  // signature into `credential`. `credentials.challenge` here is the unwrapped,
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../../src/auth/plugins/supervisor.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../../src/auth/plugins/supervisor.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGhC,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAEhG,OAAO,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAErE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACnG,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD,MAAM,GAAG,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAA;AAEzC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAyB;IAC1D,IAAI,EAAE,kBAAkB,CAAC,UAAU;IAEnC,gGAAgG;IAChG,+FAA+F;IAC/F,0DAA0D;IAC1D,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE;IAEvE,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QACjD,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;QAC5B,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;QACxC,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;QAChC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;QAEvC,8FAA8F;QAC9F,4FAA4F;QAC5F,MAAM,OAAO,GAAG,KAAK,KAAK,mBAAmB,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,IAAI,IAAI;YAC1E,CAAC,CAAC,kBAAkB,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACtD,+EAA+E;QAC/E,MAAM,MAAM,GAAG,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAE1G,MAAM,QAAQ,GAAG,KAAK,EAAE,KAAgB,EAAE,EAAE;YAC1C,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,OAAO,CAAC,IAAI,CAAC,CAAA;YACb,IAAI,CAAC;gBACH,0EAA0E;gBAC1E,uEAAuE;gBACvE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;gBAExC,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;gBAEpE,IAAI,KAAK,CAAC,KAAK,KAAK,EAAE,EAAE,CAAC;oBACvB,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAc,YAAY,CAAC,CAAA;oBAC9D,MAAM,WAAW,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;gBACvC,CAAC;gBAED,oFAAoF;gBACpF,oFAAoF;gBACpF,6EAA6E;gBAC7E,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;gBACxC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,cAAc,EAAE,CAAA;gBAC/C,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;YAC3D,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;gBAC9C,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;oBACnB,GAAG,CAAC,IAAI,CAAC,4BAA4B,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAA;gBACzH,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,KAAK,CAAC,2BAA2B,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAA;gBACtE,CAAC;gBACD,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;gBACzB,OAAO,CAAC,KAAK,CAAC,CAAA;YAChB,CAAC;QACH,CAAC,CAAA;QAED,OAAO,CACL,gBAAM,QAAQ,EAAE,QAAQ,iBAAc,sBAAsB,EAC1D,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,aAC7F,4DAAyC,EACzC,iBAAO,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,gCAEhE,gBAAO,IAAI,EAAC,QAAQ,EAAC,IAAI,EAAC,MAAM,EAAC,YAAY,EAAC,KAAK,EAAC,KAAK,EAAE,MAAM,iBACnD,oBAAoB,EAChC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,QAAQ,SAAG,IACjD,EACR,iBAAO,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,4BAEhE,gBAAO,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,UAAU,EAAC,YAAY,EAAC,KAAK,EAAC,KAAK,EAAE,EAAE,iBAC/C,eAAe,EAC3B,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,QAAQ,SAAG,IAC7C,EACR,iBAAQ,IAAI,EAAC,QAAQ,iBAAa,mBAAmB,EAAC,QAAQ,EAAE,IAAI,IAAI,MAAM,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,YAC/F,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,uBAAuB,GACxC,EACR,OAAO,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI;oBAChC,CAAC,CAAC,YAAG,IAAI,EAAC,OAAO,iBAAa,kBAAkB,wBAAqB,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,YAC3G,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,GAChF;oBACJ,CAAC,CAAC,IAAI,IACH,CACR,CAAA;IACH,CAAC;IAED,+EAA+E;IAC/E,8EAA8E;IAC9E,0EAA0E;IAC1E,YAAY,EAAE,KAAK,EAAE,WAAW,EAAsB,EAAE;QACtD,MAAM,GAAG,GAAG,gBAAgB,CAAC,WAAW,CAAC,UAAU,CAAC,CAAA;QACpD,MAAM,IAAI,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAA;QACjC,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,WAAW,CAAC,SAAS,IAAI,EAAE,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;QAC/G,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;QAE5D,8EAA8E;QAC9E,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;IACtB,CAAC;CACF,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-auth",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -29,25 +29,29 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
- "@owlmeans/auth": "^0.1.18",
33
- "@owlmeans/auth-common": "^0.1.18",
34
- "@owlmeans/basic-ids": "^0.1.18",
35
- "@owlmeans/basic-keys": "^0.1.18",
36
- "@owlmeans/client": "^0.1.18",
37
- "@owlmeans/client-auth": "^0.1.18",
38
- "@owlmeans/client-flow": "^0.1.18",
39
- "@owlmeans/config": "^0.1.18",
40
- "@owlmeans/context": "^0.1.18",
41
- "@owlmeans/web-client": "^0.1.18"
32
+ "@owlmeans/auth": "^0.1.19",
33
+ "@owlmeans/auth-common": "^0.1.19",
34
+ "@owlmeans/basic-ids": "^0.1.19",
35
+ "@owlmeans/basic-keys": "^0.1.19",
36
+ "@owlmeans/client": "^0.1.19",
37
+ "@owlmeans/client-auth": "^0.1.19",
38
+ "@owlmeans/client-flow": "^0.1.19",
39
+ "@owlmeans/config": "^0.1.19",
40
+ "@owlmeans/context": "^0.1.19",
41
+ "@owlmeans/log": "^0.1.19",
42
+ "@owlmeans/web-client": "^0.1.19"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "react": "*"
45
46
  },
46
47
  "devDependencies": {
47
- "@owlmeans/dep-config": "^0.1.18",
48
- "@types/bun": "^1.4.2",
48
+ "@owlmeans/api": "^0.1.19",
49
+ "@owlmeans/dep-config": "^0.1.19",
50
+ "@owlmeans/error": "^0.1.19",
51
+ "@types/bun": "^1.4.4",
49
52
  "@types/react": "^19.3.0",
50
53
  "nodemon": "^3.1.14",
54
+ "react-dom": "^19.3.0",
51
55
  "typescript": "^7.0.2"
52
56
  },
53
57
  "publishConfig": {
@@ -1,15 +1,18 @@
1
1
  import { useState } from 'react'
2
2
  import type { FormEvent } from 'react'
3
3
  import type { AuthenticationPlugin } from '@owlmeans/client-auth/manager/plugins'
4
- import { AuthenticationType, buildSupervisorPayload } from '@owlmeans/auth'
4
+ import { AuthenticationStage, AuthenticationType, buildSupervisorPayload } from '@owlmeans/auth'
5
5
  import type { AuthToken } from '@owlmeans/auth'
6
6
  import { DEFAULT_ALIAS as AUTH_SERVICE } from '@owlmeans/client-auth'
7
7
  import type { AuthService } from '@owlmeans/auth-common'
8
8
  import { useContext } from '@owlmeans/client'
9
- import { loginLandingOf } from '@owlmeans/client-auth/login'
9
+ import { loginFailureHelper, loginLandingOf, makeAuthTranslate } from '@owlmeans/client-auth/login'
10
+ import { logger } from '@owlmeans/log'
10
11
  import { makeKeyPairModel } from '@owlmeans/basic-keys'
11
12
  import { createIdOfLength } from '@owlmeans/basic-ids'
12
13
 
14
+ const log = logger('web-auth:supervisor')
15
+
13
16
  /**
14
17
  * PK-based supervisor login form (development-only). The operator (or an e2e
15
18
  * test) enters a target user id / email and one of the project's trusted private
@@ -30,16 +33,21 @@ export const supervisorClientPlugin: AuthenticationPlugin = {
30
33
  // rendered as a link, because it is a tool, not a way in.
31
34
  method: { order: 900, icon: 'key', emphasis: 'link', restricted: true },
32
35
 
33
- Implementation: () => ({ type, control }) => {
36
+ Implementation: () => ({ type, stage, control }) => {
34
37
  const context = useContext()
35
38
  const [userId, setUserId] = useState('')
36
39
  const [pk, setPk] = useState('')
37
- const [error, setError] = useState<string | null>(null)
38
40
  const [busy, setBusy] = useState(false)
39
41
 
42
+ // A failed attempt is the control's Error stage (`control.setError`), phrased — never the raw
43
+ // message. The next submit asks for a new allowance, which leaves that stage and clears it.
44
+ const failure = stage === AuthenticationStage.Error && control.error != null
45
+ ? loginFailureHelper.failureOf(control.error) : null
46
+ // Context-free: the form must not depend on the app mounting an i18n provider.
47
+ const phrase = failure != null ? loginFailureHelper.phrase(failure, makeAuthTranslate(context.cfg)) : null
48
+
40
49
  const onSubmit = async (event: FormEvent) => {
41
50
  event.preventDefault()
42
- setError(null)
43
51
  setBusy(true)
44
52
  try {
45
53
  // Fetch a fresh server challenge right before signing so the allowance is
@@ -60,7 +68,13 @@ export const supervisorClientPlugin: AuthenticationPlugin = {
60
68
  const landing = await landings.landAfterLogin()
61
69
  window.location.href = await landings.landingUrl(landing)
62
70
  } catch (e) {
63
- setError(e instanceof Error ? e.message : String(e))
71
+ const failed = loginFailureHelper.failureOf(e)
72
+ if (failed.refused) {
73
+ log.warn('Supervisor sign-in refused', { kind: failed.kind, incidentId: failed.incidentId }, { event: 'auth.refused' })
74
+ } else {
75
+ log.error('Supervisor sign-in failed', e, { event: 'auth.refused' })
76
+ }
77
+ await control.setError(e)
64
78
  setBusy(false)
65
79
  }
66
80
  }
@@ -84,7 +98,11 @@ export const supervisorClientPlugin: AuthenticationPlugin = {
84
98
  <button type="submit" data-testid="supervisor-submit" disabled={busy || userId === '' || pk === ''}>
85
99
  {busy ? 'Signing in…' : 'Sign in as supervisor'}
86
100
  </button>
87
- {error != null ? <p role="alert" data-testid="supervisor-error" style={{ color: 'crimson' }}>{error}</p> : null}
101
+ {failure != null && phrase != null
102
+ ? <p role="alert" data-testid="supervisor-error" data-login-failure={failure.kind} style={{ color: 'crimson' }}>
103
+ {phrase.incident != null ? `${phrase.message} ${phrase.incident}` : phrase.message}
104
+ </p>
105
+ : null}
88
106
  </form>
89
107
  )
90
108
  },
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { renderToStaticMarkup } from 'react-dom/server'
4
+ import type { FC } from 'react'
5
+ import { ACCESS_DENIED_KIND, ApiClientError, ServerCrashedError } from '@owlmeans/api'
6
+ import type { ResponseStatusCarrier } from '@owlmeans/api'
7
+ import { AuthenticationStage, AuthenticationType, AuthRefused } from '@owlmeans/auth'
8
+ import { AppType } from '@owlmeans/context'
9
+ import { ResilientError } from '@owlmeans/error'
10
+ import { Context, makeClientContext } from '@owlmeans/client'
11
+ import type { ClientConfig } from '@owlmeans/client-context'
12
+ import { makeControl } from '@owlmeans/client-auth/manager'
13
+ import type { AuthenticationRendererProps } from '@owlmeans/client-auth/manager'
14
+ import { supervisorClientPlugin } from '../src/index.js'
15
+
16
+ /** A failed call as the API client rejects it: the error, stamped with the status and denial kind. */
17
+ const answered = <T extends Error>(error: T, status: number, denialKind?: string): T =>
18
+ Object.assign(error, { responseStatus: status, denialKind } satisfies ResponseStatusCarrier)
19
+
20
+ const context = makeClientContext({
21
+ ready: false, service: 'web-auth-tests', type: AppType.Frontend, services: {}, debug: {},
22
+ } as unknown as ClientConfig)
23
+
24
+ const Form = supervisorClientPlugin.Implementation() as FC<AuthenticationRendererProps>
25
+
26
+ /** The form as the authentication screen renders it after a submit that ended in `error`. */
27
+ const renderFailed = async (error?: unknown, stage = AuthenticationStage.Error): Promise<string> => {
28
+ const control = makeControl(context)
29
+ control.type = AuthenticationType.Supervisor
30
+ if (error !== undefined) {
31
+ await control.setError(error)
32
+ }
33
+ control.stage = stage
34
+
35
+ return renderToStaticMarkup(<Context value={context}>
36
+ <Form type={AuthenticationType.Supervisor} stage={stage} control={control} params={{}} />
37
+ </Context>)
38
+ }
39
+
40
+ describe('supervisorClientPlugin — a failed submit ends phrased', () => {
41
+ test('a refusal reads "This account cannot sign in here.", never the raw message', async () => {
42
+ const out = await renderFailed(answered(new ApiClientError(`forbidden:${randomUUID()}`), 403, ACCESS_DENIED_KIND))
43
+
44
+ expect(out).toContain('data-testid="supervisor-error"')
45
+ expect(out).toContain('data-login-failure="refused"')
46
+ expect(out).toContain('This account cannot sign in here.')
47
+ expect(out).not.toContain('api:client')
48
+ })
49
+
50
+ test('a visible refusal code gets its precise sentence', async () => {
51
+ const rebuilt = answered(ResilientError.ensure(ResilientError.marshal(new AuthRefused('profile:disabled'))), 403, ACCESS_DENIED_KIND)
52
+ const out = await renderFailed(rebuilt)
53
+
54
+ expect(out).toContain('data-login-failure="profile-disabled"')
55
+ expect(out).toContain('access to this application has been blocked')
56
+ expect(out).not.toContain('auth:manager:authen')
57
+ })
58
+
59
+ test('anything else is the generic sentence with its reference, and the form stays to retry', async () => {
60
+ const incident = randomUUID()
61
+ const out = await renderFailed(answered(new ServerCrashedError(incident), 500))
62
+
63
+ expect(out).toContain('data-login-failure="failed"')
64
+ expect(out).toContain(`Sign-in did not complete. Please try again. Reference: ${incident}`)
65
+ expect(out).toContain('Sign in as supervisor')
66
+ expect(out).not.toContain('Signing in…')
67
+ })
68
+
69
+ test('no failure line outside the Error stage', async () => {
70
+ expect(await renderFailed(undefined, AuthenticationStage.Init)).not.toContain('supervisor-error')
71
+ expect(await renderFailed(new Error('stale'), AuthenticationStage.Allowence)).not.toContain('supervisor-error')
72
+ })
73
+ })