@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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-auth/SKILL.md +11 -3
- package/build/auth/plugins/supervisor.d.ts.map +1 -1
- package/build/auth/plugins/supervisor.js +22 -7
- package/build/auth/plugins/supervisor.js.map +1 -1
- package/package.json +17 -13
- package/src/auth/plugins/supervisor.tsx +25 -7
- package/tests/supervisor-failure.spec.tsx +73 -0
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.
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/web-auth",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"generatedAt": "2026-
|
|
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.
|
|
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`.
|
|
61
|
-
|
|
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;
|
|
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
|
-
|
|
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' }),
|
|
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":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGhC,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../../../src/auth/plugins/supervisor.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/web-auth",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
33
|
-
"@owlmeans/auth-common": "^0.1.
|
|
34
|
-
"@owlmeans/basic-ids": "^0.1.
|
|
35
|
-
"@owlmeans/basic-keys": "^0.1.
|
|
36
|
-
"@owlmeans/client": "^0.1.
|
|
37
|
-
"@owlmeans/client-auth": "^0.1.
|
|
38
|
-
"@owlmeans/client-flow": "^0.1.
|
|
39
|
-
"@owlmeans/config": "^0.1.
|
|
40
|
-
"@owlmeans/context": "^0.1.
|
|
41
|
-
"@owlmeans/
|
|
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/
|
|
48
|
-
"@
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
+
})
|