@oharness/sync 0.1.0
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/dist/feed.d.ts +68 -0
- package/dist/feed.js +228 -0
- package/dist/feed.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +114 -0
- package/dist/main.js.map +1 -0
- package/dist/protocol.d.ts +11 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +241 -0
- package/dist/server.js.map +1 -0
- package/package.json +36 -0
package/dist/feed.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { WireRecord, WireSession } from "./protocol.ts";
|
|
2
|
+
/**
|
|
3
|
+
* One project's history, as an append-only log.
|
|
4
|
+
*
|
|
5
|
+
* The only question a syncing client asks is "what has happened since cursor
|
|
6
|
+
* N" — which is what a log answers natively and what a document store has to
|
|
7
|
+
* be indexed into answering. So the server's storage is the same shape as its
|
|
8
|
+
* protocol: append, and read forward from an offset.
|
|
9
|
+
*
|
|
10
|
+
* A database would buy indexed queries this does not need and cost a dependency
|
|
11
|
+
* or an unstable built-in (`node:sqlite` still needs a flag on the oldest Node
|
|
12
|
+
* this package supports). If multi-tenant scale ever arrives, `Feed` is the
|
|
13
|
+
* seam to reimplement — nothing above it knows how a cursor is stored.
|
|
14
|
+
*/
|
|
15
|
+
export type Entry = ({
|
|
16
|
+
cursor: number;
|
|
17
|
+
kind: "record";
|
|
18
|
+
} & WireRecord) | ({
|
|
19
|
+
cursor: number;
|
|
20
|
+
kind: "session";
|
|
21
|
+
} & WireSession);
|
|
22
|
+
/**
|
|
23
|
+
* Who a feed belongs to.
|
|
24
|
+
*
|
|
25
|
+
* A person by default, and a team when a team is paying for the seat. The
|
|
26
|
+
* distinction is the whole of session collaboration: two people cannot hand
|
|
27
|
+
* each other a session by both being logged in, they have to be reading and
|
|
28
|
+
* writing one feed, and a feed keyed by whoever pushed first is not that.
|
|
29
|
+
*
|
|
30
|
+
* The kind is part of the directory name rather than implied by the id, so a
|
|
31
|
+
* team named after a user's id cannot land on that user's feed.
|
|
32
|
+
*/
|
|
33
|
+
export type Owner = {
|
|
34
|
+
kind: "user" | "team";
|
|
35
|
+
id: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function ownerDir(owner: Owner): string;
|
|
38
|
+
export declare class Feed {
|
|
39
|
+
private readonly file;
|
|
40
|
+
private state;
|
|
41
|
+
/** Appends run one at a time: two writers would hand out the same cursor. */
|
|
42
|
+
private tail;
|
|
43
|
+
private readonly waiters;
|
|
44
|
+
constructor(root: string, owner: Owner, project: string);
|
|
45
|
+
private load;
|
|
46
|
+
cursor(): Promise<number>;
|
|
47
|
+
/**
|
|
48
|
+
* Add what the caller does not already have.
|
|
49
|
+
*
|
|
50
|
+
* Records are identified by the client, so "already held" is a set lookup
|
|
51
|
+
* rather than a comparison — pushing the same batch twice after a failed
|
|
52
|
+
* response adds nothing the second time.
|
|
53
|
+
*/
|
|
54
|
+
append(records: WireRecord[], sessions: WireSession[]): Promise<{
|
|
55
|
+
cursor: number;
|
|
56
|
+
accepted: number;
|
|
57
|
+
}>;
|
|
58
|
+
/** Everything after `since`, oldest first. */
|
|
59
|
+
since(since: number, limit: number): Promise<{
|
|
60
|
+
entries: Entry[];
|
|
61
|
+
more: boolean;
|
|
62
|
+
}>;
|
|
63
|
+
/** Resolve when the feed moves past `since`, or when the wait runs out. */
|
|
64
|
+
wait(since: number, seconds: number): Promise<void>;
|
|
65
|
+
private wake;
|
|
66
|
+
}
|
|
67
|
+
/** Projects anything has been pushed to, under one owner. */
|
|
68
|
+
export declare function listProjects(root: string, owner: Owner): Promise<string[]>;
|
package/dist/feed.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { appendFile, mkdir, open, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseJson } from "@oharness/engine/util/json.ts";
|
|
4
|
+
/** How often a byte offset is remembered, so a pull seeks instead of scans. */
|
|
5
|
+
const CHECKPOINT_EVERY = 128;
|
|
6
|
+
export function ownerDir(owner) {
|
|
7
|
+
return `${owner.kind}-${safe(owner.id)}`;
|
|
8
|
+
}
|
|
9
|
+
export class Feed {
|
|
10
|
+
file;
|
|
11
|
+
state;
|
|
12
|
+
/** Appends run one at a time: two writers would hand out the same cursor. */
|
|
13
|
+
tail = Promise.resolve();
|
|
14
|
+
waiters = new Set();
|
|
15
|
+
constructor(root, owner, project) {
|
|
16
|
+
this.file = path.join(root, "feed", ownerDir(owner), `${safe(project)}.jsonl`);
|
|
17
|
+
}
|
|
18
|
+
load() {
|
|
19
|
+
this.state ??= scan(this.file);
|
|
20
|
+
return this.state;
|
|
21
|
+
}
|
|
22
|
+
async cursor() {
|
|
23
|
+
return (await this.load()).lastCursor;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Add what the caller does not already have.
|
|
27
|
+
*
|
|
28
|
+
* Records are identified by the client, so "already held" is a set lookup
|
|
29
|
+
* rather than a comparison — pushing the same batch twice after a failed
|
|
30
|
+
* response adds nothing the second time.
|
|
31
|
+
*/
|
|
32
|
+
append(records, sessions) {
|
|
33
|
+
const run = async () => {
|
|
34
|
+
const state = await this.load();
|
|
35
|
+
const lines = [];
|
|
36
|
+
let cursor = state.lastCursor;
|
|
37
|
+
for (const record of records) {
|
|
38
|
+
const key = `${record.session}/${record.id}`;
|
|
39
|
+
if (state.seen.has(key))
|
|
40
|
+
continue;
|
|
41
|
+
state.seen.add(key);
|
|
42
|
+
lines.push({ ...record, cursor: ++cursor, kind: "record" });
|
|
43
|
+
}
|
|
44
|
+
for (const session of sessions) {
|
|
45
|
+
const held = state.sessions.get(session.id);
|
|
46
|
+
// Generation first, then time: a rewrite beats an older device's
|
|
47
|
+
// appends however recently its clock says they happened.
|
|
48
|
+
if (held && (held.gen > session.gen ||
|
|
49
|
+
(held.gen === session.gen && held.updatedAt >= session.updatedAt)))
|
|
50
|
+
continue;
|
|
51
|
+
state.sessions.set(session.id, { gen: session.gen, updatedAt: session.updatedAt });
|
|
52
|
+
lines.push({ ...session, cursor: ++cursor, kind: "session" });
|
|
53
|
+
}
|
|
54
|
+
if (lines.length) {
|
|
55
|
+
await mkdir(path.dirname(this.file), { recursive: true });
|
|
56
|
+
const body = lines.map((line) => JSON.stringify(line)).join("\n") + "\n";
|
|
57
|
+
await appendFile(this.file, body, "utf8");
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
state.size += Buffer.byteLength(JSON.stringify(line)) + 1;
|
|
60
|
+
const last = state.checkpoints[state.checkpoints.length - 1];
|
|
61
|
+
if (!last || line.cursor - last.cursor >= CHECKPOINT_EVERY) {
|
|
62
|
+
state.checkpoints.push({ cursor: line.cursor, offset: state.size });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
state.lastCursor = cursor;
|
|
66
|
+
this.wake();
|
|
67
|
+
}
|
|
68
|
+
return { cursor: state.lastCursor, accepted: lines.length };
|
|
69
|
+
};
|
|
70
|
+
const next = this.tail.then(run, run);
|
|
71
|
+
// The chain must survive a failed append, or every later push inherits the
|
|
72
|
+
// rejection and the feed wedges.
|
|
73
|
+
this.tail = next.catch(() => { });
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
/** Everything after `since`, oldest first. */
|
|
77
|
+
async since(since, limit) {
|
|
78
|
+
const state = await this.load();
|
|
79
|
+
if (since >= state.lastCursor)
|
|
80
|
+
return { entries: [], more: false };
|
|
81
|
+
// Start from the last checkpoint at or before the caller's cursor rather
|
|
82
|
+
// than the top of the file: catching up should not cost a full history read.
|
|
83
|
+
let offset = 0;
|
|
84
|
+
for (const point of state.checkpoints) {
|
|
85
|
+
if (point.cursor > since)
|
|
86
|
+
break;
|
|
87
|
+
offset = point.offset;
|
|
88
|
+
}
|
|
89
|
+
const entries = [];
|
|
90
|
+
let more = false;
|
|
91
|
+
for (const line of await readFrom(this.file, offset)) {
|
|
92
|
+
const parsed = parseJson(line);
|
|
93
|
+
if (!parsed.ok || parsed.value.cursor <= since)
|
|
94
|
+
continue;
|
|
95
|
+
if (entries.length >= limit) {
|
|
96
|
+
more = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
entries.push(parsed.value);
|
|
100
|
+
}
|
|
101
|
+
return { entries, more };
|
|
102
|
+
}
|
|
103
|
+
/** Resolve when the feed moves past `since`, or when the wait runs out. */
|
|
104
|
+
wait(since, seconds) {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
let done = false;
|
|
107
|
+
const finish = () => {
|
|
108
|
+
if (done)
|
|
109
|
+
return;
|
|
110
|
+
done = true;
|
|
111
|
+
this.waiters.delete(finish);
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
resolve();
|
|
114
|
+
};
|
|
115
|
+
const timer = setTimeout(finish, seconds * 1000);
|
|
116
|
+
// Unref: a pending long-poll must not hold the process open on shutdown.
|
|
117
|
+
timer.unref?.();
|
|
118
|
+
this.waiters.add(finish);
|
|
119
|
+
void this.cursor().then((now) => {
|
|
120
|
+
if (now > since)
|
|
121
|
+
finish();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
wake() {
|
|
126
|
+
for (const waiter of [...this.waiters])
|
|
127
|
+
waiter();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Projects anything has been pushed to, under one owner. */
|
|
131
|
+
export async function listProjects(root, owner) {
|
|
132
|
+
const dir = path.join(root, "feed", ownerDir(owner));
|
|
133
|
+
const entries = await readdir(dir).catch(() => []);
|
|
134
|
+
return entries.filter((name) => name.endsWith(".jsonl")).map((name) => name.slice(0, -6)).sort();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Rebuild what is only kept in memory: the cursor, the checkpoints, and what
|
|
138
|
+
* has already been seen. One pass, on first touch of a feed.
|
|
139
|
+
*/
|
|
140
|
+
async function scan(file) {
|
|
141
|
+
const state = {
|
|
142
|
+
lastCursor: 0,
|
|
143
|
+
checkpoints: [],
|
|
144
|
+
seen: new Set(),
|
|
145
|
+
sessions: new Map(),
|
|
146
|
+
size: 0,
|
|
147
|
+
};
|
|
148
|
+
let handle;
|
|
149
|
+
try {
|
|
150
|
+
handle = await open(file, "r");
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return state;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
let carry = "";
|
|
157
|
+
for await (const chunk of handle.createReadStream({ encoding: "utf8" })) {
|
|
158
|
+
const text = carry + chunk;
|
|
159
|
+
const lines = text.split("\n");
|
|
160
|
+
carry = lines.pop() ?? "";
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
state.size += Buffer.byteLength(line) + 1;
|
|
163
|
+
if (line.trim() === "")
|
|
164
|
+
continue;
|
|
165
|
+
const parsed = parseJson(line);
|
|
166
|
+
// A torn line from a killed append costs that line, not the feed.
|
|
167
|
+
if (!parsed.ok || typeof parsed.value.cursor !== "number")
|
|
168
|
+
continue;
|
|
169
|
+
remember(state, parsed.value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (carry.trim() !== "") {
|
|
173
|
+
const parsed = parseJson(carry);
|
|
174
|
+
if (parsed.ok && typeof parsed.value.cursor === "number") {
|
|
175
|
+
state.size += Buffer.byteLength(carry) + 1;
|
|
176
|
+
remember(state, parsed.value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await handle.close();
|
|
182
|
+
}
|
|
183
|
+
return state;
|
|
184
|
+
}
|
|
185
|
+
function remember(state, entry) {
|
|
186
|
+
state.lastCursor = Math.max(state.lastCursor, entry.cursor);
|
|
187
|
+
if (entry.kind === "record")
|
|
188
|
+
state.seen.add(`${entry.session}/${entry.id}`);
|
|
189
|
+
if (entry.kind === "session") {
|
|
190
|
+
state.sessions.set(entry.id, { gen: entry.gen, updatedAt: entry.updatedAt });
|
|
191
|
+
}
|
|
192
|
+
const last = state.checkpoints[state.checkpoints.length - 1];
|
|
193
|
+
if (!last || entry.cursor - last.cursor >= CHECKPOINT_EVERY) {
|
|
194
|
+
state.checkpoints.push({ cursor: entry.cursor, offset: state.size });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function readFrom(file, offset) {
|
|
198
|
+
const handle = await open(file, "r").catch(() => undefined);
|
|
199
|
+
if (!handle)
|
|
200
|
+
return [];
|
|
201
|
+
try {
|
|
202
|
+
const { size } = await handle.stat();
|
|
203
|
+
const start = Math.min(offset, size);
|
|
204
|
+
const buffer = Buffer.alloc(size - start);
|
|
205
|
+
if (buffer.length === 0)
|
|
206
|
+
return [];
|
|
207
|
+
await handle.read(buffer, 0, buffer.length, start);
|
|
208
|
+
return buffer.toString("utf8").split("\n");
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
await handle.close();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Ids reach the filesystem, so they are constrained rather than trusted: a
|
|
216
|
+
* project called `../../etc` must name a file inside the feed directory.
|
|
217
|
+
*/
|
|
218
|
+
function safe(id) {
|
|
219
|
+
const cleaned = id
|
|
220
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
221
|
+
// Runs of dots go too. Separators are already gone by this point, so `..`
|
|
222
|
+
// cannot traverse anything — but a name that still reads as a traversal is
|
|
223
|
+
// one someone will eventually pass to a path join that does.
|
|
224
|
+
.replace(/\.{2,}/g, "_")
|
|
225
|
+
.replace(/^\./, "_");
|
|
226
|
+
return cleaned.slice(0, 128) || "_";
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=feed.js.map
|
package/dist/feed.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feed.js","sourceRoot":"","sources":["../src/feed.ts"],"names":[],"mappings":"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"}
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { userPath } from "@oharness/engine/paths.ts";
|
|
3
|
+
import { errorMessage } from "@oharness/engine/core/errors.ts";
|
|
4
|
+
import { issueToken, readUsers, startSyncServer } from "./server.js";
|
|
5
|
+
/**
|
|
6
|
+
* The sync server's entry point — a separate binary because it is a separate
|
|
7
|
+
* decision. Running the harness never starts one.
|
|
8
|
+
*/
|
|
9
|
+
const HELP = `oharness-sync — an optional server for syncing sessions between devices
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
oharness-sync [options] Serve
|
|
13
|
+
oharness-sync token <user> Issue a token for a user, printed once
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--port <n> Port to listen on (default 4483)
|
|
17
|
+
--host <addr> Interface to bind (default 127.0.0.1)
|
|
18
|
+
--root <dir> Where feeds are stored (default ~/.oharness/sync)
|
|
19
|
+
--team <id> With 'token': a team the holder may sync with. Repeatable.
|
|
20
|
+
-h, --help Show this help
|
|
21
|
+
|
|
22
|
+
A team is a shared feed. Everyone issued a token carrying the same team id
|
|
23
|
+
reads and writes one history for a project, which is what lets two people
|
|
24
|
+
continue each other's sessions; without one, a token reaches only its own.
|
|
25
|
+
|
|
26
|
+
This speaks plain HTTP and authenticates with a bearer token. Put it behind a
|
|
27
|
+
reverse proxy that terminates TLS before exposing it to a network you do not
|
|
28
|
+
control — a token sent in the clear is a token everyone on the path now holds.
|
|
29
|
+
|
|
30
|
+
It stores what it is given and hands it back in order. It never runs a model,
|
|
31
|
+
and it does not need to read anything it stores.
|
|
32
|
+
|
|
33
|
+
Environment, for a deployment fronting an account system. Set either the
|
|
34
|
+
secret or the JWKS URL and this also accepts tokens that system signed, so
|
|
35
|
+
subscribers are not issued one by hand:
|
|
36
|
+
|
|
37
|
+
OHARNESS_SYNC_JWT_SECRET HS256 shared secret (Supabase: the JWT secret)
|
|
38
|
+
OHARNESS_SYNC_JWKS_URL public keys, for asymmetric signing
|
|
39
|
+
OHARNESS_SYNC_JWT_ISSUER required iss, if set
|
|
40
|
+
OHARNESS_SYNC_JWT_AUDIENCE required aud (Supabase: authenticated)
|
|
41
|
+
OHARNESS_SYNC_TEAMS_CLAIM claim carrying team ids (default: teams)
|
|
42
|
+
|
|
43
|
+
Locally issued tokens keep working either way.`;
|
|
44
|
+
async function main() {
|
|
45
|
+
const argv = process.argv.slice(2);
|
|
46
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
47
|
+
console.log(HELP);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const flag = (name) => {
|
|
51
|
+
const at = argv.indexOf(`--${name}`);
|
|
52
|
+
return at === -1 ? undefined : argv[at + 1];
|
|
53
|
+
};
|
|
54
|
+
const root = flag("root") ?? userPath("sync");
|
|
55
|
+
if (argv[0] === "token") {
|
|
56
|
+
const user = argv[1];
|
|
57
|
+
if (!user) {
|
|
58
|
+
console.error("usage: oharness-sync token <user> [--team <id>]...");
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const teams = argv.flatMap((arg, at) => (arg === "--team" && argv[at + 1] ? [argv[at + 1]] : []));
|
|
63
|
+
const token = await issueToken(root, user, teams);
|
|
64
|
+
console.log(`\nToken for ${user}:\n\n ${token}\n`);
|
|
65
|
+
if (teams.length)
|
|
66
|
+
console.log(`Teams: ${teams.join(", ")}\n`);
|
|
67
|
+
console.log("Stored as a hash — this is the only time it is shown.\n");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const port = Number.parseInt(flag("port") ?? "4483", 10);
|
|
71
|
+
if (Number.isNaN(port)) {
|
|
72
|
+
console.error("--port needs a number");
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const host = flag("host");
|
|
77
|
+
const env = process.env;
|
|
78
|
+
const jwt = {
|
|
79
|
+
...(env["OHARNESS_SYNC_JWT_SECRET"] ? { secret: env["OHARNESS_SYNC_JWT_SECRET"] } : {}),
|
|
80
|
+
...(env["OHARNESS_SYNC_JWKS_URL"] ? { jwksUrl: env["OHARNESS_SYNC_JWKS_URL"] } : {}),
|
|
81
|
+
...(env["OHARNESS_SYNC_JWT_ISSUER"] ? { issuer: env["OHARNESS_SYNC_JWT_ISSUER"] } : {}),
|
|
82
|
+
...(env["OHARNESS_SYNC_JWT_AUDIENCE"] ? { audience: env["OHARNESS_SYNC_JWT_AUDIENCE"] } : {}),
|
|
83
|
+
...(env["OHARNESS_SYNC_TEAMS_CLAIM"] ? { teamsClaim: env["OHARNESS_SYNC_TEAMS_CLAIM"] } : {}),
|
|
84
|
+
};
|
|
85
|
+
const signedIn = Boolean(jwt.secret || jwt.jwksUrl);
|
|
86
|
+
const server = await startSyncServer({
|
|
87
|
+
root,
|
|
88
|
+
port,
|
|
89
|
+
...(host ? { host } : {}),
|
|
90
|
+
...(signedIn ? { jwt } : {}),
|
|
91
|
+
});
|
|
92
|
+
const users = await readUsers(root);
|
|
93
|
+
console.log(`\noharness sync · ${server.url}`);
|
|
94
|
+
console.log(`storage ${root}`);
|
|
95
|
+
console.log(signedIn ? "accounts signed tokens, plus users.json" : "accounts users.json only");
|
|
96
|
+
if (!users.length && !signedIn) {
|
|
97
|
+
console.log("\n! No users yet — nothing can authenticate.");
|
|
98
|
+
console.log("! Run: oharness-sync token <your-name>\n");
|
|
99
|
+
}
|
|
100
|
+
else if (users.length) {
|
|
101
|
+
console.log(`local tokens ${users.map((user) => user.id).join(", ")}\n`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log("");
|
|
105
|
+
}
|
|
106
|
+
const stop = () => void server.close().then(() => process.exit(0));
|
|
107
|
+
process.on("SIGINT", stop);
|
|
108
|
+
process.on("SIGTERM", stop);
|
|
109
|
+
}
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
console.error(errorMessage(err));
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
});
|
|
114
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAErE;;;GAGG;AAEH,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+CAkCkC,CAAC;AAEhD,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,IAAY,EAAsB,EAAE;QAChD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QACrC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE9C,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;YACpE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACnG,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,UAAU,KAAK,IAAI,CAAC,CAAC;QACpD,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO,CAAC,GAAG,CAAC,UAAU,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;QACvE,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;IACzD,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACvC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IACxB,MAAM,GAAG,GAAG;QACV,GAAG,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,0BAA0B,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,GAAG,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,wBAAwB,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpF,GAAG,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,0BAA0B,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,GAAG,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,4BAA4B,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7F,GAAG,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9F,CAAC;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAEpD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC;QACnC,IAAI;QACJ,IAAI;QACJ,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7B,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IAEpC,OAAO,CAAC,GAAG,CAAC,uBAAuB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAC/B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,yCAAyC,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC;IAC/F,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAC;QAC7D,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;IAC3D,CAAC;SAAM,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3E,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC3B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire, re-exported.
|
|
3
|
+
*
|
|
4
|
+
* The definitions live in the engine because both halves need them and only
|
|
5
|
+
* one direction of dependency is allowed: this package depends on the engine,
|
|
6
|
+
* so the engine cannot import from here. Keeping one copy is the point —
|
|
7
|
+
* a client and a server that each own their idea of the wire drift, and the
|
|
8
|
+
* drift shows up as records that silently fail to merge.
|
|
9
|
+
*/
|
|
10
|
+
export type { PullResponse, PushRequest, PushResponse, WireRecord, WireSession } from "@oharness/engine/sync/protocol.ts";
|
|
11
|
+
export { DEFAULT_PULL_LIMIT, MAX_BODY_BYTES, MAX_PUSH_RECORDS, MAX_WAIT_SECONDS, } from "@oharness/engine/sync/protocol.ts";
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,mCAAmC,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { JwtOptions } from "@oharness/engine/util/jwt.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The optional half of sync.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here runs unless someone starts it. A harness with no `sync` block in
|
|
6
|
+
* its config never opens a socket, and the local store is complete on its own —
|
|
7
|
+
* this exists because two machines cannot hand each other a transcript without
|
|
8
|
+
* somewhere to leave it, not because the harness needs a backend.
|
|
9
|
+
*
|
|
10
|
+
* It is deliberately dumb: it orders opaque blobs and hands them back. It does
|
|
11
|
+
* not parse messages, call models, or know what a tool is. That is what keeps
|
|
12
|
+
* it safe to leave running, and what keeps the message format free to change
|
|
13
|
+
* without a deploy.
|
|
14
|
+
*
|
|
15
|
+
* **TLS is not handled here.** Put it behind a reverse proxy that terminates
|
|
16
|
+
* HTTPS. A bearer token over plaintext on an untrusted network is a token
|
|
17
|
+
* anyone on that network now has.
|
|
18
|
+
*/
|
|
19
|
+
export interface SyncUser {
|
|
20
|
+
id: string;
|
|
21
|
+
/** sha256 of the bearer token. The token itself is never stored. */
|
|
22
|
+
tokenHash: string;
|
|
23
|
+
/**
|
|
24
|
+
* Teams this token may act for.
|
|
25
|
+
*
|
|
26
|
+
* Membership is recorded against the token rather than asked for per
|
|
27
|
+
* request, because the alternative is a sync server that has to call an
|
|
28
|
+
* account service to answer a push — putting someone else's availability in
|
|
29
|
+
* front of every write, on the one endpoint that exists to make transcripts
|
|
30
|
+
* durable.
|
|
31
|
+
*/
|
|
32
|
+
teams?: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface SyncServerOptions {
|
|
35
|
+
/** Where feeds are written. Defaults to `sync/` under the user directory. */
|
|
36
|
+
root: string;
|
|
37
|
+
port?: number;
|
|
38
|
+
/** Defaults to loopback. Bind wider only behind a proxy you control. */
|
|
39
|
+
host?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Accept tokens the account system signed, alongside locally issued ones.
|
|
42
|
+
*
|
|
43
|
+
* This is what makes the hosted deployment possible: without it every new
|
|
44
|
+
* subscriber needs a shell on this container to be given a token, which is
|
|
45
|
+
* not a sign-up flow. Self-hosting stays exactly as it was — nothing here is
|
|
46
|
+
* required, and users.json keeps working whether or not it is set.
|
|
47
|
+
*/
|
|
48
|
+
jwt?: JwtOptions;
|
|
49
|
+
}
|
|
50
|
+
export interface SyncServer {
|
|
51
|
+
url: string;
|
|
52
|
+
port: number;
|
|
53
|
+
close(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export declare function startSyncServer(opts: SyncServerOptions): Promise<SyncServer>;
|
|
56
|
+
/**
|
|
57
|
+
* Accounts, in a file.
|
|
58
|
+
*
|
|
59
|
+
* One entry is the single-user case and costs nothing; more entries isolate
|
|
60
|
+
* users by feed directory. There is no sign-up flow on purpose — issuing a
|
|
61
|
+
* token is an administrative act, and a sync server that can create accounts
|
|
62
|
+
* is a sync server with a registration endpoint to defend.
|
|
63
|
+
*/
|
|
64
|
+
export declare function readUsers(root: string): Promise<SyncUser[]>;
|
|
65
|
+
/** Issue a token for a user, returning it once. Only the hash is stored. */
|
|
66
|
+
export declare function issueToken(root: string, id: string, teams?: string[]): Promise<string>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { errorMessage } from "@oharness/engine/core/errors.ts";
|
|
6
|
+
import { parseJson } from "@oharness/engine/util/json.ts";
|
|
7
|
+
import { Feed, listProjects } from "./feed.js";
|
|
8
|
+
import { JwtVerifier } from "@oharness/engine/util/jwt.ts";
|
|
9
|
+
import { DEFAULT_PULL_LIMIT, MAX_BODY_BYTES, MAX_PUSH_RECORDS, MAX_WAIT_SECONDS, } from "./protocol.js";
|
|
10
|
+
export async function startSyncServer(opts) {
|
|
11
|
+
const host = opts.host ?? "127.0.0.1";
|
|
12
|
+
const verifier = opts.jwt ? new JwtVerifier(opts.jwt) : undefined;
|
|
13
|
+
const feeds = new Map();
|
|
14
|
+
const feed = (owner, project) => {
|
|
15
|
+
const key = `${owner.kind}\u0000${owner.id}\u0000${project}`;
|
|
16
|
+
let held = feeds.get(key);
|
|
17
|
+
if (!held) {
|
|
18
|
+
held = new Feed(opts.root, owner, project);
|
|
19
|
+
feeds.set(key, held);
|
|
20
|
+
}
|
|
21
|
+
return held;
|
|
22
|
+
};
|
|
23
|
+
const server = createServer((req, res) => {
|
|
24
|
+
void handle(req, res).catch((err) => send(res, 500, { error: errorMessage(err) }));
|
|
25
|
+
});
|
|
26
|
+
async function handle(req, res) {
|
|
27
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
28
|
+
const route = `${req.method} ${url.pathname}`;
|
|
29
|
+
// Unauthenticated on purpose: a health check that needs a credential is a
|
|
30
|
+
// health check nobody wires up.
|
|
31
|
+
if (route === "GET /v1/health")
|
|
32
|
+
return send(res, 200, { ok: true });
|
|
33
|
+
const user = await authenticate(opts.root, req, verifier);
|
|
34
|
+
if (!user) {
|
|
35
|
+
res.setHeader("www-authenticate", "Bearer");
|
|
36
|
+
return send(res, 401, { error: "missing or bad token" });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whose feed this request is for.
|
|
40
|
+
*
|
|
41
|
+
* Absent `team`, a person's own. Naming a team the token does not hold is
|
|
42
|
+
* refused rather than quietly answered with the caller's own feed — that
|
|
43
|
+
* substitution reads to the user as "the team has nothing in it", which is
|
|
44
|
+
* the one wrong answer they would act on.
|
|
45
|
+
*/
|
|
46
|
+
const owned = (named) => {
|
|
47
|
+
const team = (named ?? "").trim();
|
|
48
|
+
if (!team)
|
|
49
|
+
return { kind: "user", id: user.id };
|
|
50
|
+
return (user.teams ?? []).includes(team) ? { kind: "team", id: team } : undefined;
|
|
51
|
+
};
|
|
52
|
+
switch (route) {
|
|
53
|
+
case "GET /v1/projects": {
|
|
54
|
+
const owner = owned(url.searchParams.get("team"));
|
|
55
|
+
if (!owner)
|
|
56
|
+
return send(res, 403, { error: "not a member of that team" });
|
|
57
|
+
return send(res, 200, { projects: await listProjects(opts.root, owner) });
|
|
58
|
+
}
|
|
59
|
+
case "POST /v1/push": {
|
|
60
|
+
const body = (await readJson(req));
|
|
61
|
+
const project = typeof body.project === "string" ? body.project : "";
|
|
62
|
+
if (!project)
|
|
63
|
+
return send(res, 400, { error: "project is required" });
|
|
64
|
+
const records = (Array.isArray(body.records) ? body.records : []).filter(isRecord);
|
|
65
|
+
const sessions = (Array.isArray(body.sessions) ? body.sessions : []).filter(isSession);
|
|
66
|
+
// Refused rather than truncated: accepting half a batch and reporting
|
|
67
|
+
// success would lose turns the client then considers delivered.
|
|
68
|
+
if (records.length > MAX_PUSH_RECORDS || sessions.length > MAX_PUSH_RECORDS) {
|
|
69
|
+
return send(res, 413, { error: `at most ${MAX_PUSH_RECORDS} entries per push` });
|
|
70
|
+
}
|
|
71
|
+
const owner = owned(typeof body.team === "string" ? body.team : undefined);
|
|
72
|
+
if (!owner)
|
|
73
|
+
return send(res, 403, { error: "not a member of that team" });
|
|
74
|
+
const result = await feed(owner, project).append(records, sessions);
|
|
75
|
+
return send(res, 200, result);
|
|
76
|
+
}
|
|
77
|
+
case "GET /v1/pull": {
|
|
78
|
+
const project = url.searchParams.get("project") ?? "";
|
|
79
|
+
if (!project)
|
|
80
|
+
return send(res, 400, { error: "project is required" });
|
|
81
|
+
const since = Number.parseInt(url.searchParams.get("since") ?? "0", 10) || 0;
|
|
82
|
+
const limit = clamp(Number.parseInt(url.searchParams.get("limit") ?? "", 10) || DEFAULT_PULL_LIMIT, 1, DEFAULT_PULL_LIMIT);
|
|
83
|
+
const wait = clamp(Number.parseInt(url.searchParams.get("wait") ?? "0", 10) || 0, 0, MAX_WAIT_SECONDS);
|
|
84
|
+
const owner = owned(url.searchParams.get("team"));
|
|
85
|
+
if (!owner)
|
|
86
|
+
return send(res, 403, { error: "not a member of that team" });
|
|
87
|
+
const target = feed(owner, project);
|
|
88
|
+
// A long poll is how the second device sees a turn while it is being
|
|
89
|
+
// typed rather than on the next timer.
|
|
90
|
+
if (wait > 0 && since >= (await target.cursor()))
|
|
91
|
+
await target.wait(since, wait);
|
|
92
|
+
const { entries, more } = await target.since(since, limit);
|
|
93
|
+
const answer = {
|
|
94
|
+
cursor: entries.length ? entries[entries.length - 1].cursor : since,
|
|
95
|
+
records: entries.filter((entry) => entry.kind === "record").map(stripCursor),
|
|
96
|
+
sessions: entries.filter((entry) => entry.kind === "session").map(stripCursor),
|
|
97
|
+
more,
|
|
98
|
+
};
|
|
99
|
+
return send(res, 200, answer);
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
return send(res, 404, { error: "no such endpoint" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await new Promise((resolve, reject) => {
|
|
106
|
+
server.once("error", reject);
|
|
107
|
+
server.listen(opts.port ?? 0, host, () => {
|
|
108
|
+
server.off("error", reject);
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
const address = server.address();
|
|
113
|
+
const port = typeof address === "object" && address ? address.port : (opts.port ?? 0);
|
|
114
|
+
return {
|
|
115
|
+
url: `http://${host}:${port}`,
|
|
116
|
+
port,
|
|
117
|
+
close: () => closeServer(server),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// --- users -----------------------------------------------------------------
|
|
121
|
+
/**
|
|
122
|
+
* Accounts, in a file.
|
|
123
|
+
*
|
|
124
|
+
* One entry is the single-user case and costs nothing; more entries isolate
|
|
125
|
+
* users by feed directory. There is no sign-up flow on purpose — issuing a
|
|
126
|
+
* token is an administrative act, and a sync server that can create accounts
|
|
127
|
+
* is a sync server with a registration endpoint to defend.
|
|
128
|
+
*/
|
|
129
|
+
export async function readUsers(root) {
|
|
130
|
+
const raw = await readFile(path.join(root, "users.json"), "utf8").catch(() => undefined);
|
|
131
|
+
if (!raw)
|
|
132
|
+
return [];
|
|
133
|
+
const parsed = parseJson(raw);
|
|
134
|
+
if (!parsed.ok || !Array.isArray(parsed.value.users))
|
|
135
|
+
return [];
|
|
136
|
+
return parsed.value.users
|
|
137
|
+
.filter((user) => typeof user?.id === "string" && typeof user?.tokenHash === "string")
|
|
138
|
+
.map((user) => ({
|
|
139
|
+
...user,
|
|
140
|
+
// Normalised on read so a hand-edited users.json cannot turn a string
|
|
141
|
+
// into a membership that `includes` would match one character of.
|
|
142
|
+
teams: Array.isArray(user.teams) ? user.teams.filter((t) => typeof t === "string") : [],
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
/** Issue a token for a user, returning it once. Only the hash is stored. */
|
|
146
|
+
export async function issueToken(root, id, teams = []) {
|
|
147
|
+
const token = randomBytes(32).toString("base64url");
|
|
148
|
+
const users = (await readUsers(root)).filter((user) => user.id !== id);
|
|
149
|
+
users.push({ id, tokenHash: hash(token), ...(teams.length ? { teams } : {}) });
|
|
150
|
+
const file = path.join(root, "users.json");
|
|
151
|
+
await mkdir(root, { recursive: true });
|
|
152
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
153
|
+
await writeFile(temp, `${JSON.stringify({ users }, null, 2)}\n`, "utf8");
|
|
154
|
+
await rename(temp, file);
|
|
155
|
+
return token;
|
|
156
|
+
}
|
|
157
|
+
function hash(token) {
|
|
158
|
+
return createHash("sha256").update(token).digest("hex");
|
|
159
|
+
}
|
|
160
|
+
async function authenticate(root, req, verifier) {
|
|
161
|
+
const supplied = (req.headers.authorization ?? "").replace(/^Bearer\s+/i, "").trim();
|
|
162
|
+
if (!supplied)
|
|
163
|
+
return undefined;
|
|
164
|
+
// A locally issued token is 32 random bytes in base64url and can never hold
|
|
165
|
+
// a dot, so the two forms are told apart by shape rather than by trying one
|
|
166
|
+
// and falling through — which would make every bad JWT cost a file read.
|
|
167
|
+
if (verifier?.enabled && supplied.includes(".")) {
|
|
168
|
+
const claims = await verifier.verify(supplied);
|
|
169
|
+
if (!claims)
|
|
170
|
+
return undefined;
|
|
171
|
+
// No tokenHash: nothing about this account is stored here. The signature
|
|
172
|
+
// is the whole of what this server knows, which is the point — it holds no
|
|
173
|
+
// account records to keep in step with the account system.
|
|
174
|
+
return { id: claims.sub, tokenHash: "", teams: claims.teams };
|
|
175
|
+
}
|
|
176
|
+
const digest = Buffer.from(hash(supplied));
|
|
177
|
+
for (const user of await readUsers(root)) {
|
|
178
|
+
const known = Buffer.from(user.tokenHash);
|
|
179
|
+
// Constant time: comparing hashes rather than tokens keeps the stored form
|
|
180
|
+
// useless to whoever reads the file, and the timing safe either way.
|
|
181
|
+
if (known.length === digest.length && timingSafeEqual(known, digest))
|
|
182
|
+
return user;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
// --- plumbing --------------------------------------------------------------
|
|
187
|
+
function isRecord(value) {
|
|
188
|
+
const record = value;
|
|
189
|
+
return Boolean(record &&
|
|
190
|
+
typeof record.session === "string" &&
|
|
191
|
+
typeof record.id === "string" &&
|
|
192
|
+
typeof record.seq === "number" &&
|
|
193
|
+
typeof record.device === "string");
|
|
194
|
+
}
|
|
195
|
+
function isSession(value) {
|
|
196
|
+
const session = value;
|
|
197
|
+
return Boolean(session &&
|
|
198
|
+
typeof session.id === "string" &&
|
|
199
|
+
typeof session.gen === "number" &&
|
|
200
|
+
typeof session.updatedAt === "number");
|
|
201
|
+
}
|
|
202
|
+
function stripCursor(entry) {
|
|
203
|
+
const { cursor: _cursor, kind: _kind, ...rest } = entry;
|
|
204
|
+
return rest;
|
|
205
|
+
}
|
|
206
|
+
function clamp(value, low, high) {
|
|
207
|
+
return Math.min(high, Math.max(low, value));
|
|
208
|
+
}
|
|
209
|
+
function send(res, status, body) {
|
|
210
|
+
const payload = JSON.stringify(body);
|
|
211
|
+
res.writeHead(status, {
|
|
212
|
+
"content-type": "application/json; charset=utf-8",
|
|
213
|
+
"content-length": Buffer.byteLength(payload),
|
|
214
|
+
});
|
|
215
|
+
res.end(payload);
|
|
216
|
+
}
|
|
217
|
+
async function readJson(req) {
|
|
218
|
+
const chunks = [];
|
|
219
|
+
let size = 0;
|
|
220
|
+
for await (const chunk of req) {
|
|
221
|
+
size += chunk.length;
|
|
222
|
+
// Checked while reading: buffering the whole body first is exactly what the
|
|
223
|
+
// limit exists to prevent.
|
|
224
|
+
if (size > MAX_BODY_BYTES)
|
|
225
|
+
throw new Error("request body too large");
|
|
226
|
+
chunks.push(chunk);
|
|
227
|
+
}
|
|
228
|
+
if (!chunks.length)
|
|
229
|
+
return {};
|
|
230
|
+
const parsed = parseJson(Buffer.concat(chunks).toString("utf8"));
|
|
231
|
+
if (!parsed.ok)
|
|
232
|
+
throw new Error("invalid JSON body");
|
|
233
|
+
return parsed.value;
|
|
234
|
+
}
|
|
235
|
+
function closeServer(server) {
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
server.closeAllConnections?.();
|
|
238
|
+
server.close(() => resolve());
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oharness/sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The optional sync server: orders opaque transcript records so two machines can hand each other a session.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.6.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/server.js",
|
|
11
|
+
"types": "./dist/server.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"oharness-source": "./src/server.ts",
|
|
15
|
+
"types": "./dist/server.d.ts",
|
|
16
|
+
"default": "./dist/server.js"
|
|
17
|
+
},
|
|
18
|
+
"./*.ts": {
|
|
19
|
+
"oharness-source": "./src/*.ts",
|
|
20
|
+
"types": "./dist/*.d.ts",
|
|
21
|
+
"default": "./dist/*.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"oharness-sync": "./dist/main.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@oharness/engine": "0.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|