@bivy/bivy 0.10.1-staging.183 → 0.10.1-staging.185
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.
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
export { DEFAULT_LABEL, normalizeProvider, normalizeLabel, credKey, parseCredKey, agentNativeLabel, defaultSyncFor, inferReferenceBackend, resolveCredential, missingPresetLabels, } from "./records.js";
|
|
17
17
|
// --- The v3 document engine: schema, migration, merge (new, pure) -----------
|
|
18
18
|
export { emptyDocument, recordFromStored, preferIncomingRecord, tombstoneWinsRecord, migrateToV3, mergeDocuments, } from "./document.js";
|
|
19
|
-
// --- Selection presets (config-as-code)
|
|
20
|
-
export { parsePresets, loadPresets, defaultPresetsPath, PRESETS_FILENAME } from "./presets.js";
|
|
19
|
+
// --- Selection presets + ingest policy (config-as-code) ---------------------
|
|
20
|
+
export { parsePresets, loadPresets, defaultPresetsPath, PRESETS_FILENAME, parseIngestPolicy, loadIngestPolicy, } from "./presets.js";
|
|
21
21
|
// --- The vault (source of truth) — re-exported from its current home --------
|
|
22
22
|
export { createCredentialVault, migrateVaultDir, preferIncomingCredential, tombstoneWins, BivyCredentialStore, } from "../runtime/credential-store.js";
|
|
23
23
|
// --- The node resolver + agent env representation ---------------------------
|
|
@@ -25,4 +25,4 @@ export { createCredentialStore, buildAgentCredentialEnv, apiKeyEnvVar, NodeCrede
|
|
|
25
25
|
// --- The daemon credential API (ours — currently named pi-auth.ts) ----------
|
|
26
26
|
// Phase 5 renames the file to `src/credentials/api.ts`; the facade keeps the
|
|
27
27
|
// import path stable so call sites don't churn when it moves.
|
|
28
|
-
export { listProviders, exportProviderAuth, exportProviderAuthTombstones, importProviderAuth, setProviderApiKey, setProviderCredential, setProviderReference, removeProvider, } from "../runtime/pi-auth.js";
|
|
28
|
+
export { listProviders, exportProviderAuth, exportProviderAuthTombstones, importProviderAuth, setProviderApiKey, setProviderCredential, setProviderReference, removeProvider, listCredentialRecords, setProviderApiKeyLabeled, setProviderReferenceLabeled, removeProviderCredential, } from "../runtime/pi-auth.js";
|
|
@@ -73,3 +73,23 @@ export function loadPresets(filePath) {
|
|
|
73
73
|
return {};
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
/** Read `ingest.policy` from a parsed config; anything unrecognized → `merge`. */
|
|
77
|
+
export function parseIngestPolicy(raw) {
|
|
78
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
79
|
+
const ingest = raw.ingest;
|
|
80
|
+
if (ingest && typeof ingest === "object" && !Array.isArray(ingest)) {
|
|
81
|
+
if (ingest.policy === "separate")
|
|
82
|
+
return "separate";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return "merge";
|
|
86
|
+
}
|
|
87
|
+
/** Load the ingest policy from the config file; missing/malformed → `merge`. */
|
|
88
|
+
export function loadIngestPolicy(filePath) {
|
|
89
|
+
try {
|
|
90
|
+
return parseIngestPolicy(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return "merge";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { createCredentialVault } from "./credential-store.js";
|
|
16
|
+
import { agentNativeLabel } from "../credentials/records.js";
|
|
17
|
+
import { loadIngestPolicy, defaultPresetsPath } from "../credentials/presets.js";
|
|
16
18
|
import { resolveCodexHome } from "./codex-auth.js";
|
|
17
19
|
import { grokAuthEntryKey, resolveGrokHome, GROK_OIDC_ISSUER } from "./grok-auth.js";
|
|
18
20
|
import { claudeCredentialFiles } from "./anthropic-preflight.js";
|
|
@@ -30,6 +32,32 @@ function jwtExpiryMs(token) {
|
|
|
30
32
|
return undefined;
|
|
31
33
|
}
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Fold one mapped agent-native login into the vault per the node's ingest policy
|
|
37
|
+
* (`credentials.config.json` → `ingest.policy`; default `merge`):
|
|
38
|
+
* - `merge`: merge into the provider's `default` slot (historical behavior) — a
|
|
39
|
+
* native login updates the provider's synced Bivy credential, rotation-safe.
|
|
40
|
+
* - `separate`: land as a distinct, node-local credential under a reserved
|
|
41
|
+
* agent-derived label (with the account id when the login carries one), so it
|
|
42
|
+
* never overwrites a Bivy key and is selectable via a preset.
|
|
43
|
+
*/
|
|
44
|
+
async function foldIngested(credsDir, agent, mapped) {
|
|
45
|
+
const store = createCredentialVault(credsDir);
|
|
46
|
+
const policy = loadIngestPolicy(defaultPresetsPath(credsDir));
|
|
47
|
+
if (policy !== "separate") {
|
|
48
|
+
return store.importAll({ [mapped.providerId]: mapped.credential });
|
|
49
|
+
}
|
|
50
|
+
const rawAccount = mapped.credential.accountId;
|
|
51
|
+
const accountId = typeof rawAccount === "string" ? rawAccount : undefined;
|
|
52
|
+
const record = {
|
|
53
|
+
provider: mapped.providerId,
|
|
54
|
+
label: agentNativeLabel(agent, accountId),
|
|
55
|
+
source: { kind: "stored", cred: mapped.credential },
|
|
56
|
+
origin: "agent-native",
|
|
57
|
+
sync: "node",
|
|
58
|
+
};
|
|
59
|
+
return store.importRecords([record]);
|
|
60
|
+
}
|
|
33
61
|
/** Map Codex's `~/.codex/auth.json` into a Bivy credential (provider id → credential). */
|
|
34
62
|
export function codexAuthToCredential(raw) {
|
|
35
63
|
if (!raw || typeof raw !== "object")
|
|
@@ -64,7 +92,7 @@ export async function ingestCodexCredentials(credsDir) {
|
|
|
64
92
|
const mapped = codexAuthToCredential(raw);
|
|
65
93
|
if (!mapped)
|
|
66
94
|
return 0;
|
|
67
|
-
return
|
|
95
|
+
return foldIngested(credsDir, "codex", mapped);
|
|
68
96
|
}
|
|
69
97
|
/** Map the Claude CLI's `.credentials.json` (`claudeAiOauth`) into a Bivy credential. */
|
|
70
98
|
export function claudeAuthToCredential(raw) {
|
|
@@ -91,7 +119,7 @@ export async function ingestClaudeCredentials(credsDir) {
|
|
|
91
119
|
}
|
|
92
120
|
const mapped = claudeAuthToCredential(raw);
|
|
93
121
|
if (mapped)
|
|
94
|
-
return
|
|
122
|
+
return foldIngested(credsDir, "claude", mapped);
|
|
95
123
|
}
|
|
96
124
|
return 0;
|
|
97
125
|
}
|
|
@@ -139,7 +167,7 @@ export async function ingestGrokCredentials(credsDir) {
|
|
|
139
167
|
const mapped = grokAuthToCredential(raw);
|
|
140
168
|
if (!mapped)
|
|
141
169
|
return 0;
|
|
142
|
-
return
|
|
170
|
+
return foldIngested(credsDir, "grok", mapped);
|
|
143
171
|
}
|
|
144
172
|
/**
|
|
145
173
|
* Ingest an agent's native credential store into Bivy's vault. Called when a
|
|
@@ -299,6 +299,84 @@ export class BivyCredentialStore {
|
|
|
299
299
|
async listRecords() {
|
|
300
300
|
return Object.values(this.readDocument().credentials);
|
|
301
301
|
}
|
|
302
|
+
/** Read one credential record by its `provider:label` identity. */
|
|
303
|
+
async readRecord(provider, label = DEFAULT_LABEL) {
|
|
304
|
+
const id = providerId(provider);
|
|
305
|
+
if (!id)
|
|
306
|
+
return undefined;
|
|
307
|
+
return this.readDocument().credentials[credKey(id, label)];
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Authoritatively upsert a record at its `provider:label` — the record-addressed
|
|
311
|
+
* write behind the multi-credential API (add/label a specific account). Stamps
|
|
312
|
+
* the store-owned `updatedAt` and clears any tombstone for that slot. Use
|
|
313
|
+
* `importRecords()` (merge) for ingest/sync, where a fresher local login must win.
|
|
314
|
+
*/
|
|
315
|
+
async putRecord(record) {
|
|
316
|
+
const id = providerId(record.provider);
|
|
317
|
+
if (!id)
|
|
318
|
+
throw new Error("Provider is required");
|
|
319
|
+
const label = normalizeLabel(record.label);
|
|
320
|
+
const key = credKey(id, label);
|
|
321
|
+
await this.enqueue(id, async () => {
|
|
322
|
+
await this.acquireLock();
|
|
323
|
+
try {
|
|
324
|
+
const document = this.readDocument();
|
|
325
|
+
document.credentials[key] = { ...record, provider: id, label, updatedAt: Date.now() };
|
|
326
|
+
delete document.deletedAt[key];
|
|
327
|
+
this.writeDocument(document);
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
await this.releaseLock();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/** Forget one record by `provider:label`, leaving a tombstone for convergence. */
|
|
335
|
+
async deleteRecord(provider, label = DEFAULT_LABEL) {
|
|
336
|
+
const id = providerId(provider);
|
|
337
|
+
if (!id)
|
|
338
|
+
return;
|
|
339
|
+
const key = credKey(id, label);
|
|
340
|
+
await this.enqueue(id, async () => {
|
|
341
|
+
await this.acquireLock();
|
|
342
|
+
try {
|
|
343
|
+
const document = this.readDocument();
|
|
344
|
+
const had = key in document.credentials;
|
|
345
|
+
delete document.credentials[key];
|
|
346
|
+
const deletedAt = Date.now();
|
|
347
|
+
if (!had && (document.deletedAt[key] ?? 0) >= deletedAt)
|
|
348
|
+
return;
|
|
349
|
+
document.deletedAt[key] = deletedAt;
|
|
350
|
+
this.writeDocument(document);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
await this.releaseLock();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Merge record snapshots into the vault, record-addressed and non-destructive
|
|
359
|
+
* (freshest-OAuth-wins, rotation-safe, tombstone-newer-wins — see document.ts).
|
|
360
|
+
* This is the record-level counterpart to `importAll`, for agent-native ingest
|
|
361
|
+
* under reserved labels and (later) record-shaped cross-node sync. Returns the
|
|
362
|
+
* number of newly-added records.
|
|
363
|
+
*/
|
|
364
|
+
async importRecords(records, deletedAt = {}) {
|
|
365
|
+
await this.acquireLock();
|
|
366
|
+
try {
|
|
367
|
+
const document = this.readDocument();
|
|
368
|
+
const incoming = {};
|
|
369
|
+
for (const record of records)
|
|
370
|
+
incoming[credKey(record.provider, record.label)] = record;
|
|
371
|
+
const result = mergeDocuments(document, incoming, deletedAt);
|
|
372
|
+
if (result.changed)
|
|
373
|
+
this.writeDocument(result.document);
|
|
374
|
+
return result.imported;
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
await this.releaseLock();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
302
380
|
/** The plaintext `auth.json` path an agent's own CLI/TUI reads (`<plaintextDir>/auth.json`). */
|
|
303
381
|
get legacyAuthPath() {
|
|
304
382
|
return this.legacyFile;
|
package/dist/runtime/pi-auth.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// pi-oauth bridge supplies (Bivy has no model catalog of its own).
|
|
10
10
|
import { createCredentialVault } from "./credential-store.js";
|
|
11
11
|
import { listPiProviders } from "./pi-oauth.js";
|
|
12
|
-
import { inferReferenceBackend } from "../credentials/records.js";
|
|
12
|
+
import { inferReferenceBackend, normalizeLabel, defaultSyncFor, DEFAULT_LABEL, } from "../credentials/records.js";
|
|
13
13
|
/**
|
|
14
14
|
* Enumerate model providers with their current auth status. The catalog + status
|
|
15
15
|
* come from Pi's provider list (joined with our injected store); the stored
|
|
@@ -104,3 +104,67 @@ export async function removeProvider(credsDir, provider) {
|
|
|
104
104
|
throw new Error("Provider is required");
|
|
105
105
|
await createCredentialVault(credsDir).delete(id);
|
|
106
106
|
}
|
|
107
|
+
/** Every stored credential as a non-secret summary (never exposes key material). */
|
|
108
|
+
export async function listCredentialRecords(credsDir) {
|
|
109
|
+
const records = await createCredentialVault(credsDir).listRecords();
|
|
110
|
+
return records.map((record) => {
|
|
111
|
+
const source = record.source;
|
|
112
|
+
if (source.kind === "reference") {
|
|
113
|
+
return { provider: record.provider, label: record.label, kind: "reference", sync: record.sync, origin: record.origin, ref: source.ref };
|
|
114
|
+
}
|
|
115
|
+
const cred = source.cred;
|
|
116
|
+
const summary = { provider: record.provider, label: record.label, kind: cred.type, sync: record.sync, origin: record.origin };
|
|
117
|
+
if (cred.type === "oauth")
|
|
118
|
+
summary.expiresAt = cred.expires;
|
|
119
|
+
return summary;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Preserve an existing record's sync/origin when re-setting it (editing the key of
|
|
124
|
+
* a credential the user opted node-local must not silently re-enable sync), else
|
|
125
|
+
* default to a Bivy-first, opt-out-sync credential.
|
|
126
|
+
*/
|
|
127
|
+
async function labeledMeta(credsDir, provider, label) {
|
|
128
|
+
const existing = await createCredentialVault(credsDir).readRecord(provider, label);
|
|
129
|
+
return { sync: existing?.sync ?? defaultSyncFor("bivy"), origin: existing?.origin ?? "bivy" };
|
|
130
|
+
}
|
|
131
|
+
/** Store an API key under a specific label (multi-account). */
|
|
132
|
+
export async function setProviderApiKeyLabeled(credsDir, provider, label, key) {
|
|
133
|
+
const id = provider.trim().toLowerCase();
|
|
134
|
+
if (!id)
|
|
135
|
+
throw new Error("Provider is required");
|
|
136
|
+
const apiKey = String(key ?? "").trim();
|
|
137
|
+
if (!apiKey)
|
|
138
|
+
throw new Error("API key cannot be empty");
|
|
139
|
+
const meta = await labeledMeta(credsDir, id, label);
|
|
140
|
+
await createCredentialVault(credsDir).putRecord({
|
|
141
|
+
provider: id,
|
|
142
|
+
label: normalizeLabel(label),
|
|
143
|
+
source: { kind: "stored", cred: { type: "api_key", key: apiKey } },
|
|
144
|
+
...meta,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Store a reference (op:// / env://) under a specific label (multi-account). */
|
|
148
|
+
export async function setProviderReferenceLabeled(credsDir, provider, label, ref) {
|
|
149
|
+
const id = provider.trim().toLowerCase();
|
|
150
|
+
if (!id)
|
|
151
|
+
throw new Error("Provider is required");
|
|
152
|
+
const pointer = String(ref ?? "").trim();
|
|
153
|
+
const backend = inferReferenceBackend(pointer);
|
|
154
|
+
if (!backend)
|
|
155
|
+
throw new Error("Reference must be an op:// or env:// pointer");
|
|
156
|
+
const meta = await labeledMeta(credsDir, id, label);
|
|
157
|
+
await createCredentialVault(credsDir).putRecord({
|
|
158
|
+
provider: id,
|
|
159
|
+
label: normalizeLabel(label),
|
|
160
|
+
source: { kind: "reference", ref: pointer, backend },
|
|
161
|
+
...meta,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Forget a single labeled credential (`provider:label`). */
|
|
165
|
+
export async function removeProviderCredential(credsDir, provider, label = DEFAULT_LABEL) {
|
|
166
|
+
const id = provider.trim().toLowerCase();
|
|
167
|
+
if (!id)
|
|
168
|
+
throw new Error("Provider is required");
|
|
169
|
+
await createCredentialVault(credsDir).deleteRecord(id, label);
|
|
170
|
+
}
|
package/package.json
CHANGED