Skip to content

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Feb 11, 2026

Summary

  • SyncAuthService: HMAC-SHA256 request signing and verification with nonce-based replay protection, key-id rotation support, clock skew validation, and enforce/log-only upgrade mode
  • serve({ auth }): Server-side auth via { keys, mode } — instantiates SyncAuthService internally
  • syncWith(url, { auth }): Client-side signing via { secret, keyId } — signs each outgoing request with fresh nonce
  • 65 new tests (42 unit + 15 integration + 8 E2E over real HTTP), zero regressions across 3,200 total tests
  • SECURITY.md: Threat model, restart semantics, key rotation guide
  • Types: SyncAuthServerOptions, SyncAuthClientOptions in index.d.ts

New files

File Purpose
src/domain/services/SyncAuthService.js Core auth: canonical payload, signing, verification, nonce cache, metrics
test/unit/domain/services/SyncAuthService.test.js 42 unit tests
test/unit/domain/services/HttpSyncServer.auth.test.js 15 integration tests
test/unit/domain/WarpGraph.syncAuth.test.js 8 E2E tests (real HTTP)

Modified files

File Change
src/domain/services/HttpSyncServer.js Extracted checkBodySize(), added auth step via _checkAuth()
src/domain/WarpGraph.js Added auth to serve()/syncWith(), extracted buildSyncAuthHeaders()
index.d.ts SyncAuthServerOptions, SyncAuthClientOptions, updated method signatures
SECURITY.md Sync authentication section
docs/GUIDE.md Auth example in sync server section
CHANGELOG.md v10.6.0 entry
ROADMAP.md M1.T1.SHIELD marked DONE
package.json Version bump 10.5.0 → 10.6.0

Test plan

  • SyncAuthService.test.js — 42 tests (canonical payload, path canonicalization, signing, all reject/happy paths, metrics, constructor)
  • HttpSyncServer.auth.test.js — 15 tests (enforce, log-only, backward compat, DoS guard ordering)
  • WarpGraph.syncAuth.test.js — 8 E2E tests (enforce+matching key, no auth, wrong secret, wrong key-id, log-only, no-auth server, fresh nonces, multi-key)
  • Full suite: 3,200 tests across 157 files — zero regressions
  • ESLint clean (no complexity relaxation)
  • TypeScript clean (tsc --noEmit)
  • TS policy check passed

Summary by CodeRabbit

  • New Features

    • Optional HMAC-SHA256 sync authentication (enforce/log-only) and per-peer auth options
    • Structural diff (SEEKDIFF) in seek with CLI flags and plain-text ASCII rendering, plus defensive state snapshot API
  • Bug Fixes

    • Multiple code-review fixups across nonce/auth validation, diff logic, numeric parsing, and wording
  • Tests

    • Expanded unit, integration, and CLI tests for auth, diff generation, rendering, limits, and navigation
  • Chores

    • Version bumped to 10.6.1 and roadmap tooling/script removed; changelog/roadmap updated

Add --diff and --diff-limit flags to `git warp seek` that show which
nodes/edges were added/removed and which properties changed between
ticks, with old/new values.

- WarpGraph.getStateSnapshot(): defensive copy of materialized state
- computeStructuralDiff(): materialize before/after, call diffStates()
- ASCII renderer: colored +/-/~ lines in Changes (baseline: ...) section
- JSON payload: structuralDiff, diffBaseline, baselineTick, truncated
- 8 unit tests, 7 renderer tests, 4 BATS E2E tests
- SEEKDIFF milestone (4 tasks) added to roadmap, all closed
@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Adds SEEKDIFF structural state-diffing and CLI rendering, implements SHIELD HMAC-SHA256 sync authentication with replay protection, exposes WarpGraph.getStateSnapshot, wires auth into server/client sync, updates docs/roadmap/changelog, adds extensive tests, and removes the standalone roadmap script.

Changes

Cohort / File(s) Summary
Changelog & Version
CHANGELOG.md, jsr.json, package.json
Add changelog entries for SEEKDIFF and SHIELD; bump version to 10.6.1; remove roadmap script.
Roadmap & Support Docs
ROADMAP.md, FIXUPS.md, SECURITY.md, docs/GUIDE.md
Promote SEEKDIFF to a milestone with task details; add FIXUPS self-review; add SHIELD security doc and update guide examples to show auth options.
CLI / Seek Handler
bin/warp-graph.js
Add --diff/--diff-limit flags, compute/apply structural diffs (computeStructuralDiff, applyDiffLimit), validate flags/action combos, and include formatted diff in seek payloads.
Domain / WarpGraph API
src/domain/WarpGraph.js, index.d.ts
Add getStateSnapshot() public method; extend serve() and syncWith() options to accept auth configuration; build client auth headers via signSyncRequest/canonicalizePath.
Sync Auth Service
src/domain/services/SyncAuthService.js, src/domain/services/HttpSyncServer.js
Add SyncAuthService (canonicalization, signing, verification, nonce replay protection, metrics); wire optional auth into HttpSyncServer with pre-parse auth checks and enforce/log-only modes.
Rendering / ASCII Seek
src/visualization/renderers/ascii/seek.js
Export formatStructuralDiff(payload); add structural-diff formatting helpers, truncation hints, and extend SeekPayload shape to include diff metadata.
StateDiff Integration
src/domain/services/StateDiff.js (imported), bin/warp-graph.js
Import and use diffStates to compute deterministic structural diffs; expose formatted diffs to CLI/renderer.
Tests — Auth & Sync
test/unit/domain/services/SyncAuthService.test.js, test/unit/domain/WarpGraph.syncAuth.test.js, test/unit/domain/services/HttpSyncServer.auth.test.js
New unit/integration suites covering signing, verification, modes, replay/clock-skew, multi-key, metrics, and server behavior.
Tests — Seek / Renderer / CLI
test/unit/domain/WarpGraph.seekDiff.test.js, test/unit/visualization/ascii-seek-renderer.test.js, test/bats/cli-seek.bats
Add tests for forward/backward diffs, property diffs, renderer truncation, --diff/--diff-limit validation, and public formatStructuralDiff.
Misc / Deleted Script
scripts/roadmap.js, test/unit/infrastructure/..., other minor files
Remove the scripts/roadmap.js tool; minor test portability tweak (EPIPE acceptance); typedef/comment fixes and other nits.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as User/CLI
    participant Handler as Seek Handler\n(bin/warp-graph.js)
    participant Warp as WarpGraph\n(src/domain/WarpGraph.js)
    participant StateDiff as StateDiff\n(src/domain/services/StateDiff.js)
    participant Renderer as ASCII Renderer\n(src/visualization/renderers/ascii/seek.js)
    participant Auth as SyncAuthService\n(src/domain/services/SyncAuthService.js)
    participant Server as HttpSyncServer\n(src/domain/services/HttpSyncServer.js)

    CLI->>Handler: invoke seek (--diff [--diff-limit])
    Handler->>Warp: ensure materialized state / getStateSnapshot()
    Warp-->>Handler: state snapshot
    Handler->>StateDiff: diffStates(baseline, current)
    StateDiff-->>Handler: structural diff
    Handler->>Handler: applyDiffLimit() → payload.structuralDiff
    alt sync over HTTP with auth
        Handler->>Auth: signSyncRequest(...) (client)
        Handler->>Server: HTTP sync request with auth headers and body
        Server->>Auth: verify(request) (server)
        Auth-->>Server: verification result
        Server-->>Handler: sync response
    end
    Handler->>Renderer: formatStructuralDiff(payload)
    Renderer-->>Handler: formatted output
    Handler-->>CLI: display seek output + structural diff
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through ticks and signed each note,
Baselines cloned and tiny changes wrote.
Nodes that jumped and props that turned,
A nonce to guard where secrets burned.
SEEKDIFF and SHIELD — carrots earned! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the primary change: HMAC-SHA256 sync auth with replay protection (SHIELD protocol) released as v10.6.0. It is specific, concise, and directly reflects the major feature added in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 87.18% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch seekdiff

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/domain/services/SyncAuthService.js (1)

54-83: Normalize the path inside signSyncRequest to reduce mismatch foot‑guns.

Right now callers must pre-canonicalize the path; signing a full URL or a non-normalized path will fail against server canonicalization. Consider canonicalizing internally (and adjust the docstring accordingly).

♻️ Suggested adjustment
 export async function signSyncRequest({ method, path, contentType, body, secret, keyId }, { crypto } = {}) {
   const c = crypto || defaultCrypto;
   const timestamp = String(Date.now());
   const nonce = globalThis.crypto.randomUUID();
+  const canonicalPath = canonicalizePath(path);
 
   const bodySha256 = await c.hash('sha256', body);
   const canonical = buildCanonicalPayload({
     keyId,
     method: method.toUpperCase(),
-    path,
+    path: canonicalPath,
     timestamp,
     nonce,
     contentType,
     bodySha256,
   });

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Release Preflight

  • package version: 10.5.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.5.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bin/warp-graph.js (1)

129-178: ⚠️ Potential issue | 🟡 Minor

--diff and --diff-limit flags are missing from the help text.

The HELP_TEXT under "Seek options:" doesn't list the new --diff or --diff-limit=N flags. Users relying on --help won't discover these capabilities.

📝 Proposed fix
 Seek options:
   --tick <N|+N|-N>      Jump to tick N, or step forward/backward
   --latest              Clear cursor, return to present
   --save <name>         Save current position as named cursor
   --load <name>         Restore a saved cursor
   --list                List all saved cursors
   --drop <name>         Delete a saved cursor
+  --diff                Show structural diff (nodes/edges/props added/removed)
+  --diff-limit <N>      Cap diff output lines (default: 2000)
 `;
🤖 Fix all issues with AI agents
In `@bin/warp-graph.js`:
- Around line 2319-2321: The status action for bare `seek` ignores the `--diff`
flag because `seekSpec` is not passed into handleSeekStatus; update the call
that returns handleSeekStatus({ graph, graphName, persistence, activeCursor,
ticks, maxTick, perWriter, frontierHash }) to thread the existing seekSpec
through (e.g., add seekSpec to the object) and update handleSeekStatus's
signature and internal logic to act on seekSpec (or, if intended, add a clear
warning/log when seekSpec/--diff is provided but not applicable); reference
handleSeekStatus and the variable seekSpec when making the change.

In `@src/domain/WarpGraph.js`:
- Around line 2819-2825: getStateSnapshot currently immediately returns null
when this._cachedState is falsy, skipping auto-materialization; change it so
that if this._cachedState is falsy you first check the autoMaterialize flag
(e.g., this.autoMaterialize or this._options.autoMaterialize) and if
autoMaterialize is true call and await this._ensureFreshState(), then return the
cloned state from cloneStateV5 if it exists; only return null early when
autoMaterialize is false and _cachedState remains null. Make sure to reference
getStateSnapshot, this._cachedState and this._ensureFreshState when updating the
flow.

In `@src/visualization/renderers/ascii/seek.js`:
- Around line 342-373: In buildStructuralDiffLines, the "... and X more changes"
message under the branch where totalEntries > maxLines undercounts and omits the
diff-limit hint when payload.truncated is true; change the logic so the
"remaining" calculation uses the full payload totalChanges (or totalChanges -
shown) instead of entries.length - maxLines and include the muted "(use
--diff-limit to increase)" hint when truncated === true; update the branch that
currently checks if (totalEntries > maxLines) to compute remaining =
totalChanges - shown (or totalChanges - shownChanges if shown is different) and
push the appropriate colors.muted message that includes the diff-limit guidance
when truncated is true.
🧹 Nitpick comments (3)
src/visualization/renderers/ascii/seek.js (1)

268-295: Avoid magic number for diff line cap.

Keep the view limit in sync with the exported constant to prevent divergence.

♻️ Suggested tweak
-  const sdLines = buildStructuralDiffLines(payload, 20);
+  const sdLines = buildStructuralDiffLines(payload, MAX_DIFF_LINES);
bin/warp-graph.js (2)

2520-2537: Double materialization: computeStructuralDiff materializes at currentTick, then the caller materializes again.

In all three action paths (latest, load, tick), computeStructuralDiff already calls graph.materialize({ ceiling: currentTick }) at line 2532. Then the caller immediately re-materializes at the same ceiling (e.g., line 2293 for tick, line 2260 for load, line 2210 for latest). The graph is effectively materialized at the target tick twice.

Consider having computeStructuralDiff return the after-state node/edge counts so the caller can skip the redundant materialization+getNodes+getEdges, or document that the graph is left materialized at currentTick so the caller can rely on that.


2548-2574: Comment says "proportionally" but truncation is sequential/greedy.

Line 2558 says "Truncate each category proportionally" but the cap closure consumes from a shared remaining budget in fixed order: nodes.added → nodes.removed → edges.added → edges.removed → props.set → props.removed. A large first category will starve later ones entirely.

If that's the intended behavior (prioritize showing node changes over edge/prop changes), update the comment to say "sequentially" or "in priority order." If proportional distribution is desired, the algorithm needs a different approach.

📝 Fix the misleading comment
-  // Truncate each category proportionally, keeping order
+  // Truncate sequentially (nodes → edges → props), keeping sort order within each category

B1: reject --diff-limit=0 (must be positive integer)
P1-P2: skip redundant re-materialization when --diff already materialized
P3: short-circuit empty diff when prevTick === currentTick
U1: add --diff/--diff-limit to help text
U2: reject --diff on non-navigating actions (save/drop/list/clear-cache)
U3: combined truncation message when display + data limits both active
U4/N3: fix misleading "proportionally" comment to "greedy"
N1: proper JSDoc type for getStateSnapshot
N2: remove dead formatStructuralDiff on status render path
N4: hoist MAX_DIFF_LINES constant, eliminate magic number
N5-N6: proper JSDoc types for diff renderer params
N7-N8: 8 new tests (4 BATS + 3 renderer + 1 truncation edge case)
- Fix JSDoc import path for WarpStateV5 in CLI typedef (bin/ -> ../src/)
- Add structural diff fields to SeekPayload typedef
- Extract buildTruncationHint() to stay under ESLint complexity limit
- Add @type {*} casts in test fixtures for partial payloads
- Restore JSDoc on buildStructuralDiffLines after refactor
@github-actions
Copy link

Release Preflight

  • package version: 10.5.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.5.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@bin/warp-graph.js`:
- Around line 2084-2091: The DIFF_ACTIONS set currently contains 'status' so
passing --diff with a seek/status action (spec.action handled by
handleSeekStatus) is accepted but no diff is produced; either remove 'status'
from DIFF_ACTIONS so usageError is thrown for spec.action === 'status', or
implement diff generation in handleSeekStatus so the --diff flag is honored.
Locate the DIFF_ACTIONS definition and the condition that throws usageError, and
then either (A) delete 'status' from DIFF_ACTIONS (ensuring usageError(`--diff
cannot be used with --${spec.action}`) runs for seek/status) or (B) update
handleSeekStatus to detect spec.diff and call the same diff computation/output
path used by the tick/load handlers so --diff produces structural output. Ensure
tests or manual runs validate the chosen behavior.

In `@src/visualization/renderers/ascii/seek.js`:
- Around line 335-341: Remove the orphaned JSDoc block that precedes the current
buildStructuralDiffLines implementation (the leftover comment referring to
buildTruncationHint around the earlier comment block), so delete the detached
comment lines describing "Builds structural diff lines for the seek dashboard"
(the JSDoc at lines just above buildStructuralDiffLines) and keep the valid
JSDoc already present above the actual buildStructuralDiffLines function; ensure
no other comment content needed by buildTruncationHint or
buildStructuralDiffLines is removed.
🧹 Nitpick comments (1)
src/visualization/renderers/ascii/seek.js (1)

20-20: Consider splitting the SeekPayload typedef across multiple lines for readability.

This single-line typedef with 12+ fields is difficult to scan and maintain. A multi-line @typedef with individual @property annotations would be easier to review and extend.

Previously returned null when no cached state existed, even with
autoMaterialize enabled. Now delegates to _ensureFreshState() which
triggers auto-materialization when the flag is set.

Addresses PR #23 review comment (coderabbit).
DIFF_ACTIONS no longer includes 'status'. Running
`git warp seek --diff` without --tick/--latest/--load now fails fast
with a clear usage error instead of silently ignoring the flag.

Addresses PR #23 review comment (coderabbit).
Leftover from extracting the function out of buildStructuralDiffLines.
The actual function already has its own JSDoc at lines 356-360.

Addresses PR #23 review comment (coderabbit).
The server may reset (ECONNRESET) or break the pipe (EPIPE) depending
on platform timing. Both are valid rejection behaviors.
@github-actions
Copy link

Release Preflight

  • package version: 10.5.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.5.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bin/warp-graph.js (1)

2025-2088: ⚠️ Potential issue | 🟡 Minor

--diff-limit is silently ignored without --diff.

Right now the flag can be provided alone and becomes a no-op. Consider tracking explicit usage and rejecting when --diff isn’t set.

🐛 Proposed fix
 function parseSeekArgs(args) {
   /** `@type` {SeekSpec} */
   const spec = {
     action: 'status', // status, tick, latest, save, load, list, drop, clear-cache
     tickValue: null,
     name: null,
     noPersistentCache: false,
     diff: false,
     diffLimit: 2000,
   };
+  let diffLimitProvided = false;

   for (let i = 0; i < args.length; i++) {
     const arg = args[i];
@@
     } else if (arg === '--diff-limit' || arg.startsWith('--diff-limit=')) {
       handleDiffLimitFlag(arg, args, i, spec);
+      diffLimitProvided = true;
       if (arg === '--diff-limit') {
         i += 1;
       }
@@
   const DIFF_ACTIONS = new Set(['tick', 'latest', 'load']);
   if (spec.diff && !DIFF_ACTIONS.has(spec.action)) {
     throw usageError(`--diff cannot be used with --${spec.action}`);
   }
+  if (diffLimitProvided && !spec.diff) {
+    throw usageError('--diff-limit requires --diff');
+  }

   return spec;
 }
🤖 Fix all issues with AI agents
In `@bin/warp-graph.js`:
- Around line 1967-1989: The handleDiffLimitFlag function currently uses
parseInt which silently truncates floats (e.g., "1.5" -> 1); change the parsing
to use Number(raw) (or parseFloat(raw)) and then validate that the result is a
finite integer and >= 1 before assigning to spec.diffLimit so inputs like
"--diff-limit=1.5" are rejected; update validation to check Number.isFinite(n)
&& Number.isInteger(n) && n >= 1 and keep the existing usageError on invalid
values.

In `@src/visualization/renderers/ascii/seek.js`:
- Around line 354-372: In buildTruncationHint, clamp the computed "more changes"
numbers to avoid negatives: when computing omitted for the first branch (inside
buildTruncationHint) use a non-negative clamp like Math.max(0, (totalChanges ||
0) - shown) and likewise clamp the third-branch expression ((totalChanges || 0)
- (shownChanges || 0)); also ensure the second branch uses Math.max(0,
totalEntries - maxLines) so all returned counts are never negative.

Add SyncAuthService with canonical payload signing, nonce-based replay
defense, key-id rotation support, and enforce/log-only upgrade mode.
65 new tests (42 unit + 15 integration + 8 E2E), zero regressions.
@flyingrobots flyingrobots changed the title feat: structural seek diff (SEEKDIFF, v10.5.0) feat: HMAC-SHA256 sync auth with replay protection (SHIELD, v10.6.0) Feb 11, 2026
@github-actions
Copy link

Release Preflight

  • package version: 10.6.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.6.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/domain/services/HttpSyncServer.js`:
- Around line 183-195: The initAuth function currently trusts any truthy
auth.mode and returns it, causing invalid strings to be treated like 'log-only';
update initAuth to validate auth.mode against the allowed values 'enforce' and
'log-only' (e.g., check if auth.mode === 'enforce' or auth.mode === 'log-only')
and only set authMode to one of those; if auth.mode is missing or invalid,
choose a safe default (for example null or 'enforce' per project policy) before
returning { auth: new SyncAuthService(auth), authMode: validatedMode } to
prevent accidental log-only behavior.

In `@src/domain/services/SyncAuthService.js`:
- Around line 329-376: In verify(), move the call to this._reserveNonce(nonce)
so it runs after the await this._verifySignature(...) succeeds: keep header,
freshness, key resolution and signature verification in order, then call
_reserveNonce(nonce) and handle its failure (increment
this._metrics.authFailCount, log 'sync auth: replay detected' with { keyId,
nonce } and return the nonceResult) only after sigResult.ok; update the success
path to return { ok: true } only when signature verification and nonce
reservation both succeed. Reference: verify, _reserveNonce, _resolveKey,
_verifySignature.

@github-actions
Copy link

Release Preflight

  • package version: 10.6.1
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v10.6.1, release workflow will publish.

@flyingrobots flyingrobots merged commit 81b8f3c into main Feb 11, 2026
7 checks passed
@flyingrobots flyingrobots deleted the seekdiff branch February 11, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant