shariq-pi-extensions 0.2.24 → 0.2.25
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/extensions/factory-provider/API_KEYS.md +1 -1
- package/extensions/factory-provider/README.md +1 -1
- package/extensions/factory-provider/factory/api-keys.ts +63 -7
- package/extensions/factory-provider/factory/dashboard.ts +44 -12
- package/extensions/factory-provider/index.ts +47 -16
- package/package.json +1 -1
|
@@ -14,4 +14,4 @@ Alternatively choose Pi's top-level **Use an API key** authentication method to
|
|
|
14
14
|
|
|
15
15
|
Use `/logout` to remove the active Factory authentication selection. The configured key file is intentionally not deleted by logout.
|
|
16
16
|
|
|
17
|
-
Open `/factory` to inspect every account, its Standard/Core usage, and rotation status. For a smoke test, use the current `factory/kimi-k3` model.
|
|
17
|
+
Open `/factory` to inspect every account, its Standard/Core usage, and rotation status. File-backed keys remain visible when disabled: select one and press `d` to enable/disable it, or press `x` and confirm to remove it permanently from `api-keys.json`. Environment-provided keys are read-only. For a smoke test, use the current `factory/kimi-k3` model.
|
|
@@ -59,7 +59,7 @@ There are no `-oauth` or `-api-key` model duplicates. The user-curated catalog e
|
|
|
59
59
|
/factory
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
`/factory` opens a full-width account dashboard combining provider status, authentication, Droid/model metadata, rotation cooldowns, and separate Standard and Droid Core usage for every credential. Percentages are explicitly labeled as **used**. Navigate with the arrow keys or `j`/`k`, press `r` to force-refresh every account, and press Escape to close.
|
|
62
|
+
`/factory` opens a full-width account dashboard combining provider status, authentication, Droid/model metadata, rotation cooldowns, and separate Standard and Droid Core usage for every credential. Percentages are explicitly labeled as **used**. Navigate with the arrow keys or `j`/`k`, press `r` to force-refresh every account, use `d` to reversibly enable or disable a rotating file-backed key, use `x` plus confirmation to remove one permanently, and press Escape to close. Environment variables and single credentials managed by Pi auth remain read-only in this dashboard.
|
|
63
63
|
|
|
64
64
|
Usage is never estimated from Pi token counts: Factory's API already applies model multipliers and cache-hit discounts. Cached records refresh at most every 15 minutes during normal sessions and after Factory runs; the manual command is the explicit force-refresh path.
|
|
65
65
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
5
5
|
import { FACTORY_API_BASE_URL, FACTORY_STATE_DIR } from "./constants.ts";
|
|
@@ -25,6 +25,10 @@ export type FactoryApiKeyEntry = {
|
|
|
25
25
|
disabled?: boolean;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
type FactoryApiKeySourceEntry = FactoryApiKeyEntry & {
|
|
29
|
+
source: "environment" | "file";
|
|
30
|
+
};
|
|
31
|
+
|
|
28
32
|
type FactoryApiKeyConfig = {
|
|
29
33
|
keys?: Array<string | Partial<FactoryApiKeyEntry>>;
|
|
30
34
|
};
|
|
@@ -69,19 +73,19 @@ export function parseFactoryApiKeyFile(raw: string): { entries: FactoryApiKeyEnt
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
const entries:
|
|
76
|
+
function configuredFactoryApiKeys(): FactoryApiKeySourceEntry[] {
|
|
77
|
+
const entries: FactoryApiKeySourceEntry[] = [];
|
|
74
78
|
const envKeys = (process.env.FACTORY_API_KEYS || process.env.FACTORY_API_KEY || "")
|
|
75
79
|
.split(/[\n,]+/)
|
|
76
80
|
.map((key) => key.trim())
|
|
77
81
|
.filter(Boolean);
|
|
78
|
-
envKeys.forEach((key, index) => entries.push({ label: `env-${index + 1}`, key }));
|
|
82
|
+
envKeys.forEach((key, index) => entries.push({ label: `env-${index + 1}`, key, source: "environment" }));
|
|
79
83
|
|
|
80
84
|
lastConfigurationWarning = undefined;
|
|
81
85
|
if (existsSync(FACTORY_API_KEYS_PATH)) {
|
|
82
86
|
try {
|
|
83
87
|
const parsed = parseFactoryApiKeyFile(readFileSync(FACTORY_API_KEYS_PATH, "utf8"));
|
|
84
|
-
entries.push(...parsed.entries);
|
|
88
|
+
entries.push(...parsed.entries.map((entry) => ({ ...entry, source: "file" as const })));
|
|
85
89
|
lastConfigurationWarning = parsed.warning;
|
|
86
90
|
} catch {
|
|
87
91
|
lastConfigurationWarning = "Factory rotating-key configuration could not be read.";
|
|
@@ -90,13 +94,63 @@ export function loadFactoryApiKeys(): FactoryApiKeyEntry[] {
|
|
|
90
94
|
|
|
91
95
|
const seen = new Set<string>();
|
|
92
96
|
return entries.filter((entry) => {
|
|
93
|
-
if (entry.disabled) return false;
|
|
94
97
|
if (seen.has(entry.key)) return false;
|
|
95
98
|
seen.add(entry.key);
|
|
96
99
|
return true;
|
|
97
100
|
});
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
export function loadFactoryApiKeys(): FactoryApiKeyEntry[] {
|
|
104
|
+
return configuredFactoryApiKeys().filter((entry) => !entry.disabled);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function editableFactoryApiKeyEntries(): FactoryApiKeyEntry[] {
|
|
108
|
+
if (!existsSync(FACTORY_API_KEYS_PATH)) return [];
|
|
109
|
+
const parsed = parseFactoryApiKeyFile(readFileSync(FACTORY_API_KEYS_PATH, "utf8"));
|
|
110
|
+
if (parsed.warning) throw new Error(parsed.warning);
|
|
111
|
+
return parsed.entries;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function saveFactoryApiKeyEntries(entries: FactoryApiKeyEntry[]) {
|
|
115
|
+
mkdirSync(FACTORY_STATE_DIR, { recursive: true, mode: 0o700 });
|
|
116
|
+
chmodSync(FACTORY_STATE_DIR, 0o700);
|
|
117
|
+
const temporary = `${FACTORY_API_KEYS_PATH}.${process.pid}.tmp`;
|
|
118
|
+
writeFileSync(temporary, `${JSON.stringify({ keys: entries }, null, 2)}\n`, { mode: 0o600 });
|
|
119
|
+
renameSync(temporary, FACTORY_API_KEYS_PATH);
|
|
120
|
+
chmodSync(FACTORY_API_KEYS_PATH, 0o600);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function updateFactoryApiKeyEntries(
|
|
124
|
+
entries: FactoryApiKeyEntry[],
|
|
125
|
+
id: string,
|
|
126
|
+
action: { kind: "enabled"; enabled: boolean } | { kind: "remove" },
|
|
127
|
+
) {
|
|
128
|
+
let changed = false;
|
|
129
|
+
const next = entries.flatMap((entry): FactoryApiKeyEntry[] => {
|
|
130
|
+
if (keyId(entry) !== id) return [entry];
|
|
131
|
+
changed = true;
|
|
132
|
+
if (action.kind === "remove") return [];
|
|
133
|
+
return [{ ...entry, disabled: !action.enabled }];
|
|
134
|
+
});
|
|
135
|
+
return { entries: next, changed };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function setFactoryApiKeyEnabled(id: string, enabled: boolean) {
|
|
139
|
+
const result = updateFactoryApiKeyEntries(editableFactoryApiKeyEntries(), id, { kind: "enabled", enabled });
|
|
140
|
+
if (!result.changed) return false;
|
|
141
|
+
saveFactoryApiKeyEntries(result.entries);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function removeFactoryApiKey(id: string) {
|
|
146
|
+
const result = updateFactoryApiKeyEntries(editableFactoryApiKeyEntries(), id, { kind: "remove" });
|
|
147
|
+
if (!result.changed) return false;
|
|
148
|
+
saveFactoryApiKeyEntries(result.entries);
|
|
149
|
+
state.delete(id);
|
|
150
|
+
organizationIdCache.delete(id);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
100
154
|
export function sortFactoryApiKeysByLastUsed(
|
|
101
155
|
entries: FactoryApiKeyEntry[],
|
|
102
156
|
lastUsed = (entry: FactoryApiKeyEntry) => state.get(keyId(entry))?.lastUsedAt,
|
|
@@ -395,7 +449,7 @@ export function streamSimpleUnifiedFactoryResponses(model: any, context: any, op
|
|
|
395
449
|
}
|
|
396
450
|
|
|
397
451
|
export function factoryApiKeyStatus() {
|
|
398
|
-
const keys =
|
|
452
|
+
const keys = configuredFactoryApiKeys();
|
|
399
453
|
const now = Date.now();
|
|
400
454
|
return {
|
|
401
455
|
configured: keys.length,
|
|
@@ -408,6 +462,8 @@ export function factoryApiKeyStatus() {
|
|
|
408
462
|
id: keyId(entry),
|
|
409
463
|
label: entry.label,
|
|
410
464
|
key: maskKey(entry.key),
|
|
465
|
+
disabled: Boolean(entry.disabled),
|
|
466
|
+
editable: entry.source === "file",
|
|
411
467
|
cooldownSeconds: runtime?.cooldownUntil && runtime.cooldownUntil > now ? Math.ceil((runtime.cooldownUntil - now) / 1000) : 0,
|
|
412
468
|
lastError: runtime?.lastError ? runtime.lastError.slice(0, 200) : undefined,
|
|
413
469
|
lastUsedAt: runtime?.lastUsedAt ? new Date(runtime.lastUsedAt).toISOString() : undefined,
|
|
@@ -26,6 +26,8 @@ import { factoryWarmTheme } from "./warm-theme.ts";
|
|
|
26
26
|
export interface FactoryDashboardAccount {
|
|
27
27
|
id: string;
|
|
28
28
|
label: string;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
editable?: boolean;
|
|
29
31
|
record?: FactoryLimitRecord;
|
|
30
32
|
cooldownSeconds?: number;
|
|
31
33
|
lastUsedAt?: string;
|
|
@@ -96,6 +98,7 @@ function compactPool(
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function accountState(account: FactoryDashboardAccount) {
|
|
101
|
+
if (account.disabled) return "disabled";
|
|
99
102
|
if (account.cooldownSeconds && account.cooldownSeconds > 0) return "cooldown";
|
|
100
103
|
if (account.record?.error) return "refresh error";
|
|
101
104
|
const observedAt = account.record?.fetchedAt ?? Date.now();
|
|
@@ -122,6 +125,8 @@ export class FactoryDashboard implements Component {
|
|
|
122
125
|
private readonly keys: KeybindingsManager;
|
|
123
126
|
private snapshot: FactoryDashboardSnapshot;
|
|
124
127
|
private readonly refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>;
|
|
128
|
+
private readonly toggleAccount: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>;
|
|
129
|
+
private readonly removeAccount: (id: string, label: string) => Promise<FactoryDashboardSnapshot>;
|
|
125
130
|
private readonly done: () => void;
|
|
126
131
|
|
|
127
132
|
constructor(
|
|
@@ -130,6 +135,8 @@ export class FactoryDashboard implements Component {
|
|
|
130
135
|
keys: KeybindingsManager,
|
|
131
136
|
snapshot: FactoryDashboardSnapshot,
|
|
132
137
|
refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
138
|
+
toggleAccount: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
139
|
+
removeAccount: (id: string, label: string) => Promise<FactoryDashboardSnapshot>,
|
|
133
140
|
done: () => void,
|
|
134
141
|
) {
|
|
135
142
|
this.tui = tui;
|
|
@@ -137,6 +144,8 @@ export class FactoryDashboard implements Component {
|
|
|
137
144
|
this.keys = keys;
|
|
138
145
|
this.snapshot = snapshot;
|
|
139
146
|
this.refreshData = refreshData;
|
|
147
|
+
this.toggleAccount = toggleAccount;
|
|
148
|
+
this.removeAccount = removeAccount;
|
|
140
149
|
this.done = done;
|
|
141
150
|
}
|
|
142
151
|
|
|
@@ -145,17 +154,7 @@ export class FactoryDashboard implements Component {
|
|
|
145
154
|
this.refreshing = true;
|
|
146
155
|
this.tui.requestRender();
|
|
147
156
|
void this.refreshData(force)
|
|
148
|
-
.then((snapshot) =>
|
|
149
|
-
if (this.closed) return;
|
|
150
|
-
const selectedId = this.snapshot.accounts[this.selected]?.id;
|
|
151
|
-
this.snapshot = snapshot;
|
|
152
|
-
const nextIndex = selectedId
|
|
153
|
-
? snapshot.accounts.findIndex((account) => account.id === selectedId)
|
|
154
|
-
: -1;
|
|
155
|
-
this.selected = nextIndex >= 0
|
|
156
|
-
? nextIndex
|
|
157
|
-
: Math.min(this.selected, Math.max(0, snapshot.accounts.length - 1));
|
|
158
|
-
})
|
|
157
|
+
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
159
158
|
.catch((error) => {
|
|
160
159
|
if (this.closed) return;
|
|
161
160
|
this.snapshot = {
|
|
@@ -170,6 +169,18 @@ export class FactoryDashboard implements Component {
|
|
|
170
169
|
});
|
|
171
170
|
}
|
|
172
171
|
|
|
172
|
+
private replaceSnapshot(snapshot: FactoryDashboardSnapshot) {
|
|
173
|
+
if (this.closed) return;
|
|
174
|
+
const selectedId = this.snapshot.accounts[this.selected]?.id;
|
|
175
|
+
this.snapshot = snapshot;
|
|
176
|
+
const nextIndex = selectedId
|
|
177
|
+
? snapshot.accounts.findIndex((account) => account.id === selectedId)
|
|
178
|
+
: -1;
|
|
179
|
+
this.selected = nextIndex >= 0
|
|
180
|
+
? nextIndex
|
|
181
|
+
: Math.min(this.selected, Math.max(0, snapshot.accounts.length - 1));
|
|
182
|
+
}
|
|
183
|
+
|
|
173
184
|
dispose() {
|
|
174
185
|
this.closed = true;
|
|
175
186
|
}
|
|
@@ -191,6 +202,23 @@ export class FactoryDashboard implements Component {
|
|
|
191
202
|
if (accounts.length) this.selected = (this.selected + 1) % accounts.length;
|
|
192
203
|
} else if (data === "r") {
|
|
193
204
|
this.startRefresh(true);
|
|
205
|
+
} else if ((data === "d" || data === "x") && accounts[this.selected]?.editable && !this.refreshing) {
|
|
206
|
+
const account = accounts[this.selected]!;
|
|
207
|
+
this.refreshing = true;
|
|
208
|
+
const action = data === "x"
|
|
209
|
+
? this.removeAccount(account.id, account.label)
|
|
210
|
+
: this.toggleAccount(account.id, account.disabled === true);
|
|
211
|
+
void action
|
|
212
|
+
.then((snapshot) => this.replaceSnapshot(snapshot))
|
|
213
|
+
.catch((error) => {
|
|
214
|
+
if (!this.closed) this.snapshot = { ...this.snapshot, warning: error instanceof Error ? error.message : String(error) };
|
|
215
|
+
})
|
|
216
|
+
.finally(() => {
|
|
217
|
+
if (!this.closed) {
|
|
218
|
+
this.refreshing = false;
|
|
219
|
+
this.tui.requestRender();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
194
222
|
}
|
|
195
223
|
this.tui.requestRender();
|
|
196
224
|
}
|
|
@@ -242,7 +270,7 @@ export class FactoryDashboard implements Component {
|
|
|
242
270
|
lines.push(frameBottom(this.theme, width));
|
|
243
271
|
lines.push(
|
|
244
272
|
truncateToWidth(
|
|
245
|
-
`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh all")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`,
|
|
273
|
+
`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh all")} ${this.theme.fg("accent", "d")} ${this.theme.fg("dim", "enable/disable")} ${this.theme.fg("accent", "x")} ${this.theme.fg("dim", "remove")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`,
|
|
246
274
|
width,
|
|
247
275
|
"",
|
|
248
276
|
),
|
|
@@ -341,6 +369,8 @@ export async function openFactoryDashboard(
|
|
|
341
369
|
ctx: ExtensionContext,
|
|
342
370
|
initial: FactoryDashboardSnapshot,
|
|
343
371
|
refresh: (force: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
372
|
+
toggle: (id: string, enabled: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
373
|
+
remove: (id: string, label: string) => Promise<FactoryDashboardSnapshot>,
|
|
344
374
|
) {
|
|
345
375
|
await ctx.ui.custom<void>(
|
|
346
376
|
(tui, theme, keys, done) => {
|
|
@@ -350,6 +380,8 @@ export async function openFactoryDashboard(
|
|
|
350
380
|
keys,
|
|
351
381
|
initial,
|
|
352
382
|
refresh,
|
|
383
|
+
toggle,
|
|
384
|
+
remove,
|
|
353
385
|
() => done(undefined),
|
|
354
386
|
);
|
|
355
387
|
queueMicrotask(() => dashboard.startRefresh(false));
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
FACTORY_API_KEY_FILE_SENTINEL,
|
|
12
12
|
factoryApiKeyStatus,
|
|
13
13
|
loadFactoryApiKeys,
|
|
14
|
+
removeFactoryApiKey,
|
|
15
|
+
setFactoryApiKeyEnabled,
|
|
14
16
|
streamSimpleUnifiedFactoryResponses,
|
|
15
17
|
} from "./factory/api-keys.ts";
|
|
16
18
|
import {
|
|
@@ -88,21 +90,35 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
88
90
|
ctx: ExtensionContext,
|
|
89
91
|
): Promise<FactoryDashboardSnapshot> => {
|
|
90
92
|
const credentials = await limitCredentials(ctx);
|
|
93
|
+
const resolved = await ctx.modelRegistry.getProviderAuth(PROVIDER_ID);
|
|
94
|
+
const fileBacked = resolved?.auth.apiKey?.trim() === FACTORY_API_KEY_FILE_SENTINEL;
|
|
91
95
|
const auth = ctx.modelRegistry.getProviderAuthStatus(PROVIDER_ID);
|
|
92
96
|
const apiKeys = factoryApiKeyStatus();
|
|
93
97
|
const cache = loadFactoryLimitCache();
|
|
94
98
|
const runtimeById = new Map(apiKeys.keys.map((key) => [key.id, key]));
|
|
95
|
-
const accounts =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
const accounts = fileBacked
|
|
100
|
+
? apiKeys.keys.map((key) => ({
|
|
101
|
+
id: key.id,
|
|
102
|
+
label: key.label,
|
|
103
|
+
disabled: key.disabled,
|
|
104
|
+
editable: key.editable,
|
|
105
|
+
record: cache.records.find((record) => record.id === key.id),
|
|
106
|
+
cooldownSeconds: key.cooldownSeconds,
|
|
107
|
+
lastUsedAt: key.lastUsedAt,
|
|
108
|
+
}))
|
|
109
|
+
: credentials.map((credential) => {
|
|
110
|
+
const id = factoryCredentialId(credential.secret);
|
|
111
|
+
const runtime = runtimeById.get(id);
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
label: credential.label,
|
|
115
|
+
disabled: false,
|
|
116
|
+
editable: false,
|
|
117
|
+
record: cache.records.find((record) => record.id === id),
|
|
118
|
+
cooldownSeconds: runtime?.cooldownSeconds,
|
|
119
|
+
lastUsedAt: runtime?.lastUsedAt,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
106
122
|
return {
|
|
107
123
|
version: clientVersion,
|
|
108
124
|
modelCount: models.length,
|
|
@@ -110,7 +126,7 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
110
126
|
? auth.label || auth.source || "configured"
|
|
111
127
|
: "not configured",
|
|
112
128
|
configured: accounts.length,
|
|
113
|
-
active: accounts.filter((account) => !account.cooldownSeconds).length,
|
|
129
|
+
active: accounts.filter((account) => !account.disabled && !account.cooldownSeconds).length,
|
|
114
130
|
warning: apiKeys.warning,
|
|
115
131
|
accounts,
|
|
116
132
|
};
|
|
@@ -195,10 +211,25 @@ export default async function factoryExtension(pi: ExtensionAPI) {
|
|
|
195
211
|
description: "Open Factory account, rotation, and usage dashboard",
|
|
196
212
|
handler: async (_args, ctx) => {
|
|
197
213
|
const initial = await dashboardSnapshot(ctx);
|
|
198
|
-
await openFactoryDashboard(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
await openFactoryDashboard(
|
|
215
|
+
ctx,
|
|
216
|
+
initial,
|
|
217
|
+
async (force) => {
|
|
218
|
+
await refreshLimits(ctx, force);
|
|
219
|
+
return dashboardSnapshot(ctx);
|
|
220
|
+
},
|
|
221
|
+
async (id, enabled) => {
|
|
222
|
+
if (!setFactoryApiKeyEnabled(id, enabled)) throw new Error("This Factory credential is not editable from the rotating-key file.");
|
|
223
|
+
if (enabled) await refreshLimits(ctx, true);
|
|
224
|
+
return dashboardSnapshot(ctx);
|
|
225
|
+
},
|
|
226
|
+
async (id, label) => {
|
|
227
|
+
const confirmed = await ctx.ui.confirm("Remove Factory account?", `Permanently remove ${label} from the rotating-key file?`);
|
|
228
|
+
if (!confirmed) return dashboardSnapshot(ctx);
|
|
229
|
+
if (!removeFactoryApiKey(id)) throw new Error("This Factory credential is not editable from the rotating-key file.");
|
|
230
|
+
return dashboardSnapshot(ctx);
|
|
231
|
+
},
|
|
232
|
+
);
|
|
202
233
|
},
|
|
203
234
|
});
|
|
204
235
|
}
|