@forgezero/vault 0.1.4 → 0.1.6

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
@@ -37,10 +37,14 @@ their next fetch, and nothing is redeployed.
37
37
 
38
38
  ## Two application transports, and the socket wins
39
39
 
40
- On a ForgeZero compute the client reads the environment's in-memory replica over
41
- a Unix socket held by the local agent. Anywhere else it derives a hybrid signer
42
- from an API key and reads over HTTPS. When both are present the socket wins,
43
- because the application then holds no reusable remote credential.
40
+ On a ForgeZero compute the Agent keeps the enrolled project's environments in
41
+ RAM and the client selects one environment over its group-scoped Unix socket.
42
+ Anywhere else it derives the same hybrid request identity from an API-key seed
43
+ and reads over HTTPS. Each remote request also carries a signed one-use hybrid
44
+ ML-KEM-768 + X25519 response key; the API seals the JSON value to that key with
45
+ AES-256-GCM, so a TLS-terminating edge can route the call but cannot read its
46
+ secret response. When both are present the socket wins, because the
47
+ application then holds no reusable remote credential.
44
48
 
45
49
  ## The API key is a seed, not a password
46
50
 
package/dist/index.d.ts CHANGED
@@ -66,6 +66,7 @@ export interface SignRequest {
66
66
  path: string;
67
67
  query: string;
68
68
  body: string;
69
+ responseKey?: string;
69
70
  }
70
71
  /** Hybrid-signs one exact HTTP request. */
71
72
  export interface Signer {
@@ -76,9 +77,13 @@ export interface Signer {
76
77
  export declare function signerFromApiKey(secret: string): Signer;
77
78
  export type AgentRequest = {
78
79
  op: 'get';
80
+ project: string;
81
+ environment: string;
79
82
  name: string;
80
83
  } | {
81
84
  op: 'held';
85
+ project: string;
86
+ environment: string;
82
87
  } | {
83
88
  op: 'sync';
84
89
  };
@@ -127,6 +132,8 @@ export interface EntryMeta {
127
132
  export interface Change {
128
133
  name: string;
129
134
  version: number;
135
+ /** True when ciphertext was destroyed and the local value must be removed. */
136
+ deleted?: boolean;
130
137
  }
131
138
  export declare const DEFAULT_API_URL = "https://api.forgezero.net";
132
139
  export declare const DEFAULT_REQUEST_TIMEOUT_MS = 15000;
@@ -267,4 +274,4 @@ export declare function systemdCredentials(options?: SystemdCredentialOptions):
267
274
  readonly name: 'systemd';
268
275
  get(reference: string, field: string): Promise<string>;
269
276
  };
270
- export declare const VERSION = "0.1.4";
277
+ export declare const VERSION = "0.1.6";
package/dist/index.js CHANGED
@@ -1,5 +1,12 @@
1
1
  // src/index.ts
2
- import { deriveKeysFromSeed, signRequest } from "@forgezero/runtime/identity";
2
+ import {
3
+ deriveKeysFromSeed,
4
+ encodeSignatureHeader,
5
+ generateResponseRecipient,
6
+ openResponse,
7
+ RESPONSE_KEY_HEADER,
8
+ signRequest
9
+ } from "@forgezero/runtime/identity";
3
10
 
4
11
  class VaultError extends Error {
5
12
  code;
@@ -41,13 +48,6 @@ var unbase64url = (encoded) => {
41
48
  }
42
49
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
43
50
  };
44
- var base64url = (value) => {
45
- const bytes = new TextEncoder().encode(value);
46
- let binary = "";
47
- for (const byte of bytes)
48
- binary += String.fromCharCode(byte);
49
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
50
- };
51
51
  function signerFromApiKey(secret) {
52
52
  const parts = secret.split(".");
53
53
  const legacy = parts.length === 3 && parts[0] === "fz";
@@ -67,13 +67,7 @@ function signerFromApiKey(secret) {
67
67
  return {
68
68
  keyId,
69
69
  async sign(request) {
70
- const envelope = signRequest(keys, keyId, request);
71
- return base64url(JSON.stringify({
72
- timestamp: envelope.timestamp,
73
- nonce: envelope.nonce,
74
- edSignature: envelope.edSignature,
75
- mlDsaSignature: envelope.mlDsaSignature
76
- }));
70
+ return encodeSignatureHeader(signRequest(keys, keyId, request));
77
71
  }
78
72
  };
79
73
  }
@@ -160,14 +154,19 @@ class ForgeZero {
160
154
  "content-type": "application/json",
161
155
  ...init.headers
162
156
  };
157
+ const recipient = this.signer ? generateResponseRecipient() : undefined;
158
+ let signature;
163
159
  if (this.signer) {
164
160
  headers["x-fz-key"] = this.signer.keyId;
165
- headers["x-fz-signature"] = await this.signer.sign({
161
+ signature = await this.signer.sign({
166
162
  method: init.method ?? "GET",
167
163
  path: target.pathname,
168
164
  query: target.search.replace(/^\?/, ""),
169
- body
165
+ body,
166
+ responseKey: recipient.publicKey
170
167
  });
168
+ headers["x-fz-signature"] = signature;
169
+ headers[RESPONSE_KEY_HEADER] = recipient.publicKey;
171
170
  }
172
171
  const response = await this.doFetch(target, {
173
172
  ...init,
@@ -180,6 +179,13 @@ class ForgeZero {
180
179
  if (!response.ok) {
181
180
  throw new VaultError(payload?.error?.code ?? "REQUEST_FAILED", payload?.error?.message ?? `The vault refused with ${response.status}.`);
182
181
  }
182
+ if (recipient && signature) {
183
+ try {
184
+ return await openResponse(recipient.secretKey, signature, payload);
185
+ } catch {
186
+ throw new VaultError("RESPONSE_NOT_SEALED", "The vault response was not sealed to this request.");
187
+ }
188
+ }
183
189
  return payload;
184
190
  }
185
191
  async managedRequest(path, init) {
@@ -191,6 +197,8 @@ class ForgeZero {
191
197
  if (method === "GET" && entry) {
192
198
  response = await this.agentTransport(socketPath, {
193
199
  op: "get",
200
+ project: this.project,
201
+ environment: this.environment,
194
202
  name: decodeURIComponent(entry[1])
195
203
  });
196
204
  if (!response.ok)
@@ -200,7 +208,11 @@ class ForgeZero {
200
208
  return { value: response.value };
201
209
  }
202
210
  if (method === "GET" && target.pathname.endsWith("/list")) {
203
- response = await this.agentTransport(socketPath, { op: "held" });
211
+ response = await this.agentTransport(socketPath, {
212
+ op: "held",
213
+ project: this.project,
214
+ environment: this.environment
215
+ });
204
216
  if (!response.ok)
205
217
  throw new VaultError(response.error.code, response.error.message);
206
218
  if (response.op !== "held")
@@ -208,14 +220,23 @@ class ForgeZero {
208
220
  return { entries: response.names.map((name) => ({ name })) };
209
221
  }
210
222
  if (method === "GET" && target.pathname.endsWith("/entries")) {
211
- response = await this.agentTransport(socketPath, { op: "held" });
223
+ response = await this.agentTransport(socketPath, {
224
+ op: "held",
225
+ project: this.project,
226
+ environment: this.environment
227
+ });
212
228
  if (!response.ok)
213
229
  throw new VaultError(response.error.code, response.error.message);
214
230
  if (response.op !== "held")
215
231
  throw new VaultError("AGENT_RESPONSE_MALFORMED", "The agent answered with the wrong operation.");
216
232
  const values = {};
217
233
  for (const name of response.names) {
218
- const read = await this.agentTransport(socketPath, { op: "get", name });
234
+ const read = await this.agentTransport(socketPath, {
235
+ op: "get",
236
+ project: this.project,
237
+ environment: this.environment,
238
+ name
239
+ });
219
240
  if (!read.ok)
220
241
  throw new VaultError(read.error.code, read.error.message);
221
242
  if (read.op !== "get")
@@ -346,7 +367,7 @@ function systemdCredentials(options = {}) {
346
367
  }
347
368
  };
348
369
  }
349
- var VERSION = "0.1.4";
370
+ var VERSION = "0.1.6";
350
371
  export {
351
372
  vaultCredentials,
352
373
  systemdCredentials,
package/dist/providers.js CHANGED
@@ -1,5 +1,12 @@
1
1
  // src/index.ts
2
- import { deriveKeysFromSeed, signRequest } from "@forgezero/runtime/identity";
2
+ import {
3
+ deriveKeysFromSeed,
4
+ encodeSignatureHeader,
5
+ generateResponseRecipient,
6
+ openResponse,
7
+ RESPONSE_KEY_HEADER,
8
+ signRequest
9
+ } from "@forgezero/runtime/identity";
3
10
 
4
11
  class VaultError extends Error {
5
12
  code;
@@ -41,13 +48,6 @@ var unbase64url = (encoded) => {
41
48
  }
42
49
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
43
50
  };
44
- var base64url = (value) => {
45
- const bytes = new TextEncoder().encode(value);
46
- let binary = "";
47
- for (const byte of bytes)
48
- binary += String.fromCharCode(byte);
49
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
50
- };
51
51
  function signerFromApiKey(secret) {
52
52
  const parts = secret.split(".");
53
53
  const legacy = parts.length === 3 && parts[0] === "fz";
@@ -67,13 +67,7 @@ function signerFromApiKey(secret) {
67
67
  return {
68
68
  keyId,
69
69
  async sign(request) {
70
- const envelope = signRequest(keys, keyId, request);
71
- return base64url(JSON.stringify({
72
- timestamp: envelope.timestamp,
73
- nonce: envelope.nonce,
74
- edSignature: envelope.edSignature,
75
- mlDsaSignature: envelope.mlDsaSignature
76
- }));
70
+ return encodeSignatureHeader(signRequest(keys, keyId, request));
77
71
  }
78
72
  };
79
73
  }
@@ -160,14 +154,19 @@ class ForgeZero {
160
154
  "content-type": "application/json",
161
155
  ...init.headers
162
156
  };
157
+ const recipient = this.signer ? generateResponseRecipient() : undefined;
158
+ let signature;
163
159
  if (this.signer) {
164
160
  headers["x-fz-key"] = this.signer.keyId;
165
- headers["x-fz-signature"] = await this.signer.sign({
161
+ signature = await this.signer.sign({
166
162
  method: init.method ?? "GET",
167
163
  path: target.pathname,
168
164
  query: target.search.replace(/^\?/, ""),
169
- body
165
+ body,
166
+ responseKey: recipient.publicKey
170
167
  });
168
+ headers["x-fz-signature"] = signature;
169
+ headers[RESPONSE_KEY_HEADER] = recipient.publicKey;
171
170
  }
172
171
  const response = await this.doFetch(target, {
173
172
  ...init,
@@ -180,6 +179,13 @@ class ForgeZero {
180
179
  if (!response.ok) {
181
180
  throw new VaultError(payload?.error?.code ?? "REQUEST_FAILED", payload?.error?.message ?? `The vault refused with ${response.status}.`);
182
181
  }
182
+ if (recipient && signature) {
183
+ try {
184
+ return await openResponse(recipient.secretKey, signature, payload);
185
+ } catch {
186
+ throw new VaultError("RESPONSE_NOT_SEALED", "The vault response was not sealed to this request.");
187
+ }
188
+ }
183
189
  return payload;
184
190
  }
185
191
  async managedRequest(path, init) {
@@ -191,6 +197,8 @@ class ForgeZero {
191
197
  if (method === "GET" && entry) {
192
198
  response = await this.agentTransport(socketPath, {
193
199
  op: "get",
200
+ project: this.project,
201
+ environment: this.environment,
194
202
  name: decodeURIComponent(entry[1])
195
203
  });
196
204
  if (!response.ok)
@@ -200,7 +208,11 @@ class ForgeZero {
200
208
  return { value: response.value };
201
209
  }
202
210
  if (method === "GET" && target.pathname.endsWith("/list")) {
203
- response = await this.agentTransport(socketPath, { op: "held" });
211
+ response = await this.agentTransport(socketPath, {
212
+ op: "held",
213
+ project: this.project,
214
+ environment: this.environment
215
+ });
204
216
  if (!response.ok)
205
217
  throw new VaultError(response.error.code, response.error.message);
206
218
  if (response.op !== "held")
@@ -208,14 +220,23 @@ class ForgeZero {
208
220
  return { entries: response.names.map((name) => ({ name })) };
209
221
  }
210
222
  if (method === "GET" && target.pathname.endsWith("/entries")) {
211
- response = await this.agentTransport(socketPath, { op: "held" });
223
+ response = await this.agentTransport(socketPath, {
224
+ op: "held",
225
+ project: this.project,
226
+ environment: this.environment
227
+ });
212
228
  if (!response.ok)
213
229
  throw new VaultError(response.error.code, response.error.message);
214
230
  if (response.op !== "held")
215
231
  throw new VaultError("AGENT_RESPONSE_MALFORMED", "The agent answered with the wrong operation.");
216
232
  const values = {};
217
233
  for (const name of response.names) {
218
- const read = await this.agentTransport(socketPath, { op: "get", name });
234
+ const read = await this.agentTransport(socketPath, {
235
+ op: "get",
236
+ project: this.project,
237
+ environment: this.environment,
238
+ name
239
+ });
219
240
  if (!read.ok)
220
241
  throw new VaultError(read.error.code, read.error.message);
221
242
  if (read.op !== "get")
@@ -346,7 +367,7 @@ function systemdCredentials(options = {}) {
346
367
  }
347
368
  };
348
369
  }
349
- var VERSION = "0.1.4";
370
+ var VERSION = "0.1.6";
350
371
 
351
372
  // src/providers.ts
352
373
  var MISSING = new Set(["ENTRY_NOT_FOUND", "VERSION_NOT_FOUND"]);
package/dist/schema.js CHANGED
@@ -1,5 +1,12 @@
1
1
  // src/index.ts
2
- import { deriveKeysFromSeed, signRequest } from "@forgezero/runtime/identity";
2
+ import {
3
+ deriveKeysFromSeed,
4
+ encodeSignatureHeader,
5
+ generateResponseRecipient,
6
+ openResponse,
7
+ RESPONSE_KEY_HEADER,
8
+ signRequest
9
+ } from "@forgezero/runtime/identity";
3
10
 
4
11
  class VaultError extends Error {
5
12
  code;
@@ -41,13 +48,6 @@ var unbase64url = (encoded) => {
41
48
  }
42
49
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
43
50
  };
44
- var base64url = (value) => {
45
- const bytes = new TextEncoder().encode(value);
46
- let binary = "";
47
- for (const byte of bytes)
48
- binary += String.fromCharCode(byte);
49
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
50
- };
51
51
  function signerFromApiKey(secret) {
52
52
  const parts = secret.split(".");
53
53
  const legacy = parts.length === 3 && parts[0] === "fz";
@@ -67,13 +67,7 @@ function signerFromApiKey(secret) {
67
67
  return {
68
68
  keyId,
69
69
  async sign(request) {
70
- const envelope = signRequest(keys, keyId, request);
71
- return base64url(JSON.stringify({
72
- timestamp: envelope.timestamp,
73
- nonce: envelope.nonce,
74
- edSignature: envelope.edSignature,
75
- mlDsaSignature: envelope.mlDsaSignature
76
- }));
70
+ return encodeSignatureHeader(signRequest(keys, keyId, request));
77
71
  }
78
72
  };
79
73
  }
@@ -160,14 +154,19 @@ class ForgeZero {
160
154
  "content-type": "application/json",
161
155
  ...init.headers
162
156
  };
157
+ const recipient = this.signer ? generateResponseRecipient() : undefined;
158
+ let signature;
163
159
  if (this.signer) {
164
160
  headers["x-fz-key"] = this.signer.keyId;
165
- headers["x-fz-signature"] = await this.signer.sign({
161
+ signature = await this.signer.sign({
166
162
  method: init.method ?? "GET",
167
163
  path: target.pathname,
168
164
  query: target.search.replace(/^\?/, ""),
169
- body
165
+ body,
166
+ responseKey: recipient.publicKey
170
167
  });
168
+ headers["x-fz-signature"] = signature;
169
+ headers[RESPONSE_KEY_HEADER] = recipient.publicKey;
171
170
  }
172
171
  const response = await this.doFetch(target, {
173
172
  ...init,
@@ -180,6 +179,13 @@ class ForgeZero {
180
179
  if (!response.ok) {
181
180
  throw new VaultError(payload?.error?.code ?? "REQUEST_FAILED", payload?.error?.message ?? `The vault refused with ${response.status}.`);
182
181
  }
182
+ if (recipient && signature) {
183
+ try {
184
+ return await openResponse(recipient.secretKey, signature, payload);
185
+ } catch {
186
+ throw new VaultError("RESPONSE_NOT_SEALED", "The vault response was not sealed to this request.");
187
+ }
188
+ }
183
189
  return payload;
184
190
  }
185
191
  async managedRequest(path, init) {
@@ -191,6 +197,8 @@ class ForgeZero {
191
197
  if (method === "GET" && entry) {
192
198
  response = await this.agentTransport(socketPath, {
193
199
  op: "get",
200
+ project: this.project,
201
+ environment: this.environment,
194
202
  name: decodeURIComponent(entry[1])
195
203
  });
196
204
  if (!response.ok)
@@ -200,7 +208,11 @@ class ForgeZero {
200
208
  return { value: response.value };
201
209
  }
202
210
  if (method === "GET" && target.pathname.endsWith("/list")) {
203
- response = await this.agentTransport(socketPath, { op: "held" });
211
+ response = await this.agentTransport(socketPath, {
212
+ op: "held",
213
+ project: this.project,
214
+ environment: this.environment
215
+ });
204
216
  if (!response.ok)
205
217
  throw new VaultError(response.error.code, response.error.message);
206
218
  if (response.op !== "held")
@@ -208,14 +220,23 @@ class ForgeZero {
208
220
  return { entries: response.names.map((name) => ({ name })) };
209
221
  }
210
222
  if (method === "GET" && target.pathname.endsWith("/entries")) {
211
- response = await this.agentTransport(socketPath, { op: "held" });
223
+ response = await this.agentTransport(socketPath, {
224
+ op: "held",
225
+ project: this.project,
226
+ environment: this.environment
227
+ });
212
228
  if (!response.ok)
213
229
  throw new VaultError(response.error.code, response.error.message);
214
230
  if (response.op !== "held")
215
231
  throw new VaultError("AGENT_RESPONSE_MALFORMED", "The agent answered with the wrong operation.");
216
232
  const values = {};
217
233
  for (const name of response.names) {
218
- const read = await this.agentTransport(socketPath, { op: "get", name });
234
+ const read = await this.agentTransport(socketPath, {
235
+ op: "get",
236
+ project: this.project,
237
+ environment: this.environment,
238
+ name
239
+ });
219
240
  if (!read.ok)
220
241
  throw new VaultError(read.error.code, read.error.message);
221
242
  if (read.op !== "get")
@@ -346,7 +367,7 @@ function systemdCredentials(options = {}) {
346
367
  }
347
368
  };
348
369
  }
349
- var VERSION = "0.1.4";
370
+ var VERSION = "0.1.6";
350
371
 
351
372
  // src/schema.ts
352
373
  function managedSchemas(vault, options = {}) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "Publishing happens from an operator's machine, not CI \u2014 CLAUDE.md records that the absence of CI is deliberate. npm's `provenance` attests a tarball was built by a recognised CI provider from a named commit, so it cannot be produced here: it was set, and the first publish failed with `Automatic provenance generation not supported for provider: null`. A setting that can never be satisfied is worse than none, because it reads as a guarantee nobody is getting. Restore it the day this publishes from CI, and not before.",
3
3
  "name": "@forgezero/vault",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -32,8 +32,9 @@
32
32
  "default": "./dist/providers.js"
33
33
  }
34
34
  },
35
- "scripts": {
36
- "check": "tsc --noEmit",
35
+ "scripts": {
36
+ "check": "tsc --noEmit",
37
+ "prebuild": "rm -rf dist",
37
38
  "build": "bun build src/index.ts src/config.ts src/schema.ts src/env.ts src/frameworks.ts src/providers.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
38
39
  "prepublishOnly": "bun run check && bun run build"
39
40
  },
@@ -42,7 +43,7 @@
42
43
  "@types/bun": "latest"
43
44
  },
44
45
  "dependencies": {
45
- "@forgezero/runtime": "^0.1.2"
46
+ "@forgezero/runtime": "^0.1.3"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "@noble/curves": "^2.2.0",