@owlmeans/test-ui 0.1.11 → 0.1.14

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@owlmeans/test-ui",
4
- "version": "0.1.11",
5
- "generatedAt": "2026-06-17T10:37:52.340Z",
4
+ "version": "0.1.14",
5
+ "generatedAt": "2026-08-05T16:56:53.371Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
package/build/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './browser.js';
2
2
  export * from './mount.js';
3
+ export * from './supervisor.js';
3
4
  export type { Browser, BrowserContext, Locator, Page } from 'playwright';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA"}
package/build/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './browser.js';
2
2
  export * from './mount.js';
3
+ export * from './supervisor.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA"}
@@ -0,0 +1,91 @@
1
+ import type { Page } from 'playwright';
2
+ import type { AuthRole } from '@owlmeans/auth';
3
+ export interface PregenerateAuthTokenOptions {
4
+ /** Target user id / email the token represents. */
5
+ userId: string;
6
+ /**
7
+ * Private key to sign the bearer with, e.g. `ed25519:<base64>` from
8
+ * `.env.dev.secrets`. MUST be the key the target backend trusts as its own
9
+ * signing key (it verifies bearers against `cfg.alias`/`cfg.service`).
10
+ */
11
+ pk: string;
12
+ scopes?: string[];
13
+ role?: AuthRole;
14
+ entityId?: string;
15
+ profileId?: string;
16
+ /** `source` recorded in the Auth payload (usually the backend service alias). */
17
+ source?: string;
18
+ }
19
+ /**
20
+ * Mint a ready-to-use `ED25519-BASIC-TOKEN ...` bearer offline, reusing
21
+ * `@owlmeans/test-auth`'s `makeBearer`. This is the "set a token directly"
22
+ * fast-path for e2e/integration tests: no UI, no live auth round-trip. The
23
+ * backend accepts it because it is signed by the project's own trusted key.
24
+ *
25
+ * For a faithful end-to-end login (real supervisor plugin + registration), drive
26
+ * {@link loginViaSupervisorForm} instead.
27
+ */
28
+ export declare const pregenerateAuthToken: (opts: PregenerateAuthTokenOptions) => Promise<string>;
29
+ /**
30
+ * Inject a pre-generated bearer into an app that uses the standard owlmeans
31
+ * dispatcher: navigate to `/dispatcher?token=...` so the web app authenticates
32
+ * and redirects home natively. Note: apps that override the DISPATCHER route
33
+ * (e.g. to force an external IdP) should use {@link loginViaSupervisorForm}.
34
+ */
35
+ export declare const loginViaDispatcher: (page: Page, baseUrl: string, token: string, opts?: {
36
+ dispatcherPath?: string;
37
+ }) => Promise<void>;
38
+ export interface SupervisorApiAuthOptions {
39
+ /** Auth-manager API base URL (the backend serving /authentication/* and /authenticate). */
40
+ apiBaseUrl: string;
41
+ /** Target user id / email to authenticate (and register on first use). */
42
+ userId: string;
43
+ /** Supervisor private key, e.g. `ed25519:<base64>` from `.env.dev.secrets`. */
44
+ pk: string;
45
+ /** Paths, overridable for non-default routing. */
46
+ paths?: {
47
+ init?: string;
48
+ authenticate?: string;
49
+ dispatch?: string;
50
+ };
51
+ fetchImpl?: typeof fetch;
52
+ }
53
+ /**
54
+ * Pregenerate a real bearer by driving the live PK-based supervisor flow over the
55
+ * backend API (no browser): `/authentication/init` -> sign the challenge with the
56
+ * supervisor key -> `/authentication/authenticate` -> `/authenticate` (dispatcher
57
+ * exchange). This is the faithful "set a token directly via API" path
58
+ * (registers the user on first use), independent of any front-end routing.
59
+ *
60
+ * Returns the final `ED25519-BASIC-TOKEN ...` bearer the backend accepts.
61
+ */
62
+ export declare const authenticateViaSupervisorApi: (opts: SupervisorApiAuthOptions) => Promise<string>;
63
+ /**
64
+ * Save a full-page screenshot to `<dir>/<name>.png`, creating `dir` if needed.
65
+ * Returns the absolute file path. Use a host-backed folder (e.g. the project's
66
+ * `tmp/tests/screenshots`) so the file is visible whether the test runs on the
67
+ * host or inside a dev container.
68
+ */
69
+ export declare const saveScreenshot: (page: Page, dir: string, name: string) => Promise<string>;
70
+ export interface SupervisorFormLoginOptions {
71
+ baseUrl: string;
72
+ /** Target user id / email. */
73
+ userId: string;
74
+ /** Supervisor private key (e.g. `ed25519:<base64>` from `.env.dev.secrets`). */
75
+ pk: string;
76
+ /** Login path; defaults to the standard supervisor route. */
77
+ path?: string;
78
+ /** Predicate/URL fragment that signals a successful landing (default: leaving the login path). */
79
+ expectPath?: string;
80
+ timeout?: number;
81
+ /** When set, capture the filled login form (`supervisor-login-form.png`) before submit. */
82
+ screenshotDir?: string;
83
+ }
84
+ /**
85
+ * Drive the PK-based supervisor login FORM end-to-end (the faithful path that
86
+ * exercises the real supervisor plugin + registration): navigate to the
87
+ * supervisor login route, fill the user id + private key, submit, and wait for
88
+ * the app to leave the login screen.
89
+ */
90
+ export declare const loginViaSupervisorForm: (page: Page, opts: SupervisorFormLoginOptions) => Promise<void>;
91
+ //# sourceMappingURL=supervisor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,KAAK,EAAyB,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AASrE,MAAM,WAAW,2BAA2B;IAC1C,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAU,MAAM,2BAA2B,KAAG,OAAO,CAAC,MAAM,CAe5F,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,IAAI,EAAE,SAAS,MAAM,EAAE,OAAO,MAAM,EAAE,OAAO;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,KAC7E,OAAO,CAAC,IAAI,CAOd,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAA;IACd,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAA;IACV,kDAAkD;IAClD,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAgBD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACvC,MAAM,wBAAwB,KAC7B,OAAO,CAAC,MAAM,CAkChB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,MAAM,KAAG,OAAO,CAAC,MAAM,CAM1F,CAAA;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAA;IACV,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kGAAkG;IAClG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAID;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,MAAM,IAAI,EAAE,MAAM,0BAA0B,KAC3C,OAAO,CAAC,IAAI,CAmBd,CAAA"}
@@ -0,0 +1,132 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { AuthroizationType, AuthRole as Role, AuthenticationType, buildSupervisorPayload } from '@owlmeans/auth';
4
+ import { makeKeyPairModel } from '@owlmeans/basic-keys';
5
+ import { createIdOfLength } from '@owlmeans/basic-ids';
6
+ import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope';
7
+ import { makeBearer } from '@owlmeans/test-auth';
8
+ /**
9
+ * Mint a ready-to-use `ED25519-BASIC-TOKEN ...` bearer offline, reusing
10
+ * `@owlmeans/test-auth`'s `makeBearer`. This is the "set a token directly"
11
+ * fast-path for e2e/integration tests: no UI, no live auth round-trip. The
12
+ * backend accepts it because it is signed by the project's own trusted key.
13
+ *
14
+ * For a faithful end-to-end login (real supervisor plugin + registration), drive
15
+ * {@link loginViaSupervisorForm} instead.
16
+ */
17
+ export const pregenerateAuthToken = async (opts) => {
18
+ const auth = {
19
+ token: createIdOfLength(32),
20
+ userId: opts.userId,
21
+ profileId: opts.profileId ?? opts.userId,
22
+ entityId: opts.entityId,
23
+ scopes: opts.scopes ?? ['*'],
24
+ role: opts.role ?? Role.User,
25
+ type: AuthroizationType.Ed25519BasicToken,
26
+ source: opts.source ?? '',
27
+ isUser: true,
28
+ createdAt: new Date()
29
+ };
30
+ return makeBearer(auth, makeKeyPairModel(opts.pk));
31
+ };
32
+ /**
33
+ * Inject a pre-generated bearer into an app that uses the standard owlmeans
34
+ * dispatcher: navigate to `/dispatcher?token=...` so the web app authenticates
35
+ * and redirects home natively. Note: apps that override the DISPATCHER route
36
+ * (e.g. to force an external IdP) should use {@link loginViaSupervisorForm}.
37
+ */
38
+ export const loginViaDispatcher = async (page, baseUrl, token, opts) => {
39
+ const path = opts?.dispatcherPath ?? '/dispatcher';
40
+ const url = new URL(path, baseUrl);
41
+ url.searchParams.set('token', token);
42
+ await page.goto(url.toString());
43
+ // Wait until the dispatcher has navigated away (token consumed).
44
+ await page.waitForURL(u => !u.pathname.startsWith(path), { timeout: 30_000 });
45
+ };
46
+ const postJson = async (fetchImpl, url, body) => {
47
+ const res = await fetchImpl(url, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify(body)
51
+ });
52
+ if (!res.ok) {
53
+ throw new Error(`supervisor api ${url} -> ${res.status} ${await res.text().catch(() => '')}`);
54
+ }
55
+ return res.json();
56
+ };
57
+ /**
58
+ * Pregenerate a real bearer by driving the live PK-based supervisor flow over the
59
+ * backend API (no browser): `/authentication/init` -> sign the challenge with the
60
+ * supervisor key -> `/authentication/authenticate` -> `/authenticate` (dispatcher
61
+ * exchange). This is the faithful "set a token directly via API" path
62
+ * (registers the user on first use), independent of any front-end routing.
63
+ *
64
+ * Returns the final `ED25519-BASIC-TOKEN ...` bearer the backend accepts.
65
+ */
66
+ export const authenticateViaSupervisorApi = async (opts) => {
67
+ const fetchImpl = opts.fetchImpl ?? fetch;
68
+ const base = opts.apiBaseUrl.replace(/\/$/, '');
69
+ const initPath = opts.paths?.init ?? '/authentication/init';
70
+ const authPath = opts.paths?.authenticate ?? '/authentication/authenticate';
71
+ const dispatchPath = opts.paths?.dispatch ?? '/authenticate';
72
+ // 1. Ask the auth manager for a fresh, signed challenge envelope.
73
+ const { challenge } = await postJson(fetchImpl, base + initPath, {
74
+ type: AuthenticationType.Supervisor
75
+ });
76
+ // 2. Sign the unwrapped challenge + userId + salt with the supervisor key.
77
+ const challengeMsg = makeEnvelopeModel(challenge, EnvelopeKind.Wrap).message();
78
+ const salt = createIdOfLength(16);
79
+ const signature = await makeKeyPairModel(opts.pk)
80
+ .sign(buildSupervisorPayload(challengeMsg, opts.userId, salt));
81
+ const credentials = {
82
+ type: AuthenticationType.Supervisor,
83
+ challenge,
84
+ credential: JSON.stringify({ salt, signature }),
85
+ userId: opts.userId,
86
+ role: Role.User,
87
+ scopes: ['*']
88
+ };
89
+ // 3. Exchange for the intermediate auth-manager token.
90
+ const intermediate = await postJson(fetchImpl, base + authPath, credentials);
91
+ // 4. Exchange the intermediate token for the final project bearer.
92
+ const final = await postJson(fetchImpl, base + dispatchPath, { token: intermediate.token });
93
+ return final.token;
94
+ };
95
+ /**
96
+ * Save a full-page screenshot to `<dir>/<name>.png`, creating `dir` if needed.
97
+ * Returns the absolute file path. Use a host-backed folder (e.g. the project's
98
+ * `tmp/tests/screenshots`) so the file is visible whether the test runs on the
99
+ * host or inside a dev container.
100
+ */
101
+ export const saveScreenshot = async (page, dir, name) => {
102
+ const target = resolve(dir);
103
+ await mkdir(target, { recursive: true });
104
+ const file = join(target, `${name}.png`);
105
+ await page.screenshot({ path: file, fullPage: true });
106
+ return file;
107
+ };
108
+ const DEFAULT_SUPERVISOR_PATH = '/authentication/login/pk-supervisor';
109
+ /**
110
+ * Drive the PK-based supervisor login FORM end-to-end (the faithful path that
111
+ * exercises the real supervisor plugin + registration): navigate to the
112
+ * supervisor login route, fill the user id + private key, submit, and wait for
113
+ * the app to leave the login screen.
114
+ */
115
+ export const loginViaSupervisorForm = async (page, opts) => {
116
+ const path = opts.path ?? DEFAULT_SUPERVISOR_PATH;
117
+ const timeout = opts.timeout ?? 60_000;
118
+ await page.goto(new URL(path, opts.baseUrl).toString());
119
+ await page.getByTestId('supervisor-user-id').fill(opts.userId);
120
+ await page.getByTestId('supervisor-pk').fill(opts.pk);
121
+ if (opts.screenshotDir != null) {
122
+ await saveScreenshot(page, opts.screenshotDir, 'supervisor-login-form');
123
+ }
124
+ await page.getByTestId('supervisor-submit').click();
125
+ if (opts.expectPath != null) {
126
+ await page.waitForURL(url => url.toString().includes(opts.expectPath), { timeout });
127
+ }
128
+ else {
129
+ await page.waitForURL(url => !url.pathname.startsWith(path), { timeout });
130
+ }
131
+ };
132
+ //# sourceMappingURL=supervisor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/test-ui",
3
- "version": "0.1.11",
3
+ "version": "0.1.14",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,7 +22,12 @@
22
22
  "./harness": "./harness/index.html"
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/test": "^0.1.11",
25
+ "@owlmeans/auth": "^0.1.14",
26
+ "@owlmeans/basic-envelope": "^0.1.14",
27
+ "@owlmeans/basic-ids": "^0.1.14",
28
+ "@owlmeans/basic-keys": "^0.1.14",
29
+ "@owlmeans/test": "^0.1.14",
30
+ "@owlmeans/test-auth": "^0.1.14",
26
31
  "playwright": "^1.49.0"
27
32
  },
28
33
  "peerDependencies": {
@@ -38,11 +43,11 @@
38
43
  }
39
44
  },
40
45
  "devDependencies": {
41
- "@owlmeans/dep-config": "^0.1.11",
42
- "@types/bun": "^1.3.0",
43
- "@types/react": "^19.2.7",
44
- "nodemon": "^3.1.11",
45
- "typescript": "^6.0.2"
46
+ "@owlmeans/dep-config": "^0.1.14",
47
+ "@types/bun": "^1.3.14",
48
+ "@types/react": "^19.2.17",
49
+ "nodemon": "^3.1.14",
50
+ "typescript": "^6.0.3"
46
51
  },
47
52
  "publishConfig": {
48
53
  "access": "public"
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './browser.js'
2
2
  export * from './mount.js'
3
+ export * from './supervisor.js'
3
4
  export type { Browser, BrowserContext, Locator, Page } from 'playwright'
@@ -0,0 +1,204 @@
1
+ import { mkdir } from 'node:fs/promises'
2
+ import { join, resolve } from 'node:path'
3
+ import type { Page } from 'playwright'
4
+ import type { Auth, AuthCredentials, AuthRole } from '@owlmeans/auth'
5
+ import {
6
+ AuthroizationType, AuthRole as Role, AuthenticationType, buildSupervisorPayload
7
+ } from '@owlmeans/auth'
8
+ import { makeKeyPairModel } from '@owlmeans/basic-keys'
9
+ import { createIdOfLength } from '@owlmeans/basic-ids'
10
+ import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
11
+ import { makeBearer } from '@owlmeans/test-auth'
12
+
13
+ export interface PregenerateAuthTokenOptions {
14
+ /** Target user id / email the token represents. */
15
+ userId: string
16
+ /**
17
+ * Private key to sign the bearer with, e.g. `ed25519:<base64>` from
18
+ * `.env.dev.secrets`. MUST be the key the target backend trusts as its own
19
+ * signing key (it verifies bearers against `cfg.alias`/`cfg.service`).
20
+ */
21
+ pk: string
22
+ scopes?: string[]
23
+ role?: AuthRole
24
+ entityId?: string
25
+ profileId?: string
26
+ /** `source` recorded in the Auth payload (usually the backend service alias). */
27
+ source?: string
28
+ }
29
+
30
+ /**
31
+ * Mint a ready-to-use `ED25519-BASIC-TOKEN ...` bearer offline, reusing
32
+ * `@owlmeans/test-auth`'s `makeBearer`. This is the "set a token directly"
33
+ * fast-path for e2e/integration tests: no UI, no live auth round-trip. The
34
+ * backend accepts it because it is signed by the project's own trusted key.
35
+ *
36
+ * For a faithful end-to-end login (real supervisor plugin + registration), drive
37
+ * {@link loginViaSupervisorForm} instead.
38
+ */
39
+ export const pregenerateAuthToken = async (opts: PregenerateAuthTokenOptions): Promise<string> => {
40
+ const auth: Auth = {
41
+ token: createIdOfLength(32),
42
+ userId: opts.userId,
43
+ profileId: opts.profileId ?? opts.userId,
44
+ entityId: opts.entityId,
45
+ scopes: opts.scopes ?? ['*'],
46
+ role: opts.role ?? Role.User,
47
+ type: AuthroizationType.Ed25519BasicToken,
48
+ source: opts.source ?? '',
49
+ isUser: true,
50
+ createdAt: new Date()
51
+ }
52
+
53
+ return makeBearer(auth, makeKeyPairModel(opts.pk))
54
+ }
55
+
56
+ /**
57
+ * Inject a pre-generated bearer into an app that uses the standard owlmeans
58
+ * dispatcher: navigate to `/dispatcher?token=...` so the web app authenticates
59
+ * and redirects home natively. Note: apps that override the DISPATCHER route
60
+ * (e.g. to force an external IdP) should use {@link loginViaSupervisorForm}.
61
+ */
62
+ export const loginViaDispatcher = async (
63
+ page: Page, baseUrl: string, token: string, opts?: { dispatcherPath?: string }
64
+ ): Promise<void> => {
65
+ const path = opts?.dispatcherPath ?? '/dispatcher'
66
+ const url = new URL(path, baseUrl)
67
+ url.searchParams.set('token', token)
68
+ await page.goto(url.toString())
69
+ // Wait until the dispatcher has navigated away (token consumed).
70
+ await page.waitForURL(u => !u.pathname.startsWith(path), { timeout: 30_000 })
71
+ }
72
+
73
+ export interface SupervisorApiAuthOptions {
74
+ /** Auth-manager API base URL (the backend serving /authentication/* and /authenticate). */
75
+ apiBaseUrl: string
76
+ /** Target user id / email to authenticate (and register on first use). */
77
+ userId: string
78
+ /** Supervisor private key, e.g. `ed25519:<base64>` from `.env.dev.secrets`. */
79
+ pk: string
80
+ /** Paths, overridable for non-default routing. */
81
+ paths?: { init?: string, authenticate?: string, dispatch?: string }
82
+ fetchImpl?: typeof fetch
83
+ }
84
+
85
+ const postJson = async (
86
+ fetchImpl: typeof fetch, url: string, body: unknown
87
+ ): Promise<any> => {
88
+ const res = await fetchImpl(url, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify(body)
92
+ })
93
+ if (!res.ok) {
94
+ throw new Error(`supervisor api ${url} -> ${res.status} ${await res.text().catch(() => '')}`)
95
+ }
96
+ return res.json()
97
+ }
98
+
99
+ /**
100
+ * Pregenerate a real bearer by driving the live PK-based supervisor flow over the
101
+ * backend API (no browser): `/authentication/init` -> sign the challenge with the
102
+ * supervisor key -> `/authentication/authenticate` -> `/authenticate` (dispatcher
103
+ * exchange). This is the faithful "set a token directly via API" path
104
+ * (registers the user on first use), independent of any front-end routing.
105
+ *
106
+ * Returns the final `ED25519-BASIC-TOKEN ...` bearer the backend accepts.
107
+ */
108
+ export const authenticateViaSupervisorApi = async (
109
+ opts: SupervisorApiAuthOptions
110
+ ): Promise<string> => {
111
+ const fetchImpl = opts.fetchImpl ?? fetch
112
+ const base = opts.apiBaseUrl.replace(/\/$/, '')
113
+ const initPath = opts.paths?.init ?? '/authentication/init'
114
+ const authPath = opts.paths?.authenticate ?? '/authentication/authenticate'
115
+ const dispatchPath = opts.paths?.dispatch ?? '/authenticate'
116
+
117
+ // 1. Ask the auth manager for a fresh, signed challenge envelope.
118
+ const { challenge } = await postJson(fetchImpl, base + initPath, {
119
+ type: AuthenticationType.Supervisor
120
+ }) as { challenge: string }
121
+
122
+ // 2. Sign the unwrapped challenge + userId + salt with the supervisor key.
123
+ const challengeMsg = makeEnvelopeModel<string>(challenge, EnvelopeKind.Wrap).message()
124
+ const salt = createIdOfLength(16)
125
+ const signature = await makeKeyPairModel(opts.pk)
126
+ .sign(buildSupervisorPayload(challengeMsg, opts.userId, salt))
127
+
128
+ const credentials: AuthCredentials = {
129
+ type: AuthenticationType.Supervisor,
130
+ challenge,
131
+ credential: JSON.stringify({ salt, signature }),
132
+ userId: opts.userId,
133
+ role: Role.User,
134
+ scopes: ['*']
135
+ }
136
+
137
+ // 3. Exchange for the intermediate auth-manager token.
138
+ const intermediate = await postJson(fetchImpl, base + authPath, credentials) as { token: string }
139
+
140
+ // 4. Exchange the intermediate token for the final project bearer.
141
+ const final = await postJson(fetchImpl, base + dispatchPath, { token: intermediate.token }) as { token: string }
142
+
143
+ return final.token
144
+ }
145
+
146
+ /**
147
+ * Save a full-page screenshot to `<dir>/<name>.png`, creating `dir` if needed.
148
+ * Returns the absolute file path. Use a host-backed folder (e.g. the project's
149
+ * `tmp/tests/screenshots`) so the file is visible whether the test runs on the
150
+ * host or inside a dev container.
151
+ */
152
+ export const saveScreenshot = async (page: Page, dir: string, name: string): Promise<string> => {
153
+ const target = resolve(dir)
154
+ await mkdir(target, { recursive: true })
155
+ const file = join(target, `${name}.png`)
156
+ await page.screenshot({ path: file, fullPage: true })
157
+ return file
158
+ }
159
+
160
+ export interface SupervisorFormLoginOptions {
161
+ baseUrl: string
162
+ /** Target user id / email. */
163
+ userId: string
164
+ /** Supervisor private key (e.g. `ed25519:<base64>` from `.env.dev.secrets`). */
165
+ pk: string
166
+ /** Login path; defaults to the standard supervisor route. */
167
+ path?: string
168
+ /** Predicate/URL fragment that signals a successful landing (default: leaving the login path). */
169
+ expectPath?: string
170
+ timeout?: number
171
+ /** When set, capture the filled login form (`supervisor-login-form.png`) before submit. */
172
+ screenshotDir?: string
173
+ }
174
+
175
+ const DEFAULT_SUPERVISOR_PATH = '/authentication/login/pk-supervisor'
176
+
177
+ /**
178
+ * Drive the PK-based supervisor login FORM end-to-end (the faithful path that
179
+ * exercises the real supervisor plugin + registration): navigate to the
180
+ * supervisor login route, fill the user id + private key, submit, and wait for
181
+ * the app to leave the login screen.
182
+ */
183
+ export const loginViaSupervisorForm = async (
184
+ page: Page, opts: SupervisorFormLoginOptions
185
+ ): Promise<void> => {
186
+ const path = opts.path ?? DEFAULT_SUPERVISOR_PATH
187
+ const timeout = opts.timeout ?? 60_000
188
+ await page.goto(new URL(path, opts.baseUrl).toString())
189
+
190
+ await page.getByTestId('supervisor-user-id').fill(opts.userId)
191
+ await page.getByTestId('supervisor-pk').fill(opts.pk)
192
+
193
+ if (opts.screenshotDir != null) {
194
+ await saveScreenshot(page, opts.screenshotDir, 'supervisor-login-form')
195
+ }
196
+
197
+ await page.getByTestId('supervisor-submit').click()
198
+
199
+ if (opts.expectPath != null) {
200
+ await page.waitForURL(url => url.toString().includes(opts.expectPath!), { timeout })
201
+ } else {
202
+ await page.waitForURL(url => !url.pathname.startsWith(path), { timeout })
203
+ }
204
+ }