Sign engrams the way Nostr will verify them

da9837e4ebc1 · AtlantisPleb · · parent 2a54d8f5aa51

Sign engrams the way Nostr will verify them

The engram event shape was already NIP-01 — the id is the sha256 over
the canonical [0, pubkey, created_at, kind, tags, content] tuple — but
the signer was a local HMAC and the pubkey was derived from it. An
engram was integrity-checked against accidental edits and verifiable
by nobody else, which is not what a signed record is for.

Signing is real secp256k1 Schnorr now. The key file keeps its place
and its permissions; its 32 bytes are read as a Nostr private key, the
x-only public key is derived from them, and every read verifies the
signature and the author alongside the event id and the supersession
chain. A tampered engram, a foreign-signed one, and an HMAC-era one
all fail verification and are simply not projected — the reader
already drops what does not verify, so an old ledger degrades to empty
rather than crashing, which the tests pin.

On the dependency: `@noble/curves` and `@noble/hashes`, at exactly the
versions `nostr-effect` itself pins. That is deliberately not a
parallel Nostr implementation — it is the same audited primitive the
workspace's own Nostr package is built on, for the one operation
needed here. When relay transport lands behind EngramTransport, that
is protocol and client surface, and it should go through nostr-effect
rather than growing more of it here.

Built by a Devin child through the openagents coder's delegate tool;
839 CLI tests and 128 package tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GoYpb8FEmdxVErsv7ABCYi
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified packages/openagents-cli/package.json
  • modified packages/openagents-cli/src/coder-memory.ts
  • modified packages/openagents-cli/test/coder-memory.test.ts
  • modified pnpm-lock.yaml

Diff

4 files changed, +250 -57

packages/openagents-cli/package.json modified +2

@@ -56,6 +56,8 @@

56 56
    "@effect/platform-node": "catalog:",
57 57
    "@effect/platform-node-shared": "4.0.0-beta.94",
58 58
    "effect": "catalog:",
59
    "@noble/curves": "1.8.1",
60
    "@noble/hashes": "1.7.1",
59 61
    "ollama": "^0.6.3",
60 62
    "ws": "8.21.1"
61 63
  },
packages/openagents-cli/src/coder-memory.ts modified +107 -51

@@ -1,4 +1,4 @@

1
import { createHmac, randomBytes } from "node:crypto";
1
import { randomBytes } from "node:crypto";
2 2
import {
3 3
  appendFileSync,
4 4
  chmodSync,

@@ -9,28 +9,31 @@ import {

9 9
} from "node:fs";
10 10
import { homedir } from "node:os";
11 11
import { join } from "node:path";
12
import { schnorr } from "@noble/curves/secp256k1";
13
import { bytesToHex } from "@noble/hashes/utils";
12 14
13 15
import {
14 16
  buildEngramBody,
15
  buildEngramEvent,
17
  buildSupersedingBody,
16 18
  buildSubagentMemoryContext,
19
  computeEngramEventId,
17 20
  consolidateEpisodes,
18 21
  engramContentDigest,
19 22
  guardEngramContent,
20 23
  harvestSubagentOutcome,
21 24
  ledgerEntriesAsHeuristics,
22 25
  promoteHeuristicToPattern,
23
  signSupersedingEngram,
24 26
  EngramSyncQueue,
27
  EngramBody,
28
  EngramEvent,
29
  ENGRAM_ALT,
25 30
  MemoryTransport,
26 31
  project,
27 32
  projectedValue,
28 33
  projectionMatches,
29
  verifyEngramEventId,
30 34
  COMPANION_SCHEMA_ID,
35
  ENGRAM_KIND,
31 36
  HarvestedLedgerEntry,
32
  type EngramEvent,
33
  type EngramBody,
34 37
  type EngramTransport,
35 38
  type ParentHeuristic,
36 39
  type Projection,

@@ -47,13 +50,13 @@ import { Schema as S } from "effect";

47 50
 * bounded advisory block (#226). Between sessions the ledger is the memory —
48 51
 * one JSONL file of NIP-AE-shaped engram events under `~/.openagents/memory`,
49 52
 * every value through the hard-unsafe redaction gate before it is signed, and
50
 * every read re-verifying event ids and supersession chains.
53
 * every read re-verifying event ids, NIP-01 Schnorr signatures, supersession
54
 * chains, and author identity.
51 55
 *
52
 * The signature is a local HMAC over the canonical event id with a key held at
53
 * `~/.openagents/memory/signing-key` (0600) — integrity against accidental
54
 * edits and a stable authorship mark for this machine, not a Nostr Schnorr
55
 * signature. When the relay sync adapter (#222) lands, the same events re-sign
56
 * under a real Nostr key; the body and chain shapes are already NIP-AE.
56
 * The secret key is a 32-byte Nostr private key held at
57
 * `~/.openagents/memory/signing-key` (0600). The same key file is reused from
58
 * the HMAC era: the 64-hex bytes are now interpreted as a secp256k1 private
59
 * key. Old HMAC-signed events fail verification and are not projected.
57 60
 */
58 61
59 62
const decodeLedgerEntry = S.decodeUnknownSync(HarvestedLedgerEntry);

@@ -125,7 +128,7 @@ export class CoderMemory implements CoderDelegationMemory {

125 128
  private readonly projectScope: string;
126 129
  private readonly now: () => number;
127 130
  private readonly dreamThreshold: number;
128
  private key: Buffer | undefined;
131
  private cachedIdentity: { readonly secretKey: Uint8Array; readonly pubkey: string } | undefined;
129 132
  private cachedProjection: Projection | undefined;
130 133
  private readonly sync: EngramSyncQueue;
131 134

@@ -158,27 +161,67 @@ export class CoderMemory implements CoderDelegationMemory {

158 161
    return this.sync.drain();
159 162
  }
160 163
161
  /** The local signing key, created on first use. */
162
  private signingKey(): Buffer {
163
    if (this.key !== undefined) return this.key;
164
  /**
165
   * The local secp256k1 private key, created on first use and stored 0600.
166
   *
167
   * HMAC-era `signing-key` files were also 64-hex bytes, but their value was
168
   * used as an HMAC secret. NIP-01 migrations keep the same file and treat the
169
   * 32 bytes as a secp256k1 private key. If the bytes are not a valid Nostr
170
   * private key, the file is overwritten with a fresh generated key and the
171
   * ledger key changes. Old HMAC-signed events then fail author/signature
172
   * verification and are not projected.
173
   */
174
  private signingKey(): { readonly secretKey: Uint8Array; readonly pubkey: string } {
175
    if (this.cachedIdentity !== undefined) return this.cachedIdentity;
164 176
    mkdirSync(this.directory, { recursive: true, mode: 0o700 });
165
    if (!existsSync(this.keyPath)) {
166
      writeFileSync(this.keyPath, randomBytes(32).toString("hex"), { mode: 0o600 });
167
      chmodSync(this.keyPath, 0o600);
177
    let loaded: string | undefined;
178
    if (existsSync(this.keyPath)) {
179
      const raw = readFileSync(this.keyPath, "utf8").trim().toLowerCase();
180
      if (/^[0-9a-f]{64}$/.test(raw)) {
181
        loaded = raw;
182
      }
183
    }
184
185
    if (loaded !== undefined) {
186
      const secretKey = Buffer.from(loaded, "hex");
187
      try {
188
        const pubkey = bytesToHex(schnorr.getPublicKey(secretKey));
189
        this.cachedIdentity = { secretKey, pubkey };
190
        return this.cachedIdentity;
191
      } catch {
192
        // Invalid legacy key; fall through to generate a fresh one.
193
      }
168 194
    }
169
    this.key = Buffer.from(readFileSync(this.keyPath, "utf8").trim(), "hex");
170
    return this.key;
195
196
    let secretKey: Uint8Array;
197
    let pubkey: string;
198
    do {
199
      secretKey = randomBytes(32);
200
      try {
201
        pubkey = bytesToHex(schnorr.getPublicKey(secretKey));
202
      } catch {
203
        pubkey = "";
204
      }
205
    } while (pubkey === "");
206
    this.writePrivateKey(Buffer.from(secretKey).toString("hex"));
207
    this.cachedIdentity = { secretKey, pubkey };
208
    return this.cachedIdentity;
171 209
  }
172 210
173
  private signer(): { pubkey: string; sign: (eventId: string) => string } {
174
    const key = this.signingKey();
175
    // A stable 64-hex identity derived from the key, so events from the same
176
    // machine share an author without the key itself ever leaving the file.
177
    const pubkey = createHmac("sha256", key).update("openagents.coder-memory.pubkey").digest("hex");
178
    return {
179
      pubkey,
180
      sign: (eventId: string) => createHmac("sha256", key).update(eventId).digest("hex"),
181
    };
211
  private writePrivateKey(privateKey: string): void {
212
    writeFileSync(this.keyPath, privateKey, { mode: 0o600 });
213
    chmodSync(this.keyPath, 0o600);
214
  }
215
216
  /** True when the event id, signature, and author all match this key. */
217
  private isValidEngram(event: EngramEvent): boolean {
218
    if (event.pubkey !== this.signingKey().pubkey) return false;
219
    if (event.id !== computeEngramEventId(event)) return false;
220
    try {
221
      return schnorr.verify(event.sig, event.id, event.pubkey);
222
    } catch {
223
      return false;
224
    }
182 225
  }
183 226
184 227
  /**

@@ -201,14 +244,17 @@ export class CoderMemory implements CoderDelegationMemory {

201 244
      relations: [],
202 245
      derivedFromSlugs: [...derivedFromSlugs],
203 246
    });
204
    const { pubkey, sign } = this.signer();
205
    const event = buildEngramEvent(
206
      pubkey,
207
      Math.floor(this.now() / 1000),
208
      slug,
209
      JSON.stringify(body),
210
      sign,
211
    );
247
    const { secretKey, pubkey } = this.signingKey();
248
    const created_at = Math.floor(this.now() / 1000);
249
    const tags = [
250
      ["d", slug],
251
      ["alt", ENGRAM_ALT],
252
    ];
253
    const content = JSON.stringify(body);
254
    const partial = { kind: ENGRAM_KIND, created_at, tags, content, pubkey };
255
    const id = computeEngramEventId(partial as unknown as EngramEvent);
256
    const sig = bytesToHex(schnorr.sign(id, secretKey));
257
    const event = S.decodeUnknownSync(EngramEvent)({ kind: ENGRAM_KIND, created_at, tags, content, pubkey, id, sig });
212 258
    appendFileSync(this.ledgerPath, `${JSON.stringify(event)}\n`, { mode: 0o600 });
213 259
    this.cachedProjection = undefined;
214 260
    // Local-first: the engram is on disk and readable now. Sync catches up.

@@ -227,36 +273,46 @@ export class CoderMemory implements CoderDelegationMemory {

227 273
    if (prior === undefined) return undefined;
228 274
    const verdict = guardEngramContent(newValue);
229 275
    if (!verdict.storable) return undefined;
230
    const { pubkey, sign } = this.signer();
231
    const event = signSupersedingEngram(
232
      prior,
233
      verdict.redacted,
234
      Math.max(Math.floor(this.now() / 1000), prior.created_at + 1),
235
      pubkey,
236
      sign,
237
    );
276
    const dTag = prior.tags.find((tag) => tag[0] === "d")?.[1];
277
    if (dTag === undefined) return undefined;
278
    const priorBody = S.decodeUnknownSync(EngramBody)(JSON.parse(prior.content));
279
    const body = buildSupersedingBody(priorBody, verdict.redacted, prior.id);
280
    const { secretKey, pubkey } = this.signingKey();
281
    const created_at = Math.max(Math.floor(this.now() / 1000), prior.created_at + 1);
282
    const tags = [
283
      ["d", dTag],
284
      ["alt", ENGRAM_ALT],
285
    ];
286
    const content = JSON.stringify(body);
287
    const partial = { kind: ENGRAM_KIND, created_at, tags, content, pubkey };
288
    const id = computeEngramEventId(partial as unknown as EngramEvent);
289
    const sig = bytesToHex(schnorr.sign(id, secretKey));
290
    const event = S.decodeUnknownSync(EngramEvent)({ kind: ENGRAM_KIND, created_at, tags, content, pubkey, id, sig });
238 291
    appendFileSync(this.ledgerPath, `${JSON.stringify(event)}\n`, { mode: 0o600 });
239 292
    this.cachedProjection = undefined;
240 293
    this.sync.publish(event);
241 294
    return event;
242 295
  }
243 296
244
  /** Every engram on disk, malformed lines dropped. The projection judges them. */
297
  /** Every verified engram on disk; malformed or unverified lines are dropped. */
245 298
  private events(): ReadonlyArray<EngramEvent> {
246 299
    if (!existsSync(this.ledgerPath)) return [];
247 300
    const events: Array<EngramEvent> = [];
248 301
    for (const line of readFileSync(this.ledgerPath, "utf8").split("\n")) {
249 302
      if (line.trim().length === 0) continue;
303
      let event: EngramEvent;
250 304
      try {
251
        events.push(JSON.parse(line) as EngramEvent);
305
        event = S.decodeUnknownSync(EngramEvent)(JSON.parse(line));
252 306
      } catch {
253 307
        continue;
254 308
      }
309
      if (!this.isValidEngram(event)) continue;
310
      events.push(event);
255 311
    }
256 312
    return events;
257 313
  }
258 314
259
  /** All ledger events grouped per slug in append order, invalid lines dropped. */
315
  /** All verified ledger events grouped per slug in append order. */
260 316
  private chains(): Map<string, Array<EngramEvent>> {
261 317
    const chains = new Map<string, Array<EngramEvent>>();
262 318
    if (!existsSync(this.ledgerPath)) return chains;

@@ -264,11 +320,11 @@ export class CoderMemory implements CoderDelegationMemory {

264 320
      if (line.trim().length === 0) continue;
265 321
      let event: EngramEvent;
266 322
      try {
267
        event = JSON.parse(line) as EngramEvent;
323
        event = S.decodeUnknownSync(EngramEvent)(JSON.parse(line));
268 324
      } catch {
269 325
        continue;
270 326
      }
271
      if (!verifyEngramEventId(event)) continue;
327
      if (!this.isValidEngram(event)) continue;
272 328
      const dTag = event.tags.find((tag) => tag[0] === "d")?.[1];
273 329
      if (dTag === undefined) continue;
274 330
      const chain = chains.get(dTag) ?? [];
packages/openagents-cli/test/coder-memory.test.ts modified +113 -2

@@ -1,13 +1,44 @@

1
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
import { createHmac } from "node:crypto";
2
import { appendFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2 3
import { tmpdir } from "node:os";
3 4
import { join } from "node:path";
4 5
import { afterEach, describe, expect, it } from "vitest";
6
import { schnorr } from "@noble/curves/secp256k1";
7
import { bytesToHex } from "@noble/hashes/utils";
5 8
6 9
import { CoderMemory } from "../src/coder-memory.js";
7
import { MemoryTransport } from "../src/memory/index.js";
10
import {
11
  MemoryTransport,
12
  buildEngramBody,
13
  buildEngramEvent,
14
  engramContentDigest,
15
} from "../src/memory/index.js";
8 16
import { DelegateFleet, type DelegateEvent, type DelegateHarness } from "../src/coder-delegate.js";
9 17
import { CoderTaskRegistry } from "../src/coder-tasks.js";
10 18
19
const localPrivateKeyHex = `${"0".repeat(63)}1`;
20
const foreignPrivateKeyHex = `${"0".repeat(63)}2`;
21
22
const runPubkey = (hex: string): string => bytesToHex(schnorr.getPublicKey(Buffer.from(hex, "hex")));
23
24
const signWith = (hex: string) => (message: string): string =>
25
  bytesToHex(schnorr.sign(message, Buffer.from(hex, "hex")));
26
27
const hmacPubkeyFor = (keyHex: string): string =>
28
  createHmac("sha256", Buffer.from(keyHex, "hex")).update("openagents.coder-memory.pubkey").digest("hex");
29
30
const hmacSignFor = (keyHex: string) => (eventId: string): string =>
31
  createHmac("sha256", Buffer.from(keyHex, "hex")).update(eventId).digest("hex");
32
33
const memoryAtWithKey = (
34
  dir: string,
35
  keyHex = localPrivateKeyHex,
36
  nowMs = 1_756_000_000_000,
37
): CoderMemory => {
38
  writeFileSync(join(dir, "signing-key"), keyHex, { mode: 0o600 });
39
  return new CoderMemory({ directory: dir, projectScope: "project:test", now: () => nowMs });
40
};
41
11 42
const dirs: Array<string> = [];
12 43
const freshDir = (): string => {
13 44
  const dir = mkdtempSync(join(tmpdir(), "coder-memory-test-"));

@@ -273,3 +304,83 @@ describe("memory sync", () => {

273 304
    expect(transport.stored()).toHaveLength(1);
274 305
  });
275 306
});
307
308
describe("NIP-01 secp256k1 ledger integrity", () => {
309
  const makeBody = (slug: string, value: string) =>
310
    buildEngramBody(
311
      slug,
312
      value,
313
      {
314
        admission: "admitted",
315
        entityId: "test",
316
        contentDigest: engramContentDigest(value),
317
        sourceEventRefs: [],
318
        relations: [],
319
        derivedFromSlugs: [],
320
      } as const,
321
    );
322
323
  it("records an engram with a 128-hex NIP-01 Schnorr signature and its own public key", () => {
324
    const dir = freshDir();
325
    const memory = memoryAtWithKey(dir, localPrivateKeyHex);
326
    const event = memory.record("note/nip01", "ranged reads are better", "note-1");
327
    expect(event).toBeDefined();
328
    expect(event!.sig).toMatch(/^[0-9a-f]{128}$/);
329
    expect(event!.pubkey).toBe(runPubkey(localPrivateKeyHex));
330
    expect(memoryAtWithKey(dir, localPrivateKeyHex).bodies()).toHaveLength(1);
331
  });
332
333
  it("rejects an event with a valid content id but an invalid signature", () => {
334
    const dir = freshDir();
335
    memoryAtWithKey(dir, localPrivateKeyHex).record("note/one", "a finding", "note-1");
336
    const path = join(dir, "engrams.jsonl");
337
    const lines = readFileSync(path, "utf8").trim().split("\n");
338
    const event = JSON.parse(lines[0]!);
339
    event.sig = "f".repeat(128);
340
    rmSync(path);
341
    writeFileSync(path, `${JSON.stringify(event)}\n`);
342
    expect(memoryAtWithKey(dir, localPrivateKeyHex).bodies()).toHaveLength(0);
343
  });
344
345
  it("rejects a foreign-key engram with a valid NIP-01 signature", () => {
346
    const dir = freshDir();
347
    memoryAtWithKey(dir, localPrivateKeyHex);
348
    const foreignPubkey = runPubkey(foreignPrivateKeyHex);
349
    const foreignSign = signWith(foreignPrivateKeyHex);
350
    const foreignEvent = buildEngramEvent(
351
      foreignPubkey,
352
      1000,
353
      "foreign/one",
354
      JSON.stringify(makeBody("foreign/one", "from someone else")),
355
      foreignSign,
356
    );
357
    appendFileSync(join(dir, "engrams.jsonl"), `${JSON.stringify(foreignEvent)}\n`);
358
    expect(memoryAtWithKey(dir, localPrivateKeyHex).bodies()).toHaveLength(0);
359
  });
360
361
  it("does not crash on an HMAC-era ledger and does not project its events", () => {
362
    const dir = freshDir();
363
    const hmacPubkey = hmacPubkeyFor(localPrivateKeyHex);
364
    const hmacSign = hmacSignFor(localPrivateKeyHex);
365
    const body = makeBody("hmac/one", "hmac-era content");
366
    const hmacEvent = buildEngramEvent(
367
      hmacPubkey,
368
      1000,
369
      "hmac/one",
370
      JSON.stringify(body),
371
      hmacSign,
372
    );
373
    writeFileSync(join(dir, "signing-key"), localPrivateKeyHex, { mode: 0o600 });
374
    writeFileSync(join(dir, "engrams.jsonl"), `${JSON.stringify(hmacEvent)}\n`);
375
    const reader = new CoderMemory({
376
      directory: dir,
377
      projectScope: "project:test",
378
      now: () => 1_756_000_000_000,
379
    });
380
    expect(() => reader.bodies()).not.toThrow();
381
    expect(reader.bodies()).toHaveLength(0);
382
    // New NIP-01 records continue to work using the same key file.
383
    reader.record("note/one", "new fact", "note-1");
384
    expect(reader.bodies()).toHaveLength(1);
385
  });
386
});
pnpm-lock.yaml modified +28 -4

@@ -2214,6 +2214,12 @@ importers:

2214 2214
      '@effect/platform-node-shared':
2215 2215
        specifier: 4.0.0-beta.94
2216 2216
        version: 4.0.0-beta.94(effect@4.0.0-beta.94)
2217
      '@noble/curves':
2218
        specifier: 1.8.1
2219
        version: 1.8.1
2220
      '@noble/hashes':
2221
        specifier: 1.7.1
2222
        version: 1.7.1
2217 2223
      effect:
2218 2224
        specifier: 4.0.0-beta.94
2219 2225
        version: 4.0.0-beta.94

@@ -13367,14 +13373,13 @@ snapshots:

13367 13373
    dependencies:
13368 13374
      '@testing-library/dom': 10.4.1
13369 13375
      '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
13370
      '@vitest/browser': 4.1.10(vite@8.1.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0))(vitest@4.1.10)
13376
      '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.10)
13371 13377
      vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6)
13372 13378
    transitivePeerDependencies:
13373 13379
      - bufferutil
13374 13380
      - msw
13375 13381
      - utf-8-validate
13376 13382
      - vite
13377
    optional: true
13378 13383
13379 13384
  '@vitest/browser-preview@4.1.10(vite@8.1.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0))(vitest@4.1.10)':
13380 13385
    dependencies:

@@ -13387,6 +13392,24 @@ snapshots:

13387 13392
      - msw
13388 13393
      - utf-8-validate
13389 13394
      - vite
13395
    optional: true
13396
13397
  '@vitest/browser@4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.10)':
13398
    dependencies:
13399
      '@blazediff/core': 1.9.1
13400
      '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(typescript@6.0.3)(yaml@2.9.0))
13401
      '@vitest/utils': 4.1.10
13402
      magic-string: 0.30.21
13403
      pngjs: 7.0.0
13404
      sirv: 3.0.2
13405
      tinyrainbow: 3.1.0
13406
      vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6)
13407
      ws: 8.21.1
13408
    transitivePeerDependencies:
13409
      - bufferutil
13410
      - msw
13411
      - utf-8-validate
13412
      - vite
13390 13413
13391 13414
  '@vitest/browser@4.1.10(vite@8.1.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0))(vitest@4.1.10)':
13392 13415
    dependencies:

@@ -13404,6 +13427,7 @@ snapshots:

13404 13427
      - msw
13405 13428
      - utf-8-validate
13406 13429
      - vite
13430
    optional: true
13407 13431
13408 13432
  '@vitest/expect@4.1.10':
13409 13433
    dependencies:

@@ -17428,8 +17452,8 @@ snapshots:

17428 17452
    dependencies:
17429 17453
      '@oxc-project/types': 0.138.0
17430 17454
      '@oxlint/plugins': 1.68.0
17431
      '@vitest/browser': 4.1.10(vite@8.1.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0))(vitest@4.1.10)
17432
      '@vitest/browser-preview': 4.1.10(vite@8.1.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(yaml@2.9.0))(vitest@4.1.10)
17455
      '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.10)
17456
      '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.10)
17433 17457
      '@vitest/expect': 4.1.10
17434 17458
      '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@24.13.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(tsx@4.20.6)(typescript@6.0.3)(yaml@2.9.0))
17435 17459
      '@vitest/pretty-format': 4.1.10

This page updates live while a promote is in flight · changelog