@tuongaz/seeflow 0.1.109 → 0.1.111
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/web/assets/{architectureDiagram-3BPJPVTR-CvIAMnMC.js → architectureDiagram-3BPJPVTR-C-2fCGRP.js} +1 -1
- package/dist/web/assets/{blockDiagram-GPEHLZMM-C55itdvA.js → blockDiagram-GPEHLZMM-Br3qZBhv.js} +1 -1
- package/dist/web/assets/{c4Diagram-AAUBKEIU-BUJz6OcY.js → c4Diagram-AAUBKEIU-C4uX4ZH4.js} +1 -1
- package/dist/web/assets/channel-qhQeh_4Q.js +1 -0
- package/dist/web/assets/{chart-Ck4M6xxI.js → chart-9kjHxc0V.js} +1 -1
- package/dist/web/assets/{chunk-2J33WTMH-CYHRe8M7.js → chunk-2J33WTMH-2Fn5NN7i.js} +1 -1
- package/dist/web/assets/{chunk-4BX2VUAB-0G6VpCw_.js → chunk-4BX2VUAB-Df_7ccfg.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-YeyA7Efg.js → chunk-55IACEB6-DdU4dw35.js} +1 -1
- package/dist/web/assets/{chunk-727SXJPM-irL9oAdE.js → chunk-727SXJPM-1at9fA5f.js} +1 -1
- package/dist/web/assets/{chunk-AQP2D5EJ-DB0ZGTqs.js → chunk-AQP2D5EJ-CPe9M1hv.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-Z48rYWhG.js → chunk-FMBD7UC4-DCUEmPvJ.js} +1 -1
- package/dist/web/assets/{chunk-ND2GUHAM-BgYSDKdi.js → chunk-ND2GUHAM-tE2Ii-jJ.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-CzcjFmi-.js → chunk-QZHKN3VN-Db5o8Kzv.js} +1 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-BCHg-KrI.js +1 -0
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BCHg-KrI.js +1 -0
- package/dist/web/assets/{code-block-BwE8Ip6V.js → code-block-CTINSk3z.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-DtufDmle.js → cose-bilkent-S5V4N54A-D7tNx5-t.js} +1 -1
- package/dist/web/assets/{dagre-BM42HDAG-DwYVzLme.js → dagre-BM42HDAG-Cdb53Asb.js} +1 -1
- package/dist/web/assets/{diagram-2AECGRRQ-DDw_qvjI.js → diagram-2AECGRRQ-HS6pnGrL.js} +1 -1
- package/dist/web/assets/{diagram-5GNKFQAL-BYG2VdwF.js → diagram-5GNKFQAL-c9k4swAt.js} +1 -1
- package/dist/web/assets/{diagram-KO2AKTUF-Drp2zu_G.js → diagram-KO2AKTUF-CB3k4xVS.js} +1 -1
- package/dist/web/assets/{diagram-LMA3HP47-CXmAihfn.js → diagram-LMA3HP47-XKAfR3DB.js} +1 -1
- package/dist/web/assets/{diagram-OG6HWLK6-i6-iKnAU.js → diagram-OG6HWLK6-18dGr0GC.js} +1 -1
- package/dist/web/assets/{erDiagram-TEJ5UH35-Bo96y9hO.js → erDiagram-TEJ5UH35-9DnFE7wo.js} +1 -1
- package/dist/web/assets/{flowDiagram-I6XJVG4X-D9fneMTu.js → flowDiagram-I6XJVG4X-CglLanUO.js} +1 -1
- package/dist/web/assets/{ganttDiagram-6RSMTGT7-Cbefa8NE.js → ganttDiagram-6RSMTGT7-_zKCXbXg.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PVQCEYII-BHYd5nUs.js → gitGraphDiagram-PVQCEYII-D6axSpL3.js} +1 -1
- package/dist/web/assets/{iconify-BuhA_8An.js → iconify-CF3Xx8Oq.js} +1 -1
- package/dist/web/assets/index-BTL8cVgF.js +8629 -0
- package/dist/web/assets/{index.es-pImjWTGX.js → index.es-DyQqI4EJ.js} +1 -1
- package/dist/web/assets/{infoDiagram-5YYISTIA-Bw7MIIwj.js → infoDiagram-5YYISTIA-BfnEVJMR.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-CySt1i7t.js → ishikawaDiagram-YF4QCWOH-C-CbbOkc.js} +1 -1
- package/dist/web/assets/{journeyDiagram-JHISSGLW-q4YV61wv.js → journeyDiagram-JHISSGLW-CxcYZYhO.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-B95ptK5L.js → jspdf.es.min-BoDPBXoT.js} +3 -3
- package/dist/web/assets/{kanban-definition-UN3LZRKU-C1oN-zoM.js → kanban-definition-UN3LZRKU-Dw9dGj5D.js} +1 -1
- package/dist/web/assets/{linear-DPSv7VcC.js → linear-DJLoiaYn.js} +1 -1
- package/dist/web/assets/{markdown-btlM2PIA.js → markdown-BXIhHJY7.js} +1 -1
- package/dist/web/assets/{mermaid.core-Cmch2GTm.js → mermaid.core-CTeg8SMj.js} +4 -4
- package/dist/web/assets/{mindmap-definition-RKZ34NQL--XOTWr6e.js → mindmap-definition-RKZ34NQL-DG4pYeYK.js} +1 -1
- package/dist/web/assets/{pieDiagram-4H26LBE5-DxZgtzul.js → pieDiagram-4H26LBE5-qPHgJ6_d.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-W4KKPZXB-bbV2bAO3.js → quadrantDiagram-W4KKPZXB-C-yiyax0.js} +1 -1
- package/dist/web/assets/{requirementDiagram-4Y6WPE33-CiJM8hio.js → requirementDiagram-4Y6WPE33-vBgnScGl.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-5OEKKPKP-DABBnuaB.js → sankeyDiagram-5OEKKPKP-H-Yoexss.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-3UESZ5HK-BTQJk7bM.js → sequenceDiagram-3UESZ5HK-B0PUqbDC.js} +1 -1
- package/dist/web/assets/{stateDiagram-AJRCARHV-yL0tAd3x.js → stateDiagram-AJRCARHV-LHZJU6NT.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-Jjcvv867.js +1 -0
- package/dist/web/assets/{time-hCZUKGxT.js → time-CytNQj8l.js} +1 -1
- package/dist/web/assets/{timeline-definition-PNZ67QCA-CG6k16Wj.js → timeline-definition-PNZ67QCA-CNYtzzkX.js} +1 -1
- package/dist/web/assets/{vennDiagram-CIIHVFJN-BVH530xf.js → vennDiagram-CIIHVFJN-CR3j3MfQ.js} +1 -1
- package/dist/web/assets/{wardley-L42UT6IY-BYVkFMrL.js → wardley-L42UT6IY-DsRtYkk3.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BGMuHejP.js → wardleyDiagram-YWT4CUSO-Bkiae0No.js} +1 -1
- package/dist/web/assets/{xychartDiagram-2RQKCTM6-D6MSlnJD.js → xychartDiagram-2RQKCTM6-DFoBCrdz.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/api.ts +0 -291
- package/src/server.ts +0 -20
- package/dist/web/assets/channel-DtcQ9fhj.js +0 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-BjYXB41E.js +0 -1
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BjYXB41E.js +0 -1
- package/dist/web/assets/index-08hmlCqO.js +0 -8644
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-BzWj6i1l.js +0 -1
- package/src/share/sse-frame.ts +0 -85
- package/src/share/sse-outbound-queue.ts +0 -173
- package/src/share/sse-rate-limit.ts +0 -205
- package/src/share/sse-tap.ts +0 -183
- package/src/share-audit.ts +0 -267
- package/src/share-envelope.ts +0 -155
- package/src/share-file-request.ts +0 -399
- package/src/share-file-resolver.ts +0 -68
- package/src/share-file-upload.ts +0 -595
- package/src/share-files-manifest.ts +0 -232
- package/src/share-ratelimit.ts +0 -69
- package/src/share-rpc-schema.ts +0 -249
- package/src/share-transport.ts +0 -205
- package/src/share.ts +0 -1663
package/src/share-audit.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-session JSONL audit log for inbound share frames.
|
|
3
|
-
*
|
|
4
|
-
* One file per session at `${opts.dir}/${opts.sessionId}.jsonl`. Each accepted
|
|
5
|
-
* or rejected envelope writes one JSON.stringify(entry) + '\n' line via
|
|
6
|
-
* `fs.appendFileSync` (or an injected appender for tests). The directory is
|
|
7
|
-
* created with mkdirSync recursive on first write.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
11
|
-
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
12
|
-
import { homedir } from 'node:os';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import type { Envelope } from './share-envelope.ts';
|
|
15
|
-
|
|
16
|
-
export type AuditVerdict = 'accept' | 'reject';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Per-frame audit entry written by the WS message dispatcher. Coexists with
|
|
20
|
-
* `RpcAuditEntry`, `FileUploadAuditEntry`, and `AuditEntry` (the US-078 shape)
|
|
21
|
-
* on the same JSONL file; readers should treat each line as the union.
|
|
22
|
-
*/
|
|
23
|
-
export interface FrameAuditEntry {
|
|
24
|
-
ts: number;
|
|
25
|
-
peerId: string;
|
|
26
|
-
displayName: string;
|
|
27
|
-
type: Envelope['type'];
|
|
28
|
-
verdict: AuditVerdict;
|
|
29
|
-
reason?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AuditLog {
|
|
33
|
-
append(entry: FrameAuditEntry): void;
|
|
34
|
-
close(): Promise<void>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface AuditLogOpts {
|
|
38
|
-
dir: string;
|
|
39
|
-
sessionId: string;
|
|
40
|
-
// Injected for tests so we don't need a tmp dir. Default uses fs.appendFileSync.
|
|
41
|
-
appendFn?: (filePath: string, line: string) => void;
|
|
42
|
-
// Injected for tests so we don't need to mkdir on disk. Default uses fs.mkdirSync.
|
|
43
|
-
mkdirFn?: (dir: string) => void;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const defaultAppend = (filePath: string, line: string): void => {
|
|
47
|
-
appendFileSync(filePath, line);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const defaultMkdir = (dir: string): void => {
|
|
51
|
-
mkdirSync(dir, { recursive: true });
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* RPC-specific audit entry shape. Used by US-038's `handleRpcFrame` after a
|
|
56
|
-
* dispatch attempt — written via `appendShareAudit` to the same per-session
|
|
57
|
-
* JSONL file `createAuditLog` writes to, so the two entry shapes coexist as a
|
|
58
|
-
* union on disk (callers reading the file should treat each line as
|
|
59
|
-
* `AuditEntry | RpcAuditEntry`).
|
|
60
|
-
*/
|
|
61
|
-
export interface RpcAuditEntry {
|
|
62
|
-
ts: number;
|
|
63
|
-
peerId: string;
|
|
64
|
-
op: string;
|
|
65
|
-
flowId: string;
|
|
66
|
-
ok: boolean;
|
|
67
|
-
reason?: string;
|
|
68
|
-
// Mirrors the `attributedTo` field on the outgoing `node-patched` broadcast
|
|
69
|
-
// so the audit trail records the same originator label peers will see. For
|
|
70
|
-
// peer-originated rpcs this is `{ peerId, displayName }` from the share
|
|
71
|
-
// controller's peer map; for host-local edits it is
|
|
72
|
-
// `{ peerId: 'host', displayName: hostDisplayName }`.
|
|
73
|
-
attributedTo?: { peerId: string; displayName: string };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface AppendShareAuditOpts {
|
|
77
|
-
// Override the default `~/.seeflow/share-history` root. Tests inject a
|
|
78
|
-
// tmpdir so they never touch the user's real audit dir.
|
|
79
|
-
dir?: string;
|
|
80
|
-
appendFn?: (filePath: string, line: string) => void;
|
|
81
|
-
mkdirFn?: (dir: string) => void;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const isSafeSessionId = (sessionId: string): boolean => {
|
|
85
|
-
if (sessionId.length === 0) return false;
|
|
86
|
-
if (sessionId === '.' || sessionId === '..') return false;
|
|
87
|
-
if (sessionId.includes('/') || sessionId.includes('\\')) return false;
|
|
88
|
-
if (sessionId.includes('\0')) return false;
|
|
89
|
-
return true;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* File-upload audit entry. Mirrors the design doc shape:
|
|
94
|
-
* `{ peerId, op:'file-upload', nodeId, filename, size, sha256, ts, accept }`.
|
|
95
|
-
* Coexists with `RpcAuditEntry` + `AuditEntry` on the same JSONL file; readers
|
|
96
|
-
* should treat each line as the union.
|
|
97
|
-
*/
|
|
98
|
-
export interface FileUploadAuditEntry {
|
|
99
|
-
ts: number;
|
|
100
|
-
peerId: string;
|
|
101
|
-
op: 'file-upload';
|
|
102
|
-
nodeId: string;
|
|
103
|
-
filename: string;
|
|
104
|
-
size: number;
|
|
105
|
-
sha256: string;
|
|
106
|
-
accept: boolean;
|
|
107
|
-
reason?: string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Append one JSON line to `<dir>/<sessionId>.jsonl`. The directory is created
|
|
112
|
-
* recursively on first write. Rejects sessionIds that contain path separators,
|
|
113
|
-
* NUL bytes, or `..` traversal attempts so a tampered peer can't drop frames
|
|
114
|
-
* outside the audit root.
|
|
115
|
-
*/
|
|
116
|
-
export function appendShareAudit(
|
|
117
|
-
sessionId: string,
|
|
118
|
-
entry: RpcAuditEntry | FileUploadAuditEntry,
|
|
119
|
-
opts: AppendShareAuditOpts = {},
|
|
120
|
-
): void {
|
|
121
|
-
if (!isSafeSessionId(sessionId)) {
|
|
122
|
-
throw new Error(`invalid sessionId: ${JSON.stringify(sessionId)}`);
|
|
123
|
-
}
|
|
124
|
-
const dir = opts.dir ?? join(homedir(), '.seeflow', 'share-history');
|
|
125
|
-
const appendFn = opts.appendFn ?? defaultAppend;
|
|
126
|
-
const mkdirFn = opts.mkdirFn ?? defaultMkdir;
|
|
127
|
-
mkdirFn(dir);
|
|
128
|
-
const filePath = join(dir, `${sessionId}.jsonl`);
|
|
129
|
-
appendFn(filePath, `${JSON.stringify(entry)}\n`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function createAuditLog(opts: AuditLogOpts): AuditLog {
|
|
133
|
-
const appendFn = opts.appendFn ?? defaultAppend;
|
|
134
|
-
const mkdirFn = opts.mkdirFn ?? defaultMkdir;
|
|
135
|
-
const filePath = join(opts.dir, `${opts.sessionId}.jsonl`);
|
|
136
|
-
let dirReady = false;
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
append(entry) {
|
|
140
|
-
if (!dirReady) {
|
|
141
|
-
mkdirFn(opts.dir);
|
|
142
|
-
dirReady = true;
|
|
143
|
-
}
|
|
144
|
-
appendFn(filePath, `${JSON.stringify(entry)}\n`);
|
|
145
|
-
},
|
|
146
|
-
async close() {
|
|
147
|
-
// No buffered writes — appendFileSync is synchronous. Hook here for any
|
|
148
|
-
// future flush logic.
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Phase-8 audit shape covering RPCs, kicks, rotations, the kill-switch, plus
|
|
155
|
-
* host start/stop and peer join/leave. Coexists on disk with `FrameAuditEntry`,
|
|
156
|
-
* `RpcAuditEntry`, and `FileUploadAuditEntry` — readers should tolerate the
|
|
157
|
-
* union per-line.
|
|
158
|
-
*/
|
|
159
|
-
export type AuditKind =
|
|
160
|
-
| 'rpc-accept'
|
|
161
|
-
| 'rpc-reject'
|
|
162
|
-
| 'kick'
|
|
163
|
-
| 'rotate'
|
|
164
|
-
| 'kill-switch'
|
|
165
|
-
| 'host-start'
|
|
166
|
-
| 'host-stop'
|
|
167
|
-
| 'peer-join'
|
|
168
|
-
| 'peer-leave';
|
|
169
|
-
|
|
170
|
-
export interface AuditEntry {
|
|
171
|
-
ts: number;
|
|
172
|
-
peerId: string | null;
|
|
173
|
-
displayName: string | null;
|
|
174
|
-
kind: AuditKind;
|
|
175
|
-
op?: string;
|
|
176
|
-
reason?: string;
|
|
177
|
-
details?: Record<string, unknown>;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export interface AuditLogger {
|
|
181
|
-
append(entry: Omit<AuditEntry, 'ts'>): Promise<void>;
|
|
182
|
-
list(opts?: {
|
|
183
|
-
limit?: number;
|
|
184
|
-
cursor?: number;
|
|
185
|
-
}): Promise<{ entries: AuditEntry[]; nextCursor: number | null }>;
|
|
186
|
-
close(): Promise<void>;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const auditLockChains = new Map<string, Promise<void>>();
|
|
190
|
-
|
|
191
|
-
const defaultRoot = (): string => join(homedir(), '.seeflow', 'share-history');
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Build a per-session audit logger backed by `${root}/${sessionId}.jsonl`.
|
|
195
|
-
* `append` serializes inside-process per file so 10+ concurrent callers can't
|
|
196
|
-
* interleave partial lines; the kernel-level O_APPEND on `fs.appendFile` keeps
|
|
197
|
-
* cross-process writes safe up to PIPE_BUF. `list` paginates by byte offset so
|
|
198
|
-
* callers can resume from `nextCursor` without re-parsing what they've seen.
|
|
199
|
-
*/
|
|
200
|
-
export function createAuditLogger(sessionId: string, root?: string): AuditLogger {
|
|
201
|
-
if (!isSafeSessionId(sessionId)) {
|
|
202
|
-
throw new Error(`invalid sessionId: ${JSON.stringify(sessionId)}`);
|
|
203
|
-
}
|
|
204
|
-
const dir = root ?? defaultRoot();
|
|
205
|
-
const filePath = join(dir, `${sessionId}.jsonl`);
|
|
206
|
-
let dirReady = false;
|
|
207
|
-
|
|
208
|
-
const ensureDir = async (): Promise<void> => {
|
|
209
|
-
if (dirReady) return;
|
|
210
|
-
await mkdir(dir, { recursive: true });
|
|
211
|
-
dirReady = true;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const append = async (entry: Omit<AuditEntry, 'ts'>): Promise<void> => {
|
|
215
|
-
await ensureDir();
|
|
216
|
-
const full: AuditEntry = { ...entry, ts: Date.now() };
|
|
217
|
-
const line = `${JSON.stringify(full)}\n`;
|
|
218
|
-
const prev = auditLockChains.get(filePath) ?? Promise.resolve();
|
|
219
|
-
const task = prev.then(() => appendFile(filePath, line));
|
|
220
|
-
auditLockChains.set(
|
|
221
|
-
filePath,
|
|
222
|
-
task.then(
|
|
223
|
-
() => undefined,
|
|
224
|
-
() => undefined,
|
|
225
|
-
),
|
|
226
|
-
);
|
|
227
|
-
await task;
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const list = async (
|
|
231
|
-
opts: { limit?: number; cursor?: number } = {},
|
|
232
|
-
): Promise<{ entries: AuditEntry[]; nextCursor: number | null }> => {
|
|
233
|
-
const limit = opts.limit ?? 200;
|
|
234
|
-
const cursor = opts.cursor ?? 0;
|
|
235
|
-
let buf: Buffer;
|
|
236
|
-
try {
|
|
237
|
-
buf = await readFile(filePath);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
240
|
-
if (code === 'ENOENT') return { entries: [], nextCursor: null };
|
|
241
|
-
throw err;
|
|
242
|
-
}
|
|
243
|
-
const entries: AuditEntry[] = [];
|
|
244
|
-
let offset = cursor < 0 ? 0 : cursor;
|
|
245
|
-
while (offset < buf.length && entries.length < limit) {
|
|
246
|
-
const nl = buf.indexOf(0x0a, offset);
|
|
247
|
-
if (nl === -1) break;
|
|
248
|
-
const line = buf.subarray(offset, nl).toString('utf8');
|
|
249
|
-
offset = nl + 1;
|
|
250
|
-
if (line.length === 0) continue;
|
|
251
|
-
try {
|
|
252
|
-
entries.push(JSON.parse(line) as AuditEntry);
|
|
253
|
-
} catch {
|
|
254
|
-
// Skip corrupted line; advance past it so list() stays monotonic.
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const nextCursor = offset >= buf.length ? null : offset;
|
|
258
|
-
return { entries, nextCursor };
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const close = async (): Promise<void> => {
|
|
262
|
-
const chain = auditLockChains.get(filePath);
|
|
263
|
-
if (chain) await chain;
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
return { append, list, close };
|
|
267
|
-
}
|
package/src/share-envelope.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live Share WebSocket envelope schema and helpers.
|
|
3
|
-
*
|
|
4
|
-
* Every frame on the relay wire — host->relay and relay->peer — is shaped
|
|
5
|
-
* exactly like the design doc's envelope (v=1, typed `type`, optional `id`
|
|
6
|
-
* for rpc correlation, `from` connId, optional `to`, opaque `payload`).
|
|
7
|
-
* The relay is a dumb router and never inspects `payload`; validation
|
|
8
|
-
* happens at the host (this module) and the peer canvas.
|
|
9
|
-
*
|
|
10
|
-
* `parseEnvelope` returns a discriminated `{ ok }` result so the transport
|
|
11
|
-
* can drop invalid frames without throwing inside the WS read loop.
|
|
12
|
-
* `makeEnvelope` constructs outbound frames with `from='host'` by default
|
|
13
|
-
* so callers don't have to repeat it.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { z } from 'zod';
|
|
17
|
-
|
|
18
|
-
export const ENVELOPE_TYPES = [
|
|
19
|
-
'auth-host',
|
|
20
|
-
'auth-peer',
|
|
21
|
-
'rpc',
|
|
22
|
-
'rpc-result',
|
|
23
|
-
'sse',
|
|
24
|
-
'sse-snapshot',
|
|
25
|
-
'presence',
|
|
26
|
-
'cursor',
|
|
27
|
-
'file-request',
|
|
28
|
-
'file-bytes',
|
|
29
|
-
'file-redirect',
|
|
30
|
-
'file-upload-intent',
|
|
31
|
-
'file-upload-done',
|
|
32
|
-
'node-patched',
|
|
33
|
-
'files-manifest',
|
|
34
|
-
'kick',
|
|
35
|
-
'flow-snapshot',
|
|
36
|
-
'auth-refresh-required',
|
|
37
|
-
] as const;
|
|
38
|
-
|
|
39
|
-
export const EnvelopeTypeSchema = z.enum(ENVELOPE_TYPES);
|
|
40
|
-
|
|
41
|
-
export const EnvelopeSchema = z.object({
|
|
42
|
-
v: z.literal(1),
|
|
43
|
-
type: EnvelopeTypeSchema,
|
|
44
|
-
id: z.string().optional(),
|
|
45
|
-
from: z.string(),
|
|
46
|
-
to: z.string().optional(),
|
|
47
|
-
payload: z.unknown(),
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
export type EnvelopeType = z.infer<typeof EnvelopeTypeSchema>;
|
|
51
|
-
export type Envelope = z.infer<typeof EnvelopeSchema>;
|
|
52
|
-
|
|
53
|
-
// File frame payload schemas. Vendored copy of the cloud relay's per-type
|
|
54
|
-
// schemas (`cloud/lambda/share/shared/envelope.ts`) so the host can validate
|
|
55
|
-
// inbound file-request frames and shape outbound file-bytes / file-redirect /
|
|
56
|
-
// file-upload-intent / file-upload-done / files-manifest replies with the same
|
|
57
|
-
// invariants the relay enforces. Keep these aligned with the cloud copy.
|
|
58
|
-
const NodeIdSchema = z.string().regex(/^node-[A-Za-z0-9]{10}$/);
|
|
59
|
-
const Sha256HexSchema = z.string().length(64);
|
|
60
|
-
|
|
61
|
-
export const FileRequestPayloadSchema = z.object({
|
|
62
|
-
reqId: z.string().min(1),
|
|
63
|
-
nodeId: z.string(),
|
|
64
|
-
relPath: z.string(),
|
|
65
|
-
});
|
|
66
|
-
export type FileRequestPayload = z.infer<typeof FileRequestPayloadSchema>;
|
|
67
|
-
|
|
68
|
-
export const FileBytesPayloadSchema = z.object({
|
|
69
|
-
reqId: z.string(),
|
|
70
|
-
seq: z.number().int().nonnegative(),
|
|
71
|
-
total: z.number().int().nonnegative(),
|
|
72
|
-
base64: z.string(),
|
|
73
|
-
contentType: z.string().optional(),
|
|
74
|
-
sha256: z.string(),
|
|
75
|
-
eof: z.boolean(),
|
|
76
|
-
});
|
|
77
|
-
export type FileBytesPayload = z.infer<typeof FileBytesPayloadSchema>;
|
|
78
|
-
|
|
79
|
-
export const FileRedirectPayloadSchema = z.object({
|
|
80
|
-
reqId: z.string(),
|
|
81
|
-
getUrl: z.string().url(),
|
|
82
|
-
sha256: z.string(),
|
|
83
|
-
expiresAt: z.number().int().nonnegative(),
|
|
84
|
-
});
|
|
85
|
-
export type FileRedirectPayload = z.infer<typeof FileRedirectPayloadSchema>;
|
|
86
|
-
|
|
87
|
-
// Host-serve variant: the host asks the relay to mint a presigned PUT so it
|
|
88
|
-
// can stream a >256 KB file via S3 instead of inline WS chunks. `role` is set
|
|
89
|
-
// to `'host-serve'` to distinguish from peer-originated uploads (US-061).
|
|
90
|
-
export const FileUploadIntentPayloadSchema = z.object({
|
|
91
|
-
reqId: z.string(),
|
|
92
|
-
filename: z.string().min(1).max(255),
|
|
93
|
-
size: z.number().int().nonnegative().max(1_073_741_824),
|
|
94
|
-
contentType: z.string(),
|
|
95
|
-
nodeId: NodeIdSchema,
|
|
96
|
-
sha256: Sha256HexSchema,
|
|
97
|
-
role: z.enum(['host-serve', 'peer-upload']).optional(),
|
|
98
|
-
});
|
|
99
|
-
export type FileUploadIntentPayload = z.infer<typeof FileUploadIntentPayloadSchema>;
|
|
100
|
-
|
|
101
|
-
export const FileUploadDonePayloadSchema = z.object({
|
|
102
|
-
reqId: z.string(),
|
|
103
|
-
key: z.string(),
|
|
104
|
-
sha256: z.string(),
|
|
105
|
-
});
|
|
106
|
-
export type FileUploadDonePayload = z.infer<typeof FileUploadDonePayloadSchema>;
|
|
107
|
-
|
|
108
|
-
export const FilesManifestEntrySchema = z.object({
|
|
109
|
-
nodeId: z.string(),
|
|
110
|
-
relPath: z.string(),
|
|
111
|
-
size: z.number().int().nonnegative(),
|
|
112
|
-
etag: z.string(),
|
|
113
|
-
});
|
|
114
|
-
export type FilesManifestEntry = z.infer<typeof FilesManifestEntrySchema>;
|
|
115
|
-
|
|
116
|
-
export const FilesManifestPayloadSchema = z.object({
|
|
117
|
-
entries: z.array(FilesManifestEntrySchema),
|
|
118
|
-
});
|
|
119
|
-
export type FilesManifestPayload = z.infer<typeof FilesManifestPayloadSchema>;
|
|
120
|
-
|
|
121
|
-
export function parseEnvelope(
|
|
122
|
-
raw: unknown,
|
|
123
|
-
): { ok: true; envelope: Envelope } | { ok: false; reason: string } {
|
|
124
|
-
const result = EnvelopeSchema.safeParse(raw);
|
|
125
|
-
if (result.success) {
|
|
126
|
-
return { ok: true, envelope: result.data };
|
|
127
|
-
}
|
|
128
|
-
// Surface the first issue path/message so the transport's console.warn
|
|
129
|
-
// is actionable without leaking the payload itself.
|
|
130
|
-
const first = result.error.issues[0];
|
|
131
|
-
const reason = first ? `${first.path.join('.') || '<root>'}: ${first.message}` : 'invalid';
|
|
132
|
-
return { ok: false, reason };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface MakeEnvelopeOpts {
|
|
136
|
-
id?: string;
|
|
137
|
-
from?: string;
|
|
138
|
-
to?: string;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function makeEnvelope<T extends EnvelopeType>(
|
|
142
|
-
type: T,
|
|
143
|
-
payload: unknown,
|
|
144
|
-
opts: MakeEnvelopeOpts = {},
|
|
145
|
-
): Envelope {
|
|
146
|
-
const env: Envelope = {
|
|
147
|
-
v: 1,
|
|
148
|
-
type,
|
|
149
|
-
from: opts.from ?? 'host',
|
|
150
|
-
payload,
|
|
151
|
-
};
|
|
152
|
-
if (opts.id !== undefined) env.id = opts.id;
|
|
153
|
-
if (opts.to !== undefined) env.to = opts.to;
|
|
154
|
-
return env;
|
|
155
|
-
}
|