@forgezero/vault 0.1.24 → 0.1.26

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/README.md CHANGED
@@ -291,17 +291,17 @@ const signed = await vault.runtime.sign(signer.runtimeKey, transactionDigest);
291
291
 
292
292
  ## Three paths, and why they stay explicit
293
293
 
294
- Platform API logic already holds the requested realm master seed, so it opens the scoped envelope directly through an injected internal backend — no HTTP call, API key or local replica. A tenant on managed compute uses the local agent and its memory-only project scope. A tenant elsewhere signs HTTPS with an API-key seed. These share vault semantics and never silently fall through from a stronger posture to a weaker one.
294
+ Platform API logic opens its own scoped Vault data directly through an injected internal backend — no HTTP call, API key or local replica. A tenant on managed compute uses the local agent and its memory-only project scope. A tenant elsewhere signs HTTPS with an API-key seed. These share Vault semantics and never silently fall through from a stronger posture to a weaker one.
295
295
 
296
296
  ```text
297
- PLATFORM API resident realm seed -> direct scoped envelope
298
- MANAGED TENANT local unix socket -> agent RAM scope
299
- EXTERNAL TENANT scoped API-key seed -> signed HTTPS
297
+ PLATFORM API resident custody key -> direct scoped envelope
298
+ MANAGED TENANT local unix socket -> agent RAM scope
299
+ EXTERNAL TENANT scoped API-key seed -> signed HTTPS
300
300
  ```
301
301
 
302
302
  ## Current implementation boundary
303
303
 
304
- All three trust paths now have code: the platform opens its resident realm seed directly, an enrolled tenant agent derives its project binding and keeps every environment in that project in RAM, and createVault automatically uses either the local socket or a hybrid signer derived from an API-key seed. Live SEV-SNP report acquisition and an end-to-end production deployment are still gates; installation alone is not presented as proof.
304
+ All three trust paths now have code: the platform opens its resident custody key directly, an enrolled tenant agent derives its project binding and keeps every environment in that project in RAM, and createVault automatically uses either the local socket or a hybrid signer derived from an API-key seed. Live SEV-SNP report acquisition and an end-to-end production deployment are still gates; installation alone is not presented as proof.
305
305
 
306
306
  ## 1. Install
307
307
 
@@ -371,9 +371,9 @@ Each names what to do next.
371
371
 
372
372
  ```text
373
373
  NO_CREDENTIAL neither the socket nor FORGEZERO_API_KEY was found
374
- VAULT_LOCKED the realm has no seed in memory; a custodian must unlock
374
+ VAULT_LOCKED the account Vault is locked; a custodian must unlock it
375
375
  MANAGED_ONLY getAll needs the agent socket
376
- NO_NODE no vault node is currently available
376
+ NO_NODE no Vault node is currently available
377
377
  ```
378
378
 
379
379
  ## API keys — the key is a seed, not a password
package/dist/index.d.ts CHANGED
@@ -35,7 +35,7 @@ export declare class VaultError extends Error {
35
35
  readonly code: string;
36
36
  constructor(code: string, message: string);
37
37
  }
38
- export type CredentialMode = 'managed' | 'external';
38
+ export type CredentialMode = 'managed' | 'external' | 'local';
39
39
  export interface Credential {
40
40
  mode: CredentialMode;
41
41
  /** MANAGED only — the agent socket serving the environment's RAM replica. */
@@ -43,6 +43,11 @@ export interface Credential {
43
43
  /** EXTERNAL only — a seed, never transmitted; a keypair derives from it. */
44
44
  apiKey?: string;
45
45
  }
46
+ export interface LocalVaultStorage {
47
+ getItem(key: string): string | null;
48
+ setItem(key: string, value: string): void;
49
+ removeItem(key: string): void;
50
+ }
46
51
  export interface DiscoveryEnvironment {
47
52
  env?: Record<string, string | undefined>;
48
53
  socketPath?: string;
@@ -142,6 +147,10 @@ export interface VaultOptions {
142
147
  runtimeRequest?: RuntimeRequest;
143
148
  credential?: Credential;
144
149
  discovery?: DiscoveryEnvironment;
150
+ /** Browser-compatible persistence used only by explicit `local` development mode. */
151
+ localStorage?: LocalVaultStorage;
152
+ /** Separates multiple local applications that intentionally share one Storage implementation. */
153
+ localNamespace?: string;
145
154
  }
146
155
  export interface EntryMeta {
147
156
  name: string;
@@ -168,9 +177,15 @@ export declare class ForgeZero {
168
177
  private readonly requestTimeoutMs;
169
178
  private readonly project;
170
179
  private readonly environment;
180
+ private readonly localStorage?;
181
+ private readonly localNamespace;
171
182
  constructor(options?: VaultOptions);
172
183
  private scope;
173
184
  private request;
185
+ private localStorageKey;
186
+ private localValues;
187
+ private saveLocalValues;
188
+ private localRequest;
174
189
  private managedRequest;
175
190
  /** One value, current version unless asked otherwise. */
176
191
  get(name: string, options?: {
@@ -258,6 +273,8 @@ export declare class ForgeZero {
258
273
  * constructor that happens to do I/O-adjacent work.
259
274
  */
260
275
  export declare const createVault: (options?: VaultOptions) => ForgeZero;
276
+ /** Explicit isolated laptop/browser development. This mode never performs HTTP. */
277
+ export declare const createLocalVault: (options?: Omit<VaultOptions, "credential">) => ForgeZero;
261
278
  export declare function vaultCredentials(vault: ForgeZero): {
262
279
  readonly name: string;
263
280
  get(reference: string, field: string): Promise<string>;
@@ -296,4 +313,4 @@ export declare function systemdCredentials(options?: SystemdCredentialOptions):
296
313
  readonly name: 'systemd';
297
314
  get(reference: string, field: string): Promise<string>;
298
315
  };
299
- export declare const VERSION = "0.1.24";
316
+ export declare const VERSION = "0.1.26";
package/dist/index.js CHANGED
@@ -22,8 +22,8 @@ class VaultRuntime {
22
22
  this.transport = transport;
23
23
  }
24
24
  available() {
25
- if (this.transport === "managed-read-only") {
26
- throw new VaultRuntimeClientError("MANAGED_RUNTIME_DELEGATION_REQUIRED", "Vault runtime operations require a separately granted external API key; the managed Agent replica is read-only.");
25
+ if (this.transport === "managed-read-only" || this.transport === "local-read-only") {
26
+ throw new VaultRuntimeClientError("MANAGED_RUNTIME_DELEGATION_REQUIRED", this.transport === "local-read-only" ? "Local Vault mode stores development values only; master-seed runtime operations require a separately granted live API key." : "Vault runtime operations require a separately granted external API key; the managed Agent replica is read-only.");
27
27
  }
28
28
  }
29
29
  async create(options) {
@@ -156,7 +156,9 @@ function discover(environment = {}) {
156
156
  const apiKey = env.FORGEZERO_API_KEY;
157
157
  if (apiKey)
158
158
  return { mode: "external", apiKey };
159
- throw new VaultError("NO_CREDENTIAL", `No credential found. On managed compute the agent socket at ${socketPath} provides one; ` + "elsewhere set FORGEZERO_API_KEY.");
159
+ if (env.FORGEZERO_LOCAL_VAULT === "1")
160
+ return { mode: "local" };
161
+ throw new VaultError("NO_CREDENTIAL", `No credential found. On managed compute the agent socket at ${socketPath} provides one; ` + "elsewhere set FORGEZERO_API_KEY, or explicitly set FORGEZERO_LOCAL_VAULT=1 for isolated local development.");
160
162
  }
161
163
  var unbase64url = (encoded) => {
162
164
  const padded = encoded.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - encoded.length % 4) % 4);
@@ -264,6 +266,8 @@ class ForgeZero {
264
266
  requestTimeoutMs;
265
267
  project;
266
268
  environment;
269
+ localStorage;
270
+ localNamespace;
267
271
  constructor(options = {}) {
268
272
  const credential = options.credential ?? discover(options.discovery);
269
273
  this.credential = credential.mode === "managed" && !credential.socketPath ? { ...credential, socketPath: DEFAULT_SOCKET } : credential;
@@ -277,8 +281,13 @@ class ForgeZero {
277
281
  }
278
282
  this.project = options.project ?? "default";
279
283
  this.environment = options.environment ?? "production";
284
+ this.localStorage = options.localStorage ?? (this.credential.mode === "local" && typeof globalThis.localStorage !== "undefined" ? globalThis.localStorage : undefined);
285
+ this.localNamespace = options.localNamespace?.trim() || "fz.local.vault.v1";
286
+ if (this.credential.mode === "local" && !this.localStorage) {
287
+ throw new VaultError("LOCAL_STORAGE_UNAVAILABLE", "Local Vault mode requires browser localStorage or an injected LocalVaultStorage adapter.");
288
+ }
280
289
  const runtimeRequest = options.runtimeRequest ?? ((path, init) => this.request(`/v1/vault/${this.scope()}/runtime${path}`, init));
281
- this.runtime = new VaultRuntime(runtimeRequest, this.credential.mode === "external" ? "external" : options.runtimeRequest ? "managed-delegated" : "managed-read-only");
290
+ this.runtime = new VaultRuntime(runtimeRequest, this.credential.mode === "external" ? "external" : options.runtimeRequest ? "managed-delegated" : this.credential.mode === "local" ? "local-read-only" : "managed-read-only");
282
291
  }
283
292
  scope() {
284
293
  return `${encodeURIComponent(this.project)}/${encodeURIComponent(this.environment)}`;
@@ -286,6 +295,8 @@ class ForgeZero {
286
295
  async request(path, init = {}) {
287
296
  if (this.credential.mode === "managed")
288
297
  return this.managedRequest(path, init);
298
+ if (this.credential.mode === "local")
299
+ return this.localRequest(path, init);
289
300
  const body = typeof init.body === "string" ? init.body : "";
290
301
  const target = new URL(path, this.apiUrl);
291
302
  const headers = {
@@ -326,6 +337,46 @@ class ForgeZero {
326
337
  }
327
338
  return payload;
328
339
  }
340
+ localStorageKey() {
341
+ return `${this.localNamespace}:${encodeURIComponent(this.project)}:${encodeURIComponent(this.environment)}`;
342
+ }
343
+ localValues() {
344
+ const raw = this.localStorage.getItem(this.localStorageKey());
345
+ if (raw === null)
346
+ return {};
347
+ try {
348
+ const parsed = JSON.parse(raw);
349
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Object.entries(parsed).some(([key, value]) => !key || typeof value !== "string"))
350
+ throw new Error("invalid");
351
+ return parsed;
352
+ } catch {
353
+ throw new VaultError("LOCAL_STORE_MALFORMED", "The local Vault store is malformed. Clear it before continuing.");
354
+ }
355
+ }
356
+ saveLocalValues(values) {
357
+ this.localStorage.setItem(this.localStorageKey(), JSON.stringify(values));
358
+ }
359
+ async localRequest(path, init) {
360
+ const method = init.method ?? "GET";
361
+ const target = new URL(path, "https://vault.local.invalid");
362
+ const values = this.localValues();
363
+ const entry = /\/entries\/([^/]+)$/.exec(target.pathname);
364
+ if (method === "GET" && entry) {
365
+ const name = decodeURIComponent(entry[1]);
366
+ if (!(name in values))
367
+ throw new VaultError("NOT_FOUND", `No local Vault value named ${name}.`);
368
+ return { value: values[name] };
369
+ }
370
+ if (method === "GET" && target.pathname.endsWith("/list")) {
371
+ return { entries: Object.keys(values).sort().map((name) => ({ name })) };
372
+ }
373
+ if (method === "GET" && target.pathname.endsWith("/entries"))
374
+ return { values: { ...values } };
375
+ if (method === "GET" && target.pathname.endsWith("/changes")) {
376
+ return { version: 0, changed: [] };
377
+ }
378
+ throw new VaultError("LOCAL_OPERATION_UNSUPPORTED", "This operation is unavailable in isolated local Vault mode.");
379
+ }
329
380
  async managedRequest(path, init) {
330
381
  const socketPath = this.credential.socketPath;
331
382
  const method = init.method ?? "GET";
@@ -406,7 +457,7 @@ class ForgeZero {
406
457
  return result.value;
407
458
  }
408
459
  async getAll() {
409
- if (this.credential.mode !== "managed") {
460
+ if (this.credential.mode !== "managed" && this.credential.mode !== "local") {
410
461
  throw new VaultError("MANAGED_ONLY", "getAll is available on managed compute only. Read entries by name with an API key.");
411
462
  }
412
463
  const result = await this.request(`/v1/vault/${this.scope()}/entries`);
@@ -441,9 +492,24 @@ class ForgeZero {
441
492
  return result.entries;
442
493
  }
443
494
  async set(name, value) {
495
+ if (this.credential.mode === "local") {
496
+ if (!/^[A-Za-z0-9_.:/-]{1,256}$/.test(name) || !value) {
497
+ throw new VaultError("LOCAL_VALUE_INVALID", "Local Vault names and values must be non-empty and bounded.");
498
+ }
499
+ const values = this.localValues();
500
+ values[name] = value;
501
+ this.saveLocalValues(values);
502
+ return 1;
503
+ }
444
504
  throw new VaultError("APPLICATION_READ_ONLY", "Application vault credentials cannot write secrets; use the authenticated platform vault UI.");
445
505
  }
446
506
  async remove(name) {
507
+ if (this.credential.mode === "local") {
508
+ const values = this.localValues();
509
+ delete values[name];
510
+ this.saveLocalValues(values);
511
+ return;
512
+ }
447
513
  throw new VaultError("APPLICATION_READ_ONLY", "Application vault credentials cannot delete secrets; use the authenticated platform vault UI.");
448
514
  }
449
515
  async* watch(options = {}) {
@@ -463,6 +529,7 @@ class ForgeZero {
463
529
  }
464
530
  }
465
531
  var createVault = (options = {}) => new ForgeZero(options);
532
+ var createLocalVault = (options = {}) => new ForgeZero({ ...options, credential: { mode: "local" } });
466
533
  function vaultCredentials(vault) {
467
534
  return {
468
535
  name: "vault",
@@ -505,7 +572,7 @@ function systemdCredentials(options = {}) {
505
572
  }
506
573
  };
507
574
  }
508
- var VERSION = "0.1.24";
575
+ var VERSION = "0.1.26";
509
576
  export {
510
577
  vaultCredentials,
511
578
  systemdCredentials,
@@ -514,6 +581,7 @@ export {
514
581
  discover,
515
582
  directCredentials,
516
583
  createVault,
584
+ createLocalVault,
517
585
  VaultRuntime,
518
586
  VaultError,
519
587
  VERSION,
package/dist/providers.js CHANGED
@@ -22,8 +22,8 @@ class VaultRuntime {
22
22
  this.transport = transport;
23
23
  }
24
24
  available() {
25
- if (this.transport === "managed-read-only") {
26
- throw new VaultRuntimeClientError("MANAGED_RUNTIME_DELEGATION_REQUIRED", "Vault runtime operations require a separately granted external API key; the managed Agent replica is read-only.");
25
+ if (this.transport === "managed-read-only" || this.transport === "local-read-only") {
26
+ throw new VaultRuntimeClientError("MANAGED_RUNTIME_DELEGATION_REQUIRED", this.transport === "local-read-only" ? "Local Vault mode stores development values only; master-seed runtime operations require a separately granted live API key." : "Vault runtime operations require a separately granted external API key; the managed Agent replica is read-only.");
27
27
  }
28
28
  }
29
29
  async create(options) {
@@ -156,7 +156,9 @@ function discover(environment = {}) {
156
156
  const apiKey = env.FORGEZERO_API_KEY;
157
157
  if (apiKey)
158
158
  return { mode: "external", apiKey };
159
- throw new VaultError("NO_CREDENTIAL", `No credential found. On managed compute the agent socket at ${socketPath} provides one; ` + "elsewhere set FORGEZERO_API_KEY.");
159
+ if (env.FORGEZERO_LOCAL_VAULT === "1")
160
+ return { mode: "local" };
161
+ throw new VaultError("NO_CREDENTIAL", `No credential found. On managed compute the agent socket at ${socketPath} provides one; ` + "elsewhere set FORGEZERO_API_KEY, or explicitly set FORGEZERO_LOCAL_VAULT=1 for isolated local development.");
160
162
  }
161
163
  var unbase64url = (encoded) => {
162
164
  const padded = encoded.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - encoded.length % 4) % 4);
@@ -264,6 +266,8 @@ class ForgeZero {
264
266
  requestTimeoutMs;
265
267
  project;
266
268
  environment;
269
+ localStorage;
270
+ localNamespace;
267
271
  constructor(options = {}) {
268
272
  const credential = options.credential ?? discover(options.discovery);
269
273
  this.credential = credential.mode === "managed" && !credential.socketPath ? { ...credential, socketPath: DEFAULT_SOCKET } : credential;
@@ -277,8 +281,13 @@ class ForgeZero {
277
281
  }
278
282
  this.project = options.project ?? "default";
279
283
  this.environment = options.environment ?? "production";
284
+ this.localStorage = options.localStorage ?? (this.credential.mode === "local" && typeof globalThis.localStorage !== "undefined" ? globalThis.localStorage : undefined);
285
+ this.localNamespace = options.localNamespace?.trim() || "fz.local.vault.v1";
286
+ if (this.credential.mode === "local" && !this.localStorage) {
287
+ throw new VaultError("LOCAL_STORAGE_UNAVAILABLE", "Local Vault mode requires browser localStorage or an injected LocalVaultStorage adapter.");
288
+ }
280
289
  const runtimeRequest = options.runtimeRequest ?? ((path, init) => this.request(`/v1/vault/${this.scope()}/runtime${path}`, init));
281
- this.runtime = new VaultRuntime(runtimeRequest, this.credential.mode === "external" ? "external" : options.runtimeRequest ? "managed-delegated" : "managed-read-only");
290
+ this.runtime = new VaultRuntime(runtimeRequest, this.credential.mode === "external" ? "external" : options.runtimeRequest ? "managed-delegated" : this.credential.mode === "local" ? "local-read-only" : "managed-read-only");
282
291
  }
283
292
  scope() {
284
293
  return `${encodeURIComponent(this.project)}/${encodeURIComponent(this.environment)}`;
@@ -286,6 +295,8 @@ class ForgeZero {
286
295
  async request(path, init = {}) {
287
296
  if (this.credential.mode === "managed")
288
297
  return this.managedRequest(path, init);
298
+ if (this.credential.mode === "local")
299
+ return this.localRequest(path, init);
289
300
  const body = typeof init.body === "string" ? init.body : "";
290
301
  const target = new URL(path, this.apiUrl);
291
302
  const headers = {
@@ -326,6 +337,46 @@ class ForgeZero {
326
337
  }
327
338
  return payload;
328
339
  }
340
+ localStorageKey() {
341
+ return `${this.localNamespace}:${encodeURIComponent(this.project)}:${encodeURIComponent(this.environment)}`;
342
+ }
343
+ localValues() {
344
+ const raw = this.localStorage.getItem(this.localStorageKey());
345
+ if (raw === null)
346
+ return {};
347
+ try {
348
+ const parsed = JSON.parse(raw);
349
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Object.entries(parsed).some(([key, value]) => !key || typeof value !== "string"))
350
+ throw new Error("invalid");
351
+ return parsed;
352
+ } catch {
353
+ throw new VaultError("LOCAL_STORE_MALFORMED", "The local Vault store is malformed. Clear it before continuing.");
354
+ }
355
+ }
356
+ saveLocalValues(values) {
357
+ this.localStorage.setItem(this.localStorageKey(), JSON.stringify(values));
358
+ }
359
+ async localRequest(path, init) {
360
+ const method = init.method ?? "GET";
361
+ const target = new URL(path, "https://vault.local.invalid");
362
+ const values = this.localValues();
363
+ const entry = /\/entries\/([^/]+)$/.exec(target.pathname);
364
+ if (method === "GET" && entry) {
365
+ const name = decodeURIComponent(entry[1]);
366
+ if (!(name in values))
367
+ throw new VaultError("NOT_FOUND", `No local Vault value named ${name}.`);
368
+ return { value: values[name] };
369
+ }
370
+ if (method === "GET" && target.pathname.endsWith("/list")) {
371
+ return { entries: Object.keys(values).sort().map((name) => ({ name })) };
372
+ }
373
+ if (method === "GET" && target.pathname.endsWith("/entries"))
374
+ return { values: { ...values } };
375
+ if (method === "GET" && target.pathname.endsWith("/changes")) {
376
+ return { version: 0, changed: [] };
377
+ }
378
+ throw new VaultError("LOCAL_OPERATION_UNSUPPORTED", "This operation is unavailable in isolated local Vault mode.");
379
+ }
329
380
  async managedRequest(path, init) {
330
381
  const socketPath = this.credential.socketPath;
331
382
  const method = init.method ?? "GET";
@@ -406,7 +457,7 @@ class ForgeZero {
406
457
  return result.value;
407
458
  }
408
459
  async getAll() {
409
- if (this.credential.mode !== "managed") {
460
+ if (this.credential.mode !== "managed" && this.credential.mode !== "local") {
410
461
  throw new VaultError("MANAGED_ONLY", "getAll is available on managed compute only. Read entries by name with an API key.");
411
462
  }
412
463
  const result = await this.request(`/v1/vault/${this.scope()}/entries`);
@@ -441,9 +492,24 @@ class ForgeZero {
441
492
  return result.entries;
442
493
  }
443
494
  async set(name, value) {
495
+ if (this.credential.mode === "local") {
496
+ if (!/^[A-Za-z0-9_.:/-]{1,256}$/.test(name) || !value) {
497
+ throw new VaultError("LOCAL_VALUE_INVALID", "Local Vault names and values must be non-empty and bounded.");
498
+ }
499
+ const values = this.localValues();
500
+ values[name] = value;
501
+ this.saveLocalValues(values);
502
+ return 1;
503
+ }
444
504
  throw new VaultError("APPLICATION_READ_ONLY", "Application vault credentials cannot write secrets; use the authenticated platform vault UI.");
445
505
  }
446
506
  async remove(name) {
507
+ if (this.credential.mode === "local") {
508
+ const values = this.localValues();
509
+ delete values[name];
510
+ this.saveLocalValues(values);
511
+ return;
512
+ }
447
513
  throw new VaultError("APPLICATION_READ_ONLY", "Application vault credentials cannot delete secrets; use the authenticated platform vault UI.");
448
514
  }
449
515
  async* watch(options = {}) {
@@ -463,6 +529,7 @@ class ForgeZero {
463
529
  }
464
530
  }
465
531
  var createVault = (options = {}) => new ForgeZero(options);
532
+ var createLocalVault = (options = {}) => new ForgeZero({ ...options, credential: { mode: "local" } });
466
533
  function vaultCredentials(vault) {
467
534
  return {
468
535
  name: "vault",
@@ -505,7 +572,7 @@ function systemdCredentials(options = {}) {
505
572
  }
506
573
  };
507
574
  }
508
- var VERSION = "0.1.24";
575
+ var VERSION = "0.1.26";
509
576
 
510
577
  // src/providers.ts
511
578
  var MISSING = new Set(["ENTRY_NOT_FOUND", "VERSION_NOT_FOUND"]);
package/dist/runtime.d.ts CHANGED
@@ -48,7 +48,7 @@ export declare class VaultRuntimeClientError extends Error {
48
48
  * explicit prevents membership of the local Vault-reader group from silently
49
49
  * granting signing, decryption or certificate authority.
50
50
  */
51
- export type VaultRuntimeTransport = 'external' | 'managed-read-only' | 'managed-delegated';
51
+ export type VaultRuntimeTransport = 'external' | 'managed-read-only' | 'managed-delegated' | 'local-read-only';
52
52
  /**
53
53
  * Explicit non-exporting cryptographic capabilities.
54
54
  *
package/dist/runtime.js CHANGED
@@ -22,8 +22,8 @@ class VaultRuntime {
22
22
  this.transport = transport;
23
23
  }
24
24
  available() {
25
- if (this.transport === "managed-read-only") {
26
- throw new VaultRuntimeClientError("MANAGED_RUNTIME_DELEGATION_REQUIRED", "Vault runtime operations require a separately granted external API key; the managed Agent replica is read-only.");
25
+ if (this.transport === "managed-read-only" || this.transport === "local-read-only") {
26
+ throw new VaultRuntimeClientError("MANAGED_RUNTIME_DELEGATION_REQUIRED", this.transport === "local-read-only" ? "Local Vault mode stores development values only; master-seed runtime operations require a separately granted live API key." : "Vault runtime operations require a separately granted external API key; the managed Agent replica is read-only.");
27
27
  }
28
28
  }
29
29
  async create(options) {
package/dist/schema.js CHANGED
@@ -22,8 +22,8 @@ class VaultRuntime {
22
22
  this.transport = transport;
23
23
  }
24
24
  available() {
25
- if (this.transport === "managed-read-only") {
26
- throw new VaultRuntimeClientError("MANAGED_RUNTIME_DELEGATION_REQUIRED", "Vault runtime operations require a separately granted external API key; the managed Agent replica is read-only.");
25
+ if (this.transport === "managed-read-only" || this.transport === "local-read-only") {
26
+ throw new VaultRuntimeClientError("MANAGED_RUNTIME_DELEGATION_REQUIRED", this.transport === "local-read-only" ? "Local Vault mode stores development values only; master-seed runtime operations require a separately granted live API key." : "Vault runtime operations require a separately granted external API key; the managed Agent replica is read-only.");
27
27
  }
28
28
  }
29
29
  async create(options) {
@@ -156,7 +156,9 @@ function discover(environment = {}) {
156
156
  const apiKey = env.FORGEZERO_API_KEY;
157
157
  if (apiKey)
158
158
  return { mode: "external", apiKey };
159
- throw new VaultError("NO_CREDENTIAL", `No credential found. On managed compute the agent socket at ${socketPath} provides one; ` + "elsewhere set FORGEZERO_API_KEY.");
159
+ if (env.FORGEZERO_LOCAL_VAULT === "1")
160
+ return { mode: "local" };
161
+ throw new VaultError("NO_CREDENTIAL", `No credential found. On managed compute the agent socket at ${socketPath} provides one; ` + "elsewhere set FORGEZERO_API_KEY, or explicitly set FORGEZERO_LOCAL_VAULT=1 for isolated local development.");
160
162
  }
161
163
  var unbase64url = (encoded) => {
162
164
  const padded = encoded.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - encoded.length % 4) % 4);
@@ -264,6 +266,8 @@ class ForgeZero {
264
266
  requestTimeoutMs;
265
267
  project;
266
268
  environment;
269
+ localStorage;
270
+ localNamespace;
267
271
  constructor(options = {}) {
268
272
  const credential = options.credential ?? discover(options.discovery);
269
273
  this.credential = credential.mode === "managed" && !credential.socketPath ? { ...credential, socketPath: DEFAULT_SOCKET } : credential;
@@ -277,8 +281,13 @@ class ForgeZero {
277
281
  }
278
282
  this.project = options.project ?? "default";
279
283
  this.environment = options.environment ?? "production";
284
+ this.localStorage = options.localStorage ?? (this.credential.mode === "local" && typeof globalThis.localStorage !== "undefined" ? globalThis.localStorage : undefined);
285
+ this.localNamespace = options.localNamespace?.trim() || "fz.local.vault.v1";
286
+ if (this.credential.mode === "local" && !this.localStorage) {
287
+ throw new VaultError("LOCAL_STORAGE_UNAVAILABLE", "Local Vault mode requires browser localStorage or an injected LocalVaultStorage adapter.");
288
+ }
280
289
  const runtimeRequest = options.runtimeRequest ?? ((path, init) => this.request(`/v1/vault/${this.scope()}/runtime${path}`, init));
281
- this.runtime = new VaultRuntime(runtimeRequest, this.credential.mode === "external" ? "external" : options.runtimeRequest ? "managed-delegated" : "managed-read-only");
290
+ this.runtime = new VaultRuntime(runtimeRequest, this.credential.mode === "external" ? "external" : options.runtimeRequest ? "managed-delegated" : this.credential.mode === "local" ? "local-read-only" : "managed-read-only");
282
291
  }
283
292
  scope() {
284
293
  return `${encodeURIComponent(this.project)}/${encodeURIComponent(this.environment)}`;
@@ -286,6 +295,8 @@ class ForgeZero {
286
295
  async request(path, init = {}) {
287
296
  if (this.credential.mode === "managed")
288
297
  return this.managedRequest(path, init);
298
+ if (this.credential.mode === "local")
299
+ return this.localRequest(path, init);
289
300
  const body = typeof init.body === "string" ? init.body : "";
290
301
  const target = new URL(path, this.apiUrl);
291
302
  const headers = {
@@ -326,6 +337,46 @@ class ForgeZero {
326
337
  }
327
338
  return payload;
328
339
  }
340
+ localStorageKey() {
341
+ return `${this.localNamespace}:${encodeURIComponent(this.project)}:${encodeURIComponent(this.environment)}`;
342
+ }
343
+ localValues() {
344
+ const raw = this.localStorage.getItem(this.localStorageKey());
345
+ if (raw === null)
346
+ return {};
347
+ try {
348
+ const parsed = JSON.parse(raw);
349
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Object.entries(parsed).some(([key, value]) => !key || typeof value !== "string"))
350
+ throw new Error("invalid");
351
+ return parsed;
352
+ } catch {
353
+ throw new VaultError("LOCAL_STORE_MALFORMED", "The local Vault store is malformed. Clear it before continuing.");
354
+ }
355
+ }
356
+ saveLocalValues(values) {
357
+ this.localStorage.setItem(this.localStorageKey(), JSON.stringify(values));
358
+ }
359
+ async localRequest(path, init) {
360
+ const method = init.method ?? "GET";
361
+ const target = new URL(path, "https://vault.local.invalid");
362
+ const values = this.localValues();
363
+ const entry = /\/entries\/([^/]+)$/.exec(target.pathname);
364
+ if (method === "GET" && entry) {
365
+ const name = decodeURIComponent(entry[1]);
366
+ if (!(name in values))
367
+ throw new VaultError("NOT_FOUND", `No local Vault value named ${name}.`);
368
+ return { value: values[name] };
369
+ }
370
+ if (method === "GET" && target.pathname.endsWith("/list")) {
371
+ return { entries: Object.keys(values).sort().map((name) => ({ name })) };
372
+ }
373
+ if (method === "GET" && target.pathname.endsWith("/entries"))
374
+ return { values: { ...values } };
375
+ if (method === "GET" && target.pathname.endsWith("/changes")) {
376
+ return { version: 0, changed: [] };
377
+ }
378
+ throw new VaultError("LOCAL_OPERATION_UNSUPPORTED", "This operation is unavailable in isolated local Vault mode.");
379
+ }
329
380
  async managedRequest(path, init) {
330
381
  const socketPath = this.credential.socketPath;
331
382
  const method = init.method ?? "GET";
@@ -406,7 +457,7 @@ class ForgeZero {
406
457
  return result.value;
407
458
  }
408
459
  async getAll() {
409
- if (this.credential.mode !== "managed") {
460
+ if (this.credential.mode !== "managed" && this.credential.mode !== "local") {
410
461
  throw new VaultError("MANAGED_ONLY", "getAll is available on managed compute only. Read entries by name with an API key.");
411
462
  }
412
463
  const result = await this.request(`/v1/vault/${this.scope()}/entries`);
@@ -441,9 +492,24 @@ class ForgeZero {
441
492
  return result.entries;
442
493
  }
443
494
  async set(name, value) {
495
+ if (this.credential.mode === "local") {
496
+ if (!/^[A-Za-z0-9_.:/-]{1,256}$/.test(name) || !value) {
497
+ throw new VaultError("LOCAL_VALUE_INVALID", "Local Vault names and values must be non-empty and bounded.");
498
+ }
499
+ const values = this.localValues();
500
+ values[name] = value;
501
+ this.saveLocalValues(values);
502
+ return 1;
503
+ }
444
504
  throw new VaultError("APPLICATION_READ_ONLY", "Application vault credentials cannot write secrets; use the authenticated platform vault UI.");
445
505
  }
446
506
  async remove(name) {
507
+ if (this.credential.mode === "local") {
508
+ const values = this.localValues();
509
+ delete values[name];
510
+ this.saveLocalValues(values);
511
+ return;
512
+ }
447
513
  throw new VaultError("APPLICATION_READ_ONLY", "Application vault credentials cannot delete secrets; use the authenticated platform vault UI.");
448
514
  }
449
515
  async* watch(options = {}) {
@@ -463,6 +529,7 @@ class ForgeZero {
463
529
  }
464
530
  }
465
531
  var createVault = (options = {}) => new ForgeZero(options);
532
+ var createLocalVault = (options = {}) => new ForgeZero({ ...options, credential: { mode: "local" } });
466
533
  function vaultCredentials(vault) {
467
534
  return {
468
535
  name: "vault",
@@ -505,7 +572,7 @@ function systemdCredentials(options = {}) {
505
572
  }
506
573
  };
507
574
  }
508
- var VERSION = "0.1.24";
575
+ var VERSION = "0.1.26";
509
576
 
510
577
  // src/schema.ts
511
578
  function managedSchemas(vault, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/vault",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -46,7 +46,7 @@
46
46
  "@types/bun": "latest"
47
47
  },
48
48
  "dependencies": {
49
- "@forgezero/runtime": "^0.1.18"
49
+ "@forgezero/runtime": "^0.1.20"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@noble/ciphers": "^2.2.0",