brane 0.1.1 → 0.3.1
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/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { DbTokenStore } from "../db/tokens.js";
|
|
5
|
+
import { TokenRing, UNUSED_TOKEN_LAPSE_MS, hashToken } from "../daemon/token-ring.js";
|
|
6
|
+
import { mintDeviceTokenRoute } from "../daemon/token-routes.js";
|
|
7
|
+
const T0 = Date.UTC(2026, 9, 3, 12);
|
|
8
|
+
const LATE = T0 + UNUSED_TOKEN_LAPSE_MS;
|
|
9
|
+
function setup() {
|
|
10
|
+
const db = createTestDb();
|
|
11
|
+
const store = new DbTokenStore(db);
|
|
12
|
+
const revokedAt = (id) => db.prepare("SELECT revoked_at FROM tokens WHERE id = ?").get(id).revoked_at;
|
|
13
|
+
return { db, store, revokedAt };
|
|
14
|
+
}
|
|
15
|
+
describe("unused device tokens lapse", () => {
|
|
16
|
+
it("refuses a minted device token never used within the window, and revokes its row", () => {
|
|
17
|
+
const { store, revokedAt } = setup();
|
|
18
|
+
const rec = store.insert(hashToken("qr"), "device", "melded", T0);
|
|
19
|
+
assert.equal(store.find(hashToken("qr"), LATE), null);
|
|
20
|
+
assert.equal(revokedAt(rec.id), LATE);
|
|
21
|
+
assert.equal(store.find(hashToken("qr"), LATE + 1), null, "and stays refused");
|
|
22
|
+
});
|
|
23
|
+
it("accepts it just inside the window", () => {
|
|
24
|
+
const { store } = setup();
|
|
25
|
+
const rec = store.insert(hashToken("qr"), "device", "melded", T0);
|
|
26
|
+
assert.equal(store.find(hashToken("qr"), LATE - 1)?.id, rec.id);
|
|
27
|
+
});
|
|
28
|
+
it("keeps a token for good once it has been used", () => {
|
|
29
|
+
const { store } = setup();
|
|
30
|
+
store.insert(hashToken("qr"), "device", "melded", T0);
|
|
31
|
+
assert.ok(store.find(hashToken("qr"), T0 + 1000));
|
|
32
|
+
assert.ok(store.find(hashToken("qr"), T0 + 30 * 24 * 3600_000));
|
|
33
|
+
});
|
|
34
|
+
it("counts a first use recorded on disk by an earlier daemon", () => {
|
|
35
|
+
const { db } = setup();
|
|
36
|
+
const store = new DbTokenStore(db);
|
|
37
|
+
store.insert(hashToken("qr"), "device", "melded", T0);
|
|
38
|
+
store.find(hashToken("qr"), T0 + 1000);
|
|
39
|
+
// A restart: a new store with no memory of that use.
|
|
40
|
+
assert.ok(new DbTokenStore(db).find(hashToken("qr"), LATE + 1));
|
|
41
|
+
});
|
|
42
|
+
it("never lapses the local row or an adopted token", () => {
|
|
43
|
+
const { store } = setup();
|
|
44
|
+
store.insert(hashToken("local"), "local", "this machine", T0);
|
|
45
|
+
store.adopt(hashToken("old-shared"), "device", "melded before hashing", T0);
|
|
46
|
+
assert.ok(store.find(hashToken("local"), LATE * 2));
|
|
47
|
+
assert.ok(store.find(hashToken("old-shared"), LATE * 2));
|
|
48
|
+
});
|
|
49
|
+
it("sweeps unused rows without anyone presenting them, and only those", () => {
|
|
50
|
+
const { store, revokedAt } = setup();
|
|
51
|
+
const unused = store.insert(hashToken("unscanned"), "device", "melded", T0);
|
|
52
|
+
const used = store.insert(hashToken("scanned"), "device", "melded", T0);
|
|
53
|
+
const fresh = store.insert(hashToken("just-minted"), "device", "melded", LATE - 1000);
|
|
54
|
+
const local = store.insert(hashToken("local"), "local", "this machine", T0);
|
|
55
|
+
store.find(hashToken("scanned"), T0 + 1000);
|
|
56
|
+
assert.equal(store.lapseUnused(LATE), 1);
|
|
57
|
+
assert.equal(revokedAt(unused.id), LATE);
|
|
58
|
+
assert.equal(revokedAt(used.id), null);
|
|
59
|
+
assert.equal(revokedAt(fresh.id), null);
|
|
60
|
+
assert.equal(revokedAt(local.id), null);
|
|
61
|
+
assert.equal(store.lapseUnused(LATE), 0, "a second sweep finds nothing");
|
|
62
|
+
});
|
|
63
|
+
it("does not lapse a token used in this process whose last_used_at write was lost", () => {
|
|
64
|
+
const { db, store } = setup();
|
|
65
|
+
const rec = store.insert(hashToken("qr"), "device", "melded", T0);
|
|
66
|
+
assert.ok(store.find(hashToken("qr"), T0 + 1000));
|
|
67
|
+
// Simulate the best-effort stamp having failed.
|
|
68
|
+
db.prepare("UPDATE tokens SET last_used_at = NULL WHERE id = ?").run(rec.id);
|
|
69
|
+
assert.equal(store.lapseUnused(LATE), 0);
|
|
70
|
+
assert.ok(store.find(hashToken("qr"), LATE + 1));
|
|
71
|
+
});
|
|
72
|
+
it("writes a rotation's replacement as used, so a device that only holds a socket keeps it", () => {
|
|
73
|
+
const { db, store } = setup();
|
|
74
|
+
store.insert(hashToken("phone"), "device", "phone", T0);
|
|
75
|
+
const ring = new TokenRing(store, () => "phone-2");
|
|
76
|
+
assert.ok(ring.authenticate("phone", T0 + 1000).ok);
|
|
77
|
+
const { token } = ring.beginRotation(store.find(hashToken("phone"), T0 + 1000), T0 + 2000);
|
|
78
|
+
const auth = ring.authenticate(token, T0 + 3000);
|
|
79
|
+
assert.ok(auth.ok && auth.retired);
|
|
80
|
+
const row = db.prepare("SELECT last_used_at FROM tokens WHERE hash = ?").get(hashToken("phone-2"));
|
|
81
|
+
assert.equal(row.last_used_at, T0 + 3000);
|
|
82
|
+
// Ten minutes of nothing but an open socket later, it still authenticates.
|
|
83
|
+
assert.ok(new DbTokenStore(db).find(hashToken("phone-2"), T0 + 3000 + UNUSED_TOKEN_LAPSE_MS * 2));
|
|
84
|
+
});
|
|
85
|
+
it("tells the minting client when its token lapses", () => {
|
|
86
|
+
const { store } = setup();
|
|
87
|
+
const ring = new TokenRing(store, () => "qr");
|
|
88
|
+
const res = mintDeviceTokenRoute(ring, "{}", T0);
|
|
89
|
+
assert.ok(res.status === 200);
|
|
90
|
+
assert.equal(res.body.expiresAt, LATE);
|
|
91
|
+
assert.equal(res.body.validForMs, UNUSED_TOKEN_LAPSE_MS);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=token-lapse.test.js.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { TokenRing, MemoryTokenStore, ROTATION_GRACE_MS, hashToken, } from "../daemon/token-ring.js";
|
|
4
|
+
import { DbTokenStore } from "../db/tokens.js";
|
|
5
|
+
import { createTestDb } from "./helpers.js";
|
|
6
|
+
// Every case runs against both stores: the in-memory one, and the SQLite one
|
|
7
|
+
// the daemon uses, so what is pinned here is the real rotation, not a double.
|
|
8
|
+
const STORES = [
|
|
9
|
+
["memory", (seed) => new MemoryTokenStore(seed)],
|
|
10
|
+
[
|
|
11
|
+
"sqlite",
|
|
12
|
+
(seed) => {
|
|
13
|
+
const store = new DbTokenStore(createTestDb());
|
|
14
|
+
// Seeded as devices already in use: an unused device row minted at 0
|
|
15
|
+
// would have lapsed long before these tests' real clock.
|
|
16
|
+
for (const s of seed)
|
|
17
|
+
store.insert(hashToken(s.token), s.kind, s.label ?? s.kind, 0, { used: true });
|
|
18
|
+
return store;
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
];
|
|
22
|
+
for (const [storeName, makeStore] of STORES) {
|
|
23
|
+
function ring(...minted) {
|
|
24
|
+
const queue = [...minted];
|
|
25
|
+
const store = makeStore([
|
|
26
|
+
{ token: "local", kind: "local", label: "this machine" },
|
|
27
|
+
{ token: "phone", kind: "device", label: "phone" },
|
|
28
|
+
{ token: "laptop", kind: "device", label: "laptop" },
|
|
29
|
+
]);
|
|
30
|
+
const r = new TokenRing(store, () => queue.shift() ?? "unexpected-mint");
|
|
31
|
+
const record = (token) => {
|
|
32
|
+
const a = r.authenticate(token);
|
|
33
|
+
assert.ok(a.ok, `${token} should authenticate`);
|
|
34
|
+
return a.record;
|
|
35
|
+
};
|
|
36
|
+
return { r, store, record };
|
|
37
|
+
}
|
|
38
|
+
describe(`TokenRing (${storeName} store)`, () => {
|
|
39
|
+
it("authenticates live tokens and nothing else", () => {
|
|
40
|
+
const { r } = ring();
|
|
41
|
+
assert.equal(r.authenticate("local").ok, true);
|
|
42
|
+
assert.equal(r.authenticate("phone").ok, true);
|
|
43
|
+
assert.deepEqual(r.authenticate("nope"), { ok: false });
|
|
44
|
+
assert.deepEqual(r.authenticate(null), { ok: false });
|
|
45
|
+
assert.deepEqual(r.authenticate(""), { ok: false });
|
|
46
|
+
});
|
|
47
|
+
it("stores only hashes", () => {
|
|
48
|
+
const store = makeStore([]);
|
|
49
|
+
const r = new TokenRing(store, () => "minted");
|
|
50
|
+
const { token, record } = r.issue("device", "tablet");
|
|
51
|
+
assert.equal(token, "minted");
|
|
52
|
+
assert.deepEqual(store.find(hashToken("minted"), 0), record);
|
|
53
|
+
assert.equal(store.find("minted", 0), null);
|
|
54
|
+
});
|
|
55
|
+
it("beginRotation mints a pending token and leaves every live token in place", () => {
|
|
56
|
+
const { r, record } = ring("new");
|
|
57
|
+
const t0 = 1_000;
|
|
58
|
+
assert.deepEqual(r.beginRotation(record("phone"), t0), {
|
|
59
|
+
token: "new",
|
|
60
|
+
expiresAt: t0 + ROTATION_GRACE_MS,
|
|
61
|
+
replaced: false,
|
|
62
|
+
});
|
|
63
|
+
// Nothing durable has moved: every token still authenticates, without consequence.
|
|
64
|
+
for (const t of ["local", "phone", "laptop"]) {
|
|
65
|
+
const a = r.authenticate(t, t0 + 1);
|
|
66
|
+
assert.equal(a.ok, true);
|
|
67
|
+
assert.equal(a.ok && a.retired, undefined);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
it("first use of the pending token retires the requester and every other device, exactly once", () => {
|
|
71
|
+
const { r, record } = ring("new");
|
|
72
|
+
const phone = record("phone");
|
|
73
|
+
const laptop = record("laptop");
|
|
74
|
+
r.beginRotation(phone, 0);
|
|
75
|
+
const confirmed = r.authenticate("new", 10);
|
|
76
|
+
assert.ok(confirmed.ok);
|
|
77
|
+
assert.equal(confirmed.record.kind, "device");
|
|
78
|
+
assert.equal(confirmed.record.label, "phone");
|
|
79
|
+
assert.deepEqual(new Set(confirmed.retired?.ids), new Set([phone.id, laptop.id]));
|
|
80
|
+
assert.equal(r.pendingRotation(10), null);
|
|
81
|
+
// The retirement is reported on that one request only.
|
|
82
|
+
const again = r.authenticate("new", 11);
|
|
83
|
+
assert.ok(again.ok);
|
|
84
|
+
assert.equal(again.retired, undefined);
|
|
85
|
+
// And the old device tokens are dead from that moment.
|
|
86
|
+
assert.deepEqual(r.authenticate("phone", 11), { ok: false });
|
|
87
|
+
assert.deepEqual(r.authenticate("laptop", 11), { ok: false });
|
|
88
|
+
// This machine rides through a device's rotation: the CLI and MCP
|
|
89
|
+
// bridges are not "other devices".
|
|
90
|
+
assert.equal(r.authenticate("local", 11).ok, true);
|
|
91
|
+
});
|
|
92
|
+
it("refuses to begin a rotation for this account, which would mint a token that opens the local row", () => {
|
|
93
|
+
const { r, record } = ring("new-local");
|
|
94
|
+
const local = record("local");
|
|
95
|
+
assert.throws(() => r.beginRotation(local, 0), /holds no token to rotate/);
|
|
96
|
+
assert.equal(r.pendingRotation(0), null);
|
|
97
|
+
assert.deepEqual(r.authenticate("new-local", 1), { ok: false }, "nothing was minted");
|
|
98
|
+
for (const t of ["local", "phone", "laptop"])
|
|
99
|
+
assert.equal(r.authenticate(t, 2).ok, true, `${t} is untouched`);
|
|
100
|
+
});
|
|
101
|
+
it("signOutDevices revokes every device at once, keeps this account, and voids a pending rotation", () => {
|
|
102
|
+
const { r, record } = ring("phone-2");
|
|
103
|
+
const local = record("local");
|
|
104
|
+
r.beginRotation(record("phone"), 0);
|
|
105
|
+
const out = r.signOutDevices(local, 1);
|
|
106
|
+
assert.equal(out.devices, 2);
|
|
107
|
+
assert.equal(out.ids.length, 2);
|
|
108
|
+
assert.equal(out.replaced, true);
|
|
109
|
+
for (const t of ["phone", "laptop"])
|
|
110
|
+
assert.deepEqual(r.authenticate(t, 2), { ok: false });
|
|
111
|
+
assert.equal(r.authenticate("local", 2).ok, true);
|
|
112
|
+
// The phone's pending token must not write it back in.
|
|
113
|
+
assert.deepEqual(r.authenticate("phone-2", 2), { ok: false });
|
|
114
|
+
assert.deepEqual(r.signOutDevices(local, 3), { ids: [], devices: 0, replaced: false });
|
|
115
|
+
});
|
|
116
|
+
it("a second rotate during the window replaces the pending token, never re-issues it", () => {
|
|
117
|
+
// Anyone holding a token can call rotate. If the daemon answered a second
|
|
118
|
+
// call with the token already pending, an attacker racing the operator
|
|
119
|
+
// would be handed the new credential and could use it first — and the
|
|
120
|
+
// operator's own confirm would still succeed, hiding the guest.
|
|
121
|
+
const { r, record } = ring("first", "second");
|
|
122
|
+
const a = r.beginRotation(record("phone"), 0);
|
|
123
|
+
const b = r.beginRotation(record("laptop"), 30_000);
|
|
124
|
+
assert.equal(a.replaced, false);
|
|
125
|
+
assert.deepEqual(b, { token: "second", expiresAt: 30_000 + ROTATION_GRACE_MS, replaced: true });
|
|
126
|
+
// The first pending token is dead: the requester who got it sees a 401
|
|
127
|
+
// on confirm, which is the visible failure the design wants.
|
|
128
|
+
assert.deepEqual(r.authenticate("first", 31_000), { ok: false });
|
|
129
|
+
const confirmed = r.authenticate("second", 31_000);
|
|
130
|
+
assert.ok(confirmed.ok && confirmed.retired);
|
|
131
|
+
assert.equal(confirmed.record.label, "laptop");
|
|
132
|
+
});
|
|
133
|
+
it("an unused pending token lapses and every token simply remains", () => {
|
|
134
|
+
const { r, record } = ring("new", "newer");
|
|
135
|
+
const phone = record("phone");
|
|
136
|
+
r.beginRotation(phone, 0);
|
|
137
|
+
const late = ROTATION_GRACE_MS;
|
|
138
|
+
assert.deepEqual(r.authenticate("new", late), { ok: false });
|
|
139
|
+
assert.equal(r.authenticate("phone", late).ok, true);
|
|
140
|
+
assert.equal(r.pendingRotation(late), null);
|
|
141
|
+
// A fresh rotate after the lapse mints anew rather than reviving the stale one.
|
|
142
|
+
// A lapsed pending token is gone, not "replaced" — nothing was live.
|
|
143
|
+
assert.deepEqual(r.beginRotation(phone, late), {
|
|
144
|
+
token: "newer",
|
|
145
|
+
expiresAt: late + ROTATION_GRACE_MS,
|
|
146
|
+
replaced: false,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
it("revoked tokens stay revoked across several rotations", () => {
|
|
150
|
+
const { r, record } = ring("b", "c");
|
|
151
|
+
r.beginRotation(record("phone"), 0);
|
|
152
|
+
const b = r.authenticate("b", 1);
|
|
153
|
+
assert.ok(b.ok);
|
|
154
|
+
r.beginRotation(b.record, 2);
|
|
155
|
+
const c = r.authenticate("c", 3);
|
|
156
|
+
assert.ok(c.ok && c.retired);
|
|
157
|
+
assert.deepEqual(c.retired.ids, [b.record.id]);
|
|
158
|
+
for (const t of ["phone", "laptop", "b"])
|
|
159
|
+
assert.deepEqual(r.authenticate(t, 4), { ok: false });
|
|
160
|
+
assert.equal(r.authenticate("c", 4).ok, true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe(`TokenRing rotation is all or nothing (${storeName} store)`, () => {
|
|
164
|
+
it("a failed write leaves every token as it was and the pending token usable", () => {
|
|
165
|
+
const store = makeStore([
|
|
166
|
+
{ token: "phone", kind: "device" },
|
|
167
|
+
{ token: "laptop", kind: "device" },
|
|
168
|
+
]);
|
|
169
|
+
const r = new TokenRing(store, () => "new");
|
|
170
|
+
const phone = r.authenticate("phone");
|
|
171
|
+
assert.ok(phone.ok);
|
|
172
|
+
r.beginRotation(phone.record, 0);
|
|
173
|
+
const revoke = store.revoke.bind(store);
|
|
174
|
+
store.revoke = () => { throw new Error("disk full"); };
|
|
175
|
+
assert.throws(() => r.authenticate("new", 1), /disk full/);
|
|
176
|
+
// Nothing landed: the old tokens work, and the new one is not a row yet.
|
|
177
|
+
assert.equal(r.authenticate("phone", 2).ok, true);
|
|
178
|
+
assert.equal(r.authenticate("laptop", 2).ok, true);
|
|
179
|
+
assert.equal(store.find(hashToken("new"), 2), null);
|
|
180
|
+
// The pending token survived, so the retry completes the rotation.
|
|
181
|
+
store.revoke = revoke;
|
|
182
|
+
const retry = r.authenticate("new", 3);
|
|
183
|
+
assert.ok(retry.ok && retry.retired);
|
|
184
|
+
assert.equal(r.authenticate("phone", 4).ok, false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=token-ring.test.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `brane token rotate` is a thin client of POST /api/token/rotate, and from
|
|
3
|
+
* the shell it is one request: this machine holds no token, so there is none
|
|
4
|
+
* to swap and nothing to confirm — the daemon signs every device out before
|
|
5
|
+
* it answers. This pins that shape against a scripted daemon, and that an
|
|
6
|
+
* older daemon's two-step answer is reported instead of being taken for a
|
|
7
|
+
* sign-out that never happened.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, before, after, beforeEach } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
import { rotateDaemonToken, formatRotateResult } from "../commands/token.js";
|
|
13
|
+
describe("brane token rotate", () => {
|
|
14
|
+
let server;
|
|
15
|
+
let endpoint;
|
|
16
|
+
let answer;
|
|
17
|
+
const seen = [];
|
|
18
|
+
before(async () => {
|
|
19
|
+
server = createServer((req, res) => {
|
|
20
|
+
seen.push([req.method ?? "", req.url ?? ""]);
|
|
21
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
22
|
+
res.end(JSON.stringify(answer));
|
|
23
|
+
});
|
|
24
|
+
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
25
|
+
const port = server.address().port;
|
|
26
|
+
// Over TCP with a token only so the test needs no socket; who may ask is
|
|
27
|
+
// the daemon's business (src/test/presence-gate.test.ts).
|
|
28
|
+
endpoint = { baseUrl: `http://127.0.0.1:${port}`, token: "t", port: String(port) };
|
|
29
|
+
});
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
seen.length = 0;
|
|
32
|
+
});
|
|
33
|
+
after(async () => {
|
|
34
|
+
server.closeAllConnections();
|
|
35
|
+
await new Promise((r) => server.close(() => r()));
|
|
36
|
+
});
|
|
37
|
+
it("is one request, with nothing to confirm", async () => {
|
|
38
|
+
answer = { signed_out: 2, replaced: false };
|
|
39
|
+
assert.deepEqual(await rotateDaemonToken(endpoint), { signed_out: 2, replaced: false });
|
|
40
|
+
assert.deepEqual(seen, [["POST", "/api/token/rotate"]]);
|
|
41
|
+
});
|
|
42
|
+
it("reports an older daemon's two-step answer rather than claiming a sign-out", async () => {
|
|
43
|
+
answer = { token: "new", expires_at: 0, replaced: false };
|
|
44
|
+
await assert.rejects(rotateDaemonToken(endpoint), /older brane.*nobody was signed out.*brane daemon restart/);
|
|
45
|
+
assert.deepEqual(seen, [["POST", "/api/token/rotate"]], "and never presents the token it was handed");
|
|
46
|
+
});
|
|
47
|
+
it("prints what changed and what did not", () => {
|
|
48
|
+
const text = formatRotateResult({ signed_out: 2, replaced: false });
|
|
49
|
+
assert.match(text, /Signed out 2 devices/);
|
|
50
|
+
assert.match(text, /This shell and running agents are unaffected/);
|
|
51
|
+
assert.match(text, /brane meld/);
|
|
52
|
+
assert.doesNotMatch(text, /pending/);
|
|
53
|
+
assert.match(formatRotateResult({ signed_out: 1, replaced: false }), /Signed out 1 device\./);
|
|
54
|
+
assert.match(formatRotateResult({ signed_out: 0, replaced: false }), /no devices/);
|
|
55
|
+
assert.match(formatRotateResult({ signed_out: 1, replaced: true }), /never confirmed was pending/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=token-rotate-cli.test.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token routes' own logic, outside the server closure: minting a device
|
|
3
|
+
* token from a request body, and finishing a rotation — closing the sockets
|
|
4
|
+
* the retired rows opened, and no others.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { TokenRing, MemoryTokenStore, WS_CLOSE_TOKEN_ROTATED } from "../daemon/token-ring.js";
|
|
9
|
+
import { mintDeviceTokenRoute, retireTokens } from "../daemon/token-routes.js";
|
|
10
|
+
function freshRing(...minted) {
|
|
11
|
+
const queue = [...minted];
|
|
12
|
+
return new TokenRing(new MemoryTokenStore([
|
|
13
|
+
{ token: "local", kind: "local", label: "this machine" },
|
|
14
|
+
{ token: "phone", kind: "device", label: "phone" },
|
|
15
|
+
]), () => queue.shift() ?? "unexpected-mint");
|
|
16
|
+
}
|
|
17
|
+
describe("mintDeviceTokenRoute", () => {
|
|
18
|
+
it("mints a device token that authenticates", () => {
|
|
19
|
+
const ring = freshRing("tablet-token");
|
|
20
|
+
const res = mintDeviceTokenRoute(ring, JSON.stringify({ label: "tablet" }));
|
|
21
|
+
assert.equal(res.status, 200);
|
|
22
|
+
assert.ok(res.status === 200);
|
|
23
|
+
assert.equal(res.body.token, "tablet-token");
|
|
24
|
+
assert.equal(res.body.label, "tablet");
|
|
25
|
+
const auth = ring.authenticate("tablet-token");
|
|
26
|
+
assert.ok(auth.ok);
|
|
27
|
+
assert.equal(auth.record.kind, "device");
|
|
28
|
+
});
|
|
29
|
+
it("labels by date when no label is given, empty body included", () => {
|
|
30
|
+
const now = Date.UTC(2026, 9, 3);
|
|
31
|
+
for (const body of [undefined, "", "{}", JSON.stringify({ label: " " })]) {
|
|
32
|
+
const res = mintDeviceTokenRoute(freshRing("t"), body, now);
|
|
33
|
+
assert.ok(res.status === 200, `body ${JSON.stringify(body)}`);
|
|
34
|
+
assert.equal(res.body.label, "melded 2026-10-03");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
it("caps a label at 200 characters", () => {
|
|
38
|
+
const res = mintDeviceTokenRoute(freshRing("t"), JSON.stringify({ label: "x".repeat(500) }));
|
|
39
|
+
assert.ok(res.status === 200);
|
|
40
|
+
assert.equal(res.body.label.length, 200);
|
|
41
|
+
});
|
|
42
|
+
it("refuses a body that is not a JSON object, and mints nothing", () => {
|
|
43
|
+
for (const body of ["not json", "null", "[]", "42"]) {
|
|
44
|
+
const ring = freshRing("should-not-mint");
|
|
45
|
+
const res = mintDeviceTokenRoute(ring, body);
|
|
46
|
+
assert.equal(res.status, 400, `body ${body}`);
|
|
47
|
+
assert.equal(ring.authenticate("should-not-mint").ok, false);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
class FakeSocket {
|
|
52
|
+
closed = [];
|
|
53
|
+
close(code, reason) {
|
|
54
|
+
this.closed.push([code, reason]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function rotate(ring, requester, next) {
|
|
58
|
+
const r = ring.authenticate(requester);
|
|
59
|
+
assert.ok(r.ok);
|
|
60
|
+
ring.beginRotation(r.record);
|
|
61
|
+
const confirmed = ring.authenticate(next);
|
|
62
|
+
assert.ok(confirmed.ok && confirmed.retired);
|
|
63
|
+
return { requester: r.record, confirmed: { record: confirmed.record, retired: confirmed.retired } };
|
|
64
|
+
}
|
|
65
|
+
describe("retireTokens", () => {
|
|
66
|
+
it("closes only the sockets opened under revoked rows, with 4401", () => {
|
|
67
|
+
const ring = freshRing("new-phone");
|
|
68
|
+
const local = ring.authenticate("local");
|
|
69
|
+
assert.ok(local.ok);
|
|
70
|
+
const { requester, confirmed } = rotate(ring, "phone", "new-phone");
|
|
71
|
+
const onOldPhone = new FakeSocket();
|
|
72
|
+
const onLocal = new FakeSocket();
|
|
73
|
+
const onNewPhone = new FakeSocket();
|
|
74
|
+
const sockets = new Map([
|
|
75
|
+
[onOldPhone, requester.id],
|
|
76
|
+
[onLocal, local.record.id],
|
|
77
|
+
[onNewPhone, confirmed.record.id],
|
|
78
|
+
]);
|
|
79
|
+
const { closed } = retireTokens({ ...confirmed, viewerSockets: sockets });
|
|
80
|
+
assert.equal(closed, 1);
|
|
81
|
+
assert.deepEqual(onOldPhone.closed, [[WS_CLOSE_TOKEN_ROTATED, "token rotated"]]);
|
|
82
|
+
assert.deepEqual(onLocal.closed, []);
|
|
83
|
+
assert.deepEqual(onNewPhone.closed, []);
|
|
84
|
+
assert.deepEqual([...sockets.keys()], [onLocal, onNewPhone]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("mintDeviceTokenRoute labels", () => {
|
|
88
|
+
it("replaces control characters, so a label cannot forge a log line", () => {
|
|
89
|
+
const res = mintDeviceTokenRoute(freshRing("t"), JSON.stringify({ label: "phone\n[token] minted device token \"x\"\u0007" }));
|
|
90
|
+
assert.ok(res.status === 200);
|
|
91
|
+
assert.doesNotMatch(res.body.label, /[\u0000-\u001f\u007f-\u009f]/);
|
|
92
|
+
assert.equal(res.body.label, 'phone [token] minted device token "x"');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
//# sourceMappingURL=token-routes.test.js.map
|