eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// πŸ€– The IO half of the ledger streams (issue #1224): fetch HEAD, append
2// entries, put blobs and derived views. Reads ride the bucket's public https
3// door (no credentials); writes shell out to the AWS CLI (the repo's
4// standing convention β€” no SDK dependency, human-runnable one command at a
5// time). Credentials come from the ambient AWS profile/env: the scoped
6// status-ledger-writer key (PutObject on ledgers/* only β€” infra/prod/
7// status.tf), or an operator's own.
8import { execFileSync } from "node:child_process";
9import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
10import { tmpdir } from "node:os";
11import { join } from "node:path";
12import { buildEntry, sha256hex } from "./ledgerchain.mjs";
13
14export const BUCKET = process.env.STATUS_BUCKET || "eleven-status-427880590669";
15export const REGION = process.env.STATUS_REGION || "eu-west-1";
16const publicUrl = (key) => "https://" + BUCKET + ".s3." + REGION + ".amazonaws.com/" + key;
17
18export async function fetchPublic(key) {
19 const r = await fetch(publicUrl(key));
20 if (r.status === 404 || r.status === 403) return null; // absent (403 = no such key without list rights)
21 if (!r.ok) throw new Error("GET " + key + " β†’ " + r.status);
22 return Buffer.from(await r.arrayBuffer());
23}
24
25export function putObject(key, bytes, contentType) {
26 const dir = mkdtempSync(join(tmpdir(), "ledger-"));
27 const f = join(dir, "body");
28 try {
29 writeFileSync(f, bytes);
30 execFileSync("aws", ["s3api", "put-object",
31 "--bucket", BUCKET, "--key", key, "--body", f,
32 "--content-type", contentType, "--region", REGION], { stdio: ["ignore", "ignore", "inherit"] });
33 } finally {
34 rmSync(dir, { recursive: true, force: true });
35 }
36}
37
38export const headKey = (ledger) => "ledgers/" + ledger + "/HEAD.json";
39
40export async function getHead(ledger) {
41 const bytes = await fetchPublic(headKey(ledger));
42 return bytes ? JSON.parse(bytes.toString("utf8")) : null;
43}
44
45// Append one entry and advance HEAD. Returns the new head descriptor.
46export async function append(ledger, writer, ts, payload) {
47 const head = await getHead(ledger);
48 const prev = head ? { key: head.head_key, sha256: head.head_sha256 } : null;
49 const { bytes, sha256, key } = buildEntry({ ledger, writer, ts, payload, prev });
50 putObject(key, bytes, "application/json");
51 const next = {
52 updated_at: ts,
53 head_key: key,
54 head_sha256: sha256,
55 count: (head ? head.count : 0) + 1,
56 };
57 putObject(headKey(ledger), Buffer.from(JSON.stringify(next, null, 1) + "\n"), "application/json");
58 return next;
59}
60
61// Content-addressed blob beside the stream (album art etc.): write-once by
62// construction β€” the key IS the hash, so re-putting identical bytes is a
63// no-op and different bytes get a different key. Returns the page-relative
64// path the derived views embed (same-origin: the no-external-hosts rule).
65export async function putBlob(ledger, bytes, ext, contentType) {
66 const key = "ledgers/" + ledger + "/blobs/" + sha256hex(bytes) + "." + ext;
67 if (!(await fetchPublic(key))) putObject(key, bytes, contentType);
68 return "ledgers/" + ledger + "/blobs/" + sha256hex(bytes) + "." + ext;
69}
70
71// Derived views (an index, month manifests): rebuildable conveniences the
72// pages read in one fetch β€” the entries remain the canonical record.
73export function putDerived(ledger, name, obj) {
74 putObject("ledgers/" + ledger + "/" + name, Buffer.from(JSON.stringify(obj, null, 1) + "\n"), "application/json");
75}
76