seamless-cli 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -29
- package/dist/commands/apps.js +147 -0
- package/dist/commands/apps.js.map +1 -0
- package/dist/commands/config.js +16 -15
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/help.js +65 -187
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/helpTopics.js +376 -0
- package/dist/commands/helpTopics.js.map +1 -0
- package/dist/commands/init.js +295 -76
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/instanceLogin.js +55 -0
- package/dist/commands/instanceLogin.js.map +1 -0
- package/dist/commands/login.js +42 -80
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +18 -8
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/org.js +5 -4
- package/dist/commands/org.js.map +1 -1
- package/dist/commands/profile.js +29 -2
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/sessions.js +9 -7
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/templates.js +58 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/commands/users.js +9 -7
- package/dist/commands/users.js.map +1 -1
- package/dist/commands/verify.js +5 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/whoami.js +13 -5
- package/dist/commands/whoami.js.map +1 -1
- package/dist/core/args.js +11 -0
- package/dist/core/args.js.map +1 -1
- package/dist/core/authClient.js +29 -6
- package/dist/core/authClient.js.map +1 -1
- package/dist/core/cancel.js +32 -0
- package/dist/core/cancel.js.map +1 -0
- package/dist/core/config.js +45 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/confirmAction.js +26 -0
- package/dist/core/confirmAction.js.map +1 -0
- package/dist/core/images.js +3 -3
- package/dist/core/interactiveLogin.js +72 -0
- package/dist/core/interactiveLogin.js.map +1 -0
- package/dist/core/output.js +23 -9
- package/dist/core/output.js.map +1 -1
- package/dist/core/portal.js +112 -8
- package/dist/core/portal.js.map +1 -1
- package/dist/core/templates.js +10 -0
- package/dist/core/templates.js.map +1 -1
- package/dist/core/tty.js +25 -0
- package/dist/core/tty.js.map +1 -0
- package/dist/generators/auth/auth.js +8 -7
- package/dist/generators/auth/auth.js.map +1 -1
- package/dist/generators/docker/docker.js +27 -21
- package/dist/generators/docker/docker.js.map +1 -1
- package/dist/index.js +89 -21
- package/dist/index.js.map +1 -1
- package/dist/prompts/appSelect.js +16 -10
- package/dist/prompts/appSelect.js.map +1 -1
- package/dist/prompts/initMode.js +77 -0
- package/dist/prompts/initMode.js.map +1 -0
- package/dist/prompts/oauthSetup.js +4 -3
- package/dist/prompts/oauthSetup.js.map +1 -1
- package/dist/prompts/projectSetup.js +77 -50
- package/dist/prompts/projectSetup.js.map +1 -1
- package/dist/testSetup.js +9 -0
- package/dist/testSetup.js.map +1 -0
- package/package.json +1 -1
- package/verify/adapter-app/package.json +1 -1
- package/verify/docker-compose.verify.yml +4 -3
- package/verify/harness/api/ownerAdmin.spec.ts +24 -0
- package/verify/harness/lib/env.ts +3 -4
- package/verify/harness/lib/flows.ts +1 -36
- package/verify/harness/playwright.config.ts +8 -3
- package/dist/commands/bootstrapAdmin.js +0 -96
- package/dist/commands/bootstrapAdmin.js.map +0 -1
- package/dist/core/bootstrapSecret.js +0 -39
- package/dist/core/bootstrapSecret.js.map +0 -1
- package/verify/harness/api/adminBootstrap.spec.ts +0 -34
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# SEAMLESS_ADAPTER_DIR absolute path to the express adapter source
|
|
10
10
|
# API_SERVICE_TOKEN shared secret between API and adapter
|
|
11
11
|
# JWKS_KID key id the adapter expects (must match the API's active kid)
|
|
12
|
-
#
|
|
12
|
+
# OWNER_EMAIL tenant owner; this email gets the admin role at signup
|
|
13
13
|
|
|
14
14
|
name: seamless-verify
|
|
15
15
|
|
|
@@ -72,8 +72,9 @@ services:
|
|
|
72
72
|
DB_PASSWORD: seamless
|
|
73
73
|
DB_NAME: seamless_auth
|
|
74
74
|
API_SERVICE_TOKEN: ${API_SERVICE_TOKEN}
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
# The first admin comes from the OWNER_EMAIL grant at signup, so the
|
|
76
|
+
# harness can assert admin promotion without a separate invite flow.
|
|
77
|
+
OWNER_EMAIL: ${OWNER_EMAIL}
|
|
77
78
|
# OAuth: a single "mock" provider backed by the harness's in-process OIDC.
|
|
78
79
|
# authorizationUrl is browser/harness-visible (localhost); token/userInfo are
|
|
79
80
|
# called server-side from the container, so they go via host.docker.internal.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { OWNER_EMAIL } from '../lib/env';
|
|
2
|
+
import { expect, test } from '../lib/fixtures';
|
|
3
|
+
import { registerEmail, requestEmailOtp, verifyEmailOtp } from '../lib/flows';
|
|
4
|
+
|
|
5
|
+
test.describe('owner admin (api)', () => {
|
|
6
|
+
// The owner address is fixed in the stack's OWNER_EMAIL before boot, so this
|
|
7
|
+
// spec registers that exact email rather than the actor fixture's random one.
|
|
8
|
+
test('registering the owner email grants the admin role at signup', async ({ actor }) => {
|
|
9
|
+
const ephemeral = await registerEmail(actor.ctx, OWNER_EMAIL);
|
|
10
|
+
const code = await requestEmailOtp(actor.ctx, ephemeral);
|
|
11
|
+
|
|
12
|
+
const res = await verifyEmailOtp(actor.ctx, ephemeral, code);
|
|
13
|
+
expect(res.ok(), `verify-email-otp -> ${res.status()} ${await res.text()}`).toBeTruthy();
|
|
14
|
+
|
|
15
|
+
const body = await res.json();
|
|
16
|
+
expect(body.token, 'session access token').toBeTruthy();
|
|
17
|
+
|
|
18
|
+
const roles = (body.roles ?? []) as string[];
|
|
19
|
+
expect(
|
|
20
|
+
roles.includes('admin'),
|
|
21
|
+
`owner signup grants the admin role (got ${JSON.stringify(roles)})`,
|
|
22
|
+
).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -11,10 +11,9 @@ export const MOCK_OIDC_PORT = Number(process.env.SEAMLESS_MOCK_OIDC_PORT ?? 9000
|
|
|
11
11
|
export const API_SERVICE_TOKEN =
|
|
12
12
|
process.env.SEAMLESS_API_SERVICE_TOKEN ?? 'verify-dev-service-token';
|
|
13
13
|
|
|
14
|
-
// Must match the API's
|
|
15
|
-
//
|
|
16
|
-
export const
|
|
17
|
-
process.env.SEAMLESS_BOOTSTRAP_SECRET ?? 'verify-dev-bootstrap-secret';
|
|
14
|
+
// Must match the API's OWNER_EMAIL so the harness can assert that registering
|
|
15
|
+
// the tenant owner grants the admin role at signup.
|
|
16
|
+
export const OWNER_EMAIL = process.env.SEAMLESS_OWNER_EMAIL ?? 'owner@verify.local';
|
|
18
17
|
|
|
19
18
|
// Non-production seam: makes the API return OTP / magic-link tokens in the
|
|
20
19
|
// response `delivery` object instead of sending real email/SMS.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { APIRequestContext, expect } from '@playwright/test';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { EXTERNAL_DELIVERY } from './env';
|
|
4
4
|
|
|
5
5
|
export interface SessionTokens {
|
|
6
6
|
token: string; // signed access token
|
|
@@ -43,41 +43,6 @@ export async function registerWithPhone(
|
|
|
43
43
|
return body.token as string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* Mint the one-time bootstrap admin invite (returns the raw invite token via
|
|
48
|
-
* the external-delivery seam). Only succeeds before any admin exists — the API
|
|
49
|
-
* gates re-use with 410, so this assumes a fresh DB (what `seamless verify` runs).
|
|
50
|
-
*/
|
|
51
|
-
export async function createBootstrapInvite(
|
|
52
|
-
ctx: APIRequestContext,
|
|
53
|
-
email: string,
|
|
54
|
-
): Promise<string> {
|
|
55
|
-
const res = await ctx.post('/internal/bootstrap/admin-invite', {
|
|
56
|
-
headers: { Authorization: `Bearer ${BOOTSTRAP_SECRET}`, ...EXTERNAL_DELIVERY },
|
|
57
|
-
data: { email },
|
|
58
|
-
});
|
|
59
|
-
expect(res.ok(), `bootstrap invite -> ${res.status()} ${await res.text()}`).toBeTruthy();
|
|
60
|
-
const token = (await res.json())?.data?.token;
|
|
61
|
-
expect(token, 'bootstrap invite returns a token via external delivery').toBeTruthy();
|
|
62
|
-
return String(token);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Register a user carrying a bootstrap invite token; returns the ephemeral token. */
|
|
66
|
-
export async function registerWithBootstrapToken(
|
|
67
|
-
ctx: APIRequestContext,
|
|
68
|
-
email: string,
|
|
69
|
-
bootstrapToken: string,
|
|
70
|
-
): Promise<string> {
|
|
71
|
-
const res = await ctx.post('/registration/register', {
|
|
72
|
-
headers: EXTERNAL_DELIVERY,
|
|
73
|
-
data: { email, bootstrapToken },
|
|
74
|
-
});
|
|
75
|
-
expect(res.ok(), `register ${email} -> ${res.status()} ${await res.text()}`).toBeTruthy();
|
|
76
|
-
const body = await res.json();
|
|
77
|
-
expect(body.token, 'register returns an ephemeral token').toBeTruthy();
|
|
78
|
-
return body.token as string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
46
|
/** Generate a phone OTP and return the raw code via the external-delivery seam. */
|
|
82
47
|
export async function requestPhoneOtp(ctx: APIRequestContext, ephemeral: string): Promise<string> {
|
|
83
48
|
const res = await ctx.get('/otp/generate-phone-otp', {
|
|
@@ -18,12 +18,17 @@ export default defineConfig({
|
|
|
18
18
|
['junit', { outputFile: 'results/junit.xml' }],
|
|
19
19
|
['html', { outputFolder: 'results/html', open: 'never' }],
|
|
20
20
|
],
|
|
21
|
+
// Each project scopes itself with testDir, not testMatch. A testMatch regex is
|
|
22
|
+
// applied to the absolute path, so `/api\/.*\.spec\.ts$/` also matched every
|
|
23
|
+
// adapter and react spec whenever the checkout lived under a directory
|
|
24
|
+
// containing "api/" (the seamless-auth-api conformance run does exactly that),
|
|
25
|
+
// pulling browser specs into a project with no baseURL.
|
|
21
26
|
projects: [
|
|
22
|
-
{ name: 'api',
|
|
23
|
-
{ name: 'adapter',
|
|
27
|
+
{ name: 'api', testDir: './api' },
|
|
28
|
+
{ name: 'adapter', testDir: './adapter' },
|
|
24
29
|
{
|
|
25
30
|
name: 'react',
|
|
26
|
-
|
|
31
|
+
testDir: './react',
|
|
27
32
|
use: { ...devices['Desktop Chrome'], baseURL: REACT_URL },
|
|
28
33
|
},
|
|
29
34
|
],
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { intro, outro, text, confirm, spinner } from "@clack/prompts";
|
|
2
|
-
import kleur from "kleur";
|
|
3
|
-
import { extractFlag } from "../core/args.js";
|
|
4
|
-
import { resolveBootstrapSecret } from "../core/bootstrapSecret.js";
|
|
5
|
-
const DEFAULT_API_URL = "http://localhost:3000";
|
|
6
|
-
// Bootstrap targets the app API (the SeamlessAuth server adapter), which is what
|
|
7
|
-
// exposes /auth/internal/bootstrap/admin-invite and delivers the invite. That is a
|
|
8
|
-
// different service from a login profile's auth-server URL, which does not serve
|
|
9
|
-
// that route — so this resolves independently of any profile: an explicit
|
|
10
|
-
// --api-url wins, then SEAMLESS_API_URL, then the local dev default.
|
|
11
|
-
function resolveApiUrl(apiUrlFlag) {
|
|
12
|
-
return (apiUrlFlag?.trim() ||
|
|
13
|
-
process.env.SEAMLESS_API_URL?.trim() ||
|
|
14
|
-
DEFAULT_API_URL);
|
|
15
|
-
}
|
|
16
|
-
export async function runBootstrapAdmin(args = []) {
|
|
17
|
-
intro("Seamless Auth Bootstrap");
|
|
18
|
-
const { value: apiUrlFlag, rest } = extractFlag(args, "api-url");
|
|
19
|
-
let email = rest.find((a) => !a.startsWith("-"));
|
|
20
|
-
if (!email) {
|
|
21
|
-
email = (await text({
|
|
22
|
-
message: "Admin email address",
|
|
23
|
-
placeholder: "admin@example.com",
|
|
24
|
-
validate: (value) => {
|
|
25
|
-
if (!value || !value.includes("@")) {
|
|
26
|
-
return "Enter a valid email address";
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
}));
|
|
30
|
-
}
|
|
31
|
-
const proceed = await confirm({
|
|
32
|
-
message: "Create bootstrap admin invite?",
|
|
33
|
-
initialValue: true,
|
|
34
|
-
});
|
|
35
|
-
if (!proceed) {
|
|
36
|
-
outro("Cancelled.");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const apiUrl = resolveApiUrl(apiUrlFlag);
|
|
40
|
-
let secret = resolveBootstrapSecret();
|
|
41
|
-
if (secret) {
|
|
42
|
-
console.log(kleur.gray("Using bootstrap secret from local environment"));
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
console.log("");
|
|
46
|
-
console.log(kleur.yellow("No bootstrap secret detected automatically."));
|
|
47
|
-
console.log("This may happen if the project is not initialized locally or running in production.");
|
|
48
|
-
secret = (await text({
|
|
49
|
-
message: "Bootstrap secret",
|
|
50
|
-
placeholder: "Enter your bootstrap secret",
|
|
51
|
-
validate: (value) => {
|
|
52
|
-
if (!value)
|
|
53
|
-
return "Bootstrap secret is required";
|
|
54
|
-
},
|
|
55
|
-
}));
|
|
56
|
-
}
|
|
57
|
-
const s = spinner();
|
|
58
|
-
s.start("Creating bootstrap invite...");
|
|
59
|
-
try {
|
|
60
|
-
const res = await fetch(`${apiUrl}/auth/internal/bootstrap/admin-invite`, {
|
|
61
|
-
method: "POST",
|
|
62
|
-
headers: {
|
|
63
|
-
"Content-Type": "application/json",
|
|
64
|
-
Authorization: `Bearer ${secret}`,
|
|
65
|
-
},
|
|
66
|
-
body: JSON.stringify({ email }),
|
|
67
|
-
});
|
|
68
|
-
const data = await res.json();
|
|
69
|
-
if (!res.ok) {
|
|
70
|
-
s.stop("Failed");
|
|
71
|
-
console.error(kleur.red("Error creating bootstrap invite"));
|
|
72
|
-
console.error(data);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
s.stop("Done");
|
|
76
|
-
console.log("");
|
|
77
|
-
if (data?.data?.url) {
|
|
78
|
-
console.log(kleur.bold("Registration URL"));
|
|
79
|
-
console.log(kleur.cyan(data.data.url));
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
console.log(kleur.green(`Invite sent to ${email}`));
|
|
83
|
-
}
|
|
84
|
-
console.log("");
|
|
85
|
-
console.log("Next step:");
|
|
86
|
-
console.log("The invited user must complete registration to receive admin access.");
|
|
87
|
-
outro("Bootstrap complete.");
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
s.stop("Failed");
|
|
91
|
-
console.error(kleur.red("Unexpected error"));
|
|
92
|
-
console.error(err.message);
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
//# sourceMappingURL=bootstrapAdmin.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrapAdmin.js","sourceRoot":"","sources":["../../src/commands/bootstrapAdmin.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
export function resolveBootstrapSecret() {
|
|
4
|
-
const root = process.cwd();
|
|
5
|
-
if (process.env.SEAMLESS_BOOTSTRAP_SECRET) {
|
|
6
|
-
return normalizeSecretValue(process.env.SEAMLESS_BOOTSTRAP_SECRET);
|
|
7
|
-
}
|
|
8
|
-
const rootEnv = path.join(root, ".env");
|
|
9
|
-
if (fs.existsSync(rootEnv)) {
|
|
10
|
-
const content = fs.readFileSync(rootEnv, "utf-8");
|
|
11
|
-
const match = content.match(/SEAMLESS_BOOTSTRAP_SECRET=(.*)/);
|
|
12
|
-
if (match)
|
|
13
|
-
return normalizeSecretValue(match[1]);
|
|
14
|
-
}
|
|
15
|
-
const authEnv = path.join(root, "auth", ".env");
|
|
16
|
-
if (fs.existsSync(authEnv)) {
|
|
17
|
-
const content = fs.readFileSync(authEnv, "utf-8");
|
|
18
|
-
const match = content.match(/SEAMLESS_BOOTSTRAP_SECRET=(.*)/);
|
|
19
|
-
if (match)
|
|
20
|
-
return normalizeSecretValue(match[1]);
|
|
21
|
-
}
|
|
22
|
-
const compose = path.join(root, "docker-compose.yml");
|
|
23
|
-
if (fs.existsSync(compose)) {
|
|
24
|
-
const content = fs.readFileSync(compose, "utf-8");
|
|
25
|
-
const match = content.match(/SEAMLESS_BOOTSTRAP_SECRET:\s*(.*)/);
|
|
26
|
-
if (match)
|
|
27
|
-
return normalizeSecretValue(match[1]);
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
function normalizeSecretValue(value) {
|
|
32
|
-
const trimmed = value.trim();
|
|
33
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
34
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
35
|
-
return trimmed.slice(1, -1);
|
|
36
|
-
}
|
|
37
|
-
return trimmed;
|
|
38
|
-
}
|
|
39
|
-
//# sourceMappingURL=bootstrapSecret.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrapSecret.js","sourceRoot":"","sources":["../../src/core/bootstrapSecret.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,UAAU,sBAAsB;IACpC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAE3B,IAAI,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,CAAC;QAC1C,OAAO,oBAAoB,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAClD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QAC9D,IAAI,KAAK;YAAE,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAClD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QAC9D,IAAI,KAAK;YAAE,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;IACtD,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAElD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACjE,IAAI,KAAK;YAAE,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,IACE,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAClD,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAClD,CAAC;QACD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { expect, test } from '../lib/fixtures';
|
|
2
|
-
import {
|
|
3
|
-
createBootstrapInvite,
|
|
4
|
-
pollMagicLink,
|
|
5
|
-
registerWithBootstrapToken,
|
|
6
|
-
requestMagicLink,
|
|
7
|
-
verifyMagicLink,
|
|
8
|
-
} from '../lib/flows';
|
|
9
|
-
|
|
10
|
-
test.describe('admin bootstrap (api)', () => {
|
|
11
|
-
// The first-admin invite is single-use per DB (the API returns 410 once an
|
|
12
|
-
// admin exists), so this relies on the fresh DB `seamless verify` provisions.
|
|
13
|
-
test('invite -> register -> magic-link completion promotes the user to admin', async ({
|
|
14
|
-
actor,
|
|
15
|
-
}) => {
|
|
16
|
-
const inviteToken = await createBootstrapInvite(actor.ctx, actor.email);
|
|
17
|
-
const ephemeral = await registerWithBootstrapToken(actor.ctx, actor.email, inviteToken);
|
|
18
|
-
|
|
19
|
-
const { token } = await requestMagicLink(actor.ctx, ephemeral);
|
|
20
|
-
const verified = await verifyMagicLink(actor.ctx, token);
|
|
21
|
-
expect(verified.ok(), `verify -> ${verified.status()}`).toBeTruthy();
|
|
22
|
-
|
|
23
|
-
const completed = await pollMagicLink(actor.ctx, ephemeral);
|
|
24
|
-
expect(completed.status(), 'poll after verify issues a session').toBe(200);
|
|
25
|
-
|
|
26
|
-
const body = await completed.json();
|
|
27
|
-
expect(body.token, 'session access token').toBeTruthy();
|
|
28
|
-
const roles = (body.roles ?? []) as string[];
|
|
29
|
-
expect(
|
|
30
|
-
roles.includes('admin'),
|
|
31
|
-
`bootstrap promotion grants the admin role (got ${JSON.stringify(roles)})`,
|
|
32
|
-
).toBeTruthy();
|
|
33
|
-
});
|
|
34
|
-
});
|