@forgezero/vault 0.1.8 → 0.1.10
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 +166 -56
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/providers.d.ts +22 -12
- package/dist/providers.js +24 -13
- package/dist/schema.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,90 +1,200 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED FILE — do not edit.
|
|
3
|
+
|
|
4
|
+
Change scripts/generate-guides.ts or its typed sources, run `bun run guides`,
|
|
5
|
+
and commit the generator and rendered files together.
|
|
6
|
+
-->
|
|
7
|
+
|
|
1
8
|
# @forgezero/vault
|
|
2
9
|
|
|
3
|
-
|
|
10
|
+
One scoped contract over platform-direct, managed-agent and external API-key trust paths.
|
|
11
|
+
|
|
12
|
+
## Global package root and supported runtimes
|
|
13
|
+
|
|
14
|
+
The scoped vault facade. A tenant application uses its local agent or an API key; trusted platform code injects a direct realm backend instead of calling itself. The one package that ends up in somebody else's production dependency tree, which is why it stays alone and stays small. Supported runtimes: bun, node, workers, deno. The global base/root import is @forgezero/vault. Every public import or command is listed below; the documentation inventory is checked in both directions against package.json exports.
|
|
4
15
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
```text
|
|
17
|
+
import * as root from '@forgezero/vault';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
packages do not.
|
|
22
|
+
bun add @forgezero/vault — Install the scoped Vault client and its public subpaths.
|
|
13
23
|
|
|
14
|
-
```
|
|
24
|
+
```text
|
|
15
25
|
bun add @forgezero/vault
|
|
16
26
|
```
|
|
17
27
|
|
|
18
|
-
|
|
28
|
+
## @forgezero/vault
|
|
29
|
+
|
|
30
|
+
One scoped contract over an explicitly injected platform backend, a managed tenant socket or an automatically derived external API-key signer.
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
import * as api from '@forgezero/vault';
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## @forgezero/vault/schema
|
|
37
|
+
|
|
38
|
+
Where an entry’s shape comes from: pulled from the platform when ForgeZero has to render it, declared locally when only the tenant needs it.
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
import * as api from '@forgezero/vault/schema';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## @forgezero/vault/config
|
|
45
|
+
|
|
46
|
+
Read `.fz/config.json` — which project, which environment, which secrets, and what to call them.
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
import * as api from '@forgezero/vault/config';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## @forgezero/vault/env
|
|
53
|
+
|
|
54
|
+
Envless: fill `process.env` from the vault at boot, refusing to run during a build.
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
import * as api from '@forgezero/vault/env';
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## @forgezero/vault/frameworks
|
|
61
|
+
|
|
62
|
+
SvelteKit and Next.js wiring, attached at the one place that runs once, on the server, before any request.
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
import * as api from '@forgezero/vault/frameworks';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## @forgezero/vault/providers
|
|
69
|
+
|
|
70
|
+
Adapters that make Vault a provider-configuration and credential source without coupling the providers package to ForgeZero.
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
import * as api from '@forgezero/vault/providers';
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Read a scoped secret
|
|
77
|
+
|
|
78
|
+
The same client discovers the managed Agent socket or uses an external API-key signer. Project and environment remain explicit.
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
import { createVault } from '@forgezero/vault';
|
|
82
|
+
|
|
83
|
+
const vault = createVault({ project: 'payments', environment: 'production' });
|
|
84
|
+
const token = await vault.get('STRIPE_KEY');
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Three paths, and why they stay explicit
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
PLATFORM API resident realm seed -> direct scoped envelope
|
|
93
|
+
MANAGED TENANT local unix socket -> agent RAM scope
|
|
94
|
+
EXTERNAL TENANT scoped API-key seed -> signed HTTPS
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Current implementation boundary
|
|
98
|
+
|
|
99
|
+
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.
|
|
100
|
+
|
|
101
|
+
## 1. Install
|
|
102
|
+
|
|
103
|
+
One ForgeZero identity dependency plus its required audited Noble crypto peers. The HTTPS path works anywhere fetch and Web Crypto exist; managed socket discovery requires a Bun or Node server runtime.
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
bun add @forgezero/vault
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 2. Read a secret
|
|
110
|
+
|
|
111
|
+
The project and environment are explicit application scope. Managed compute uses the local Agent socket; external runtimes use the stable API origin and their scoped key.
|
|
112
|
+
|
|
113
|
+
```text
|
|
19
114
|
import { ForgeZero } from '@forgezero/vault';
|
|
20
115
|
|
|
21
116
|
const fz = new ForgeZero({ project: 'altpilot', environment: 'production' });
|
|
22
117
|
|
|
23
118
|
const key = await fz.get('STRIPE_KEY');
|
|
24
|
-
const names = await fz.list();
|
|
119
|
+
const names = await fz.list(); // names; HTTPS reads also carry metadata
|
|
25
120
|
```
|
|
26
121
|
|
|
27
|
-
##
|
|
122
|
+
## 3. Ship it unchanged — target discovery
|
|
28
123
|
|
|
29
|
-
|
|
124
|
+
On managed tenant compute the Agent holds the node identity and the complete project RAM replica; the application holds no reusable remote credential and selects its environment in each client instance. The client finds the group-scoped socket instead of the environment variable. Outside managed compute it automatically derives the exact hybrid request signer from FORGEZERO_API_KEY.
|
|
30
125
|
|
|
31
|
-
```
|
|
32
|
-
|
|
126
|
+
```text
|
|
127
|
+
new ForgeZero()
|
|
128
|
+
|- /run/forgezero/vault.sock exists -> MANAGED, app holds nothing
|
|
129
|
+
|- FORGEZERO_API_KEY present -> EXTERNAL, derive and sign locally
|
|
130
|
+
`- neither -> throws, naming BOTH remedies
|
|
33
131
|
```
|
|
34
132
|
|
|
35
|
-
|
|
36
|
-
their next fetch, and nothing is redeployed.
|
|
133
|
+
## When both are present, the socket wins
|
|
37
134
|
|
|
38
|
-
|
|
135
|
+
On managed compute an API key in the environment is a leftover. Honouring it would silently downgrade a machine that holds nothing into one holding a signing seed, so discovery always prefers the stronger posture.
|
|
39
136
|
|
|
40
|
-
|
|
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.
|
|
137
|
+
## Rotation is human-authorised and application-observed
|
|
48
138
|
|
|
49
|
-
|
|
139
|
+
Writes stay on the authenticated platform surface with a human session, step-up proof and named audit actor. The application credential is read-only. External API-key reads may request a historical version; the managed RAM replica intentionally serves only the current value.
|
|
50
140
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
141
|
+
```text
|
|
142
|
+
// after an authorised rotation in the platform UI
|
|
143
|
+
for await (const change of fz.watch()) {
|
|
144
|
+
console.log(change.name, change.version);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
57
147
|
|
|
58
|
-
|
|
148
|
+
## getAll is managed-only, deliberately
|
|
59
149
|
|
|
60
|
-
|
|
61
|
-
exists in plaintext only inside a signing call.
|
|
62
|
-
- **supplied** — you generate it, the vault seals it.
|
|
150
|
+
One compromised API key should not hand over an entire environment in a single call, so a key holder gets list() plus reads by name. On managed compute there is no key to compromise, which is why the bulk read is allowed there and only there.
|
|
63
151
|
|
|
64
|
-
|
|
65
|
-
stay inside explicit trusted platform workflows; the general application
|
|
66
|
-
credential remains a read-only secret client and is not a transaction-signing
|
|
67
|
-
oracle.
|
|
152
|
+
## One stable API origin
|
|
68
153
|
|
|
69
|
-
|
|
154
|
+
The client always calls api.forgezero.net. Cloudflare owns routing and node health behind that name; the SDK never receives a node hostname, polls an assignment endpoint or replays a state-changing request against a different process.
|
|
70
155
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
| `/config` | read `.fz/config.json` — project, environment, which secrets |
|
|
76
|
-
| `/schema` | where an entry's shape comes from: pulled from the platform, or local |
|
|
77
|
-
| `/frameworks` | SvelteKit and Next.js wiring, at the one place that runs once |
|
|
156
|
+
```text
|
|
157
|
+
410 421 502 503 504 530 -> ask for a different node, retry once
|
|
158
|
+
401 403 -> an answer, not a routing problem
|
|
159
|
+
```
|
|
78
160
|
|
|
79
161
|
## Errors you will actually hit
|
|
80
162
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
163
|
+
Each names what to do next.
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
NO_CREDENTIAL neither the socket nor FORGEZERO_API_KEY was found
|
|
167
|
+
VAULT_LOCKED the realm has no seed in memory; a custodian must unlock
|
|
168
|
+
MANAGED_ONLY getAll needs the agent socket
|
|
169
|
+
NO_NODE no vault node is currently available
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## API keys — the key is a seed, not a password
|
|
173
|
+
|
|
174
|
+
The SDK derives an Ed25519 + ML-DSA-65 keypair from it and signs every request, including a one-use hybrid ML-KEM-768 + X25519 response key. The API AES-256-GCM seals the result to that request before it crosses the edge. The seed itself is never transmitted, and ForgeZero stores only public halves.
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
fz.live.<keyId>.<seed> fz.test.<keyId>.<seed>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Scope every key to one project and only the environments it needs
|
|
181
|
+
|
|
182
|
+
An API key is fixed to one project, optionally narrowed to named environments, and issued with exact route grants or project-owned API-key roles. Single-entry read, metadata listing and change following are the least-privilege default; bulk environment read and exact pipeline release are separate opt-ins.
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
project: payments
|
|
186
|
+
environments: production, staging
|
|
187
|
+
routes: read one, list metadata, follow changes
|
|
188
|
+
bulk read: explicit opt-in
|
|
189
|
+
pipeline release: explicit live-key role
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Controls, and what each is actually worth
|
|
193
|
+
|
|
194
|
+
Project, environment, exact route grants and revocation are real: status is read fresh on every call, so a revoked key stops working on the next request. API keys do not currently expire automatically; rotate or revoke them deliberately. An origin header is not treated as server authentication because any server-side caller can set it.
|
|
84
195
|
|
|
85
|
-
|
|
196
|
+
## Vault read-only; release intent is the narrow machine write
|
|
86
197
|
|
|
87
|
-
|
|
198
|
+
The external Vault SDK has no secret write endpoint. A live API key may separately receive pipeline:release:create: it queues one full Git commit on an existing enabled pipeline, requires a stable idempotency key, and cannot edit a pipeline, target, secret or compute. Sandbox and legacy keys never inherit it, and the bound Agent still claims execution.
|
|
88
199
|
|
|
89
|
-
|
|
90
|
-
deploys.
|
|
200
|
+
Full rendered documentation: https://www.forgezero.net/docs/vault-package
|
package/dist/index.d.ts
CHANGED
|
@@ -282,4 +282,4 @@ export declare function systemdCredentials(options?: SystemdCredentialOptions):
|
|
|
282
282
|
readonly name: 'systemd';
|
|
283
283
|
get(reference: string, field: string): Promise<string>;
|
|
284
284
|
};
|
|
285
|
-
export declare const VERSION = "0.1.
|
|
285
|
+
export declare const VERSION = "0.1.10";
|
package/dist/index.js
CHANGED
package/dist/providers.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { ForgeZero } from './index';
|
|
2
|
-
/** Mirrors
|
|
3
|
-
export interface
|
|
2
|
+
/** Mirrors a provider identity in `@forgezero/providers`, matched structurally. */
|
|
3
|
+
export interface StoredProviderInstance {
|
|
4
|
+
instanceKey: string;
|
|
4
5
|
providerId: string;
|
|
5
|
-
/** Names one instance when a provider is configured more than once. */
|
|
6
|
-
instanceKey?: string;
|
|
7
|
-
/** 1 is tried first. */
|
|
8
|
-
priority: number;
|
|
9
6
|
enabled: boolean;
|
|
10
7
|
config: Record<string, unknown>;
|
|
11
|
-
/** Names the vault entry holding the secret. Never the secret. */
|
|
12
8
|
secretRef: string;
|
|
9
|
+
}
|
|
10
|
+
/** Mirrors one service-method attachment in `@forgezero/providers`. */
|
|
11
|
+
export interface StoredServiceMethodAttachment {
|
|
12
|
+
instanceKey: string;
|
|
13
|
+
providerMethod: string;
|
|
14
|
+
priority: number;
|
|
15
|
+
enabled: boolean;
|
|
13
16
|
health?: {
|
|
14
17
|
strikes: number;
|
|
15
18
|
status: 'ok' | 'degraded' | 'offline';
|
|
@@ -18,12 +21,14 @@ export interface StoredProvider {
|
|
|
18
21
|
}
|
|
19
22
|
export interface VaultConfigOptions {
|
|
20
23
|
/**
|
|
21
|
-
* Entry
|
|
24
|
+
* Entry containing provider identities, independent of services.
|
|
22
25
|
*
|
|
23
26
|
* Overridable because a tenant already using `providers.*` for something else
|
|
24
27
|
* should not have to rename it to adopt this.
|
|
25
28
|
*/
|
|
26
|
-
|
|
29
|
+
providersEntry?: string;
|
|
30
|
+
/** Entry containing one service method's ordered attachments. */
|
|
31
|
+
serviceEntryFor?: (serviceKey: string, methodKey: string) => string;
|
|
27
32
|
/**
|
|
28
33
|
* Persist health back to the vault.
|
|
29
34
|
*
|
|
@@ -50,8 +55,9 @@ export interface VaultConfigOptions {
|
|
|
50
55
|
*/
|
|
51
56
|
export declare function vaultConfig(vault: ForgeZero, options?: VaultConfigOptions): {
|
|
52
57
|
name: string;
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
provider(instanceKey: string): Promise<StoredProviderInstance | undefined>;
|
|
59
|
+
list(serviceKey: string, methodKey: string): Promise<readonly StoredServiceMethodAttachment[]>;
|
|
60
|
+
recordHealth(serviceKey: string, methodKey: string, instanceKey: string, providerMethod: string, health: NonNullable<StoredServiceMethodAttachment["health"]>): Promise<void>;
|
|
55
61
|
};
|
|
56
62
|
/**
|
|
57
63
|
* Credentials out of the vault, by the reference a provider config names.
|
|
@@ -62,5 +68,9 @@ export declare function vaultConfig(vault: ForgeZero, options?: VaultConfigOptio
|
|
|
62
68
|
*/
|
|
63
69
|
export declare function vaultCredentials(vault: ForgeZero): {
|
|
64
70
|
name: string;
|
|
65
|
-
get
|
|
71
|
+
get(reference: string, field: string): Promise<string>;
|
|
72
|
+
};
|
|
73
|
+
export declare function vaultCredentials(vault: ForgeZero, reference: string): {
|
|
74
|
+
name: string;
|
|
75
|
+
get(field: string): Promise<string>;
|
|
66
76
|
};
|
package/dist/providers.js
CHANGED
|
@@ -382,14 +382,14 @@ function systemdCredentials(options = {}) {
|
|
|
382
382
|
}
|
|
383
383
|
};
|
|
384
384
|
}
|
|
385
|
-
var VERSION = "0.1.
|
|
385
|
+
var VERSION = "0.1.10";
|
|
386
386
|
|
|
387
387
|
// src/providers.ts
|
|
388
388
|
var MISSING = new Set(["ENTRY_NOT_FOUND", "VERSION_NOT_FOUND"]);
|
|
389
389
|
function vaultConfig(vault, options = {}) {
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
|
|
390
|
+
const providersEntry = options.providersEntry ?? "providers.instances";
|
|
391
|
+
const serviceEntryFor = options.serviceEntryFor ?? ((serviceKey, methodKey) => `services.${serviceKey}.${methodKey}`);
|
|
392
|
+
const readArray = async (name) => {
|
|
393
393
|
let raw;
|
|
394
394
|
try {
|
|
395
395
|
raw = await vault.get(name);
|
|
@@ -405,28 +405,39 @@ function vaultConfig(vault, options = {}) {
|
|
|
405
405
|
throw new VaultError("INVALID", `Vault entry "${name}" is not valid JSON.`);
|
|
406
406
|
}
|
|
407
407
|
if (!Array.isArray(parsed)) {
|
|
408
|
-
throw new VaultError("INVALID", `Vault entry "${name}" must be an array
|
|
408
|
+
throw new VaultError("INVALID", `Vault entry "${name}" must be an array.`);
|
|
409
409
|
}
|
|
410
410
|
return parsed;
|
|
411
411
|
};
|
|
412
412
|
return {
|
|
413
413
|
name: "vault",
|
|
414
|
-
async
|
|
415
|
-
|
|
414
|
+
async provider(instanceKey) {
|
|
415
|
+
const instances = await readArray(providersEntry);
|
|
416
|
+
return instances.find((instance) => instance.instanceKey === instanceKey);
|
|
416
417
|
},
|
|
417
|
-
async
|
|
418
|
+
async list(serviceKey, methodKey) {
|
|
419
|
+
return (await readArray(serviceEntryFor(serviceKey, methodKey))).slice().sort((a, b) => a.priority - b.priority);
|
|
420
|
+
},
|
|
421
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, health) {
|
|
418
422
|
if (!options.persistHealth)
|
|
419
423
|
return;
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
424
|
+
const entry = serviceEntryFor(serviceKey, methodKey);
|
|
425
|
+
const attachments = await readArray(entry);
|
|
426
|
+
const next = attachments.map((attachment) => attachment.instanceKey === instanceKey && attachment.providerMethod === providerMethod ? { ...attachment, health } : attachment);
|
|
427
|
+
await vault.set(entry, JSON.stringify(next));
|
|
423
428
|
}
|
|
424
429
|
};
|
|
425
430
|
}
|
|
426
|
-
function vaultCredentials2(vault) {
|
|
431
|
+
function vaultCredentials2(vault, reference) {
|
|
432
|
+
if (reference !== undefined) {
|
|
433
|
+
return {
|
|
434
|
+
name: `vault:${reference}`,
|
|
435
|
+
get: (field) => vault.get(`${reference}.${field}`)
|
|
436
|
+
};
|
|
437
|
+
}
|
|
427
438
|
return {
|
|
428
439
|
name: "vault",
|
|
429
|
-
get: (
|
|
440
|
+
get: (reference2, field) => vault.get(`${reference2}.${field}`)
|
|
430
441
|
};
|
|
431
442
|
}
|
|
432
443
|
export {
|
package/dist/schema.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgezero/vault",
|
|
3
|
-
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@types/bun": "latest"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
|
|
46
|
+
"@forgezero/runtime": "^0.1.6"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@noble/curves": "^2.2.0",
|