@owlmeans/test-ui 0.1.11 → 0.1.15
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/agent-meta/manifest.json +2 -2
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/supervisor.d.ts +91 -0
- package/build/supervisor.d.ts.map +1 -0
- package/build/supervisor.js +132 -0
- package/build/supervisor.js.map +1 -0
- package/package.json +12 -7
- package/src/index.ts +1 -0
- package/src/supervisor.ts +204 -0
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@owlmeans/test-ui",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.15",
|
|
5
|
+
"generatedAt": "2026-08-07T17:06:32.892Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -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
package/build/index.js.map
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.15",
|
|
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/
|
|
25
|
+
"@owlmeans/auth": "^0.1.15",
|
|
26
|
+
"@owlmeans/basic-envelope": "^0.1.15",
|
|
27
|
+
"@owlmeans/basic-ids": "^0.1.15",
|
|
28
|
+
"@owlmeans/basic-keys": "^0.1.15",
|
|
29
|
+
"@owlmeans/test": "^0.1.15",
|
|
30
|
+
"@owlmeans/test-auth": "^0.1.15",
|
|
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.
|
|
42
|
-
"@types/bun": "^1.3.
|
|
43
|
-
"@types/react": "^19.2.
|
|
44
|
-
"nodemon": "^3.1.
|
|
45
|
-
"typescript": "^6.0.
|
|
46
|
+
"@owlmeans/dep-config": "^0.1.15",
|
|
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
|
@@ -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
|
+
}
|