@indigoai-us/hq-cli 5.225.0 → 5.227.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/CHANGELOG.md +47 -1
- package/dist/command-catalog.generated.d.ts +100 -0
- package/dist/command-catalog.generated.js +128 -0
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +6 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +17 -2
- package/dist/commands/cloud.d.ts +10 -0
- package/dist/commands/cloud.js +52 -47
- package/dist/commands/lanes-checks.d.ts +2 -0
- package/dist/commands/lanes-checks.js +13 -11
- package/dist/commands/lanes-import.d.ts +1 -0
- package/dist/commands/lanes-import.js +12 -11
- package/dist/commands/lanes-report.js +21 -14
- package/dist/commands/lanes-tracker.d.ts +1 -0
- package/dist/commands/lanes-tracker.js +39 -31
- package/dist/commands/lanes.d.ts +8 -0
- package/dist/commands/lanes.js +818 -314
- package/dist/commands/staff/reset-test-account.d.ts +110 -0
- package/dist/commands/staff/reset-test-account.js +268 -0
- package/dist/commands/staff/test-account.d.ts +51 -0
- package/dist/commands/staff/test-account.js +196 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +4 -6
- package/dist/lib/checkpoint-ontology/plan.js +8 -26
- package/dist/lib/doctor/client-health-flag-token.d.ts +15 -0
- package/dist/lib/doctor/client-health-flag-token.js +66 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +1 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.js +16 -5
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +1 -0
- package/dist/lib/doctor/client-health-sentry-flag.js +17 -6
- package/dist/lib/lanes/dropbox.d.ts +9 -0
- package/dist/lib/lanes/dropbox.js +70 -1
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +28 -0
- package/dist/lib/lanes/store.js +9 -1
- package/dist/lib/lanes/tracker-mesh-questions.d.ts +54 -0
- package/dist/lib/lanes/tracker-mesh-questions.js +138 -0
- package/dist/lib/lanes/tracker-mesh.d.ts +19 -0
- package/dist/lib/lanes/tracker-mesh.js +228 -0
- package/dist/lib/lanes/tracker-store.d.ts +13 -2
- package/dist/lib/lanes/tracker-store.js +1 -1
- package/dist/lib/lanes/types.d.ts +4 -0
- package/dist/lib/lanes/watcher-main.js +16 -0
- package/dist/lib/lanes/watcher.d.ts +4 -1
- package/dist/lib/lanes/watcher.js +77 -3
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +198 -3
- package/dist/utils/upload-author.d.ts +15 -0
- package/dist/utils/upload-author.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `hq staff reset-test-account <email>` — onboarding-test-accounts US-007.
|
|
4
|
+
*
|
|
5
|
+
* Thin CLI wrapper over `POST /v1/admin/test-account/reset` (US-003 dry-run +
|
|
6
|
+
* US-006 execute). Server owns every safety gate; this file owns two operator
|
|
7
|
+
* ergonomics the wire cannot: (1) a LOCAL staff-email preflight so a non-staff
|
|
8
|
+
* caller stops here instead of paying a round trip for the same 403, and (2)
|
|
9
|
+
* the typed-email confirmation prompt for `--execute`, whose value is what the
|
|
10
|
+
* server matches against `plan.target.normalizedEmail` via `confirmEmailMatches`.
|
|
11
|
+
*
|
|
12
|
+
* OQ-003 recommendation: this ships as a subcommand of the existing hq CLI
|
|
13
|
+
* (`hq staff …`) rather than a separate `hq-staff` binary — reuses auth,
|
|
14
|
+
* distribution, and telemetry with zero new plumbing.
|
|
15
|
+
*/
|
|
16
|
+
interface PlanCognitoUser {
|
|
17
|
+
username: string;
|
|
18
|
+
sub: string;
|
|
19
|
+
provider: string;
|
|
20
|
+
email: string;
|
|
21
|
+
}
|
|
22
|
+
interface PlanCompany {
|
|
23
|
+
uid: string;
|
|
24
|
+
slug: string;
|
|
25
|
+
memberCount: number;
|
|
26
|
+
}
|
|
27
|
+
interface Plan {
|
|
28
|
+
target: {
|
|
29
|
+
input: string;
|
|
30
|
+
normalizedEmail: string;
|
|
31
|
+
};
|
|
32
|
+
personUid: string | null;
|
|
33
|
+
cognitoUsers: PlanCognitoUser[];
|
|
34
|
+
ownedCompanies: PlanCompany[];
|
|
35
|
+
personalVault: {
|
|
36
|
+
bucketName: string | null;
|
|
37
|
+
kmsKeyAlias: string | null;
|
|
38
|
+
};
|
|
39
|
+
stripe: {
|
|
40
|
+
customerIds: string[];
|
|
41
|
+
subscriptionState: unknown | null;
|
|
42
|
+
};
|
|
43
|
+
welcomeSeries: {
|
|
44
|
+
present: boolean;
|
|
45
|
+
};
|
|
46
|
+
workMesh: {
|
|
47
|
+
eventCount: number | null;
|
|
48
|
+
note?: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface StepOutcome {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
skipped?: boolean;
|
|
54
|
+
reason?: string;
|
|
55
|
+
error?: string;
|
|
56
|
+
}
|
|
57
|
+
interface ExecuteResult {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
steps: Record<string, StepOutcome & Record<string, unknown>>;
|
|
60
|
+
counts: {
|
|
61
|
+
stripeDeleted: number;
|
|
62
|
+
welcomeDeleted: number;
|
|
63
|
+
rawTelemetryDeleted: number | null;
|
|
64
|
+
workMeshDeleted: number | null;
|
|
65
|
+
membershipsDeleted: number;
|
|
66
|
+
companiesPurged: number;
|
|
67
|
+
entitiesPurged: number;
|
|
68
|
+
cognitoDeleted: number;
|
|
69
|
+
kms: string;
|
|
70
|
+
invitesDeleted: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Same predicate the server's admin gate applies at the FIRST layer
|
|
75
|
+
* (`isStaffEmail` in the vault-service admin handler). Duplicated here so a
|
|
76
|
+
* non-staff caller stops before the round trip; the server remains the
|
|
77
|
+
* authorization boundary (this file could not enforce it if it wanted to).
|
|
78
|
+
*
|
|
79
|
+
* Exact suffix match. `foo@getindigo.ai.evil.test` and `bar@xgetindigo.ai`
|
|
80
|
+
* do not qualify.
|
|
81
|
+
*/
|
|
82
|
+
export declare function isStaffEmail(candidate: unknown): boolean;
|
|
83
|
+
/** Pull the caller email out of a locally cached id_token. */
|
|
84
|
+
export declare function callerEmailFromToken(token: string): string | undefined;
|
|
85
|
+
export type ConfirmFn = (question: string) => Promise<string>;
|
|
86
|
+
export interface RunResetTestAccountParams {
|
|
87
|
+
email: string;
|
|
88
|
+
execute: boolean;
|
|
89
|
+
/** Skip the typed-confirmation prompt on --execute (for scripts). */
|
|
90
|
+
yes?: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface RunResetTestAccountDeps {
|
|
93
|
+
prompt?: ConfirmFn;
|
|
94
|
+
/** Whether to treat this session as an interactive TTY. Defaults to `process.stdin.isTTY`. */
|
|
95
|
+
isTty?: boolean;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Print a compact dry-run plan. Kept flat so the operator can eyeball what the
|
|
99
|
+
* executor would touch without a JSON pretty-printer in the loop.
|
|
100
|
+
*/
|
|
101
|
+
export declare function renderPlan(plan: Plan): string;
|
|
102
|
+
/**
|
|
103
|
+
* One-line-per-step summary of the executor's result. AC calls for "per-step
|
|
104
|
+
* counts, ok/failed".
|
|
105
|
+
*/
|
|
106
|
+
export declare function renderExecuteSummary(result: ExecuteResult): string;
|
|
107
|
+
export declare function runResetTestAccount(params: RunResetTestAccountParams, deps?: RunResetTestAccountDeps): Promise<void>;
|
|
108
|
+
export declare function registerResetTestAccountCommand(staff: Command, deps?: RunResetTestAccountDeps): Command;
|
|
109
|
+
export {};
|
|
110
|
+
//# sourceMappingURL=reset-test-account.d.ts.map
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as readline from "node:readline";
|
|
3
|
+
import { ensureCognitoToken } from "../../utils/cognito-session.js";
|
|
4
|
+
import { peekIdToken } from "../../utils/id-token.js";
|
|
5
|
+
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
6
|
+
// ── Local staff-email preflight ─────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Same predicate the server's admin gate applies at the FIRST layer
|
|
9
|
+
* (`isStaffEmail` in the vault-service admin handler). Duplicated here so a
|
|
10
|
+
* non-staff caller stops before the round trip; the server remains the
|
|
11
|
+
* authorization boundary (this file could not enforce it if it wanted to).
|
|
12
|
+
*
|
|
13
|
+
* Exact suffix match. `foo@getindigo.ai.evil.test` and `bar@xgetindigo.ai`
|
|
14
|
+
* do not qualify.
|
|
15
|
+
*/
|
|
16
|
+
export function isStaffEmail(candidate) {
|
|
17
|
+
if (typeof candidate !== "string")
|
|
18
|
+
return false;
|
|
19
|
+
const parts = candidate.trim().toLowerCase().split("@");
|
|
20
|
+
if (parts.length !== 2 || parts[0].length === 0)
|
|
21
|
+
return false;
|
|
22
|
+
const domain = parts[1];
|
|
23
|
+
return domain === "getindigo.ai" || domain === "hqforwork.com";
|
|
24
|
+
}
|
|
25
|
+
/** Pull the caller email out of a locally cached id_token. */
|
|
26
|
+
export function callerEmailFromToken(token) {
|
|
27
|
+
const claims = peekIdToken(token);
|
|
28
|
+
const email = claims.email;
|
|
29
|
+
return typeof email === "string" && email.trim().length > 0
|
|
30
|
+
? email.trim()
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Real readline prompt, used when stdin is a TTY. */
|
|
34
|
+
function realPrompt(question) {
|
|
35
|
+
const rl = readline.createInterface({
|
|
36
|
+
input: process.stdin,
|
|
37
|
+
output: process.stdout,
|
|
38
|
+
});
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
rl.question(question, (line) => {
|
|
41
|
+
rl.close();
|
|
42
|
+
resolve(line);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Print a compact dry-run plan. Kept flat so the operator can eyeball what the
|
|
48
|
+
* executor would touch without a JSON pretty-printer in the loop.
|
|
49
|
+
*/
|
|
50
|
+
export function renderPlan(plan) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
lines.push(chalk.bold(`\nReset plan for ${plan.target.normalizedEmail}`));
|
|
53
|
+
if (plan.target.input !== plan.target.normalizedEmail) {
|
|
54
|
+
lines.push(chalk.dim(` raw input: ${plan.target.input}`));
|
|
55
|
+
}
|
|
56
|
+
lines.push(` person uid: ${plan.personUid ?? chalk.dim("(none)")}`);
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push(chalk.bold(" Cognito users"));
|
|
59
|
+
if (plan.cognitoUsers.length === 0) {
|
|
60
|
+
lines.push(chalk.dim(" (none)"));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
for (const u of plan.cognitoUsers) {
|
|
64
|
+
lines.push(` - ${u.username} [${u.provider}] ${u.email}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push(chalk.bold(" Owned companies"));
|
|
69
|
+
if (plan.ownedCompanies.length === 0) {
|
|
70
|
+
lines.push(chalk.dim(" (none)"));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (const c of plan.ownedCompanies) {
|
|
74
|
+
lines.push(` - ${c.slug} (${c.uid}) members: ${c.memberCount}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
lines.push("");
|
|
78
|
+
lines.push(chalk.bold(" Personal vault"));
|
|
79
|
+
lines.push(` bucket: ${plan.personalVault.bucketName ?? chalk.dim("(none)")}`);
|
|
80
|
+
lines.push(` kms alias: ${plan.personalVault.kmsKeyAlias ?? chalk.dim("(none)")}`);
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push(chalk.bold(" Stripe"));
|
|
83
|
+
if (plan.stripe.customerIds.length === 0) {
|
|
84
|
+
lines.push(chalk.dim(" (no customer)"));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
for (const id of plan.stripe.customerIds) {
|
|
88
|
+
lines.push(` - ${id}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (plan.stripe.subscriptionState !== null && plan.stripe.subscriptionState !== undefined) {
|
|
92
|
+
lines.push(` subscription: ${JSON.stringify(plan.stripe.subscriptionState)}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push(chalk.bold(" Other"));
|
|
96
|
+
lines.push(` welcome-series row: ${plan.welcomeSeries.present ? "yes" : "no"}`);
|
|
97
|
+
const wm = plan.workMesh.eventCount === null
|
|
98
|
+
? plan.workMesh.note
|
|
99
|
+
? `not enumerated (${plan.workMesh.note})`
|
|
100
|
+
: "not enumerated"
|
|
101
|
+
: String(plan.workMesh.eventCount);
|
|
102
|
+
lines.push(` work-mesh events: ${wm}`);
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One-line-per-step summary of the executor's result. AC calls for "per-step
|
|
107
|
+
* counts, ok/failed".
|
|
108
|
+
*/
|
|
109
|
+
export function renderExecuteSummary(result) {
|
|
110
|
+
const c = result.counts;
|
|
111
|
+
const s = result.steps;
|
|
112
|
+
const line = (label, step, count) => {
|
|
113
|
+
const flag = step.skipped
|
|
114
|
+
? chalk.dim("skipped")
|
|
115
|
+
: step.ok
|
|
116
|
+
? chalk.green("ok")
|
|
117
|
+
: chalk.red("failed");
|
|
118
|
+
const note = step.error ? ` ${chalk.red(step.error)}` : "";
|
|
119
|
+
return ` ${label.padEnd(20)} ${flag.padEnd(10)} ${String(count)}${note}`;
|
|
120
|
+
};
|
|
121
|
+
const overall = result.ok ? chalk.green("ok") : chalk.yellow("partial");
|
|
122
|
+
const rawTelemetry = c.rawTelemetryDeleted ?? "n/a";
|
|
123
|
+
const workMesh = c.workMeshDeleted ?? "n/a";
|
|
124
|
+
return [
|
|
125
|
+
"",
|
|
126
|
+
chalk.bold(`Reset result: ${overall}`),
|
|
127
|
+
line("stripe", s.stripe ?? { ok: true }, c.stripeDeleted),
|
|
128
|
+
line("welcome-series", s.welcomeSeries ?? { ok: true }, c.welcomeDeleted),
|
|
129
|
+
line("raw-telemetry", s.rawTelemetry ?? { ok: true }, rawTelemetry),
|
|
130
|
+
line("work-mesh", s.workMesh ?? { ok: true }, workMesh),
|
|
131
|
+
line("memberships", s.memberships ?? { ok: true }, c.membershipsDeleted),
|
|
132
|
+
line("companies", s.companies ?? { ok: true }, c.companiesPurged),
|
|
133
|
+
line("entities", s.entity ?? { ok: true }, c.entitiesPurged),
|
|
134
|
+
line("cognito", s.cognito ?? { ok: true }, c.cognitoDeleted),
|
|
135
|
+
line("kms (personal)", s.kms ?? { ok: true }, c.kms),
|
|
136
|
+
line("invites", s.invites ?? { ok: true }, c.invitesDeleted),
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
/** Read a JSON body, tolerant of an empty body (returns `{}`). */
|
|
140
|
+
async function readBody(res) {
|
|
141
|
+
try {
|
|
142
|
+
return (await res.json());
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function renderApiError(status, body) {
|
|
149
|
+
const code = typeof body.code === "string" ? body.code : undefined;
|
|
150
|
+
const message = typeof body.error === "string" ? body.error : `reset failed (${status})`;
|
|
151
|
+
const detail = body.details ? ` ${JSON.stringify(body.details)}` : "";
|
|
152
|
+
return code ? `${code}: ${message}${detail}` : `${message}${detail}`;
|
|
153
|
+
}
|
|
154
|
+
export async function runResetTestAccount(params, deps = {}) {
|
|
155
|
+
const email = params.email.trim();
|
|
156
|
+
if (email.length === 0) {
|
|
157
|
+
throw new Error("email argument is required");
|
|
158
|
+
}
|
|
159
|
+
const token = await ensureCognitoToken();
|
|
160
|
+
// LOCAL preflight — a non-staff caller's Cognito token cannot pass the
|
|
161
|
+
// server's admin gate anyway, so we surface the refusal immediately and
|
|
162
|
+
// avoid a round trip.
|
|
163
|
+
const callerEmail = callerEmailFromToken(token);
|
|
164
|
+
if (!callerEmail || !isStaffEmail(callerEmail)) {
|
|
165
|
+
throw new Error("reset-test-account requires a super-admin (staff) HQ session. " +
|
|
166
|
+
"Sign in with an @getindigo.ai account and retry.");
|
|
167
|
+
}
|
|
168
|
+
// Dry-run first. The AC calls out "Without --execute, runs dry-run and
|
|
169
|
+
// prints the plan" — so a bare invocation always ends here.
|
|
170
|
+
const dryRunRes = await vaultApiFetch({
|
|
171
|
+
token,
|
|
172
|
+
path: "/v1/admin/test-account/reset",
|
|
173
|
+
method: "POST",
|
|
174
|
+
body: { email, dryRun: true },
|
|
175
|
+
});
|
|
176
|
+
const dryRunBody = await readBody(dryRunRes);
|
|
177
|
+
if (!dryRunRes.ok) {
|
|
178
|
+
throw new Error(renderApiError(dryRunRes.status, dryRunBody));
|
|
179
|
+
}
|
|
180
|
+
const plan = dryRunBody.plan;
|
|
181
|
+
if (!plan) {
|
|
182
|
+
throw new Error("server did not return a plan for the dry-run request");
|
|
183
|
+
}
|
|
184
|
+
console.log(renderPlan(plan));
|
|
185
|
+
if (!params.execute) {
|
|
186
|
+
console.log(chalk.dim("\n(dry-run — no writes occurred. Pass --execute after reviewing.)"));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Execute path — require the operator to type the email back.
|
|
190
|
+
console.log(chalk.yellow(`\nThis will DELETE every artifact above for ${plan.target.normalizedEmail}.`));
|
|
191
|
+
let confirmEmail;
|
|
192
|
+
if (params.yes === true) {
|
|
193
|
+
// `--yes` is an explicit operator act. The server still requires a
|
|
194
|
+
// matching `confirmEmail`, so we supply the normalized target on the
|
|
195
|
+
// operator's behalf; the guard on the wire is unchanged.
|
|
196
|
+
confirmEmail = plan.target.normalizedEmail;
|
|
197
|
+
console.log(chalk.dim(` (--yes: sending confirmEmail=${confirmEmail})`));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const prompt = deps.prompt ?? realPrompt;
|
|
201
|
+
const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
|
|
202
|
+
if (!isTty && !params.yes) {
|
|
203
|
+
throw new Error("--execute needs an interactive terminal for the typed-email " +
|
|
204
|
+
"confirmation. Re-run in a TTY, or pass --yes to skip the prompt.");
|
|
205
|
+
}
|
|
206
|
+
const answer = await prompt(`Type the target email to confirm (${plan.target.normalizedEmail}): `);
|
|
207
|
+
confirmEmail = (answer ?? "").trim();
|
|
208
|
+
if (confirmEmail.length === 0) {
|
|
209
|
+
throw new Error("Aborted — no confirmation entered.");
|
|
210
|
+
}
|
|
211
|
+
// Local pre-check so the operator gets the friendlier message than the
|
|
212
|
+
// server's CONFIRM_EMAIL_MISMATCH. Server still enforces via
|
|
213
|
+
// confirmEmailMatches, so a stale local check cannot bypass the wire gate.
|
|
214
|
+
if (confirmEmail.toLowerCase() !== plan.target.normalizedEmail.toLowerCase()) {
|
|
215
|
+
throw new Error(`Aborted — the typed email (${confirmEmail}) does not match the ` +
|
|
216
|
+
`target (${plan.target.normalizedEmail}).`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const executeRes = await vaultApiFetch({
|
|
220
|
+
token,
|
|
221
|
+
path: "/v1/admin/test-account/reset",
|
|
222
|
+
method: "POST",
|
|
223
|
+
body: { email, dryRun: false, confirmEmail },
|
|
224
|
+
});
|
|
225
|
+
const executeBody = await readBody(executeRes);
|
|
226
|
+
// The handler answers 200 (all-ok) and 207 (best-effort partial). Anything
|
|
227
|
+
// else is a refusal from a safety gate and should surface as an error.
|
|
228
|
+
if (executeRes.status !== 200 && executeRes.status !== 207) {
|
|
229
|
+
throw new Error(renderApiError(executeRes.status, executeBody));
|
|
230
|
+
}
|
|
231
|
+
const result = executeBody.result;
|
|
232
|
+
if (!result) {
|
|
233
|
+
throw new Error("server did not return an execute result");
|
|
234
|
+
}
|
|
235
|
+
console.log(renderExecuteSummary(result));
|
|
236
|
+
if (!result.ok) {
|
|
237
|
+
// Non-zero exit so a scripted caller can react. The summary line above
|
|
238
|
+
// already shows which step failed.
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// ── Commander wiring ────────────────────────────────────────────────────────
|
|
243
|
+
function fail(err) {
|
|
244
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
export function registerResetTestAccountCommand(staff, deps = {}) {
|
|
248
|
+
return staff
|
|
249
|
+
.command("reset-test-account")
|
|
250
|
+
.description("Reset a designated real-email test account (dry-run by default). " +
|
|
251
|
+
"Wraps POST /v1/admin/test-account/reset — super-admin only.")
|
|
252
|
+
.argument("<email>", "Designated test-account email (must be in the registry)")
|
|
253
|
+
.option("--execute", "Actually delete artifacts. Prompts for a typed-email confirmation.")
|
|
254
|
+
.option("-y, --yes", "Skip the typed-email confirmation prompt (still sends confirmEmail).")
|
|
255
|
+
.action(async (email, opts) => {
|
|
256
|
+
try {
|
|
257
|
+
await runResetTestAccount({
|
|
258
|
+
email,
|
|
259
|
+
execute: opts.execute === true,
|
|
260
|
+
yes: opts.yes === true,
|
|
261
|
+
}, deps);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
fail(err);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=reset-test-account.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `hq staff test-account {add|remove|list}` — onboarding-test-accounts US-007
|
|
4
|
+
* follow-up. Thin CLI wrappers over the vault-service registry routes:
|
|
5
|
+
*
|
|
6
|
+
* POST /v1/admin/test-account-registry (add)
|
|
7
|
+
* DELETE /v1/admin/test-account-registry/{emailB64} (remove)
|
|
8
|
+
* GET /v1/admin/test-account-registry (list)
|
|
9
|
+
*
|
|
10
|
+
* The runbook now points staff at these commands instead of the previous
|
|
11
|
+
* hand-rolled `curl` + `~/.hq/cognito-tokens.json` pattern. Reusing
|
|
12
|
+
* `ensureCognitoToken` + `vaultApiFetch` means the id_token is never printed,
|
|
13
|
+
* never handed to the operator, and never lifted out of the CLI's secure state
|
|
14
|
+
* directory. The server owns every write; this file's local staff-email
|
|
15
|
+
* preflight only saves an already-refused round trip.
|
|
16
|
+
*/
|
|
17
|
+
interface RegistryEntry {
|
|
18
|
+
email: string;
|
|
19
|
+
addedBy: string;
|
|
20
|
+
addedAt: string;
|
|
21
|
+
note: string | null;
|
|
22
|
+
active: boolean;
|
|
23
|
+
removedBy?: string;
|
|
24
|
+
removedAt?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Encode an email as the base64url path segment the DELETE route expects. The
|
|
28
|
+
* server decodes and normalizes (`decodeRegistryEmailKey` → `normalizeRegistryEmail`),
|
|
29
|
+
* so the operator can type `Bohemian.HW@gmail.com` and hit the same row as
|
|
30
|
+
* `bohemianhw@gmail.com`. Local encoding is a pure transform on the raw input;
|
|
31
|
+
* we never persist or print a locally normalized value.
|
|
32
|
+
*/
|
|
33
|
+
export declare function encodeEmailForPath(email: string): string;
|
|
34
|
+
export declare function renderEntry(entry: RegistryEntry): string;
|
|
35
|
+
export declare function renderList(entries: RegistryEntry[]): string;
|
|
36
|
+
export interface RunAddParams {
|
|
37
|
+
email: string;
|
|
38
|
+
note?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function runAddTestAccount(params: RunAddParams): Promise<void>;
|
|
41
|
+
export interface RunRemoveParams {
|
|
42
|
+
email: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function runRemoveTestAccount(params: RunRemoveParams): Promise<void>;
|
|
45
|
+
export interface RunListParams {
|
|
46
|
+
includeInactive?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function runListTestAccounts(params?: RunListParams): Promise<void>;
|
|
49
|
+
export declare function registerTestAccountCommand(staff: Command): Command;
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=test-account.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ensureCognitoToken } from "../../utils/cognito-session.js";
|
|
3
|
+
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
4
|
+
import { callerEmailFromToken, isStaffEmail } from "./reset-test-account.js";
|
|
5
|
+
// ── URL-safe base64 encode for the DELETE path parameter ────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Encode an email as the base64url path segment the DELETE route expects. The
|
|
8
|
+
* server decodes and normalizes (`decodeRegistryEmailKey` → `normalizeRegistryEmail`),
|
|
9
|
+
* so the operator can type `Bohemian.HW@gmail.com` and hit the same row as
|
|
10
|
+
* `bohemianhw@gmail.com`. Local encoding is a pure transform on the raw input;
|
|
11
|
+
* we never persist or print a locally normalized value.
|
|
12
|
+
*/
|
|
13
|
+
export function encodeEmailForPath(email) {
|
|
14
|
+
return Buffer.from(email.trim(), "utf8")
|
|
15
|
+
.toString("base64")
|
|
16
|
+
.replace(/\+/g, "-")
|
|
17
|
+
.replace(/\//g, "_")
|
|
18
|
+
.replace(/=+$/, "");
|
|
19
|
+
}
|
|
20
|
+
// ── Rendering ───────────────────────────────────────────────────────────────
|
|
21
|
+
function renderEntryLine(entry) {
|
|
22
|
+
const flag = entry.active ? chalk.green("active") : chalk.dim("inactive");
|
|
23
|
+
const note = entry.note ? ` ${chalk.dim(entry.note)}` : "";
|
|
24
|
+
return ` ${entry.email.padEnd(40)} ${flag} added ${entry.addedAt} by ${entry.addedBy}${note}`;
|
|
25
|
+
}
|
|
26
|
+
export function renderEntry(entry) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
lines.push(chalk.bold(`\nRegistry entry for ${entry.email}`));
|
|
29
|
+
lines.push(` active: ${entry.active ? chalk.green("yes") : chalk.dim("no")}`);
|
|
30
|
+
lines.push(` added by: ${entry.addedBy}`);
|
|
31
|
+
lines.push(` added at: ${entry.addedAt}`);
|
|
32
|
+
lines.push(` note: ${entry.note ?? chalk.dim("(none)")}`);
|
|
33
|
+
if (entry.removedBy !== undefined) {
|
|
34
|
+
lines.push(` removed by: ${entry.removedBy}`);
|
|
35
|
+
}
|
|
36
|
+
if (entry.removedAt !== undefined) {
|
|
37
|
+
lines.push(` removed at: ${entry.removedAt}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
export function renderList(entries) {
|
|
42
|
+
if (entries.length === 0) {
|
|
43
|
+
return chalk.dim("\n(no registry entries)");
|
|
44
|
+
}
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push(chalk.bold(`\nTest-account registry (${entries.length})`));
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
lines.push(renderEntryLine(entry));
|
|
49
|
+
}
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
// ── Shared HTTP helpers ─────────────────────────────────────────────────────
|
|
53
|
+
async function readBody(res) {
|
|
54
|
+
try {
|
|
55
|
+
return (await res.json());
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function renderApiError(status, body) {
|
|
62
|
+
const code = typeof body.code === "string" ? body.code : undefined;
|
|
63
|
+
const message = typeof body.error === "string" ? body.error : `request failed (${status})`;
|
|
64
|
+
return code ? `${code}: ${message}` : message;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Same local preflight the reset command runs. Duplicated in shape (not code)
|
|
68
|
+
* so both commands surface the same message without a circular import; the
|
|
69
|
+
* server remains the authorization boundary.
|
|
70
|
+
*/
|
|
71
|
+
async function requireStaffToken() {
|
|
72
|
+
const token = await ensureCognitoToken();
|
|
73
|
+
const callerEmail = callerEmailFromToken(token);
|
|
74
|
+
if (!callerEmail || !isStaffEmail(callerEmail)) {
|
|
75
|
+
throw new Error("hq staff test-account requires a super-admin (staff) HQ session. " +
|
|
76
|
+
"Sign in with an @getindigo.ai account and retry.");
|
|
77
|
+
}
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
export async function runAddTestAccount(params) {
|
|
81
|
+
const email = params.email.trim();
|
|
82
|
+
if (email.length === 0) {
|
|
83
|
+
throw new Error("email argument is required");
|
|
84
|
+
}
|
|
85
|
+
const token = await requireStaffToken();
|
|
86
|
+
const body = { email };
|
|
87
|
+
if (params.note !== undefined)
|
|
88
|
+
body.note = params.note;
|
|
89
|
+
const res = await vaultApiFetch({
|
|
90
|
+
token,
|
|
91
|
+
path: "/v1/admin/test-account-registry",
|
|
92
|
+
method: "POST",
|
|
93
|
+
body,
|
|
94
|
+
});
|
|
95
|
+
const parsed = await readBody(res);
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
throw new Error(renderApiError(res.status, parsed));
|
|
98
|
+
}
|
|
99
|
+
const entry = parsed.entry;
|
|
100
|
+
if (!entry) {
|
|
101
|
+
throw new Error("server did not return the registry entry");
|
|
102
|
+
}
|
|
103
|
+
console.log(renderEntry(entry));
|
|
104
|
+
console.log(chalk.dim("\n(entry added — reset command will now accept this email.)"));
|
|
105
|
+
}
|
|
106
|
+
export async function runRemoveTestAccount(params) {
|
|
107
|
+
const email = params.email.trim();
|
|
108
|
+
if (email.length === 0) {
|
|
109
|
+
throw new Error("email argument is required");
|
|
110
|
+
}
|
|
111
|
+
const token = await requireStaffToken();
|
|
112
|
+
const emailB64 = encodeEmailForPath(email);
|
|
113
|
+
const res = await vaultApiFetch({
|
|
114
|
+
token,
|
|
115
|
+
path: `/v1/admin/test-account-registry/${emailB64}`,
|
|
116
|
+
method: "DELETE",
|
|
117
|
+
});
|
|
118
|
+
const parsed = await readBody(res);
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
throw new Error(renderApiError(res.status, parsed));
|
|
121
|
+
}
|
|
122
|
+
const entry = parsed.entry;
|
|
123
|
+
if (!entry) {
|
|
124
|
+
throw new Error("server did not return the registry entry");
|
|
125
|
+
}
|
|
126
|
+
console.log(renderEntry(entry));
|
|
127
|
+
console.log(chalk.dim("\n(entry soft-deleted — row stays with active: false.)"));
|
|
128
|
+
}
|
|
129
|
+
export async function runListTestAccounts(params = {}) {
|
|
130
|
+
const token = await requireStaffToken();
|
|
131
|
+
const res = await vaultApiFetch({
|
|
132
|
+
token,
|
|
133
|
+
path: "/v1/admin/test-account-registry",
|
|
134
|
+
method: "GET",
|
|
135
|
+
query: params.includeInactive ? { includeInactive: "true" } : undefined,
|
|
136
|
+
});
|
|
137
|
+
const parsed = await readBody(res);
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
throw new Error(renderApiError(res.status, parsed));
|
|
140
|
+
}
|
|
141
|
+
const entries = parsed.entries;
|
|
142
|
+
if (!Array.isArray(entries)) {
|
|
143
|
+
throw new Error("server did not return a list of entries");
|
|
144
|
+
}
|
|
145
|
+
console.log(renderList(entries));
|
|
146
|
+
}
|
|
147
|
+
// ── Commander wiring ────────────────────────────────────────────────────────
|
|
148
|
+
function fail(err) {
|
|
149
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
export function registerTestAccountCommand(staff) {
|
|
153
|
+
const group = staff
|
|
154
|
+
.command("test-account")
|
|
155
|
+
.description("Manage the designated test-account registry (super-admin only). " +
|
|
156
|
+
"Wraps /v1/admin/test-account-registry.");
|
|
157
|
+
group
|
|
158
|
+
.command("add")
|
|
159
|
+
.description("Add an email to the registry so `hq staff reset-test-account` will sweep it.")
|
|
160
|
+
.argument("<email>", "Designated test-account email")
|
|
161
|
+
.option("--note <text>", "Short reason recorded on the row")
|
|
162
|
+
.action(async (email, opts) => {
|
|
163
|
+
try {
|
|
164
|
+
await runAddTestAccount({ email, note: opts.note });
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
fail(err);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
group
|
|
171
|
+
.command("remove")
|
|
172
|
+
.description("Soft-delete a registry entry (row stays with active: false).")
|
|
173
|
+
.argument("<email>", "Registered test-account email")
|
|
174
|
+
.action(async (email) => {
|
|
175
|
+
try {
|
|
176
|
+
await runRemoveTestAccount({ email });
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
fail(err);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
group
|
|
183
|
+
.command("list")
|
|
184
|
+
.description("List active registry entries.")
|
|
185
|
+
.option("--include-inactive", "Also list soft-deleted rows (active: false).")
|
|
186
|
+
.action(async (opts) => {
|
|
187
|
+
try {
|
|
188
|
+
await runListTestAccounts({ includeInactive: opts.includeInactive === true });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
fail(err);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return group;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=test-account.js.map
|
|
@@ -11,7 +11,6 @@ import { type CompanyBriefResult } from "./company-brief.js";
|
|
|
11
11
|
import { type OntologyCompany } from "./company.js";
|
|
12
12
|
import { type MultiWriterStart } from "./multi-writer.js";
|
|
13
13
|
export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
|
|
14
|
-
export declare const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
|
|
15
14
|
export declare const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
|
|
16
15
|
/**
|
|
17
16
|
* Applies only when the payload's sibling backend is grok. On gold-v2, Claude
|
|
@@ -43,7 +42,7 @@ export declare const BUDGET: {
|
|
|
43
42
|
readonly maxTokens: 60000;
|
|
44
43
|
readonly chunkTokens: 12000;
|
|
45
44
|
};
|
|
46
|
-
export type PendingReason = "session-touched"
|
|
45
|
+
export type PendingReason = "session-touched";
|
|
47
46
|
export interface LedgerEntry {
|
|
48
47
|
sha256: string;
|
|
49
48
|
size: number;
|
|
@@ -89,7 +88,6 @@ export interface OntologyPlan {
|
|
|
89
88
|
created_at: string;
|
|
90
89
|
first_run: boolean;
|
|
91
90
|
boundary: string | null;
|
|
92
|
-
shared_inputs: boolean;
|
|
93
91
|
/** Present only when `checkpoint.ontology.company-brief` was on for this plan. */
|
|
94
92
|
company_brief?: {
|
|
95
93
|
status: CompanyBriefResult["status"];
|
|
@@ -152,10 +150,10 @@ export declare function sessionTouched(hqRoot: string, company: OntologyCompany,
|
|
|
152
150
|
export declare function chunkLines(content: string, chunkTokens: number): Array<[number, number]>;
|
|
153
151
|
/**
|
|
154
152
|
* Update the ledger with the current tree and return it. New or changed files
|
|
155
|
-
*
|
|
156
|
-
*
|
|
153
|
+
* the session touched become pending; others are recorded as seen. The first
|
|
154
|
+
* run records the entity boundary (design 14.3).
|
|
157
155
|
*/
|
|
158
|
-
export declare function refreshLedger(company: OntologyCompany, existing: Ledger | null, touched: Set<string
|
|
156
|
+
export declare function refreshLedger(company: OntologyCompany, existing: Ledger | null, touched: Set<string>): {
|
|
159
157
|
ledger: Ledger;
|
|
160
158
|
firstRun: boolean;
|
|
161
159
|
};
|