@rehearsal-db/core 0.1.0-beta.16 → 0.1.0-beta.18
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/CHANGELOG.md +36 -1
- package/README.md +1 -0
- package/dist/package.json +1 -2
- package/dist/src/application/interactive_task.d.mts +30 -0
- package/dist/src/application/interactive_task.mjs +83 -0
- package/dist/src/application/session.d.mts +1 -0
- package/dist/src/cli/arguments.mjs +2 -0
- package/dist/src/cli/basic_command_handlers.mjs +1 -0
- package/dist/src/cli/cli_commands.mjs +2 -0
- package/dist/src/cli/guided.mjs +8 -0
- package/dist/src/cli/identity_command_handlers.d.mts +6 -2
- package/dist/src/cli/identity_command_handlers.mjs +216 -20
- package/dist/src/cli/identity_renderer.d.mts +8 -0
- package/dist/src/cli/identity_renderer.mjs +33 -0
- package/dist/src/cli/project_commands.d.mts +125 -1
- package/dist/src/cli/project_commands.mjs +11 -2
- package/dist/src/cli/runtime_commands.d.mts +25 -0
- package/dist/src/cli/runtime_commands.mjs +24 -6
- package/dist/src/identity/claim_identity_lookup.d.mts +7 -1
- package/dist/src/identity/claim_identity_lookup.mjs +20 -11
- package/dist/src/identity/connect.d.mts +30 -0
- package/dist/src/identity/connect.mjs +111 -0
- package/dist/src/project/configuration_rendering.mjs +2 -0
- package/docs/README.md +1 -0
- package/docs/architecture.md +2 -0
- package/docs/commands.md +18 -15
- package/docs/configuration.md +1 -0
- package/docs/releasing.md +13 -8
- package/docs/runtime-policies.md +12 -3
- package/docs/stability.md +69 -0
- package/docs/troubleshooting.md +17 -0
- package/package.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## [0.1.0-beta.18] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- A concise stability contract now defines the supported public surface, defects that
|
|
13
|
+
block the first stable release, and the exact beta-to-RC-to-stable promotion gate.
|
|
14
|
+
- Automated release-policy tests cover beta, release-candidate, stable, malformed, and
|
|
15
|
+
mismatched GitHub release metadata.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Protected OIDC publication now supports both prerelease and stable versions. Beta and
|
|
20
|
+
release-candidate versions update `beta`; once a stable version exists they cannot
|
|
21
|
+
replace `latest`. Stable publication updates `latest` without rewriting `beta`.
|
|
22
|
+
- npm channel selection lives only in the protected release workflow instead of static
|
|
23
|
+
package metadata, and missing beta.16 and beta.17 changelog comparisons are restored.
|
|
24
|
+
- Vitest's locked patch release is updated from 5.0.2 to 5.0.3 after the complete CI
|
|
25
|
+
matrix passed.
|
|
26
|
+
|
|
27
|
+
## [0.1.0-beta.17] - 2026-10-08
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `rehearsal identity connect` and the guided home screen now keep the sandbox app
|
|
32
|
+
available during ordinary provider sign-in, detect exactly one approved verified local
|
|
33
|
+
identity, show a redacted copied-account preview, and apply the existing exact claim
|
|
34
|
+
after one deliberate confirmation. The app is reopened automatically with an honest
|
|
35
|
+
fresh-session instruction; explicit `identity plan` and `identity claim` commands remain
|
|
36
|
+
available for scripts.
|
|
37
|
+
|
|
38
|
+
## [0.1.0-beta.16] - 2026-10-08
|
|
39
|
+
|
|
8
40
|
### Changed
|
|
9
41
|
|
|
10
42
|
- The CLI now routes guided and scripted requests through focused typed command
|
|
@@ -512,7 +544,10 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
512
544
|
publication uses short-lived trusted OIDC, and every release tag must already exist on
|
|
513
545
|
protected `main`.
|
|
514
546
|
|
|
515
|
-
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.
|
|
547
|
+
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.18...HEAD
|
|
548
|
+
[0.1.0-beta.18]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.17...v0.1.0-beta.18
|
|
549
|
+
[0.1.0-beta.17]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.16...v0.1.0-beta.17
|
|
550
|
+
[0.1.0-beta.16]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.15...v0.1.0-beta.16
|
|
516
551
|
[0.1.0-beta.15]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.14...v0.1.0-beta.15
|
|
517
552
|
[0.1.0-beta.14]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.13...v0.1.0-beta.14
|
|
518
553
|
[0.1.0-beta.13]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.12...v0.1.0-beta.13
|
package/README.md
CHANGED
|
@@ -155,6 +155,7 @@ Reference:
|
|
|
155
155
|
- [Runtime and local identity policies](docs/runtime-policies.md)
|
|
156
156
|
- [Glossary](docs/glossary.md)
|
|
157
157
|
- [Repository architecture](docs/architecture.md)
|
|
158
|
+
- [Stability and the first stable release](docs/stability.md)
|
|
158
159
|
- [Release process](docs/releasing.md)
|
|
159
160
|
|
|
160
161
|
## Getting support
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehearsal-db/core",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Safely rehearse PostgreSQL and Supabase migrations against sanitized, production-shaped data.",
|
|
6
6
|
"repository": {
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"homepage": "https://ddupasquier.github.io/rehearsal-db/",
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public",
|
|
16
|
-
"tag": "beta",
|
|
17
16
|
"provenance": true
|
|
18
17
|
},
|
|
19
18
|
"type": "module",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Run identity discovery while an owned local application remains available. */
|
|
2
|
+
import type { EventEmitter } from "node:events";
|
|
3
|
+
import { type ApplicationDiagnostics, type ApplicationSession, type ApplicationSessionSignal } from "./session.mjs";
|
|
4
|
+
type SignalTarget = Pick<EventEmitter, "prependListener" | "removeListener">;
|
|
5
|
+
interface SessionInput {
|
|
6
|
+
readonly isTTY?: boolean;
|
|
7
|
+
prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
8
|
+
removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A terminal interruption aborts the task and retires the owned process group.
|
|
12
|
+
* A completed task leaves the session running so its caller can deliberately
|
|
13
|
+
* stop, reuse, or hand it back to the persistent application holder.
|
|
14
|
+
*/
|
|
15
|
+
export declare const runApplicationSessionTask: <Value>({ session, task, signalTarget, input, onReady, }: {
|
|
16
|
+
session: ApplicationSession;
|
|
17
|
+
task: (signal: AbortSignal) => Promise<Value>;
|
|
18
|
+
signalTarget?: SignalTarget;
|
|
19
|
+
input?: SessionInput;
|
|
20
|
+
onReady?: (ready: ApplicationSession["ready"]) => void;
|
|
21
|
+
}) => Promise<Readonly<{
|
|
22
|
+
completed: true;
|
|
23
|
+
value: Value;
|
|
24
|
+
diagnostics: ApplicationDiagnostics;
|
|
25
|
+
}> | Readonly<{
|
|
26
|
+
completed: false;
|
|
27
|
+
signal: ApplicationSessionSignal;
|
|
28
|
+
diagnostics: ApplicationDiagnostics;
|
|
29
|
+
}>>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Run identity discovery while an owned local application remains available. */
|
|
2
|
+
import { summarizeProjectCommandFailure, } from "./session.mjs";
|
|
3
|
+
const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
|
|
4
|
+
/**
|
|
5
|
+
* A terminal interruption aborts the task and retires the owned process group.
|
|
6
|
+
* A completed task leaves the session running so its caller can deliberately
|
|
7
|
+
* stop, reuse, or hand it back to the persistent application holder.
|
|
8
|
+
*/
|
|
9
|
+
export const runApplicationSessionTask = async ({ session, task, signalTarget = process, input = process.stdin, onReady = () => undefined, }) => {
|
|
10
|
+
if (!session ||
|
|
11
|
+
typeof session.wait !== "function" ||
|
|
12
|
+
typeof session.stop !== "function") {
|
|
13
|
+
throw new Error("A started application session is required.");
|
|
14
|
+
}
|
|
15
|
+
let finishInterruption = () => undefined;
|
|
16
|
+
const interruption = new Promise((resolve) => {
|
|
17
|
+
finishInterruption = resolve;
|
|
18
|
+
});
|
|
19
|
+
const handlers = new Map(SESSION_SIGNALS.map((signal) => [
|
|
20
|
+
signal,
|
|
21
|
+
() => finishInterruption({ kind: "interrupted", signal }),
|
|
22
|
+
]));
|
|
23
|
+
for (const [signal, handler] of handlers) {
|
|
24
|
+
signalTarget.prependListener(signal, handler);
|
|
25
|
+
}
|
|
26
|
+
const onInput = (chunk) => {
|
|
27
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
28
|
+
if (bytes.includes(0x03)) {
|
|
29
|
+
finishInterruption({ kind: "interrupted", signal: "SIGINT" });
|
|
30
|
+
}
|
|
31
|
+
else if (bytes.includes(0x1a)) {
|
|
32
|
+
finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (input?.isTTY)
|
|
36
|
+
input.prependListener("data", onInput);
|
|
37
|
+
const abortController = new AbortController();
|
|
38
|
+
try {
|
|
39
|
+
onReady(session.ready);
|
|
40
|
+
const outcome = await Promise.race([
|
|
41
|
+
interruption,
|
|
42
|
+
task(abortController.signal).then((value) => ({
|
|
43
|
+
kind: "completed",
|
|
44
|
+
value,
|
|
45
|
+
})),
|
|
46
|
+
session.wait().then((exit) => ({ kind: "exited", ...exit })),
|
|
47
|
+
]);
|
|
48
|
+
if (outcome.kind === "exited") {
|
|
49
|
+
throw new Error(`Application exited before identity connection finished (${summarizeProjectCommandFailure({
|
|
50
|
+
status: outcome.status,
|
|
51
|
+
signal: outcome.signal,
|
|
52
|
+
stdoutByteCount: session.diagnostics().stdoutBytes,
|
|
53
|
+
stderrByteCount: session.diagnostics().stderrBytes,
|
|
54
|
+
})}).`);
|
|
55
|
+
}
|
|
56
|
+
if (outcome.kind === "interrupted") {
|
|
57
|
+
abortController.abort(new Error(`Identity connection was interrupted by ${outcome.signal}.`));
|
|
58
|
+
await session.stop();
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
completed: false,
|
|
61
|
+
signal: outcome.signal,
|
|
62
|
+
diagnostics: session.diagnostics(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
completed: true,
|
|
67
|
+
value: outcome.value,
|
|
68
|
+
diagnostics: session.diagnostics(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
abortController.abort(error);
|
|
73
|
+
await session.stop();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
for (const [signal, handler] of handlers) {
|
|
78
|
+
signalTarget.removeListener(signal, handler);
|
|
79
|
+
}
|
|
80
|
+
if (input?.isTTY)
|
|
81
|
+
input.removeListener("data", onInput);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import type { EventEmitter } from "node:events";
|
|
4
4
|
declare const SESSION_SIGNALS: string[];
|
|
5
5
|
type SessionSignal = (typeof SESSION_SIGNALS)[number];
|
|
6
|
+
export type ApplicationSessionSignal = SessionSignal;
|
|
6
7
|
export interface ApplicationReadiness {
|
|
7
8
|
readonly url: string;
|
|
8
9
|
readonly expectedStatus?: number;
|
|
@@ -113,6 +113,8 @@ export const commandMutatesProject = ({ command, flags, guided, }) => {
|
|
|
113
113
|
return Boolean(flags.refreshConfirmation);
|
|
114
114
|
if (command === "identity claim")
|
|
115
115
|
return Boolean(flags.identityConfirmation);
|
|
116
|
+
if (command === "identity connect")
|
|
117
|
+
return true;
|
|
116
118
|
if (command === "run")
|
|
117
119
|
return !flags.dryRun;
|
|
118
120
|
return [
|
|
@@ -18,6 +18,7 @@ Commands:
|
|
|
18
18
|
refresh [--confirm-refresh=] Replace the database copy, reset locally, and remove old copies
|
|
19
19
|
identity plan --identity= Preview an approved local identity association
|
|
20
20
|
identity claim --identity= --confirm-identity= Apply the exact local association
|
|
21
|
+
identity connect [--identity=] Keep the app open, detect sign-in, and connect once
|
|
21
22
|
doctor Check whether Rehearsal is safe and ready
|
|
22
23
|
support Print a safe, copy-ready support report
|
|
23
24
|
explain Show the immutable execution plan
|
|
@@ -18,6 +18,8 @@ export const createCliCommandRegistry = ({ projectRoot, projectCommands, runtime
|
|
|
18
18
|
...createSourceCommandHandlers({ runRefreshWorkflow }),
|
|
19
19
|
...createIdentityCommandHandlers({
|
|
20
20
|
loadIdentityClaimPlan: projectCommands.loadIdentityClaimPlan,
|
|
21
|
+
loadIdentityPolicy: projectCommands.loadIdentityPolicy,
|
|
22
|
+
startRuntimeApplication: runtimeCommands.startRuntimeApplication,
|
|
21
23
|
}),
|
|
22
24
|
...createInspectionCommandHandlers(),
|
|
23
25
|
...createApplicationCommandHandlers({
|
package/dist/src/cli/guided.mjs
CHANGED
|
@@ -95,6 +95,7 @@ const inspectGuidedProject = async ({ projectRoot, planOptions, topologyCandidat
|
|
|
95
95
|
policyAbsolutePath: loaded.paths.sanitizationPolicy,
|
|
96
96
|
...(migrationSummary === undefined ? {} : { migrationSummary }),
|
|
97
97
|
refreshConfigured: Boolean(loaded.config.preparation),
|
|
98
|
+
identityConfigured: Boolean(loaded.paths.identityPolicy),
|
|
98
99
|
dependentCount: topology?.dependents.length ?? 0,
|
|
99
100
|
runtime: topology
|
|
100
101
|
? (await Promise.all(topology.targets.map((target) => pathExists(join(target.paths.runtimeWorkdir, "baseline.json"))))).every(Boolean)
|
|
@@ -334,6 +335,13 @@ const guidedOptions = (state, hasLastDetails) => {
|
|
|
334
335
|
options.push({ label: "Check readiness", command: "doctor" });
|
|
335
336
|
}
|
|
336
337
|
if (state.node.supported && state.runtime) {
|
|
338
|
+
if (state.identityConfigured) {
|
|
339
|
+
options.push({
|
|
340
|
+
label: "Connect my copied account",
|
|
341
|
+
hint: "Sign in normally, review once, then open the copied profile",
|
|
342
|
+
command: "identity connect",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
337
345
|
options.push({
|
|
338
346
|
label: "Open the sandbox app",
|
|
339
347
|
hint: "Keep it running for hands-on testing",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
/** Provider-neutral copied-account planning and
|
|
1
|
+
/** Provider-neutral copied-account planning, application, and guided connection. */
|
|
2
2
|
import type { CommandDefinition } from "./command_contract.mjs";
|
|
3
3
|
import type { createProjectCommands } from "./project_commands.mjs";
|
|
4
|
+
import type { createRuntimeCommands } from "./runtime_commands.mjs";
|
|
4
5
|
type ProjectCommands = ReturnType<typeof createProjectCommands>;
|
|
5
|
-
|
|
6
|
+
type RuntimeCommands = ReturnType<typeof createRuntimeCommands>;
|
|
7
|
+
export declare const createIdentityCommandHandlers: ({ loadIdentityClaimPlan, loadIdentityPolicy, startRuntimeApplication, }: {
|
|
6
8
|
loadIdentityClaimPlan: ProjectCommands["loadIdentityClaimPlan"];
|
|
9
|
+
loadIdentityPolicy: ProjectCommands["loadIdentityPolicy"];
|
|
10
|
+
startRuntimeApplication: RuntimeCommands["startRuntimeApplication"];
|
|
7
11
|
}) => readonly CommandDefinition[];
|
|
8
12
|
export {};
|
|
@@ -1,36 +1,232 @@
|
|
|
1
|
-
/** Provider-neutral copied-account planning and
|
|
1
|
+
/** Provider-neutral copied-account planning, application, and guided connection. */
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { parseEnv } from "node:util";
|
|
4
|
+
import * as prompts from "@clack/prompts";
|
|
5
|
+
import { holdApplicationSession } from "../application/session.mjs";
|
|
6
|
+
import { runApplicationSessionTask } from "../application/interactive_task.mjs";
|
|
4
7
|
import { applyIdentityClaim } from "../identity/claim.mjs";
|
|
8
|
+
import { waitForIdentityConnection } from "../identity/connect.mjs";
|
|
5
9
|
import { createSupabaseStorageTransfer } from "../identity/storage.mjs";
|
|
6
|
-
import { exactCommands, renderedCommand } from "./command_contract.mjs";
|
|
10
|
+
import { exactCommands, renderedCommand, textCommand, } from "./command_contract.mjs";
|
|
11
|
+
import { renderIdentityConnection } from "./identity_renderer.mjs";
|
|
7
12
|
import { renderIdentityPlan } from "./renderers.mjs";
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
import { isHumanTerminal, promptForConfirmation, promptForSelection, useStyledPrompts, } from "./terminal.mjs";
|
|
14
|
+
const loadIdentityRuntime = async (loaded) => {
|
|
15
|
+
const environment = parseEnv(await readFile(loaded.paths.applicationEnvironment, "utf8"));
|
|
16
|
+
if (!environment.DATABASE_URL) {
|
|
17
|
+
throw new Error("The local runtime environment has no DATABASE_URL.");
|
|
18
|
+
}
|
|
19
|
+
return environment;
|
|
20
|
+
};
|
|
21
|
+
const storageTransferFor = ({ plan, environment, }) => {
|
|
22
|
+
const needsStorageTransfer = plan.identity.assets.some((asset) => asset.rewritePath);
|
|
23
|
+
if (!needsStorageTransfer)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (!environment.SUPABASE_URL || !environment.SUPABASE_SERVICE_ROLE_KEY) {
|
|
26
|
+
throw new Error("Identity asset transfer requires local Supabase URL and service-role credentials.");
|
|
27
|
+
}
|
|
28
|
+
return createSupabaseStorageTransfer({
|
|
29
|
+
url: environment.SUPABASE_URL,
|
|
30
|
+
serviceRoleKey: environment.SUPABASE_SERVICE_ROLE_KEY,
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
export const createIdentityCommandHandlers = ({ loadIdentityClaimPlan, loadIdentityPolicy, startRuntimeApplication, }) => [
|
|
34
|
+
exactCommands("identity", ["identity plan", "identity claim", "identity connect"], async ({ command, flags, planOptions, guided, state }) => {
|
|
35
|
+
if (command === "identity connect") {
|
|
36
|
+
if (!isHumanTerminal(flags)) {
|
|
37
|
+
throw new Error("identity connect requires an interactive terminal. Scripts should keep using identity plan and identity claim.");
|
|
38
|
+
}
|
|
39
|
+
if (!flags.identityName) {
|
|
40
|
+
const { policy } = await loadIdentityPolicy({
|
|
41
|
+
flags,
|
|
42
|
+
planOptions,
|
|
43
|
+
session: state,
|
|
44
|
+
});
|
|
45
|
+
if (policy.identities.length === 1) {
|
|
46
|
+
flags.identityName = policy.identities[0].name;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const selected = await promptForSelection({
|
|
50
|
+
message: "Which copied account would you like to connect?",
|
|
51
|
+
flags,
|
|
52
|
+
options: policy.identities.map((identity) => ({
|
|
53
|
+
label: identity.name,
|
|
54
|
+
value: identity.name,
|
|
55
|
+
})),
|
|
56
|
+
});
|
|
57
|
+
if (!selected) {
|
|
58
|
+
return textCommand("COPIED ACCOUNT CONNECTION CANCELLED\nNothing was changed.");
|
|
59
|
+
}
|
|
60
|
+
flags.identityName = selected;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const initial = await loadIdentityClaimPlan({
|
|
64
|
+
flags,
|
|
65
|
+
planOptions,
|
|
66
|
+
session: state,
|
|
67
|
+
});
|
|
68
|
+
const initialEnvironment = await loadIdentityRuntime(initial.loaded);
|
|
69
|
+
let active;
|
|
70
|
+
let reopened;
|
|
71
|
+
try {
|
|
72
|
+
active = await startRuntimeApplication({
|
|
73
|
+
flags,
|
|
74
|
+
planOptions,
|
|
75
|
+
session: state,
|
|
76
|
+
});
|
|
77
|
+
const observed = await runApplicationSessionTask({
|
|
78
|
+
session: active.session,
|
|
79
|
+
onReady: (ready) => {
|
|
80
|
+
const lines = [
|
|
81
|
+
`Application: ${ready.url}`,
|
|
82
|
+
"Sign in normally with the provider allowed by your reviewed identity policy.",
|
|
83
|
+
"Rehearsal will continue when exactly one approved, verified local identity appears.",
|
|
84
|
+
"It never reads your provider password, passkey, MFA secret, browser cookies, or session tokens.",
|
|
85
|
+
"Press Ctrl+C to cancel or Ctrl+Z to exit Rehearsal.",
|
|
86
|
+
];
|
|
87
|
+
if (useStyledPrompts(flags)) {
|
|
88
|
+
prompts.note(lines.join("\n"), "SIGN IN TO THE SANDBOX");
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log(["", "SIGN IN TO THE SANDBOX", ...lines, ""].join("\n"));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
task: (signal) => waitForIdentityConnection({
|
|
95
|
+
plan: initial.plan,
|
|
96
|
+
connectionString: initialEnvironment.DATABASE_URL,
|
|
97
|
+
signal,
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
if (!observed.completed) {
|
|
101
|
+
if (guided && observed.signal === "SIGTSTP") {
|
|
102
|
+
flags.exitGuidedSession = true;
|
|
103
|
+
}
|
|
104
|
+
return textCommand([
|
|
105
|
+
"COPIED ACCOUNT CONNECTION CANCELLED",
|
|
106
|
+
`Application stopped after ${observed.signal}.`,
|
|
107
|
+
"Nothing was changed. The database runtime was preserved.",
|
|
108
|
+
].join("\n"));
|
|
109
|
+
}
|
|
110
|
+
const preview = observed.value;
|
|
111
|
+
if (preview.associated) {
|
|
112
|
+
const closed = await holdApplicationSession({
|
|
113
|
+
session: active.session,
|
|
114
|
+
onReady: () => {
|
|
115
|
+
const message = renderIdentityConnection({
|
|
116
|
+
mode: "already-connected",
|
|
117
|
+
preview,
|
|
118
|
+
});
|
|
119
|
+
if (useStyledPrompts(flags)) {
|
|
120
|
+
prompts.note(message, "SANDBOX READY");
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(`\n${message}\n`);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
if (guided && closed.signal === "SIGTSTP") {
|
|
128
|
+
flags.exitGuidedSession = true;
|
|
129
|
+
}
|
|
130
|
+
return renderedCommand({
|
|
131
|
+
data: {
|
|
132
|
+
mode: "already-connected",
|
|
133
|
+
preview,
|
|
134
|
+
application: { stoppedBy: closed.signal },
|
|
135
|
+
},
|
|
136
|
+
render: ({ mode, preview: renderedPreview }) => renderIdentityConnection({ mode, preview: renderedPreview }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
await active.session.stop();
|
|
140
|
+
console.log(`\n${renderIdentityConnection({ mode: "preview", preview })}\n`);
|
|
141
|
+
const accepted = await promptForConfirmation("Connect my copied account using exactly this plan?", flags);
|
|
142
|
+
if (!accepted) {
|
|
143
|
+
return renderedCommand({
|
|
144
|
+
data: { mode: "cancelled", preview },
|
|
145
|
+
render: ({ mode, preview: renderedPreview }) => renderIdentityConnection({ mode, preview: renderedPreview }),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const refreshed = await loadIdentityClaimPlan({
|
|
149
|
+
flags,
|
|
150
|
+
planOptions,
|
|
151
|
+
session: state,
|
|
152
|
+
});
|
|
153
|
+
if (refreshed.plan.digest !== preview.planDigest) {
|
|
154
|
+
throw new Error("The reviewed identity plan changed after sign-in. Nothing was changed; start the connection again.");
|
|
155
|
+
}
|
|
156
|
+
const refreshedEnvironment = await loadIdentityRuntime(refreshed.loaded);
|
|
157
|
+
const storageTransfer = storageTransferFor({
|
|
158
|
+
plan: refreshed.plan,
|
|
159
|
+
environment: refreshedEnvironment,
|
|
160
|
+
});
|
|
161
|
+
const result = await applyIdentityClaim({
|
|
162
|
+
plan: refreshed.plan,
|
|
163
|
+
confirmation: preview.planDigest,
|
|
164
|
+
connectionString: refreshedEnvironment.DATABASE_URL,
|
|
165
|
+
...(storageTransfer === undefined ? {} : { storageTransfer }),
|
|
166
|
+
});
|
|
167
|
+
reopened = await startRuntimeApplication({
|
|
168
|
+
flags,
|
|
169
|
+
planOptions,
|
|
170
|
+
session: state,
|
|
171
|
+
});
|
|
172
|
+
const closed = await holdApplicationSession({
|
|
173
|
+
session: reopened.session,
|
|
174
|
+
onReady: (ready) => {
|
|
175
|
+
const lines = [
|
|
176
|
+
renderIdentityConnection({
|
|
177
|
+
mode: "connected",
|
|
178
|
+
preview,
|
|
179
|
+
result,
|
|
180
|
+
}),
|
|
181
|
+
"",
|
|
182
|
+
`Application: ${ready.url}`,
|
|
183
|
+
"The app is running again. Sign out and sign in once for fresh claims.",
|
|
184
|
+
"Press Ctrl+C to close the app or Ctrl+Z to exit Rehearsal.",
|
|
185
|
+
];
|
|
186
|
+
if (useStyledPrompts(flags)) {
|
|
187
|
+
prompts.note(lines.join("\n"), "ACCOUNT CONNECTED");
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
console.log(["", ...lines, ""].join("\n"));
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
if (guided && closed.signal === "SIGTSTP") {
|
|
195
|
+
flags.exitGuidedSession = true;
|
|
196
|
+
}
|
|
197
|
+
return renderedCommand({
|
|
198
|
+
data: {
|
|
199
|
+
mode: "connected",
|
|
200
|
+
preview,
|
|
201
|
+
result,
|
|
202
|
+
application: { stoppedBy: closed.signal },
|
|
203
|
+
},
|
|
204
|
+
render: ({ mode, preview: renderedPreview, result: claimResult }) => renderIdentityConnection({
|
|
205
|
+
mode,
|
|
206
|
+
preview: renderedPreview,
|
|
207
|
+
result: claimResult,
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await active?.session.stop();
|
|
213
|
+
await reopened?.session.stop();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
10
216
|
const { loaded, plan } = await loadIdentityClaimPlan({
|
|
11
217
|
flags,
|
|
12
218
|
planOptions,
|
|
219
|
+
session: state,
|
|
13
220
|
});
|
|
14
221
|
if (command === "identity plan" || !flags.identityConfirmation) {
|
|
15
222
|
const data = { mode: "preview", plan };
|
|
16
223
|
return renderedCommand({ data, render: renderIdentityPlan });
|
|
17
224
|
}
|
|
18
|
-
const runtimeEnvironment =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (needsStorageTransfer &&
|
|
24
|
-
(!runtimeEnvironment.SUPABASE_URL ||
|
|
25
|
-
!runtimeEnvironment.SUPABASE_SERVICE_ROLE_KEY)) {
|
|
26
|
-
throw new Error("Identity asset transfer requires local Supabase URL and service-role credentials.");
|
|
27
|
-
}
|
|
28
|
-
const storageTransfer = needsStorageTransfer
|
|
29
|
-
? createSupabaseStorageTransfer({
|
|
30
|
-
url: runtimeEnvironment.SUPABASE_URL,
|
|
31
|
-
serviceRoleKey: runtimeEnvironment.SUPABASE_SERVICE_ROLE_KEY,
|
|
32
|
-
})
|
|
33
|
-
: undefined;
|
|
225
|
+
const runtimeEnvironment = await loadIdentityRuntime(loaded);
|
|
226
|
+
const storageTransfer = storageTransferFor({
|
|
227
|
+
plan,
|
|
228
|
+
environment: runtimeEnvironment,
|
|
229
|
+
});
|
|
34
230
|
const result = await applyIdentityClaim({
|
|
35
231
|
plan,
|
|
36
232
|
confirmation: flags.identityConfirmation,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Redacted terminal output for the interactive copied-account connection. */
|
|
2
|
+
import type { IdentityClaimResult } from "../identity/claim_contract.mjs";
|
|
3
|
+
import type { IdentityConnectionPreview } from "../identity/connect.mjs";
|
|
4
|
+
export declare const renderIdentityConnection: ({ mode, preview, result, }: {
|
|
5
|
+
mode: "preview" | "connected" | "already-connected" | "cancelled";
|
|
6
|
+
preview: IdentityConnectionPreview;
|
|
7
|
+
result?: IdentityClaimResult;
|
|
8
|
+
}) => string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Redacted terminal output for the interactive copied-account connection. */
|
|
2
|
+
export const renderIdentityConnection = ({ mode, preview, result, }) => [
|
|
3
|
+
`COPIED ACCOUNT — ${mode.replaceAll("-", " ").toUpperCase()}`,
|
|
4
|
+
`Account label: ${preview.name}`,
|
|
5
|
+
`Verified provider: ${preview.provider}`,
|
|
6
|
+
`Data groups: ${preview.relationalGroups} relational; ${preview.nestedJsonGroups} nested JSON`,
|
|
7
|
+
`Images: ${preview.assetScopes} reviewed Storage scope${preview.assetScopes === 1 ? "" : "s"}; ${preview.storagePathGroups} path reference group${preview.storagePathGroups === 1 ? "" : "s"}`,
|
|
8
|
+
`Permissions: ${preview.claimKeys} reviewed claim${preview.claimKeys === 1 ? "" : "s"}; token hook ${preview.tokenHookConfigured ? "verified during connection" : "not configured"}`,
|
|
9
|
+
`Immutable history: ${preview.preservedAuditGroups} reference group${preview.preservedAuditGroups === 1 ? "" : "s"} stays with the copied audit identity`,
|
|
10
|
+
`Plan: ${preview.planDigest.slice(0, 12)}`,
|
|
11
|
+
"",
|
|
12
|
+
...(mode === "preview"
|
|
13
|
+
? [
|
|
14
|
+
"This changes only the disposable local sandbox.",
|
|
15
|
+
"Edited or independent local account data will make the connection stop safely.",
|
|
16
|
+
"Nothing has changed yet.",
|
|
17
|
+
]
|
|
18
|
+
: mode === "cancelled"
|
|
19
|
+
? [
|
|
20
|
+
"Nothing was changed. Your local database runtime is still available.",
|
|
21
|
+
]
|
|
22
|
+
: mode === "already-connected"
|
|
23
|
+
? [
|
|
24
|
+
"This copied account is already connected in the current local runtime.",
|
|
25
|
+
"No transfer was repeated.",
|
|
26
|
+
]
|
|
27
|
+
: [
|
|
28
|
+
`Transferred images: ${result?.storageObjectsTransferred ?? 0}`,
|
|
29
|
+
`Removed untouched signup defaults: ${result?.removedSignupDefaults ?? 0}`,
|
|
30
|
+
"The copied account is connected and verified.",
|
|
31
|
+
"Sign out and sign in once more so the application receives fresh role claims.",
|
|
32
|
+
]),
|
|
33
|
+
].join("\n");
|