@forgezero/runtime 0.1.17 → 0.1.18

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
@@ -7,7 +7,7 @@
7
7
 
8
8
  # Platform runtime
9
9
 
10
- The machinery behind a request handler — jobs, queues, an outbox, a hash-chained audit trail, mail, backups, schema validation and exact money. 30 public modules, each imported on its own.
10
+ The machinery behind a request handler — jobs, queues, an outbox, a hash-chained audit trail, mail, backups, schema validation and exact money. 31 public modules, each imported on its own.
11
11
 
12
12
  ## Package overview
13
13
 
@@ -53,6 +53,7 @@ These are supported consumer entry points, not every internal module shipped for
53
53
  | @forgezero/runtime/ssh-cert | OpenSSH certificates, so access expires instead of having to be hunted down. Takes a signing FUNCTION rather than a secret key, which is what lets the CA live in a vault that never hands it out. | portable | [Reference + usage](#forgezero-runtime-ssh-cert) |
54
54
  | @forgezero/runtime/slip10 | SLIP-0010 derivation for ed25519, hardened-only — BIP-32 does not work on this curve and produces halves that do not correspond. | portable | [Reference + usage](#forgezero-runtime-slip10) |
55
55
  | @forgezero/runtime/identity | Hybrid Ed25519 + ML-DSA-65 request signing. One canonical string, so the compute agent that signs inside a guest and the API that verifies cannot drift — which two implementations of it certainly would. | portable | [Reference + usage](#forgezero-runtime-identity) |
56
+ | @forgezero/runtime/vault-runtime | Provider-neutral algorithms for non-exportable Vault runtime records: passwords, DEK/KEK operations, HMAC/JWS, Ed25519/secp256k1 and Ethereum/Solana address encoding. | portable | [Reference + usage](#forgezero-runtime-vault-runtime) |
56
57
  | @forgezero/runtime/schema | Validate against JSON Schema, restrict what a caller may declare, and describe a schema as a form. | portable | [Reference + usage](#forgezero-runtime-schema) |
57
58
  | @forgezero/runtime/schema/typebox | The TypeBox validator behind that interface. | portable | [Reference + usage](#forgezero-runtime-schema-typebox) |
58
59
  | @forgezero/runtime/finance/discounts | Promotions as arithmetic over integer minor units. They never stack — one winner — and a percentage rounds down, because rounding a discount up gives away a unit of currency per invoice forever. | portable | [Reference + usage](#forgezero-runtime-finance-discounts) |
@@ -509,6 +510,29 @@ import {
509
510
  export const selectedCapability = CLOCK_SKEW_SECONDS;
510
511
  ```
511
512
 
513
+ <a id="forgezero-runtime-vault-runtime"></a>
514
+ ## @forgezero/runtime/vault-runtime
515
+
516
+ Provider-neutral algorithms for non-exportable Vault runtime records: passwords, DEK/KEK operations, HMAC/JWS, Ed25519/secp256k1 and Ethereum/Solana address encoding. This is a supported entry point. Import only the named values used by the adjacent task example; the declaration file remains the complete API reference.
517
+
518
+ ```text
519
+ import {
520
+ DEFAULT_PASSWORD_ALPHABET,
521
+ } from '@forgezero/runtime/vault-runtime';
522
+ ```
523
+
524
+ ## @forgezero/runtime/vault-runtime — Use this entry point
525
+
526
+ This minimal executable use imports one concrete value from this exact entry point without a wildcard or package-root detour. Keep the value or values the application actually needs; the full named inventory remains directly above it.
527
+
528
+ ```text
529
+ import {
530
+ DEFAULT_PASSWORD_ALPHABET,
531
+ } from '@forgezero/runtime/vault-runtime';
532
+
533
+ export const selectedCapability = DEFAULT_PASSWORD_ALPHABET;
534
+ ```
535
+
512
536
  <a id="forgezero-runtime-schema"></a>
513
537
  ## @forgezero/runtime/schema
514
538
 
package/dist/jobs.d.ts CHANGED
@@ -58,7 +58,12 @@ export interface Lease {
58
58
  until: number;
59
59
  }
60
60
  export interface JobLock {
61
- acquire(key: string, ttlMs: number): Promise<Lease | undefined>;
61
+ /**
62
+ * Acquire the job. A scheduled occurrence is a durable once-only token: after
63
+ * the holder releases its lease, another replica still cannot execute that
64
+ * same due slot. Manual runs omit it and remain deliberately repeatable.
65
+ */
66
+ acquire(key: string, ttlMs: number, occurrence?: string): Promise<Lease | undefined>;
62
67
  /** Extend a lease held by this fence. False means it was lost. */
63
68
  renew(key: string, fence: number, ttlMs: number): Promise<boolean>;
64
69
  release(key: string, fence: number): Promise<void>;
@@ -72,7 +77,7 @@ export declare function memoryLock(clock?: Clock): JobLock;
72
77
  * the same lease to two processes under exactly the load that makes it matter.
73
78
  */
74
79
  export interface LockStore {
75
- claim(key: string, until: number, now: number): Promise<Lease | undefined>;
80
+ claim(key: string, until: number, now: number, occurrence?: string): Promise<Lease | undefined>;
76
81
  extend(key: string, fence: number, until: number): Promise<boolean>;
77
82
  clear(key: string, fence: number): Promise<void>;
78
83
  }
package/dist/jobs.js CHANGED
@@ -379,15 +379,20 @@ function nextWallClockAt(schedule, afterMs) {
379
379
  }
380
380
  function memoryLock(clock = systemClock) {
381
381
  const held = new Map;
382
+ const claimedOccurrences = new Map;
382
383
  let fences = 0;
383
384
  return {
384
- async acquire(key, ttlMs) {
385
+ async acquire(key, ttlMs, occurrence) {
386
+ if (occurrence !== undefined && claimedOccurrences.get(key) === occurrence)
387
+ return;
385
388
  const current = held.get(key);
386
389
  if (current && current.until > clock.now())
387
390
  return;
388
391
  fences += 1;
389
392
  const lease = { fence: fences, until: clock.now() + ttlMs };
390
393
  held.set(key, lease);
394
+ if (occurrence !== undefined)
395
+ claimedOccurrences.set(key, occurrence);
391
396
  return lease;
392
397
  },
393
398
  async renew(key, fence, ttlMs) {
@@ -405,7 +410,7 @@ function memoryLock(clock = systemClock) {
405
410
  }
406
411
  function storeLock(store, clock = systemClock) {
407
412
  return {
408
- acquire: (key, ttlMs) => store.claim(key, clock.now() + ttlMs, clock.now()),
413
+ acquire: (key, ttlMs, occurrence) => store.claim(key, clock.now() + ttlMs, clock.now(), occurrence),
409
414
  renew: (key, fence, ttlMs) => store.extend(key, fence, clock.now() + ttlMs),
410
415
  release: (key, fence) => store.clear(key, fence)
411
416
  };
@@ -465,10 +470,10 @@ function createScheduler(options) {
465
470
  let paused = false;
466
471
  let running = false;
467
472
  const outstanding = new Map;
468
- const submit = async (job) => {
473
+ const submit = async (job, occurrence) => {
469
474
  outstanding.set(job.key, (outstanding.get(job.key) ?? 0) + 1);
470
475
  try {
471
- await work.run(job.key, execute, job).result;
476
+ await work.run(job.key, execute, job, occurrence).result;
472
477
  } finally {
473
478
  const left = (outstanding.get(job.key) ?? 1) - 1;
474
479
  if (left === 0)
@@ -477,14 +482,14 @@ function createScheduler(options) {
477
482
  outstanding.set(job.key, left);
478
483
  }
479
484
  };
480
- async function execute(job) {
485
+ async function execute(job, occurrence) {
481
486
  const report = reports.get(job.key);
482
487
  const leaseMs = job.leaseMs ?? (job.every ? everyMs(job.every) * 4 : 60000);
483
488
  let lease;
484
489
  let leaseHeld = true;
485
490
  let renewTimer;
486
491
  if (!job.unlocked) {
487
- lease = await lock.acquire(job.key, leaseMs);
492
+ lease = await lock.acquire(job.key, leaseMs, occurrence);
488
493
  if (!lease) {
489
494
  report.skippedLocked += 1;
490
495
  return;
@@ -553,8 +558,11 @@ function createScheduler(options) {
553
558
  }
554
559
  }
555
560
  function nextDelay(job) {
556
- if (job.every !== undefined)
557
- return everyMs(job.every);
561
+ if (job.every !== undefined) {
562
+ const interval = everyMs(job.every);
563
+ const now = clock.now();
564
+ return Math.max(0, (Math.floor(now / interval) + 1) * interval - now);
565
+ }
558
566
  if (job.schedule)
559
567
  return Math.max(0, nextWallClockAt(job.schedule, clock.now()) - clock.now());
560
568
  return;
@@ -563,7 +571,8 @@ function createScheduler(options) {
563
571
  const next = delayMs ?? nextDelay(job);
564
572
  if (!running || next === undefined)
565
573
  return;
566
- reports.get(job.key).nextRunAtMs = clock.now() + next;
574
+ const dueAt = clock.now() + next;
575
+ reports.get(job.key).nextRunAtMs = dueAt;
567
576
  timers.set(job.key, setTimeout(() => {
568
577
  timers.delete(job.key);
569
578
  if (!running || paused)
@@ -574,12 +583,14 @@ function createScheduler(options) {
574
583
  reports.get(job.key).skippedOverlap += 1;
575
584
  return;
576
585
  }
577
- submit(job).catch(() => {
586
+ const occurrence2 = job.every === undefined ? `wall:${dueAt}` : `interval:${Math.floor(clock.now() / everyMs(job.every))}`;
587
+ submit(job, occurrence2).catch(() => {
578
588
  return;
579
589
  });
580
590
  return;
581
591
  }
582
- submit(job).then(() => schedule(job), () => {
592
+ const occurrence = job.every === undefined ? `wall:${dueAt}` : `interval:${Math.floor(clock.now() / everyMs(job.every))}`;
593
+ submit(job, occurrence).then(() => schedule(job), () => {
583
594
  return;
584
595
  });
585
596
  }, next));
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Provider-neutral Vault runtime primitives.
3
+ *
4
+ * This module never derives from a realm seed and never stores a key. The API
5
+ * owns those two security boundaries and lends this module one short-lived key
6
+ * for one operation. Keeping the algorithms here makes their byte formats
7
+ * independently testable without making `@forgezero/vault.get()` a signer.
8
+ */
9
+ export declare const VAULT_RUNTIME_KINDS: readonly ["random-password", "data-encryption-key", "key-encryption-key", "hmac-key", "jwt-hmac-key", "ed25519-key", "secp256k1-key", "ssh-ca-key", "chain-key"];
10
+ export type VaultRuntimeKind = typeof VAULT_RUNTIME_KINDS[number];
11
+ export declare const VAULT_RUNTIME_ALGORITHM_IDS: readonly ["password-v1", "aes-256-gcm", "aes-256-kw", "hmac-sha256", "hmac-sha512", "jws-hs256", "ed25519", "secp256k1", "ssh-ed25519-ca", "ethereum-secp256k1", "solana-ed25519"];
12
+ export type VaultRuntimeAlgorithm = typeof VAULT_RUNTIME_ALGORITHM_IDS[number];
13
+ export type VaultRuntimeOperation = 'generate-password' | 'encrypt' | 'decrypt' | 'wrap' | 'unwrap' | 'sign' | 'verify' | 'sign-jwt' | 'verify-jwt' | 'issue-ssh-certificate' | 'derive-public-key' | 'derive-address';
14
+ export interface VaultRuntimeAlgorithmDefinition {
15
+ kind: VaultRuntimeKind;
16
+ algorithm: VaultRuntimeAlgorithm;
17
+ operations: readonly VaultRuntimeOperation[];
18
+ /** Private material is never a supported output. */
19
+ exports: 'password-only' | 'public-only' | 'none';
20
+ standard: string;
21
+ }
22
+ /**
23
+ * Typed algorithm truth shared by the API, package documentation and future UI.
24
+ * Adding another chain or credential generator requires a reviewed definition;
25
+ * callers cannot invent an algorithm string and reach an unreviewed primitive.
26
+ */
27
+ export declare const VAULT_RUNTIME_ALGORITHMS: readonly [{
28
+ readonly kind: "random-password";
29
+ readonly algorithm: "password-v1";
30
+ readonly operations: readonly ["generate-password"];
31
+ readonly exports: "password-only";
32
+ readonly standard: "ForgeZero password-v1";
33
+ }, {
34
+ readonly kind: "data-encryption-key";
35
+ readonly algorithm: "aes-256-gcm";
36
+ readonly operations: readonly ["encrypt", "decrypt"];
37
+ readonly exports: "none";
38
+ readonly standard: "NIST SP 800-38D";
39
+ }, {
40
+ readonly kind: "key-encryption-key";
41
+ readonly algorithm: "aes-256-kw";
42
+ readonly operations: readonly ["wrap", "unwrap"];
43
+ readonly exports: "none";
44
+ readonly standard: "NIST SP 800-38F";
45
+ }, {
46
+ readonly kind: "hmac-key";
47
+ readonly algorithm: "hmac-sha256";
48
+ readonly operations: readonly ["sign", "verify"];
49
+ readonly exports: "none";
50
+ readonly standard: "RFC 2104 / SHA-256";
51
+ }, {
52
+ readonly kind: "hmac-key";
53
+ readonly algorithm: "hmac-sha512";
54
+ readonly operations: readonly ["sign", "verify"];
55
+ readonly exports: "none";
56
+ readonly standard: "RFC 2104 / SHA-512";
57
+ }, {
58
+ readonly kind: "jwt-hmac-key";
59
+ readonly algorithm: "jws-hs256";
60
+ readonly operations: readonly ["sign-jwt", "verify-jwt"];
61
+ readonly exports: "none";
62
+ readonly standard: "RFC 7515 / RFC 7518";
63
+ }, {
64
+ readonly kind: "ed25519-key";
65
+ readonly algorithm: "ed25519";
66
+ readonly operations: readonly ["derive-public-key", "sign", "verify"];
67
+ readonly exports: "public-only";
68
+ readonly standard: "RFC 8032";
69
+ }, {
70
+ readonly kind: "secp256k1-key";
71
+ readonly algorithm: "secp256k1";
72
+ readonly operations: readonly ["derive-public-key", "sign", "verify"];
73
+ readonly exports: "public-only";
74
+ readonly standard: "SEC 2 secp256k1";
75
+ }, {
76
+ readonly kind: "ssh-ca-key";
77
+ readonly algorithm: "ssh-ed25519-ca";
78
+ readonly operations: readonly ["derive-public-key", "issue-ssh-certificate"];
79
+ readonly exports: "public-only";
80
+ readonly standard: "OpenSSH PROTOCOL.certkeys";
81
+ }, {
82
+ readonly kind: "chain-key";
83
+ readonly algorithm: "ethereum-secp256k1";
84
+ readonly operations: readonly ["derive-public-key", "derive-address", "sign", "verify"];
85
+ readonly exports: "public-only";
86
+ readonly standard: "BIP-32/BIP-44/SLIP-44/EIP-55";
87
+ }, {
88
+ readonly kind: "chain-key";
89
+ readonly algorithm: "solana-ed25519";
90
+ readonly operations: readonly ["derive-public-key", "derive-address", "sign", "verify"];
91
+ readonly exports: "public-only";
92
+ readonly standard: "SLIP-10/BIP-44/SLIP-44";
93
+ }];
94
+ export declare const VAULT_RUNTIME_CHAINS: readonly [{
95
+ readonly chain: "ethereum";
96
+ readonly coinType: 60;
97
+ readonly curve: "secp256k1";
98
+ readonly path: "m/44'/60'/account'/change/index";
99
+ }, {
100
+ readonly chain: "solana";
101
+ readonly coinType: 501;
102
+ readonly curve: "ed25519";
103
+ readonly path: "m/44'/501'/account'/change'/index'";
104
+ }];
105
+ export interface PasswordPolicy {
106
+ length: number;
107
+ alphabet?: string;
108
+ }
109
+ export interface EthereumDerivation {
110
+ chain: 'ethereum';
111
+ network?: 'mainnet' | 'sepolia' | string;
112
+ account?: number;
113
+ change?: number;
114
+ index?: number;
115
+ }
116
+ export interface SolanaDerivation {
117
+ chain: 'solana';
118
+ network?: 'mainnet-beta' | 'devnet' | 'testnet' | string;
119
+ account?: number;
120
+ change?: number;
121
+ index?: number;
122
+ }
123
+ export type ChainDerivation = EthereumDerivation | SolanaDerivation;
124
+ type RuntimeDefinition = typeof VAULT_RUNTIME_ALGORITHMS[number];
125
+ /**
126
+ * A correlated creation contract: an algorithm cannot be paired with the wrong
127
+ * key kind, password keys require a policy, and chain keys require a reviewed
128
+ * chain derivation. The API repeats these checks at its trust boundary.
129
+ */
130
+ export type VaultRuntimeCreateDescriptor = RuntimeDefinition extends infer Definition ? Definition extends {
131
+ kind: infer Kind extends VaultRuntimeKind;
132
+ algorithm: infer Algorithm extends VaultRuntimeAlgorithm;
133
+ } ? {
134
+ kind: Kind;
135
+ algorithm: Algorithm;
136
+ label: string;
137
+ } & (Kind extends 'random-password' ? {
138
+ password: PasswordPolicy;
139
+ chain?: never;
140
+ } : Kind extends 'chain-key' ? {
141
+ chain: ChainDerivation;
142
+ password?: never;
143
+ } : {
144
+ password?: never;
145
+ chain?: never;
146
+ }) : never : never;
147
+ export interface VaultRuntimeDescriptor {
148
+ format: 1;
149
+ kind: VaultRuntimeKind;
150
+ algorithm: VaultRuntimeAlgorithm;
151
+ label: string;
152
+ createdAtTs: number;
153
+ password?: PasswordPolicy;
154
+ chain?: ChainDerivation;
155
+ }
156
+ export interface RuntimeCiphertext {
157
+ algorithm: 'aes-256-gcm';
158
+ nonce: string;
159
+ ciphertext: string;
160
+ }
161
+ export interface RuntimeWrappedKey {
162
+ algorithm: 'aes-256-kw';
163
+ ciphertext: string;
164
+ }
165
+ export declare const DEFAULT_PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*()-_=+";
166
+ export declare function validatePasswordPolicy(policy: PasswordPolicy): void;
167
+ export declare const toBase64Url: (bytes: Uint8Array) => string;
168
+ export declare const fromBase64Url: (value: string) => Uint8Array;
169
+ export declare function passwordFromBytes(entropy: Uint8Array, policy: PasswordPolicy): string;
170
+ export declare function encryptWithDek(key: Uint8Array, plaintext: Uint8Array, aad: string): RuntimeCiphertext;
171
+ export declare function decryptWithDek(key: Uint8Array, box: RuntimeCiphertext, aad: string): Uint8Array;
172
+ export declare function wrapWithKek(key: Uint8Array, plaintextKey: Uint8Array): RuntimeWrappedKey;
173
+ export declare function unwrapWithKek(key: Uint8Array, wrapped: RuntimeWrappedKey): Uint8Array;
174
+ export declare function hmacSign(algorithm: 'hmac-sha256' | 'hmac-sha512', key: Uint8Array, message: Uint8Array): Uint8Array;
175
+ export declare function hmacVerify(algorithm: 'hmac-sha256' | 'hmac-sha512', key: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
176
+ export declare function signCompactJws(key: Uint8Array, claims: Record<string, unknown>): string;
177
+ export declare function verifyCompactJws(key: Uint8Array, token: string, options?: {
178
+ nowSec?: number;
179
+ issuer?: string;
180
+ audience?: string;
181
+ }): Record<string, unknown>;
182
+ export declare const ed25519PublicKey: (secret: Uint8Array) => Uint8Array;
183
+ export declare const ed25519Sign: (secret: Uint8Array, message: Uint8Array) => Uint8Array;
184
+ export declare const ed25519Verify: (publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => boolean;
185
+ export declare const secp256k1PublicKey: (secret: Uint8Array, compressed?: boolean) => Uint8Array;
186
+ export declare const secp256k1SignDigest: (secret: Uint8Array, digest: Uint8Array) => Uint8Array;
187
+ export declare const secp256k1VerifyDigest: (publicKey: Uint8Array, digest: Uint8Array, signature: Uint8Array) => boolean;
188
+ export declare function chainPath(chain: ChainDerivation): string;
189
+ export declare function ethereumAddress(uncompressedPublicKey: Uint8Array): string;
190
+ export declare const solanaAddress: (publicKey: Uint8Array) => string;
191
+ export {};
@@ -0,0 +1,272 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/vault-runtime.ts
10
+ import { aeskw, gcm } from "@noble/ciphers/aes.js";
11
+ import { ed25519 } from "@noble/curves/ed25519.js";
12
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
13
+ import { hmac } from "@noble/hashes/hmac.js";
14
+ import { keccak_256 } from "@noble/hashes/sha3.js";
15
+ import { sha256, sha512 } from "@noble/hashes/sha2.js";
16
+ import { base58 } from "@scure/base";
17
+ var VAULT_RUNTIME_KINDS = [
18
+ "random-password",
19
+ "data-encryption-key",
20
+ "key-encryption-key",
21
+ "hmac-key",
22
+ "jwt-hmac-key",
23
+ "ed25519-key",
24
+ "secp256k1-key",
25
+ "ssh-ca-key",
26
+ "chain-key"
27
+ ];
28
+ var VAULT_RUNTIME_ALGORITHM_IDS = [
29
+ "password-v1",
30
+ "aes-256-gcm",
31
+ "aes-256-kw",
32
+ "hmac-sha256",
33
+ "hmac-sha512",
34
+ "jws-hs256",
35
+ "ed25519",
36
+ "secp256k1",
37
+ "ssh-ed25519-ca",
38
+ "ethereum-secp256k1",
39
+ "solana-ed25519"
40
+ ];
41
+ var VAULT_RUNTIME_ALGORITHMS = [
42
+ { kind: "random-password", algorithm: "password-v1", operations: ["generate-password"], exports: "password-only", standard: "ForgeZero password-v1" },
43
+ { kind: "data-encryption-key", algorithm: "aes-256-gcm", operations: ["encrypt", "decrypt"], exports: "none", standard: "NIST SP 800-38D" },
44
+ { kind: "key-encryption-key", algorithm: "aes-256-kw", operations: ["wrap", "unwrap"], exports: "none", standard: "NIST SP 800-38F" },
45
+ { kind: "hmac-key", algorithm: "hmac-sha256", operations: ["sign", "verify"], exports: "none", standard: "RFC 2104 / SHA-256" },
46
+ { kind: "hmac-key", algorithm: "hmac-sha512", operations: ["sign", "verify"], exports: "none", standard: "RFC 2104 / SHA-512" },
47
+ { kind: "jwt-hmac-key", algorithm: "jws-hs256", operations: ["sign-jwt", "verify-jwt"], exports: "none", standard: "RFC 7515 / RFC 7518" },
48
+ { kind: "ed25519-key", algorithm: "ed25519", operations: ["derive-public-key", "sign", "verify"], exports: "public-only", standard: "RFC 8032" },
49
+ { kind: "secp256k1-key", algorithm: "secp256k1", operations: ["derive-public-key", "sign", "verify"], exports: "public-only", standard: "SEC 2 secp256k1" },
50
+ { kind: "ssh-ca-key", algorithm: "ssh-ed25519-ca", operations: ["derive-public-key", "issue-ssh-certificate"], exports: "public-only", standard: "OpenSSH PROTOCOL.certkeys" },
51
+ { kind: "chain-key", algorithm: "ethereum-secp256k1", operations: ["derive-public-key", "derive-address", "sign", "verify"], exports: "public-only", standard: "BIP-32/BIP-44/SLIP-44/EIP-55" },
52
+ { kind: "chain-key", algorithm: "solana-ed25519", operations: ["derive-public-key", "derive-address", "sign", "verify"], exports: "public-only", standard: "SLIP-10/BIP-44/SLIP-44" }
53
+ ];
54
+ var VAULT_RUNTIME_CHAINS = [
55
+ { chain: "ethereum", coinType: 60, curve: "secp256k1", path: "m/44'/60'/account'/change/index" },
56
+ { chain: "solana", coinType: 501, curve: "ed25519", path: "m/44'/501'/account'/change'/index'" }
57
+ ];
58
+ var DEFAULT_PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*()-_=+";
59
+ function validatePasswordPolicy(policy) {
60
+ const alphabet = policy.alphabet ?? DEFAULT_PASSWORD_ALPHABET;
61
+ if (!Number.isInteger(policy.length) || policy.length < 16 || policy.length > 256) {
62
+ throw new Error("vault-runtime: password length must be in 16..256");
63
+ }
64
+ if (alphabet.length < 16 || alphabet.length > 128 || new Set(alphabet).size !== alphabet.length) {
65
+ throw new Error("vault-runtime: password alphabet must contain 16..128 unique characters");
66
+ }
67
+ }
68
+ var toBase64Url = (bytes) => {
69
+ let binary = "";
70
+ for (const byte of bytes)
71
+ binary += String.fromCharCode(byte);
72
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
73
+ };
74
+ var fromBase64Url = (value) => {
75
+ if (!/^[A-Za-z0-9_-]*$/.test(value))
76
+ throw new Error("vault-runtime: invalid base64url");
77
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - value.length % 4) % 4);
78
+ let binary;
79
+ try {
80
+ binary = atob(padded);
81
+ } catch {
82
+ throw new Error("vault-runtime: invalid base64url");
83
+ }
84
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
85
+ };
86
+ var utf8 = (value) => new TextEncoder().encode(value);
87
+ function passwordFromBytes(entropy, policy) {
88
+ const alphabet = policy.alphabet ?? DEFAULT_PASSWORD_ALPHABET;
89
+ validatePasswordPolicy(policy);
90
+ const limit = 256 - 256 % alphabet.length;
91
+ let output = "";
92
+ for (const byte of entropy) {
93
+ if (byte >= limit)
94
+ continue;
95
+ output += alphabet[byte % alphabet.length];
96
+ if (output.length === policy.length)
97
+ return output;
98
+ }
99
+ throw new Error("vault-runtime: insufficient unbiased password entropy");
100
+ }
101
+ function encryptWithDek(key, plaintext, aad) {
102
+ if (key.length !== 32)
103
+ throw new Error("vault-runtime: DEK must be 32 bytes");
104
+ if (!aad)
105
+ throw new Error("vault-runtime: authenticated data is required");
106
+ const nonce = crypto.getRandomValues(new Uint8Array(12));
107
+ const ciphertext = gcm(key, nonce, utf8(aad)).encrypt(plaintext);
108
+ return { algorithm: "aes-256-gcm", nonce: toBase64Url(nonce), ciphertext: toBase64Url(ciphertext) };
109
+ }
110
+ function decryptWithDek(key, box, aad) {
111
+ if (key.length !== 32)
112
+ throw new Error("vault-runtime: DEK must be 32 bytes");
113
+ if (!aad || box.algorithm !== "aes-256-gcm")
114
+ throw new Error("vault-runtime: invalid ciphertext");
115
+ return gcm(key, fromBase64Url(box.nonce), utf8(aad)).decrypt(fromBase64Url(box.ciphertext));
116
+ }
117
+ function wrapWithKek(key, plaintextKey) {
118
+ if (key.length !== 32)
119
+ throw new Error("vault-runtime: KEK must be 32 bytes");
120
+ if (plaintextKey.length < 16 || plaintextKey.length % 8 !== 0) {
121
+ throw new Error("vault-runtime: wrapped key must be a non-empty multiple of 8 bytes, at least 16 bytes");
122
+ }
123
+ return { algorithm: "aes-256-kw", ciphertext: toBase64Url(aeskw(key).encrypt(plaintextKey)) };
124
+ }
125
+ function unwrapWithKek(key, wrapped) {
126
+ if (key.length !== 32 || wrapped.algorithm !== "aes-256-kw") {
127
+ throw new Error("vault-runtime: invalid wrapped key");
128
+ }
129
+ return aeskw(key).decrypt(fromBase64Url(wrapped.ciphertext));
130
+ }
131
+ function hmacSign(algorithm, key, message) {
132
+ return hmac(algorithm === "hmac-sha256" ? sha256 : sha512, key, message);
133
+ }
134
+ function hmacVerify(algorithm, key, message, signature) {
135
+ const actual = hmacSign(algorithm, key, message);
136
+ if (actual.length !== signature.length)
137
+ return false;
138
+ let difference = 0;
139
+ for (let index = 0;index < actual.length; index += 1)
140
+ difference |= actual[index] ^ signature[index];
141
+ return difference === 0;
142
+ }
143
+ function signCompactJws(key, claims) {
144
+ if (!claims || Array.isArray(claims) || Object.getPrototypeOf(claims) !== Object.prototype) {
145
+ throw new Error("vault-runtime: JWT claims must be a plain object");
146
+ }
147
+ const header = toBase64Url(utf8(JSON.stringify({ alg: "HS256", typ: "JWT" })));
148
+ let serialized;
149
+ try {
150
+ serialized = JSON.stringify(claims);
151
+ } catch {
152
+ throw new Error("vault-runtime: JWT claims must be JSON serializable");
153
+ }
154
+ const payload = toBase64Url(utf8(serialized));
155
+ const input = `${header}.${payload}`;
156
+ return `${input}.${toBase64Url(hmac(sha256, key, utf8(input)))}`;
157
+ }
158
+ function verifyCompactJws(key, token, options = {}) {
159
+ const parts = token.split(".");
160
+ if (parts.length !== 3)
161
+ throw new Error("vault-runtime: malformed compact JWS");
162
+ const [headerPart, payloadPart, signaturePart] = parts;
163
+ let header;
164
+ try {
165
+ header = JSON.parse(new TextDecoder().decode(fromBase64Url(headerPart)));
166
+ } catch {
167
+ throw new Error("vault-runtime: malformed compact JWS");
168
+ }
169
+ if (!header || Array.isArray(header) || typeof header !== "object") {
170
+ throw new Error("vault-runtime: malformed compact JWS");
171
+ }
172
+ const headerRecord = header;
173
+ if (headerRecord.alg !== "HS256" || headerRecord.typ !== "JWT")
174
+ throw new Error("vault-runtime: unsupported JWS header");
175
+ if (!hmacVerify("hmac-sha256", key, utf8(`${headerPart}.${payloadPart}`), fromBase64Url(signaturePart))) {
176
+ throw new Error("vault-runtime: invalid JWS signature");
177
+ }
178
+ let parsedClaims;
179
+ try {
180
+ parsedClaims = JSON.parse(new TextDecoder().decode(fromBase64Url(payloadPart)));
181
+ } catch {
182
+ throw new Error("vault-runtime: malformed JWT claims");
183
+ }
184
+ if (!parsedClaims || Array.isArray(parsedClaims) || typeof parsedClaims !== "object") {
185
+ throw new Error("vault-runtime: JWT claims must be an object");
186
+ }
187
+ const claims = parsedClaims;
188
+ const now = options.nowSec ?? Math.floor(Date.now() / 1000);
189
+ if (claims.exp !== undefined && typeof claims.exp !== "number")
190
+ throw new Error("vault-runtime: JWT exp must be a NumericDate");
191
+ if (claims.nbf !== undefined && typeof claims.nbf !== "number")
192
+ throw new Error("vault-runtime: JWT nbf must be a NumericDate");
193
+ if (typeof claims.exp === "number" && now >= claims.exp)
194
+ throw new Error("vault-runtime: JWT expired");
195
+ if (typeof claims.nbf === "number" && now < claims.nbf)
196
+ throw new Error("vault-runtime: JWT not active");
197
+ if (options.issuer !== undefined && claims.iss !== options.issuer)
198
+ throw new Error("vault-runtime: JWT issuer mismatch");
199
+ if (options.audience !== undefined) {
200
+ const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
201
+ if (audiences.some((audience) => typeof audience !== "string"))
202
+ throw new Error("vault-runtime: JWT aud must contain strings");
203
+ if (!audiences.includes(options.audience))
204
+ throw new Error("vault-runtime: JWT audience mismatch");
205
+ }
206
+ return claims;
207
+ }
208
+ var ed25519PublicKey = (secret) => ed25519.getPublicKey(secret);
209
+ var ed25519Sign = (secret, message) => ed25519.sign(message, secret);
210
+ var ed25519Verify = (publicKey, message, signature) => ed25519.verify(signature, message, publicKey);
211
+ var secp256k1PublicKey = (secret, compressed = true) => secp256k1.getPublicKey(secret, compressed);
212
+ var secp256k1SignDigest = (secret, digest) => {
213
+ if (digest.length !== 32)
214
+ throw new Error("vault-runtime: secp256k1 requires a 32-byte digest");
215
+ return secp256k1.sign(digest, secret, { prehash: false, format: "recovered" });
216
+ };
217
+ var secp256k1VerifyDigest = (publicKey, digest, signature) => digest.length === 32 && secp256k1.verify(signature, digest, publicKey, { prehash: false, format: "recovered" });
218
+ function chainPath(chain) {
219
+ const bound = (value, label, fallback) => {
220
+ const result = value ?? fallback;
221
+ if (!Number.isInteger(result) || result < 0 || result >= 2147483648) {
222
+ throw new Error(`vault-runtime: ${label} must be in 0..2147483647`);
223
+ }
224
+ return result;
225
+ };
226
+ const account = bound(chain.account, "account", 0);
227
+ const change = bound(chain.change, "change", 0);
228
+ const index = bound(chain.index, "index", 0);
229
+ return chain.chain === "ethereum" ? `m/44'/60'/${account}'/${change}/${index}` : `m/44'/501'/${account}'/${change}'/${index}'`;
230
+ }
231
+ function ethereumAddress(uncompressedPublicKey) {
232
+ if (uncompressedPublicKey.length !== 65 || uncompressedPublicKey[0] !== 4) {
233
+ throw new Error("vault-runtime: Ethereum requires an uncompressed secp256k1 public key");
234
+ }
235
+ const lower = Array.from(keccak_256(uncompressedPublicKey.slice(1)).slice(-20), (byte) => byte.toString(16).padStart(2, "0")).join("");
236
+ const checksum = Array.from(keccak_256(utf8(lower)), (byte) => byte.toString(16).padStart(2, "0")).join("");
237
+ let address = "0x";
238
+ for (let index = 0;index < lower.length; index += 1) {
239
+ const character = lower[index];
240
+ address += Number.parseInt(checksum[index], 16) >= 8 ? character.toUpperCase() : character;
241
+ }
242
+ return address;
243
+ }
244
+ var solanaAddress = (publicKey) => base58.encode(publicKey);
245
+ export {
246
+ wrapWithKek,
247
+ verifyCompactJws,
248
+ validatePasswordPolicy,
249
+ unwrapWithKek,
250
+ toBase64Url,
251
+ solanaAddress,
252
+ signCompactJws,
253
+ secp256k1VerifyDigest,
254
+ secp256k1SignDigest,
255
+ secp256k1PublicKey,
256
+ passwordFromBytes,
257
+ hmacVerify,
258
+ hmacSign,
259
+ fromBase64Url,
260
+ ethereumAddress,
261
+ encryptWithDek,
262
+ ed25519Verify,
263
+ ed25519Sign,
264
+ ed25519PublicKey,
265
+ decryptWithDek,
266
+ chainPath,
267
+ VAULT_RUNTIME_KINDS,
268
+ VAULT_RUNTIME_CHAINS,
269
+ VAULT_RUNTIME_ALGORITHM_IDS,
270
+ VAULT_RUNTIME_ALGORITHMS,
271
+ DEFAULT_PASSWORD_ALPHABET
272
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/runtime",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -59,6 +59,10 @@
59
59
  "types": "./dist/identity.d.ts",
60
60
  "default": "./dist/identity.js"
61
61
  },
62
+ "./vault-runtime": {
63
+ "types": "./dist/vault-runtime.d.ts",
64
+ "default": "./dist/vault-runtime.js"
65
+ },
62
66
  "./passkey-hybrid": {
63
67
  "types": "./dist/passkey-hybrid.d.ts",
64
68
  "default": "./dist/passkey-hybrid.js"
@@ -134,13 +138,14 @@
134
138
  "prepublishOnly": "bun ../tools/package-task.ts prepublish runtime"
135
139
  },
136
140
  "dependencies": {
137
- "@forgezero/access": "^0.1.12"
141
+ "@forgezero/access": "^0.1.13"
138
142
  },
139
143
  "peerDependencies": {
140
144
  "@noble/ciphers": "^2.2.0",
141
145
  "@noble/curves": "^2.2.0",
142
146
  "@noble/post-quantum": "^0.6.1",
143
147
  "@noble/hashes": "^2.2.0",
148
+ "@scure/base": "^2.2.0",
144
149
  "@sinclair/typebox": "^0.34.0",
145
150
  "@scure/bip39": "^2.2.0"
146
151
  },
@@ -162,6 +167,9 @@
162
167
  },
163
168
  "@scure/bip39": {
164
169
  "optional": true
170
+ },
171
+ "@scure/base": {
172
+ "optional": true
165
173
  }
166
174
  },
167
175
  "devDependencies": {
@@ -170,9 +178,12 @@
170
178
  "@sinclair/typebox": "^0.34.0",
171
179
  "@noble/curves": "^2.2.0",
172
180
  "@noble/post-quantum": "^0.6.1",
181
+ "@noble/ciphers": "^2.2.0",
182
+ "@noble/hashes": "^2.2.0",
183
+ "@scure/base": "^2.2.0",
173
184
  "@scure/bip39": "^2.2.0"
174
185
  },
175
- "description": "The machinery a service needs behind its request handlers: jobs, keyed queues, a transactional outbox, a hash-chained audit trail, templated mail, encrypted backups and schema validation.",
186
+ "description": "Server-neutral runtime primitives for jobs, queues, outbox, audit, Vault cryptography, finance, identity, recovery and deployment automation.",
176
187
  "keywords": [
177
188
  "jobs",
178
189
  "scheduler",
@@ -186,7 +197,10 @@
186
197
  "validation",
187
198
  "calendar",
188
199
  "billing-period",
189
- "working-days"
200
+ "working-days",
201
+ "vault-runtime",
202
+ "envelope-encryption",
203
+ "signing"
190
204
  ],
191
205
  "license": "MIT",
192
206
  "homepage": "https://www.forgezero.net/docs/runtime",