seamless-cli 0.10.2 → 0.12.1
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 +198 -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 +43 -11
- 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 +7 -4
- package/dist/core/images.js.map +1 -1
- package/dist/core/interactiveLogin.js +75 -0
- package/dist/core/interactiveLogin.js.map +1 -0
- package/dist/core/loginFlow.js +24 -8
- package/dist/core/loginFlow.js.map +1 -1
- 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 +32 -22
- 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/adapter-fastify-app/Dockerfile +14 -0
- package/verify/adapter-fastify-app/package.json +12 -0
- package/verify/adapter-fastify-app/server.mjs +64 -0
- package/verify/adapter-fastify-app/vendor/.gitignore +3 -0
- package/verify/adapter-fastify-app/vendor/.gitkeep +0 -0
- package/verify/docker-compose.cache.yml +43 -0
- package/verify/docker-compose.verify.yml +34 -4
- package/verify/harness/api/loginEnumeration.spec.ts +139 -0
- package/verify/harness/api/ownerAdmin.spec.ts +24 -0
- package/verify/harness/global-setup.ts +4 -1
- package/verify/harness/lib/client.ts +7 -3
- package/verify/harness/lib/env.ts +7 -4
- package/verify/harness/lib/fixtures.ts +12 -3
- package/verify/harness/lib/flows.ts +1 -36
- package/verify/harness/lib/matrixReporter.ts +19 -6
- package/verify/harness/lib/reactFlows.ts +0 -2
- package/verify/harness/playwright.config.ts +19 -5
- 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
|
@@ -4,6 +4,10 @@ import { randomInt, randomUUID } from 'crypto';
|
|
|
4
4
|
|
|
5
5
|
export const API_URL = process.env.SEAMLESS_API_URL ?? 'http://localhost:5312';
|
|
6
6
|
export const ADAPTER_URL = process.env.SEAMLESS_ADAPTER_URL ?? 'http://localhost:3000';
|
|
7
|
+
// The Fastify twin of the adapter, on its own port. The adapter specs run against
|
|
8
|
+
// both, so a regression in one framework is visible on its own row of the matrix.
|
|
9
|
+
export const FASTIFY_ADAPTER_URL =
|
|
10
|
+
process.env.SEAMLESS_FASTIFY_ADAPTER_URL ?? 'http://localhost:3001';
|
|
7
11
|
export const REACT_URL = process.env.SEAMLESS_REACT_URL ?? 'http://localhost:5173';
|
|
8
12
|
export const MOCK_OIDC_PORT = Number(process.env.SEAMLESS_MOCK_OIDC_PORT ?? 9000);
|
|
9
13
|
|
|
@@ -11,10 +15,9 @@ export const MOCK_OIDC_PORT = Number(process.env.SEAMLESS_MOCK_OIDC_PORT ?? 9000
|
|
|
11
15
|
export const API_SERVICE_TOKEN =
|
|
12
16
|
process.env.SEAMLESS_API_SERVICE_TOKEN ?? 'verify-dev-service-token';
|
|
13
17
|
|
|
14
|
-
// Must match the API's
|
|
15
|
-
//
|
|
16
|
-
export const
|
|
17
|
-
process.env.SEAMLESS_BOOTSTRAP_SECRET ?? 'verify-dev-bootstrap-secret';
|
|
18
|
+
// Must match the API's OWNER_EMAIL so the harness can assert that registering
|
|
19
|
+
// the tenant owner grants the admin role at signup.
|
|
20
|
+
export const OWNER_EMAIL = process.env.SEAMLESS_OWNER_EMAIL ?? 'owner@verify.local';
|
|
18
21
|
|
|
19
22
|
// Non-production seam: makes the API return OTP / magic-link tokens in the
|
|
20
23
|
// response `delivery` object instead of sending real email/SMS.
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { test as base } from '@playwright/test';
|
|
2
2
|
|
|
3
3
|
import { Actor, newAdapterActor, newApiActor } from './client';
|
|
4
|
+
import { ADAPTER_URL } from './env';
|
|
5
|
+
|
|
6
|
+
// `adapterUrl` is a project option rather than something a spec sets: the adapter
|
|
7
|
+
// suite is written once and each project points it at a different adopter backend
|
|
8
|
+
// (Express, Fastify). A spec cannot tell which one answered, which is the point.
|
|
9
|
+
export interface AdapterOptions {
|
|
10
|
+
adapterUrl: string;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
// `actor` drives the API directly (Bearer + service token); `adapterActor` drives
|
|
6
14
|
// the adopter backend over cookies. Both are auto-created and disposed per test.
|
|
7
|
-
export const test = base.extend<{ actor: Actor; adapterActor: Actor }>({
|
|
15
|
+
export const test = base.extend<AdapterOptions & { actor: Actor; adapterActor: Actor }>({
|
|
16
|
+
adapterUrl: [ADAPTER_URL, { option: true }],
|
|
8
17
|
actor: async ({}, use) => {
|
|
9
18
|
const actor = await newApiActor();
|
|
10
19
|
await use(actor);
|
|
11
20
|
await actor.dispose();
|
|
12
21
|
},
|
|
13
|
-
adapterActor: async ({}, use) => {
|
|
14
|
-
const actor = await newAdapterActor();
|
|
22
|
+
adapterActor: async ({ adapterUrl }, use) => {
|
|
23
|
+
const actor = await newAdapterActor(adapterUrl);
|
|
15
24
|
await use(actor);
|
|
16
25
|
await actor.dispose();
|
|
17
26
|
},
|
|
@@ -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', {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { FullResult, Reporter, TestCase, TestResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
3
|
// Prints a flow x layer conformance grid at the end of the run. Layer comes from
|
|
4
|
-
// the
|
|
5
|
-
//
|
|
4
|
+
// the Playwright project; flow from the spec file name, with a few aliases folded
|
|
5
|
+
// together so the same flow lines up across layers.
|
|
6
6
|
|
|
7
|
-
const LAYERS = ['api', 'adapter', 'react'] as const;
|
|
7
|
+
const LAYERS = ['api', 'adapter', 'adapter-fastify', 'react'] as const;
|
|
8
8
|
type Layer = (typeof LAYERS)[number];
|
|
9
9
|
|
|
10
|
+
function isLayer(name: string): name is Layer {
|
|
11
|
+
return (LAYERS as readonly string[]).includes(name);
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
const FLOW_ALIASES: Record<string, string> = {
|
|
11
15
|
emailOtpLogin: 'emailOtp',
|
|
12
16
|
registration: 'register',
|
|
@@ -29,7 +33,11 @@ export default class MatrixReporter implements Reporter {
|
|
|
29
33
|
onTestEnd(test: TestCase, result: TestResult): void {
|
|
30
34
|
const match = test.location.file.match(/\/(api|adapter|react)\/([^/]+)\.spec\.[tj]s$/);
|
|
31
35
|
if (!match) return;
|
|
32
|
-
|
|
36
|
+
// The project name, not the directory: the two adapter projects run the same
|
|
37
|
+
// specs from ./adapter, so the path cannot tell them apart. Falls back to the
|
|
38
|
+
// directory for a run driven by something that does not name its projects.
|
|
39
|
+
const projectName = test.parent.project()?.name ?? '';
|
|
40
|
+
const layer = isLayer(projectName) ? projectName : (match[1] as Layer);
|
|
33
41
|
const base = match[2];
|
|
34
42
|
const flow = FLOW_ALIASES[base] ?? base;
|
|
35
43
|
// Keyed by test id so the final attempt (after retries) is the one that counts.
|
|
@@ -51,11 +59,16 @@ export default class MatrixReporter implements Reporter {
|
|
|
51
59
|
};
|
|
52
60
|
|
|
53
61
|
const flowWidth = Math.max('flow'.length, ...flows.map((f) => f.length));
|
|
62
|
+
// Each column is as wide as its own header, so a long layer name (like
|
|
63
|
+
// adapter-fastify) widens its column instead of running into the next one.
|
|
64
|
+
const layerWidth = (layer: Layer) => layer.length + 3;
|
|
54
65
|
const pad = (text: string, width: number) =>
|
|
55
66
|
text + ' '.repeat(Math.max(0, width - text.length));
|
|
56
67
|
const row = (label: string, get: (layer: Layer) => string) =>
|
|
57
|
-
` ${pad(label, flowWidth)} ${LAYERS.map((l) => pad(get(l),
|
|
58
|
-
const
|
|
68
|
+
` ${pad(label, flowWidth)} ${LAYERS.map((l) => pad(get(l), layerWidth(l))).join('')}`;
|
|
69
|
+
const totalWidth =
|
|
70
|
+
flowWidth + 3 + LAYERS.reduce((sum, l) => sum + layerWidth(l), 0);
|
|
71
|
+
const rule = ` ${'-'.repeat(totalWidth)}`;
|
|
59
72
|
|
|
60
73
|
const lines = [
|
|
61
74
|
'',
|
|
@@ -90,8 +90,6 @@ export async function registerWithPasskey(page: Page, email: string): Promise<vo
|
|
|
90
90
|
page.getByRole('heading', { name: /Secure Your Account with a Passkey/ }),
|
|
91
91
|
).toBeVisible();
|
|
92
92
|
await page.getByRole('button', { name: 'Register Passkey' }).click();
|
|
93
|
-
await page.getByPlaceholder(/MacBook/).fill('Verify Device');
|
|
94
|
-
await page.getByRole('button', { name: 'Continue' }).click();
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
/** Sign in an existing user whose passkey is registered (the ceremony auto-runs). */
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { defineConfig, devices } from '@playwright/test';
|
|
2
2
|
|
|
3
|
-
import { REACT_URL } from './lib/env';
|
|
3
|
+
import { FASTIFY_ADAPTER_URL, REACT_URL } from './lib/env';
|
|
4
|
+
import type { AdapterOptions } from './lib/fixtures';
|
|
4
5
|
|
|
5
6
|
// One runner, multiple projects. `api` and `adapter` hit HTTP directly (no
|
|
6
7
|
// browser); `react` drives chromium against the starter SPA. global-setup
|
|
7
8
|
// health-gates the stack before any project runs.
|
|
8
|
-
export default defineConfig({
|
|
9
|
+
export default defineConfig<AdapterOptions>({
|
|
9
10
|
testDir: '.',
|
|
10
11
|
globalSetup: './global-setup.ts',
|
|
11
12
|
timeout: 30_000,
|
|
@@ -18,12 +19,25 @@ export default defineConfig({
|
|
|
18
19
|
['junit', { outputFile: 'results/junit.xml' }],
|
|
19
20
|
['html', { outputFolder: 'results/html', open: 'never' }],
|
|
20
21
|
],
|
|
22
|
+
// Each project scopes itself with testDir, not testMatch. A testMatch regex is
|
|
23
|
+
// applied to the absolute path, so `/api\/.*\.spec\.ts$/` also matched every
|
|
24
|
+
// adapter and react spec whenever the checkout lived under a directory
|
|
25
|
+
// containing "api/" (the seamless-auth-api conformance run does exactly that),
|
|
26
|
+
// pulling browser specs into a project with no baseURL.
|
|
21
27
|
projects: [
|
|
22
|
-
{ name: 'api',
|
|
23
|
-
|
|
28
|
+
{ name: 'api', testDir: './api' },
|
|
29
|
+
// Both adapter projects run the same specs from the same directory; only the
|
|
30
|
+
// adopter backend they point at differs. Adding a framework is a project
|
|
31
|
+
// entry plus a compose service, not a copy of the suite.
|
|
32
|
+
{ name: 'adapter', testDir: './adapter' },
|
|
33
|
+
{
|
|
34
|
+
name: 'adapter-fastify',
|
|
35
|
+
testDir: './adapter',
|
|
36
|
+
use: { adapterUrl: FASTIFY_ADAPTER_URL },
|
|
37
|
+
},
|
|
24
38
|
{
|
|
25
39
|
name: 'react',
|
|
26
|
-
|
|
40
|
+
testDir: './react',
|
|
27
41
|
use: { ...devices['Desktop Chrome'], baseURL: REACT_URL },
|
|
28
42
|
},
|
|
29
43
|
],
|
|
@@ -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
|
-
});
|