@dharta/cli 0.6.77
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/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/agent.d.ts +17 -0
- package/dist/agent.js +134 -0
- package/dist/agent.js.map +1 -0
- package/dist/api-error.d.ts +3 -0
- package/dist/api-error.js +80 -0
- package/dist/api-error.js.map +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +19 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +1978 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agents.d.ts +131 -0
- package/dist/commands/agents.js +439 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/analytics.d.ts +25 -0
- package/dist/commands/analytics.js +166 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/commands/api.d.ts +19 -0
- package/dist/commands/api.js +278 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/appearance.d.ts +19 -0
- package/dist/commands/appearance.js +63 -0
- package/dist/commands/appearance.js.map +1 -0
- package/dist/commands/artifact.d.ts +33 -0
- package/dist/commands/artifact.js +235 -0
- package/dist/commands/artifact.js.map +1 -0
- package/dist/commands/audit-log.d.ts +32 -0
- package/dist/commands/audit-log.js +102 -0
- package/dist/commands/audit-log.js.map +1 -0
- package/dist/commands/auth.d.ts +47 -0
- package/dist/commands/auth.js +117 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/caps.d.ts +82 -0
- package/dist/commands/caps.js +213 -0
- package/dist/commands/caps.js.map +1 -0
- package/dist/commands/create.d.ts +22 -0
- package/dist/commands/create.js +76 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deploy.d.ts +45 -0
- package/dist/commands/deploy.js +892 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts +33 -0
- package/dist/commands/dev.js +655 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/directories.d.ts +60 -0
- package/dist/commands/directories.js +83 -0
- package/dist/commands/directories.js.map +1 -0
- package/dist/commands/doctor.d.ts +13 -0
- package/dist/commands/doctor.js +123 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/embed-keys.d.ts +61 -0
- package/dist/commands/embed-keys.js +165 -0
- package/dist/commands/embed-keys.js.map +1 -0
- package/dist/commands/env.d.ts +59 -0
- package/dist/commands/env.js +300 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/files.d.ts +14 -0
- package/dist/commands/files.js +84 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/git-credential.d.ts +1 -0
- package/dist/commands/git-credential.js +70 -0
- package/dist/commands/git-credential.js.map +1 -0
- package/dist/commands/kartas.d.ts +101 -0
- package/dist/commands/kartas.js +289 -0
- package/dist/commands/kartas.js.map +1 -0
- package/dist/commands/keys.d.ts +33 -0
- package/dist/commands/keys.js +141 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/login.d.ts +22 -0
- package/dist/commands/login.js +78 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logs.d.ts +14 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/members.d.ts +32 -0
- package/dist/commands/members.js +77 -0
- package/dist/commands/members.js.map +1 -0
- package/dist/commands/model-keys.d.ts +22 -0
- package/dist/commands/model-keys.js +222 -0
- package/dist/commands/model-keys.js.map +1 -0
- package/dist/commands/open.d.ts +29 -0
- package/dist/commands/open.js +119 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/parity.d.ts +15 -0
- package/dist/commands/parity.js +73 -0
- package/dist/commands/parity.js.map +1 -0
- package/dist/commands/rollback.d.ts +21 -0
- package/dist/commands/rollback.js +26 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/schedules.d.ts +52 -0
- package/dist/commands/schedules.js +231 -0
- package/dist/commands/schedules.js.map +1 -0
- package/dist/commands/scopes.d.ts +3 -0
- package/dist/commands/scopes.js +21 -0
- package/dist/commands/scopes.js.map +1 -0
- package/dist/commands/sessions.d.ts +53 -0
- package/dist/commands/sessions.js +142 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/setup.d.ts +28 -0
- package/dist/commands/setup.js +949 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/status.d.ts +19 -0
- package/dist/commands/status.js +63 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +120 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/usage.d.ts +3 -0
- package/dist/commands/usage.js +37 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/webhooks.d.ts +47 -0
- package/dist/commands/webhooks.js +156 -0
- package/dist/commands/webhooks.js.map +1 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/completions.d.ts +13 -0
- package/dist/completions.js +120 -0
- package/dist/completions.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +14 -0
- package/dist/context.js +38 -0
- package/dist/context.js.map +1 -0
- package/dist/device-flow.d.ts +9 -0
- package/dist/device-flow.js +127 -0
- package/dist/device-flow.js.map +1 -0
- package/dist/harness-preflight.d.ts +24 -0
- package/dist/harness-preflight.js +252 -0
- package/dist/harness-preflight.js.map +1 -0
- package/dist/harnesses.d.ts +11 -0
- package/dist/harnesses.js +112 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/login-offer.d.ts +9 -0
- package/dist/login-offer.js +36 -0
- package/dist/login-offer.js.map +1 -0
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.js +142 -0
- package/dist/manifest.js.map +1 -0
- package/dist/money.d.ts +1 -0
- package/dist/money.js +16 -0
- package/dist/money.js.map +1 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.js +20 -0
- package/dist/output.js.map +1 -0
- package/dist/runtime.d.ts +19 -0
- package/dist/runtime.js +95 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scaffold.d.ts +16 -0
- package/dist/scaffold.js +74 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/setup-scan.d.ts +21 -0
- package/dist/setup-scan.js +127 -0
- package/dist/setup-scan.js.map +1 -0
- package/dist/templates.d.ts +14 -0
- package/dist/templates.js +65 -0
- package/dist/templates.js.map +1 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +116 -0
- package/dist/validators.js.map +1 -0
- package/dist/version-check.d.ts +25 -0
- package/dist/version-check.js +68 -0
- package/dist/version-check.js.map +1 -0
- package/node_modules/@dharta/sdk/LICENSE +21 -0
- package/node_modules/@dharta/sdk/README.md +87 -0
- package/node_modules/@dharta/sdk/dist/errors.d.ts +20 -0
- package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/errors.js +51 -0
- package/node_modules/@dharta/sdk/dist/errors.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/http.d.ts +26 -0
- package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/http.js +136 -0
- package/node_modules/@dharta/sdk/dist/http.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/index.d.ts +15 -0
- package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/index.js +21 -0
- package/node_modules/@dharta/sdk/dist/index.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/resources.d.ts +48 -0
- package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/resources.js +150 -0
- package/node_modules/@dharta/sdk/dist/resources.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/types.d.ts +94 -0
- package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/types.js +59 -0
- package/node_modules/@dharta/sdk/dist/types.js.map +1 -0
- package/node_modules/@dharta/sdk/package.json +32 -0
- package/node_modules/@dharta/sdk/src/errors.ts +58 -0
- package/node_modules/@dharta/sdk/src/http.ts +166 -0
- package/node_modules/@dharta/sdk/src/index.ts +36 -0
- package/node_modules/@dharta/sdk/src/resources.ts +197 -0
- package/node_modules/@dharta/sdk/src/types.ts +179 -0
- package/package.json +38 -0
- package/src/agent.ts +147 -0
- package/src/api-error.ts +88 -0
- package/src/browser.ts +20 -0
- package/src/cli.ts +2339 -0
- package/src/commands/agents.ts +635 -0
- package/src/commands/analytics.ts +181 -0
- package/src/commands/api.ts +297 -0
- package/src/commands/appearance.ts +79 -0
- package/src/commands/artifact.ts +331 -0
- package/src/commands/audit-log.ts +125 -0
- package/src/commands/auth.ts +186 -0
- package/src/commands/caps.ts +302 -0
- package/src/commands/create.ts +114 -0
- package/src/commands/deploy.ts +1122 -0
- package/src/commands/dev.ts +792 -0
- package/src/commands/directories.ts +136 -0
- package/src/commands/doctor.ts +138 -0
- package/src/commands/embed-keys.ts +237 -0
- package/src/commands/env.ts +369 -0
- package/src/commands/files.ts +124 -0
- package/src/commands/git-credential.ts +78 -0
- package/src/commands/kartas.ts +439 -0
- package/src/commands/keys.ts +206 -0
- package/src/commands/login.ts +115 -0
- package/src/commands/logs.ts +111 -0
- package/src/commands/members.ts +109 -0
- package/src/commands/model-keys.ts +278 -0
- package/src/commands/open.ts +173 -0
- package/src/commands/parity.ts +96 -0
- package/src/commands/rollback.ts +45 -0
- package/src/commands/schedules.ts +290 -0
- package/src/commands/scopes.ts +22 -0
- package/src/commands/sessions.ts +225 -0
- package/src/commands/setup.ts +1176 -0
- package/src/commands/status.ts +94 -0
- package/src/commands/update.ts +157 -0
- package/src/commands/usage.ts +41 -0
- package/src/commands/webhooks.ts +200 -0
- package/src/commands/whoami.ts +24 -0
- package/src/completions.ts +168 -0
- package/src/config.ts +143 -0
- package/src/context.ts +59 -0
- package/src/device-flow.ts +174 -0
- package/src/harness-preflight.ts +293 -0
- package/src/harnesses.ts +117 -0
- package/src/index.ts +3 -0
- package/src/login-offer.ts +49 -0
- package/src/manifest.ts +153 -0
- package/src/money.ts +15 -0
- package/src/output.ts +22 -0
- package/src/runtime.ts +127 -0
- package/src/scaffold.ts +102 -0
- package/src/setup-scan.ts +154 -0
- package/src/templates.ts +85 -0
- package/src/validators.ts +124 -0
- package/src/version-check.ts +75 -0
- package/templates/blank/CLAUDE.md +21 -0
- package/templates/blank/dharta.toml +11 -0
- package/templates/faq-agent/CLAUDE.md +51 -0
- package/templates/faq-agent/dharta.toml +11 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { CliContext } from "../context.js";
|
|
3
|
+
import { table } from "../output.js";
|
|
4
|
+
import { maskEnvValue } from "./env.js";
|
|
5
|
+
import { closestChoice, normalizeNonBlank } from "../validators.js";
|
|
6
|
+
|
|
7
|
+
const DHARTA_AUDIENCES = ["members", "anonymous", "off"] as const;
|
|
8
|
+
const DHARTA_ROLES = ["viewer", "participant", "manager"] as const;
|
|
9
|
+
type KartaAudience = (typeof DHARTA_AUDIENCES)[number];
|
|
10
|
+
type KartaRole = (typeof DHARTA_ROLES)[number];
|
|
11
|
+
|
|
12
|
+
export interface KartaMember {
|
|
13
|
+
id: string | number;
|
|
14
|
+
email: string;
|
|
15
|
+
role: string;
|
|
16
|
+
joined_at: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface KartaRecord {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string | null;
|
|
22
|
+
vanity_slug: string | null;
|
|
23
|
+
access_audience: string | null;
|
|
24
|
+
access_model: string;
|
|
25
|
+
state: string;
|
|
26
|
+
agent: { id: string | number; slug: string; name: string } | null;
|
|
27
|
+
url: string | null;
|
|
28
|
+
members?: KartaMember[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface KartaConfigureOptions {
|
|
32
|
+
name?: string;
|
|
33
|
+
slug?: string;
|
|
34
|
+
access?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface KartaListOptions {
|
|
38
|
+
agent?: string;
|
|
39
|
+
named?: boolean;
|
|
40
|
+
archived?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface KartaNameOptions {
|
|
44
|
+
name?: string;
|
|
45
|
+
slug?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface KartaCreateOptions {
|
|
49
|
+
agent?: string;
|
|
50
|
+
slug?: string;
|
|
51
|
+
access?: string;
|
|
52
|
+
members?: string[];
|
|
53
|
+
memberRole?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateKartaCreateOptions(name: string, options: KartaCreateOptions): void {
|
|
57
|
+
normalizeNonBlank(name, "dharta name");
|
|
58
|
+
normalizeNonBlank(options.agent ?? "", "Agent slug");
|
|
59
|
+
normalizeNonBlank(options.slug ?? "", "dharta vanity slug");
|
|
60
|
+
normalizeAudience(options.access ?? "members");
|
|
61
|
+
normalizeRole(options.memberRole ?? "participant");
|
|
62
|
+
for (const email of options.members ?? []) normalizeNonBlank(email, "karta member email");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function kartasCreateData(
|
|
66
|
+
ctx: CliContext,
|
|
67
|
+
name: string,
|
|
68
|
+
options: KartaCreateOptions,
|
|
69
|
+
): Promise<{ karta: KartaRecord }> {
|
|
70
|
+
validateKartaCreateOptions(name, options);
|
|
71
|
+
const access = normalizeAudience(options.access ?? "members");
|
|
72
|
+
const role = normalizeRole(options.memberRole ?? "participant");
|
|
73
|
+
const agent = normalizeNonBlank(options.agent ?? "", "Agent slug");
|
|
74
|
+
const slug = normalizeNonBlank(options.slug ?? "", "dharta vanity slug");
|
|
75
|
+
return ctx.http.request<{ karta: KartaRecord }>({
|
|
76
|
+
method: "POST",
|
|
77
|
+
path: "/api/kartas",
|
|
78
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
79
|
+
body: {
|
|
80
|
+
agent,
|
|
81
|
+
name: name.trim(),
|
|
82
|
+
vanity_slug: slug.toLowerCase(),
|
|
83
|
+
access_audience: access === "off" ? null : access,
|
|
84
|
+
members: (options.members ?? []).map((email) => ({ email: email.trim(), role })),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function kartasCreate(ctx: CliContext, name: string, options: KartaCreateOptions): Promise<string> {
|
|
90
|
+
const { karta } = await kartasCreateData(ctx, name, options);
|
|
91
|
+
return karta.url
|
|
92
|
+
? `Created ${karta.name ?? karta.id}. Stable URL: ${karta.url}`
|
|
93
|
+
: `Created ${karta.name ?? karta.id}. Stable URL is off.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function kartasListData(ctx: CliContext, options: KartaListOptions = {}): Promise<KartaRecord[]> {
|
|
97
|
+
const query = new URLSearchParams();
|
|
98
|
+
if (options.agent !== undefined) query.set("agent", normalizeNonBlank(options.agent, "Agent slug"));
|
|
99
|
+
if (options.named) query.set("named", "true");
|
|
100
|
+
// Archived kartas are hidden by default; --archived lists only those.
|
|
101
|
+
if (options.archived) query.set("archived", "true");
|
|
102
|
+
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
103
|
+
const response = await ctx.http.request<{ data: KartaRecord[] }>({ path: `/api/kartas${suffix}` });
|
|
104
|
+
return response.data;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function kartasList(ctx: CliContext, options: KartaListOptions = {}): Promise<string> {
|
|
108
|
+
const kartas = await kartasListData(ctx, options);
|
|
109
|
+
if (kartas.length === 0) return options.named ? "No named kartas yet." : "No kartas yet.";
|
|
110
|
+
return table(kartas.map((karta) => ({
|
|
111
|
+
id: karta.id,
|
|
112
|
+
name: karta.name ?? "—",
|
|
113
|
+
agent: karta.agent?.slug ?? "—",
|
|
114
|
+
slug: karta.vanity_slug ?? "—",
|
|
115
|
+
access: accessLabel(karta),
|
|
116
|
+
url: karta.url ?? "—",
|
|
117
|
+
})));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function kartasShowData(ctx: CliContext, id: string): Promise<KartaRecord> {
|
|
121
|
+
const kartaId = normalizeNonBlank(id, "dharta id");
|
|
122
|
+
const response = await ctx.http.request<{ karta: KartaRecord }>({
|
|
123
|
+
path: `/api/kartas/${encodeURIComponent(kartaId)}`,
|
|
124
|
+
});
|
|
125
|
+
return response.karta;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function kartasShow(ctx: CliContext, id: string): Promise<string> {
|
|
129
|
+
const karta = await kartasShowData(ctx, id);
|
|
130
|
+
const members = karta.members?.length
|
|
131
|
+
? table(karta.members.map((member) => ({
|
|
132
|
+
id: String(member.id), email: member.email, role: member.role,
|
|
133
|
+
})))
|
|
134
|
+
: "No people listed.";
|
|
135
|
+
return [
|
|
136
|
+
`karta: ${karta.name ?? karta.id} (${karta.id})`,
|
|
137
|
+
`Agent: ${karta.agent?.slug ?? "—"}`,
|
|
138
|
+
`Vanity slug: ${karta.vanity_slug ?? "—"}`,
|
|
139
|
+
`Stable URL access: ${accessLabel(karta)}`,
|
|
140
|
+
`Signed-in access: ${karta.access_model === "native_shared" ? "managed here" : "controlled by the existing agent integration"}`,
|
|
141
|
+
`URL: ${karta.url ?? "—"}`,
|
|
142
|
+
"",
|
|
143
|
+
"People",
|
|
144
|
+
members,
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function validateKartaConfigureOptions(options: KartaConfigureOptions): void {
|
|
149
|
+
if (options.name === undefined && options.slug === undefined && options.access === undefined) {
|
|
150
|
+
throw new Error("Set at least one of --name, --slug, or --access.");
|
|
151
|
+
}
|
|
152
|
+
if (options.name !== undefined) normalizeNonBlank(options.name, "dharta name");
|
|
153
|
+
if (options.slug !== undefined) normalizeNonBlank(options.slug, "dharta vanity slug");
|
|
154
|
+
if (options.access !== undefined) normalizeAudience(options.access);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function validateKartaNameOptions(options: KartaNameOptions): void {
|
|
158
|
+
normalizeNonBlank(options.name ?? "", "dharta name");
|
|
159
|
+
normalizeNonBlank(options.slug ?? "", "dharta vanity slug");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function kartasNameData(
|
|
163
|
+
ctx: CliContext,
|
|
164
|
+
id: string,
|
|
165
|
+
options: KartaNameOptions,
|
|
166
|
+
): Promise<{ karta: KartaRecord }> {
|
|
167
|
+
validateKartaNameOptions(options);
|
|
168
|
+
return kartasConfigureData(ctx, id, { name: options.name, slug: options.slug });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function kartasName(ctx: CliContext, id: string, options: KartaNameOptions): Promise<string> {
|
|
172
|
+
const { karta } = await kartasNameData(ctx, id, options);
|
|
173
|
+
return `Named ${karta.id} as ${karta.name} (/kartas/${karta.vanity_slug}). Stable URL access remains ${accessLabel(karta)}.`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function kartasConfigureData(
|
|
177
|
+
ctx: CliContext,
|
|
178
|
+
id: string,
|
|
179
|
+
options: KartaConfigureOptions,
|
|
180
|
+
): Promise<{ karta: KartaRecord }> {
|
|
181
|
+
const kartaId = normalizeNonBlank(id, "dharta id");
|
|
182
|
+
validateKartaConfigureOptions(options);
|
|
183
|
+
const body: Record<string, unknown> = {};
|
|
184
|
+
if (options.name !== undefined) body.name = options.name.trim();
|
|
185
|
+
if (options.slug !== undefined) body.vanity_slug = options.slug.trim().toLowerCase();
|
|
186
|
+
if (options.access !== undefined) {
|
|
187
|
+
const access = normalizeAudience(options.access);
|
|
188
|
+
body.access_audience = access === "off" ? null : access;
|
|
189
|
+
}
|
|
190
|
+
return ctx.http.request<{ karta: KartaRecord }>({
|
|
191
|
+
method: "PATCH",
|
|
192
|
+
path: `/api/kartas/${encodeURIComponent(kartaId)}`,
|
|
193
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
194
|
+
body,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function kartasConfigure(ctx: CliContext, id: string, options: KartaConfigureOptions): Promise<string> {
|
|
199
|
+
const { karta } = await kartasConfigureData(ctx, id, options);
|
|
200
|
+
return karta.url
|
|
201
|
+
? `Updated ${karta.name ?? karta.id}. Stable URL: ${karta.url}`
|
|
202
|
+
: `Updated ${karta.name ?? karta.id}. Stable URL is off.`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Retire a dharta. A REVERSIBLE archive, never a delete: the durable workspace
|
|
206
|
+
// and its history survive untouched, and `dharta kartas restore` brings them
|
|
207
|
+
// back. Erasing the underlying tree is a separate admin operation (RFC 0036
|
|
208
|
+
// W7) - list-tidying must never destroy an end user's accumulated work.
|
|
209
|
+
export async function kartasArchiveData(
|
|
210
|
+
ctx: CliContext,
|
|
211
|
+
id: string,
|
|
212
|
+
): Promise<{ karta: KartaRecord }> {
|
|
213
|
+
const kartaId = normalizeNonBlank(id, "dharta id");
|
|
214
|
+
return ctx.http.request<{ karta: KartaRecord }>({
|
|
215
|
+
method: "DELETE",
|
|
216
|
+
path: `/api/kartas/${encodeURIComponent(kartaId)}`,
|
|
217
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function kartasArchive(ctx: CliContext, id: string): Promise<string> {
|
|
222
|
+
const { karta } = await kartasArchiveData(ctx, id);
|
|
223
|
+
return (
|
|
224
|
+
`Archived ${karta.name ?? karta.id}. Its files and history are kept - ` +
|
|
225
|
+
`restore with: dharta kartas restore ${karta.id}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function kartasRestoreData(
|
|
230
|
+
ctx: CliContext,
|
|
231
|
+
id: string,
|
|
232
|
+
): Promise<{ karta: KartaRecord }> {
|
|
233
|
+
const kartaId = normalizeNonBlank(id, "dharta id");
|
|
234
|
+
return ctx.http.request<{ karta: KartaRecord }>({
|
|
235
|
+
method: "POST",
|
|
236
|
+
path: `/api/kartas/${encodeURIComponent(kartaId)}/restore`,
|
|
237
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function kartasRestore(ctx: CliContext, id: string): Promise<string> {
|
|
242
|
+
const { karta } = await kartasRestoreData(ctx, id);
|
|
243
|
+
return karta.url
|
|
244
|
+
? `Restored ${karta.name ?? karta.id}. Stable URL: ${karta.url}`
|
|
245
|
+
: `Restored ${karta.name ?? karta.id}.`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function kartaMembersGrantData(
|
|
249
|
+
ctx: CliContext,
|
|
250
|
+
id: string,
|
|
251
|
+
email: string,
|
|
252
|
+
role: string,
|
|
253
|
+
): Promise<{ member: KartaMember }> {
|
|
254
|
+
const kartaId = normalizeNonBlank(id, "dharta id");
|
|
255
|
+
const normalizedEmail = normalizeNonBlank(email, "person email");
|
|
256
|
+
const normalizedRole = normalizeRole(role);
|
|
257
|
+
return ctx.http.request<{ member: KartaMember }>({
|
|
258
|
+
method: "POST",
|
|
259
|
+
path: `/api/kartas/${encodeURIComponent(kartaId)}/members`,
|
|
260
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
261
|
+
body: { email: normalizedEmail, role: normalizedRole },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function kartaMembersGrant(ctx: CliContext, id: string, email: string, role: string): Promise<string> {
|
|
266
|
+
const { member } = await kartaMembersGrantData(ctx, id, email, role);
|
|
267
|
+
return `Granted ${member.email} ${member.role} access to karta ${id}.`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function kartaMembersSetRoleData(
|
|
271
|
+
ctx: CliContext,
|
|
272
|
+
id: string,
|
|
273
|
+
membershipId: string,
|
|
274
|
+
role: string,
|
|
275
|
+
): Promise<{ member: KartaMember }> {
|
|
276
|
+
const kartaId = normalizeNonBlank(id, "dharta id");
|
|
277
|
+
const memberId = normalizeNonBlank(membershipId, "dharta membership id");
|
|
278
|
+
const normalizedRole = normalizeRole(role);
|
|
279
|
+
return ctx.http.request<{ member: KartaMember }>({
|
|
280
|
+
method: "PATCH",
|
|
281
|
+
path: `/api/kartas/${encodeURIComponent(kartaId)}/members/${encodeURIComponent(memberId)}`,
|
|
282
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
283
|
+
body: { role: normalizedRole },
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function kartaMembersSetRole(
|
|
288
|
+
ctx: CliContext,
|
|
289
|
+
id: string,
|
|
290
|
+
membershipId: string,
|
|
291
|
+
role: string,
|
|
292
|
+
): Promise<string> {
|
|
293
|
+
const { member } = await kartaMembersSetRoleData(ctx, id, membershipId, role);
|
|
294
|
+
return `${member.email} is now a ${member.role} on karta ${id}.`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function kartaMembersRevokeData(
|
|
298
|
+
ctx: CliContext,
|
|
299
|
+
id: string,
|
|
300
|
+
membershipId: string,
|
|
301
|
+
): Promise<{ id: string; membership_id: string; revoked: true }> {
|
|
302
|
+
const kartaId = normalizeNonBlank(id, "dharta id");
|
|
303
|
+
const memberId = normalizeNonBlank(membershipId, "dharta membership id");
|
|
304
|
+
await ctx.http.request<void>({
|
|
305
|
+
method: "DELETE",
|
|
306
|
+
path: `/api/kartas/${encodeURIComponent(kartaId)}/members/${encodeURIComponent(memberId)}`,
|
|
307
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
308
|
+
});
|
|
309
|
+
return { id: kartaId, membership_id: memberId, revoked: true };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function kartaMembersRevoke(ctx: CliContext, id: string, membershipId: string): Promise<string> {
|
|
313
|
+
await kartaMembersRevokeData(ctx, id, membershipId);
|
|
314
|
+
return `Revoked dharta membership ${membershipId} from ${id}.`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeAudience(audience: string): KartaAudience {
|
|
318
|
+
const normalized = normalizeNonBlank(audience, "karta access audience");
|
|
319
|
+
if ((DHARTA_AUDIENCES as readonly string[]).includes(normalized)) return normalized as KartaAudience;
|
|
320
|
+
const suggestion = closestChoice(normalized, DHARTA_AUDIENCES);
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Unknown karta access ${normalized}. Allowed values: ${DHARTA_AUDIENCES.join(", ")}.` +
|
|
323
|
+
(suggestion ? ` Did you mean ${suggestion}?` : ""),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function normalizeRole(role: string): KartaRole {
|
|
328
|
+
const normalized = normalizeNonBlank(role, "karta member role");
|
|
329
|
+
if ((DHARTA_ROLES as readonly string[]).includes(normalized)) return normalized as KartaRole;
|
|
330
|
+
const suggestion = closestChoice(normalized, DHARTA_ROLES);
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Unknown karta member role ${normalized}. Allowed roles: ${DHARTA_ROLES.join(", ")}.` +
|
|
333
|
+
(suggestion ? ` Did you mean ${suggestion}?` : ""),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function accessLabel(karta: KartaRecord): string {
|
|
338
|
+
if (karta.access_audience === "members") return "specific people (sign-in required)";
|
|
339
|
+
if (karta.access_audience === "anonymous") return "anyone with the link";
|
|
340
|
+
return "off";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// RFC 0073 - per-dharta environment variables.
|
|
344
|
+
//
|
|
345
|
+
// Addressed by (agent, karta) rather than a karta id alone: a Type 1 karta has
|
|
346
|
+
// no `agent_instances` row and so no /api/kartas identity, and the same
|
|
347
|
+
// end-user subject holds a DIFFERENT binding of the same name on every agent
|
|
348
|
+
// they use. The :karta segment also accepts a named karta's vanity slug.
|
|
349
|
+
export interface KartaEnvRecord {
|
|
350
|
+
name: string;
|
|
351
|
+
secret: boolean;
|
|
352
|
+
value?: string | null;
|
|
353
|
+
updated_at?: string;
|
|
354
|
+
updated_by?: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function envPath(agent: string, karta: string, name?: string): string {
|
|
358
|
+
const base = `/api/agents/${encodeURIComponent(agent)}/instances/${encodeURIComponent(karta)}/env`;
|
|
359
|
+
return name === undefined ? base : `${base}/${encodeURIComponent(name)}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function kartaEnvListData(
|
|
363
|
+
ctx: CliContext,
|
|
364
|
+
agent: string,
|
|
365
|
+
karta: string,
|
|
366
|
+
): Promise<KartaEnvRecord[]> {
|
|
367
|
+
const agentSlug = normalizeNonBlank(agent, "agent slug");
|
|
368
|
+
const kartaId = normalizeNonBlank(karta, "dharta id");
|
|
369
|
+
const response = await ctx.http.request<{ env: KartaEnvRecord[] }>({
|
|
370
|
+
path: envPath(agentSlug, kartaId),
|
|
371
|
+
});
|
|
372
|
+
return response.env ?? [];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function kartaEnvList(ctx: CliContext, agent: string, karta: string): Promise<string> {
|
|
376
|
+
const env = await kartaEnvListData(ctx, agent, karta);
|
|
377
|
+
if (env.length === 0) return "No per-dharta environment variables set.";
|
|
378
|
+
return table(env.map((row) => ({
|
|
379
|
+
name: row.name,
|
|
380
|
+
// Secret values are never returned by the API; non-secret (--plain) values
|
|
381
|
+
// are masked here too so a routine list never prints a token to the
|
|
382
|
+
// terminal. Use `--json` for raw values. Shared with `dharta env list`.
|
|
383
|
+
value: maskEnvValue(row, false),
|
|
384
|
+
updated: row.updated_at ?? "—",
|
|
385
|
+
by: row.updated_by ?? "—",
|
|
386
|
+
})));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function kartaEnvSetData(
|
|
390
|
+
ctx: CliContext,
|
|
391
|
+
agent: string,
|
|
392
|
+
karta: string,
|
|
393
|
+
name: string,
|
|
394
|
+
value: string,
|
|
395
|
+
options: { secret?: boolean } = {},
|
|
396
|
+
): Promise<KartaEnvRecord> {
|
|
397
|
+
const agentSlug = normalizeNonBlank(agent, "agent slug");
|
|
398
|
+
const kartaId = normalizeNonBlank(karta, "dharta id");
|
|
399
|
+
const varName = normalizeNonBlank(name, "variable name");
|
|
400
|
+
return ctx.http.request<KartaEnvRecord>({
|
|
401
|
+
method: "PUT",
|
|
402
|
+
path: envPath(agentSlug, kartaId, varName),
|
|
403
|
+
body: options.secret === undefined ? { value } : { value, secret: options.secret },
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export async function kartaEnvSet(
|
|
408
|
+
ctx: CliContext,
|
|
409
|
+
agent: string,
|
|
410
|
+
karta: string,
|
|
411
|
+
name: string,
|
|
412
|
+
value: string,
|
|
413
|
+
options: { secret?: boolean } = {},
|
|
414
|
+
): Promise<string> {
|
|
415
|
+
const row = await kartaEnvSetData(ctx, agent, karta, name, value, options);
|
|
416
|
+
return `Set ${row.name} on ${karta}.`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function kartaEnvUnsetData(
|
|
420
|
+
ctx: CliContext,
|
|
421
|
+
agent: string,
|
|
422
|
+
karta: string,
|
|
423
|
+
name: string,
|
|
424
|
+
): Promise<void> {
|
|
425
|
+
const agentSlug = normalizeNonBlank(agent, "agent slug");
|
|
426
|
+
const kartaId = normalizeNonBlank(karta, "dharta id");
|
|
427
|
+
const varName = normalizeNonBlank(name, "variable name");
|
|
428
|
+
await ctx.http.request<void>({ method: "DELETE", path: envPath(agentSlug, kartaId, varName) });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function kartaEnvUnset(
|
|
432
|
+
ctx: CliContext,
|
|
433
|
+
agent: string,
|
|
434
|
+
karta: string,
|
|
435
|
+
name: string,
|
|
436
|
+
): Promise<string> {
|
|
437
|
+
await kartaEnvUnsetData(ctx, agent, karta, name);
|
|
438
|
+
return `Removed ${name} from ${karta}.`;
|
|
439
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { CliContext } from "../context.js";
|
|
2
|
+
import { API_KEY_SCOPES, type ApiKeyScope } from "@dharta/sdk";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { fmtTs, table } from "../output.js";
|
|
5
|
+
import { closestChoice, normalizeIsoTimestamp, normalizeNonBlank } from "../validators.js";
|
|
6
|
+
import { formatScopes, SENSITIVE_API_KEY_SCOPES } from "./scopes.js";
|
|
7
|
+
|
|
8
|
+
interface ListEnvelope<T> {
|
|
9
|
+
data: T[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ApiKey {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
prefix: string;
|
|
16
|
+
scopes: ApiKeyScope[];
|
|
17
|
+
mode?: string;
|
|
18
|
+
expires_at?: string | null;
|
|
19
|
+
created_at: string;
|
|
20
|
+
last_used_at: string | null;
|
|
21
|
+
revoked_at: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ApiKeyCreateResponse {
|
|
25
|
+
api_key: ApiKey;
|
|
26
|
+
secret: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ApiKeyCreateOptions {
|
|
30
|
+
expiresAt?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MAX_FORCED_EXPIRY_MS = 90 * 24 * 60 * 60 * 1000;
|
|
34
|
+
export async function keysListData(ctx: CliContext) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await ctx.http.request<ListEnvelope<ApiKey>>({ path: "/api_keys" });
|
|
37
|
+
return res.data;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw addKeysListScopeGuidance(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function keysList(ctx: CliContext): Promise<string> {
|
|
44
|
+
const keys = await keysListData(ctx);
|
|
45
|
+
if (keys.length === 0) return "No API keys. Create one with: dharta keys create <name> --scope agents:read";
|
|
46
|
+
return table(
|
|
47
|
+
keys.map((k) => ({
|
|
48
|
+
prefix: k.prefix,
|
|
49
|
+
name: k.name,
|
|
50
|
+
scopes: formatScopes(k.scopes, ","),
|
|
51
|
+
created: fmtTs(k.created_at),
|
|
52
|
+
last_used: fmtTs(k.last_used_at),
|
|
53
|
+
revoked: fmtTs(k.revoked_at),
|
|
54
|
+
})),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function keysCreateData(
|
|
59
|
+
ctx: CliContext,
|
|
60
|
+
name: string,
|
|
61
|
+
scopes: ApiKeyScope[],
|
|
62
|
+
opts: ApiKeyCreateOptions = {},
|
|
63
|
+
) {
|
|
64
|
+
validateKeysCreateInput(name, scopes, opts);
|
|
65
|
+
const normalizedScopes = normalizeScopes(scopes);
|
|
66
|
+
const normalizedOpts = normalizeCreateOptions(normalizedScopes, opts);
|
|
67
|
+
const body: Record<string, unknown> = { name: normalizeKeyName(name), scopes: normalizedScopes };
|
|
68
|
+
if (normalizedOpts.expiresAt !== undefined) body.expires_at = normalizedOpts.expiresAt;
|
|
69
|
+
return ctx.http.request<ApiKeyCreateResponse>({
|
|
70
|
+
method: "POST",
|
|
71
|
+
path: "/api_keys",
|
|
72
|
+
body,
|
|
73
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function keysCreate(
|
|
78
|
+
ctx: CliContext,
|
|
79
|
+
name: string,
|
|
80
|
+
scopes: ApiKeyScope[],
|
|
81
|
+
opts: ApiKeyCreateOptions = {},
|
|
82
|
+
): Promise<string> {
|
|
83
|
+
const res = await keysCreateData(ctx, name, scopes, opts);
|
|
84
|
+
return [
|
|
85
|
+
`Created key ${res.api_key.prefix} (${res.api_key.id})`,
|
|
86
|
+
`Scopes: ${res.api_key.scopes.join(",")}`,
|
|
87
|
+
res.api_key.expires_at ? `Expires: ${res.api_key.expires_at}` : null,
|
|
88
|
+
"",
|
|
89
|
+
`Secret (shown once — store it now):`,
|
|
90
|
+
res.secret,
|
|
91
|
+
].filter((line) => line !== null).join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function validateKeysCreateInput(
|
|
95
|
+
name: string,
|
|
96
|
+
scopes: ApiKeyScope[],
|
|
97
|
+
opts: ApiKeyCreateOptions = {},
|
|
98
|
+
): void {
|
|
99
|
+
normalizeKeyName(name);
|
|
100
|
+
normalizeCreateOptions(normalizeScopes(scopes), opts);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function keysRevokeData(
|
|
104
|
+
ctx: CliContext,
|
|
105
|
+
id: string,
|
|
106
|
+
): Promise<{ id: string; revoked: boolean }> {
|
|
107
|
+
const normalizedId = normalizeNonBlank(id, "API key id");
|
|
108
|
+
await ctx.http.request<void>({
|
|
109
|
+
method: "DELETE",
|
|
110
|
+
path: `/api_keys/${encodeURIComponent(normalizedId)}`,
|
|
111
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
112
|
+
});
|
|
113
|
+
return { id: normalizedId, revoked: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function keysRevoke(ctx: CliContext, id: string): Promise<string> {
|
|
117
|
+
const res = await keysRevokeData(ctx, id);
|
|
118
|
+
return `Revoked ${res.id}.`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function keysRenameData(
|
|
122
|
+
ctx: CliContext,
|
|
123
|
+
id: string,
|
|
124
|
+
name: string,
|
|
125
|
+
): Promise<ApiKey> {
|
|
126
|
+
const normalizedId = normalizeNonBlank(id, "API key id");
|
|
127
|
+
const normalizedName = normalizeKeyName(name);
|
|
128
|
+
const res = await ctx.http.request<{ api_key: ApiKey }>({
|
|
129
|
+
method: "PATCH",
|
|
130
|
+
path: `/api_keys/${encodeURIComponent(normalizedId)}`,
|
|
131
|
+
body: { name: normalizedName },
|
|
132
|
+
headers: { "Idempotency-Key": randomUUID() },
|
|
133
|
+
});
|
|
134
|
+
return res.api_key;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function keysRename(ctx: CliContext, id: string, name: string): Promise<string> {
|
|
138
|
+
const key = await keysRenameData(ctx, id, name);
|
|
139
|
+
return `Renamed ${key.prefix} to ${key.name}.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeKeyName(name: string): string {
|
|
143
|
+
const normalized = name.trim();
|
|
144
|
+
if (!normalized) {
|
|
145
|
+
throw new Error("API key name cannot be blank.");
|
|
146
|
+
}
|
|
147
|
+
return normalized;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeScopes(scopes: ApiKeyScope[]): ApiKeyScope[] {
|
|
151
|
+
if (scopes.length === 0) {
|
|
152
|
+
throw new Error("Pass at least one --scope, e.g. --scope agents:read.");
|
|
153
|
+
}
|
|
154
|
+
const allowed = new Set<string>(API_KEY_SCOPES);
|
|
155
|
+
const normalized = scopes.map((scope) => normalizeNonBlank(scope, "API key scope") as ApiKeyScope);
|
|
156
|
+
const unknown = normalized.filter((scope) => !allowed.has(scope));
|
|
157
|
+
if (unknown.length > 0) {
|
|
158
|
+
const suggestion = unknown.length === 1 ? closestChoice(unknown[0]!, API_KEY_SCOPES) : null;
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Unknown --scope ${unknown.join(", ")}. Allowed scopes: ${API_KEY_SCOPES.join(", ")}.` +
|
|
161
|
+
(suggestion ? ` Did you mean ${suggestion}?` : ""),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return normalized;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeCreateOptions(scopes: ApiKeyScope[], opts: ApiKeyCreateOptions): ApiKeyCreateOptions {
|
|
168
|
+
const normalized: ApiKeyCreateOptions = { ...opts };
|
|
169
|
+
if (opts.expiresAt !== undefined) {
|
|
170
|
+
normalized.expiresAt = normalizeIsoTimestamp(opts.expiresAt, "--expires-at");
|
|
171
|
+
if (new Date(normalized.expiresAt).getTime() <= Date.now()) {
|
|
172
|
+
throw new Error("--expires-at must be in the future.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const forced = scopes.filter((scope) => SENSITIVE_API_KEY_SCOPES.has(scope));
|
|
177
|
+
if (forced.length === 0) return normalized;
|
|
178
|
+
|
|
179
|
+
if (normalized.expiresAt === undefined) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Sensitive scopes require --expires-at within 90 days: ${forced.join(", ")}.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (new Date(normalized.expiresAt).getTime() > Date.now() + MAX_FORCED_EXPIRY_MS) {
|
|
185
|
+
throw new Error("Sensitive scopes require --expires-at within 90 days.");
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function addKeysListScopeGuidance(error: unknown): Error {
|
|
191
|
+
if (error instanceof Error) {
|
|
192
|
+
const status = (error as Error & { status?: unknown }).status;
|
|
193
|
+
if (
|
|
194
|
+
status === 403 &&
|
|
195
|
+
/scope/i.test(error.message) &&
|
|
196
|
+
/(keys:read|keys:write)/.test(error.message) &&
|
|
197
|
+
!error.message.includes("Listing API keys requires")
|
|
198
|
+
) {
|
|
199
|
+
error.message +=
|
|
200
|
+
`${error.message.endsWith(".") ? "" : "."} ` +
|
|
201
|
+
"Listing API keys requires `keys:read` or `keys:write`.";
|
|
202
|
+
}
|
|
203
|
+
return error;
|
|
204
|
+
}
|
|
205
|
+
return new Error(String(error));
|
|
206
|
+
}
|