baesian 0.0.0-stage → 1.0.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/LICENSE.md +105 -0
- package/README.md +217 -2
- package/dist/agents.js +249 -0
- package/dist/api.js +13 -0
- package/dist/bin.js +14 -0
- package/dist/ci.js +82 -0
- package/dist/cite.js +66 -0
- package/dist/cli.js +831 -0
- package/dist/client/assets/alegreya-cyrillic-ext-wght-italic-DzWRXZb0.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-ext-wght-normal-CN0-BfWD.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-wght-italic-Dhrv2xfk.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-wght-normal-CU5lb38f.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-ext-wght-italic-CoUI9EEu.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-ext-wght-normal-C8PPRmwJ.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-wght-italic-CVt9-Jr4.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-wght-normal-BPx8yWcZ.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-ext-wght-italic-BIuo_2sR.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-ext-wght-normal-o8axeAGu.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-wght-italic-Bxkcgmge.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-wght-normal-BG9rTJo4.woff2 +0 -0
- package/dist/client/assets/alegreya-vietnamese-wght-italic-BAPuWgnQ.woff2 +0 -0
- package/dist/client/assets/alegreya-vietnamese-wght-normal-DOV9JYup.woff2 +0 -0
- package/dist/client/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
- package/dist/client/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
- package/dist/client/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
- package/dist/client/assets/index-9yf70Vqv.css +1 -0
- package/dist/client/assets/index-Dwg9e0YR.js +15 -0
- package/dist/client/index.html +15 -0
- package/dist/facts.js +278 -0
- package/dist/folder-storage.js +71 -0
- package/dist/frontmatter.js +38 -0
- package/dist/gate-paths.js +77 -0
- package/dist/gate.js +237 -0
- package/dist/git.js +100 -0
- package/dist/globs.js +120 -0
- package/dist/hookblock.js +146 -0
- package/dist/hooks.js +911 -0
- package/dist/ids.js +23 -0
- package/dist/login.js +301 -0
- package/dist/mcp.js +303 -0
- package/dist/merges.js +849 -0
- package/dist/modes.js +245 -0
- package/dist/notify.js +91 -0
- package/dist/ops.js +381 -0
- package/dist/passkey.js +86 -0
- package/dist/personal.js +163 -0
- package/dist/practice-answers.js +42 -0
- package/dist/proposals.js +197 -0
- package/dist/quote.js +28 -0
- package/dist/report.js +261 -0
- package/dist/routing.js +411 -0
- package/dist/schema.js +1178 -0
- package/dist/server.js +514 -0
- package/dist/similarity.js +45 -0
- package/dist/sshsig.js +159 -0
- package/dist/state.js +135 -0
- package/dist/storage.js +61 -0
- package/dist/store.js +2259 -0
- package/dist/style.js +408 -0
- package/dist/sync.js +797 -0
- package/dist/trailers.js +121 -0
- package/dist/trust.js +222 -0
- package/dist/usage-fields.js +161 -0
- package/dist/vault.js +849 -0
- package/dist/verify.js +443 -0
- package/dist/version.js +70 -0
- package/dist/wait.js +449 -0
- package/dist/wikilinks.js +25 -0
- package/package.json +71 -4
package/dist/sshsig.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHash, createPublicKey, sign, verify } from "node:crypto";
|
|
2
|
+
import { NewerFormat, newestFormat } from "./schema.js";
|
|
3
|
+
// OpenSSH signatures (D160): the hosted service signs each file a person writes with Ed25519, in OpenSSH's own format
|
|
4
|
+
// (PROTOCOL.sshsig), so `ssh-keygen -Y verify -n baesian` can check one by hand. Node's crypto does the Ed25519; this
|
|
5
|
+
// module reads and writes the format around it. docs/vault/Modules/src-trust.md describes it.
|
|
6
|
+
/** The namespace every signature on a file carries (D160): a signature made for anything else never counts here. */
|
|
7
|
+
export const NAMESPACE = "baesian";
|
|
8
|
+
/** The namespace of the relay's draw proofs (P16 section 3), kept apart so a draw can never pass for a file's signature. */
|
|
9
|
+
export const DRAW_NAMESPACE = "baesian-draw";
|
|
10
|
+
const MAGIC = Buffer.from("SSHSIG");
|
|
11
|
+
const BEGIN = "-----BEGIN SSH SIGNATURE-----";
|
|
12
|
+
const END = "-----END SSH SIGNATURE-----";
|
|
13
|
+
const ED25519 = "ssh-ed25519";
|
|
14
|
+
/** A signature file that isn't one: unreadable, or not what this namespace and key type allow. */
|
|
15
|
+
export class BadSignature extends Error {
|
|
16
|
+
}
|
|
17
|
+
/** Reads SSH's wire format: big-endian lengths before strings. */
|
|
18
|
+
class Reader {
|
|
19
|
+
buf;
|
|
20
|
+
at = 0;
|
|
21
|
+
constructor(buf) {
|
|
22
|
+
this.buf = buf;
|
|
23
|
+
}
|
|
24
|
+
bytes(n) {
|
|
25
|
+
if (this.at + n > this.buf.length)
|
|
26
|
+
throw new BadSignature("it ends too soon");
|
|
27
|
+
const out = this.buf.subarray(this.at, this.at + n);
|
|
28
|
+
this.at += n;
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
uint32() {
|
|
32
|
+
return this.bytes(4).readUInt32BE(0);
|
|
33
|
+
}
|
|
34
|
+
string() {
|
|
35
|
+
return this.bytes(this.uint32());
|
|
36
|
+
}
|
|
37
|
+
done() {
|
|
38
|
+
return this.at === this.buf.length;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const uint32 = (n) => {
|
|
42
|
+
const b = Buffer.alloc(4);
|
|
43
|
+
b.writeUInt32BE(n);
|
|
44
|
+
return b;
|
|
45
|
+
};
|
|
46
|
+
const sshString = (b) => {
|
|
47
|
+
const buf = typeof b === "string" ? Buffer.from(b) : b;
|
|
48
|
+
return Buffer.concat([uint32(buf.length), buf]);
|
|
49
|
+
};
|
|
50
|
+
/** An `ssh-ed25519 AAAA…` key's wire form: the base64 part, checked to be an Ed25519 key. */
|
|
51
|
+
export function publicKeyBlob(openssh) {
|
|
52
|
+
const [type, b64] = openssh.trim().split(/\s+/);
|
|
53
|
+
if (type !== ED25519 || !b64)
|
|
54
|
+
throw new BadSignature(`not an Ed25519 key in OpenSSH's form: ${type ?? "nothing"}`);
|
|
55
|
+
const blob = Buffer.from(b64, "base64");
|
|
56
|
+
const r = new Reader(blob);
|
|
57
|
+
if (r.string().toString() !== ED25519 || r.string().length !== 32 || !r.done())
|
|
58
|
+
throw new BadSignature("the key's bytes aren't an Ed25519 key");
|
|
59
|
+
return blob;
|
|
60
|
+
}
|
|
61
|
+
/** An Ed25519 public key in OpenSSH's form, `ssh-ed25519 AAAA…`, from Node's key object. */
|
|
62
|
+
export function opensshPublicKey(key) {
|
|
63
|
+
const x = createPublicKey(key).export({ format: "jwk" }).x;
|
|
64
|
+
if (!x)
|
|
65
|
+
throw new BadSignature("not an Ed25519 key");
|
|
66
|
+
return `${ED25519} ${Buffer.concat([sshString(ED25519), sshString(Buffer.from(x, "base64url"))]).toString("base64")}`;
|
|
67
|
+
}
|
|
68
|
+
/** Node's key object for a wire-form Ed25519 public key. */
|
|
69
|
+
function keyOf(blob) {
|
|
70
|
+
const r = new Reader(blob);
|
|
71
|
+
if (r.string().toString() !== ED25519)
|
|
72
|
+
throw new BadSignature("only Ed25519 keys sign Baesian's files");
|
|
73
|
+
const raw = r.string();
|
|
74
|
+
if (raw.length !== 32)
|
|
75
|
+
throw new BadSignature("the key isn't 32 bytes");
|
|
76
|
+
return createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") }, format: "jwk" });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read a signature: armored, as `ssh-keygen -Y sign` writes it, or its base64 alone, as a draw proof is kept. A newer
|
|
80
|
+
* SSHSIG version than this Baesian reads throws `NewerFormat` (D157), naming `file`; anything else unreadable throws
|
|
81
|
+
* `BadSignature`.
|
|
82
|
+
*/
|
|
83
|
+
export function parseSignature(text, file = "the signature") {
|
|
84
|
+
const trimmed = text.trim();
|
|
85
|
+
let b64 = trimmed;
|
|
86
|
+
if (trimmed.startsWith(BEGIN)) {
|
|
87
|
+
if (!trimmed.endsWith(END))
|
|
88
|
+
throw new BadSignature("it has no end line");
|
|
89
|
+
b64 = trimmed.slice(BEGIN.length, -END.length);
|
|
90
|
+
}
|
|
91
|
+
b64 = b64.replace(/\s+/g, "");
|
|
92
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64))
|
|
93
|
+
throw new BadSignature("it isn't base64");
|
|
94
|
+
const r = new Reader(Buffer.from(b64, "base64"));
|
|
95
|
+
if (!r.bytes(6).equals(MAGIC))
|
|
96
|
+
throw new BadSignature("it isn't an SSH signature");
|
|
97
|
+
const version = r.uint32();
|
|
98
|
+
if (version > newestFormat("signature"))
|
|
99
|
+
throw new NewerFormat(file, "signature", version);
|
|
100
|
+
if (version < 1)
|
|
101
|
+
throw new BadSignature(`it has version ${version}`);
|
|
102
|
+
const publicKey = r.string();
|
|
103
|
+
const namespace = r.string().toString();
|
|
104
|
+
r.string(); // reserved
|
|
105
|
+
const hashAlgorithm = r.string().toString();
|
|
106
|
+
if (hashAlgorithm !== "sha256" && hashAlgorithm !== "sha512")
|
|
107
|
+
throw new BadSignature(`it uses ${hashAlgorithm}, not sha256 or sha512`);
|
|
108
|
+
const sig = new Reader(r.string());
|
|
109
|
+
if (!r.done())
|
|
110
|
+
throw new BadSignature("it has bytes after the signature");
|
|
111
|
+
const keyType = sig.string().toString();
|
|
112
|
+
if (keyType !== ED25519)
|
|
113
|
+
throw new BadSignature(`it's a ${keyType} signature; only Ed25519 signs Baesian's files`);
|
|
114
|
+
const signature = sig.string();
|
|
115
|
+
if (signature.length !== 64 || !sig.done())
|
|
116
|
+
throw new BadSignature("the Ed25519 signature isn't 64 bytes");
|
|
117
|
+
return { publicKey, namespace, hashAlgorithm, signature };
|
|
118
|
+
}
|
|
119
|
+
/** The bytes an SSH signature signs (PROTOCOL.sshsig): the namespace, and the message's hash, never the message itself. */
|
|
120
|
+
function toBeSigned(namespace, hashAlgorithm, message) {
|
|
121
|
+
const digest = createHash(hashAlgorithm).update(message).digest();
|
|
122
|
+
return Buffer.concat([MAGIC, sshString(namespace), sshString(""), sshString(hashAlgorithm), sshString(digest)]);
|
|
123
|
+
}
|
|
124
|
+
/** Whether `sig` is its own key's signature over `message`, in `namespace`. Which keys to trust is the caller's question. */
|
|
125
|
+
export function signatureMatches(sig, message, namespace) {
|
|
126
|
+
if (sig.namespace !== namespace)
|
|
127
|
+
return false;
|
|
128
|
+
try {
|
|
129
|
+
return verify(null, toBeSigned(namespace, sig.hashAlgorithm, message), keyOf(sig.publicKey), sig.signature);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** The signature's blob, armored as `ssh-keygen` writes it: lines of 70 characters. */
|
|
136
|
+
function armor(blob) {
|
|
137
|
+
const b64 = blob.toString("base64");
|
|
138
|
+
const lines = b64.match(/.{1,70}/g) ?? [];
|
|
139
|
+
return `${BEGIN}\n${lines.join("\n")}\n${END}\n`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Sign `message` with an Ed25519 private key, as `ssh-keygen -Y sign -n <namespace>` does (SHA-512 by default). The
|
|
143
|
+
* relay signs this way, as do the tests' stand-in for it; `bare` gives the base64 alone, for a draw proof.
|
|
144
|
+
*/
|
|
145
|
+
export function signMessage(privateKey, message, namespace, opts = {}) {
|
|
146
|
+
const hashAlgorithm = opts.hashAlgorithm ?? "sha512";
|
|
147
|
+
const signature = sign(null, toBeSigned(namespace, hashAlgorithm, message), privateKey);
|
|
148
|
+
const blob = Buffer.concat([
|
|
149
|
+
MAGIC,
|
|
150
|
+
uint32(1),
|
|
151
|
+
sshString(publicKeyBlob(opensshPublicKey(privateKey))),
|
|
152
|
+
sshString(namespace),
|
|
153
|
+
sshString(""),
|
|
154
|
+
sshString(hashAlgorithm),
|
|
155
|
+
sshString(Buffer.concat([sshString(ED25519), sshString(signature)])),
|
|
156
|
+
]);
|
|
157
|
+
return opts.bare ? blob.toString("base64") : armor(blob);
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=sshsig.js.map
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { blankRow, countedCards, replayCards, routingReport } from "./report.js";
|
|
2
|
+
import { ALL_ROWS, LEVEL_AT, POSITIONS, POSITION_NAMES, PRESETS, ROW_FLOOR, ROW_NAMES, SURVEY, SURVEY_QUESTION, isFixed, replay } from "./routing.js";
|
|
3
|
+
import { linkText } from "./wikilinks.js";
|
|
4
|
+
/** A deck's context notes where there's no vault to resolve them in: each link, with what it shows. */
|
|
5
|
+
export const plainNotes = (links) => links.map((link) => ({ link, text: linkText(link) }));
|
|
6
|
+
/** The state `GET /api/state` returns, read as one consistent view (D96). A file that can't be read is in `problems`. */
|
|
7
|
+
export function snapshot(store, ctx) {
|
|
8
|
+
const problems = [];
|
|
9
|
+
const notes = ctx.notes ?? plainNotes;
|
|
10
|
+
return store.view(() => {
|
|
11
|
+
const cards = [];
|
|
12
|
+
const all = store.cardIds().flatMap((id) => store.card(id) ?? []);
|
|
13
|
+
const dependents = new Map();
|
|
14
|
+
for (const c of all)
|
|
15
|
+
for (const p of c.meta.depends_on)
|
|
16
|
+
dependents.set(p, [...(dependents.get(p) ?? []), c.meta.id]);
|
|
17
|
+
const nowMs = ctx.now ?? Date.now();
|
|
18
|
+
for (const c of all) {
|
|
19
|
+
const id = c.meta.id;
|
|
20
|
+
try {
|
|
21
|
+
const decision = store.decision(id);
|
|
22
|
+
const status = store.status(id);
|
|
23
|
+
const gate = store.gateRecord(id, c.meta.revision);
|
|
24
|
+
cards.push({
|
|
25
|
+
meta: c.meta,
|
|
26
|
+
body: c.body,
|
|
27
|
+
revisions: store.revisions(id).length,
|
|
28
|
+
replied: store.revisions(id).some((r) => r.meta.reply !== undefined),
|
|
29
|
+
status,
|
|
30
|
+
...(decision !== undefined ? { decision } : {}),
|
|
31
|
+
canonicalTopic: store.canonicalTopic(c.meta.topic),
|
|
32
|
+
verdicts: store.verdicts(id),
|
|
33
|
+
dependents: dependents.get(id) ?? [],
|
|
34
|
+
briefs: store.briefs(id),
|
|
35
|
+
waiting: store.waitingOn(id, nowMs).map(({ marker, stale }) => ({ agent: marker.agent, since: marker.since, at: marker.at, stale })),
|
|
36
|
+
fallbacks: store.fallbacks(id),
|
|
37
|
+
attachments: store.attachments(id).length,
|
|
38
|
+
...(gate ? { gate } : {}),
|
|
39
|
+
...(status === "held" && gate?.drawn ? { audit: true } : {}),
|
|
40
|
+
...(status === "resolved" && store.decidedBeforeLinking(id) ? { before_linking: true } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
problems.push({ path: `cards/${id}`, error: e instanceof Error ? e.message : String(e) });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const presence = store.presence();
|
|
48
|
+
const defaultBranch = readDefaultBranch(store, problems);
|
|
49
|
+
return {
|
|
50
|
+
version: ctx.version,
|
|
51
|
+
server: { ...(ctx.server.pid !== undefined ? { pid: ctx.server.pid } : {}), started: ctx.server.started, now: new Date(nowMs).toISOString(), watch: ctx.server.watch },
|
|
52
|
+
project: ctx.project,
|
|
53
|
+
...(defaultBranch ? { defaultBranch } : {}),
|
|
54
|
+
...(presence ? { presence } : {}),
|
|
55
|
+
decks: store.decks().map((d) => ({ ...d, notes: notes(d.meta.context_notes) })),
|
|
56
|
+
rules: store.rules(),
|
|
57
|
+
cards,
|
|
58
|
+
routing: routingState(store, problems),
|
|
59
|
+
problems,
|
|
60
|
+
...branchesState(store),
|
|
61
|
+
};
|
|
62
|
+
}, (path, error) => problems.push({ path, error }));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The repo's default branch (B-jr8mk), the trunk the branches panel draws on top: read from git on your machine, from
|
|
66
|
+
* what a machine sent on the relay (`Store.defaultBranch`). One that can't be read is left out, as the panel then guesses.
|
|
67
|
+
*/
|
|
68
|
+
function readDefaultBranch(store, problems) {
|
|
69
|
+
try {
|
|
70
|
+
return store.defaultBranch();
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
problems.push({ path: "default-branch.json", error: e instanceof Error ? e.message : String(e) });
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* What the branches panel draws besides the cards (D181): the merges the hooks found, each with the tip that landed and
|
|
79
|
+
* the commits citing cards it brought (B-w41p7), and the commits the ledger says cite cards, each once: its ID, time,
|
|
80
|
+
* branch and card, never its subject.
|
|
81
|
+
*/
|
|
82
|
+
function branchesState(store) {
|
|
83
|
+
const merges = store.merges().map(({ type: _t, format: _f, ...m }) => m);
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const commits = store.ledgerEntries().flatMap((e) => {
|
|
86
|
+
const commit = e.meta.commit;
|
|
87
|
+
if (!commit || seen.has(`${commit} ${e.meta.card}`))
|
|
88
|
+
return [];
|
|
89
|
+
seen.add(`${commit} ${e.meta.card}`);
|
|
90
|
+
return [{ commit, branch: e.meta.branch, card: e.meta.card, at: e.meta.at }];
|
|
91
|
+
});
|
|
92
|
+
return { merges, commits };
|
|
93
|
+
}
|
|
94
|
+
/** The preferences in force, their history, and the report per row (M8), for the state. */
|
|
95
|
+
function routingState(store, problems) {
|
|
96
|
+
const force = store.inForce();
|
|
97
|
+
const history = store.preferenceNames().flatMap((name) => {
|
|
98
|
+
const file = store.preferenceFile(name);
|
|
99
|
+
return file ? [{ name, file }] : [];
|
|
100
|
+
});
|
|
101
|
+
let report;
|
|
102
|
+
try {
|
|
103
|
+
report = routingReport(store, force);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
problems.push({ path: "preferences", error: e instanceof Error ? e.message : String(e) });
|
|
107
|
+
report = { rows: {}, pooled: blankRow(), beforeGate: 0 };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
...(force.name && force.file ? { preferences: { name: force.name, file: force.file } } : {}),
|
|
111
|
+
...(force.problem ? { problem: force.problem } : {}),
|
|
112
|
+
settings: force.settings,
|
|
113
|
+
history,
|
|
114
|
+
report,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** `GET /api/presets` (M8): the rows and positions, each preset and the settings in force replayed over past cards, and the setup survey. */
|
|
118
|
+
export function presets(store) {
|
|
119
|
+
return store.view(() => {
|
|
120
|
+
const cards = replayCards(countedCards(store));
|
|
121
|
+
const force = store.inForce();
|
|
122
|
+
return {
|
|
123
|
+
rows: ALL_ROWS.map((row) => ({ row, name: ROW_NAMES[row], ...(isFixed(row) ? { fixed: true } : ROW_FLOOR[row] ? { floor: ROW_FLOOR[row] } : {}) })),
|
|
124
|
+
positions: POSITIONS.map((position) => ({ position, name: POSITION_NAMES[position], ...(LEVEL_AT[position] ? { level: LEVEL_AT[position] } : {}) })),
|
|
125
|
+
presets: PRESETS.map((p) => {
|
|
126
|
+
const settings = { ...force.settings, rows: p.rows };
|
|
127
|
+
return { id: p.id, name: p.name, summary: p.summary, settings, replay: replay(cards, settings) };
|
|
128
|
+
}),
|
|
129
|
+
inForce: { settings: force.settings, replay: replay(cards, force.settings) },
|
|
130
|
+
survey: { ...SURVEY_QUESTION, items: [...SURVEY] },
|
|
131
|
+
};
|
|
132
|
+
// A file that can't be read is left out, as the snapshot leaves it, rather than failing the request (D84).
|
|
133
|
+
}, () => { });
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=state.js.map
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The live layer's storage (B-0ag7h): what the store needs of a folder of files, and nothing more, so the same store
|
|
2
|
+
// runs over a folder on your machine (`FolderStorage`, src/folder-storage.ts) and, on the relay, over a table in a
|
|
3
|
+
// Durable Object (M9b). This module imports nothing from Node, so the relay can import it.
|
|
4
|
+
/** A write-once file is already there: `Storage.create` refuses to write it again. Its `code` is Node's, `EEXIST`. */
|
|
5
|
+
export class FileExists extends Error {
|
|
6
|
+
path;
|
|
7
|
+
code = "EEXIST";
|
|
8
|
+
constructor(path) {
|
|
9
|
+
super(`${path} is already written: a file is written once`);
|
|
10
|
+
this.path = path;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Whether an error says the file was already there: a `FileExists`, or Node's own `EEXIST`. */
|
|
14
|
+
export function alreadyExists(e) {
|
|
15
|
+
return typeof e === "object" && e !== null && e.code === "EEXIST";
|
|
16
|
+
}
|
|
17
|
+
/** A path the store can name: relative, `/`-separated, with no empty, `.` or `..` part. The root folder is "". */
|
|
18
|
+
export function checkPath(path, root = false) {
|
|
19
|
+
if (root && path === "")
|
|
20
|
+
return path;
|
|
21
|
+
if (path.split("/").some((p) => p === "" || p === "." || p === "..") || path.includes("\\"))
|
|
22
|
+
throw new Error(`not a path in the live layer: ${JSON.stringify(path)}`);
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
const copy = (bytes) => new Uint8Array(bytes);
|
|
26
|
+
/**
|
|
27
|
+
* The live layer in memory (B-0ag7h): one entry per file, keyed by its path, as the relay's table will be. It stands in
|
|
28
|
+
* for the Durable Object's storage in tests, and in anything that runs the store with no repo. What it hands out is a
|
|
29
|
+
* copy, so nothing can change a file after it's written.
|
|
30
|
+
*/
|
|
31
|
+
export class MemoryStorage {
|
|
32
|
+
files = new Map();
|
|
33
|
+
read(path) {
|
|
34
|
+
const bytes = this.files.get(checkPath(path));
|
|
35
|
+
return bytes && copy(bytes);
|
|
36
|
+
}
|
|
37
|
+
list(dir) {
|
|
38
|
+
const prefix = checkPath(dir, true) === "" ? "" : `${dir}/`;
|
|
39
|
+
const names = new Set();
|
|
40
|
+
for (const path of this.files.keys())
|
|
41
|
+
if (path.startsWith(prefix))
|
|
42
|
+
names.add(path.slice(prefix.length).split("/")[0]);
|
|
43
|
+
return [...names];
|
|
44
|
+
}
|
|
45
|
+
create(path, bytes) {
|
|
46
|
+
if (this.files.has(checkPath(path)))
|
|
47
|
+
throw new FileExists(path);
|
|
48
|
+
this.files.set(path, copy(bytes));
|
|
49
|
+
}
|
|
50
|
+
replace(path, bytes) {
|
|
51
|
+
this.files.set(checkPath(path), copy(bytes));
|
|
52
|
+
}
|
|
53
|
+
remove(path) {
|
|
54
|
+
this.files.delete(checkPath(path));
|
|
55
|
+
}
|
|
56
|
+
/** Every file's path, sorted. */
|
|
57
|
+
paths() {
|
|
58
|
+
return [...this.files.keys()].sort();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=storage.js.map
|