@mdbase-dev/connect-protocol 0.1.0-beta.21 → 0.1.0-beta.23

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/files.js ADDED
@@ -0,0 +1,417 @@
1
+ export const FILE_PROTOCOL_VERSION = 1;
2
+ export const FILE_TRANSFER_PROTOCOL_VERSION = 1;
3
+ export const DEFAULT_FILE_CHUNK_BYTES = 1024 * 1024;
4
+ export const MAX_FILE_CHUNK_BYTES = 4 * 1024 * 1024;
5
+ export const MAX_FILE_FRAME_HEADER_BYTES = 16 * 1024;
6
+ export const FILE_FRAME_PREFIX_BYTES = 16;
7
+ export const FILE_FRAME_MAGIC = "MDBF";
8
+ export const MAX_FILE_FRAME_BYTES = FILE_FRAME_PREFIX_BYTES
9
+ + MAX_FILE_FRAME_HEADER_BYTES
10
+ + MAX_FILE_CHUNK_BYTES
11
+ + 16;
12
+ export const RELAY_FILE_PROTOCOL_VERSION = 1;
13
+ export const RELAY_FILE_PREFIX_BYTES = 16;
14
+ export const MAX_RELAY_FILE_HEADER_BYTES = 1024;
15
+ export const MAX_RELAY_FILE_PAYLOAD_BYTES = MAX_FILE_FRAME_BYTES;
16
+ export const RELAY_FILE_MAGIC = "MDBR";
17
+ const FILE_FRAME_MAGIC_BYTES = new Uint8Array([0x4d, 0x44, 0x42, 0x46]);
18
+ const FILE_FRAME_VERSION = 1;
19
+ const FILE_FRAME_FLAGS = 0;
20
+ const RELAY_FILE_MAGIC_BYTES = new Uint8Array([0x4d, 0x44, 0x42, 0x52]);
21
+ const RELAY_FILE_FRAME_VERSION = 1;
22
+ const RELAY_FILE_FRAME_FLAGS = 0;
23
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
24
+ export class FileFrameError extends Error {
25
+ code;
26
+ constructor(code, message) {
27
+ super(message);
28
+ this.code = code;
29
+ this.name = "FileFrameError";
30
+ }
31
+ }
32
+ export class RelayFileFrameError extends Error {
33
+ code;
34
+ constructor(code, message) {
35
+ super(message);
36
+ this.code = code;
37
+ this.name = "RelayFileFrameError";
38
+ }
39
+ }
40
+ function frameKindCode(kind) {
41
+ return kind === "upload_chunk" ? 1 : 2;
42
+ }
43
+ function frameKindFromCode(code) {
44
+ if (code === 1)
45
+ return "upload_chunk";
46
+ if (code === 2)
47
+ return "download_chunk";
48
+ throw new FileFrameError("invalid_kind", `Unknown file frame kind ${code}`);
49
+ }
50
+ function expectObject(value) {
51
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
52
+ throw new FileFrameError("invalid_header", "File frame header must be a JSON object");
53
+ }
54
+ return value;
55
+ }
56
+ function expectString(object, name) {
57
+ const value = object[name];
58
+ if (typeof value !== "string" || value.length === 0) {
59
+ throw new FileFrameError("invalid_header", `File frame header ${name} must be a non-empty string`);
60
+ }
61
+ return value;
62
+ }
63
+ function expectUuid(object, name) {
64
+ const value = expectString(object, name);
65
+ if (!UUID_PATTERN.test(value)) {
66
+ throw new FileFrameError("invalid_header", `File frame header ${name} must be a UUID`);
67
+ }
68
+ return value;
69
+ }
70
+ function expectInteger(object, name, maximum = Number.MAX_SAFE_INTEGER) {
71
+ const value = object[name];
72
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
73
+ throw new FileFrameError("invalid_header", `File frame header ${name} is outside its allowed range`);
74
+ }
75
+ return value;
76
+ }
77
+ function canonicalHeader(header) {
78
+ if (header.protocol_version !== FILE_TRANSFER_PROTOCOL_VERSION) {
79
+ throw new FileFrameError("unsupported_version", `Unsupported file transfer protocol ${header.protocol_version}`);
80
+ }
81
+ if (header.protection !== "grant_aead_v1" && header.protection !== "transport_tls") {
82
+ throw new FileFrameError("invalid_header", "Unknown file transfer protection");
83
+ }
84
+ if (header.direction !== "upload" && header.direction !== "download") {
85
+ throw new FileFrameError("invalid_header", "Unknown file transfer direction");
86
+ }
87
+ for (const [name, value] of [
88
+ ["grant_id", header.grant_id],
89
+ ["authority_id", header.authority_id],
90
+ ["collection_id", header.collection_id],
91
+ ["transfer_id", header.transfer_id]
92
+ ]) {
93
+ if (!UUID_PATTERN.test(value)) {
94
+ throw new FileFrameError("invalid_header", `File frame header ${name} must be a UUID`);
95
+ }
96
+ }
97
+ for (const [name, value, maximum] of [
98
+ ["chunk_size", header.chunk_size, MAX_FILE_CHUNK_BYTES],
99
+ ["chunk_index", header.chunk_index, Number.MAX_SAFE_INTEGER],
100
+ ["offset", header.offset, Number.MAX_SAFE_INTEGER],
101
+ ["plaintext_length", header.plaintext_length, MAX_FILE_CHUNK_BYTES],
102
+ ["total_size", header.total_size, Number.MAX_SAFE_INTEGER]
103
+ ]) {
104
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
105
+ throw new FileFrameError("invalid_header", `File frame header ${name} is outside its allowed range`);
106
+ }
107
+ }
108
+ if (header.chunk_size < 64 * 1024) {
109
+ throw new FileFrameError("invalid_header", "File frame chunk_size is below the protocol minimum");
110
+ }
111
+ if (!Number.isSafeInteger(header.scope_epoch) || header.scope_epoch < 1) {
112
+ throw new FileFrameError("invalid_header", "File frame header scope_epoch must be a positive integer");
113
+ }
114
+ if (header.protection === "grant_aead_v1" && !header.key_id) {
115
+ throw new FileFrameError("invalid_header", "AEAD-protected file frames require key_id");
116
+ }
117
+ return {
118
+ protocol_version: FILE_TRANSFER_PROTOCOL_VERSION,
119
+ protection: header.protection,
120
+ grant_id: header.grant_id,
121
+ authority_id: header.authority_id,
122
+ collection_id: header.collection_id,
123
+ transfer_id: header.transfer_id,
124
+ direction: header.direction,
125
+ chunk_size: header.chunk_size,
126
+ chunk_index: header.chunk_index,
127
+ offset: header.offset,
128
+ plaintext_length: header.plaintext_length,
129
+ total_size: header.total_size,
130
+ scope_epoch: header.scope_epoch,
131
+ ...(header.key_id === undefined ? {} : { key_id: header.key_id })
132
+ };
133
+ }
134
+ function parseHeader(bytes) {
135
+ let source;
136
+ try {
137
+ source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
138
+ }
139
+ catch {
140
+ throw new FileFrameError("invalid_header", "File frame header is not valid UTF-8");
141
+ }
142
+ let object;
143
+ try {
144
+ object = expectObject(JSON.parse(source));
145
+ }
146
+ catch (error) {
147
+ if (error instanceof FileFrameError)
148
+ throw error;
149
+ throw new FileFrameError("invalid_header", "File frame header is not valid JSON");
150
+ }
151
+ const allowed = new Set([
152
+ "protocol_version", "protection", "grant_id", "authority_id", "collection_id",
153
+ "transfer_id", "direction", "chunk_size", "chunk_index", "offset", "plaintext_length",
154
+ "total_size", "scope_epoch", "key_id"
155
+ ]);
156
+ if (Object.keys(object).some((key) => !allowed.has(key))) {
157
+ throw new FileFrameError("invalid_header", "File frame header contains an unknown field");
158
+ }
159
+ const protocolVersion = expectInteger(object, "protocol_version");
160
+ if (protocolVersion !== FILE_TRANSFER_PROTOCOL_VERSION) {
161
+ throw new FileFrameError("unsupported_version", `Unsupported file transfer protocol ${protocolVersion}`);
162
+ }
163
+ const protection = expectString(object, "protection");
164
+ const direction = expectString(object, "direction");
165
+ const keyId = object.key_id === undefined ? undefined : expectString(object, "key_id");
166
+ const header = canonicalHeader({
167
+ protocol_version: FILE_TRANSFER_PROTOCOL_VERSION,
168
+ protection: protection,
169
+ grant_id: expectUuid(object, "grant_id"),
170
+ authority_id: expectUuid(object, "authority_id"),
171
+ collection_id: expectUuid(object, "collection_id"),
172
+ transfer_id: expectUuid(object, "transfer_id"),
173
+ direction: direction,
174
+ chunk_size: expectInteger(object, "chunk_size", MAX_FILE_CHUNK_BYTES),
175
+ chunk_index: expectInteger(object, "chunk_index"),
176
+ offset: expectInteger(object, "offset"),
177
+ plaintext_length: expectInteger(object, "plaintext_length", MAX_FILE_CHUNK_BYTES),
178
+ total_size: expectInteger(object, "total_size"),
179
+ scope_epoch: expectInteger(object, "scope_epoch"),
180
+ ...(keyId === undefined ? {} : { key_id: keyId })
181
+ });
182
+ if (JSON.stringify(header) !== source) {
183
+ throw new FileFrameError("invalid_header", "File frame header must use the canonical field order without duplicate fields or whitespace");
184
+ }
185
+ return header;
186
+ }
187
+ function validateFrameSemantics(kind, header, payloadLength) {
188
+ const expectedDirection = kind === "upload_chunk" ? "upload" : "download";
189
+ if (header.direction !== expectedDirection) {
190
+ throw new FileFrameError("invalid_header", `Frame kind ${kind} requires direction ${expectedDirection}`);
191
+ }
192
+ const expectedOffset = header.chunk_index * header.chunk_size;
193
+ if (!Number.isSafeInteger(expectedOffset) || header.offset !== expectedOffset) {
194
+ throw new FileFrameError("invalid_header", "Chunk offset does not match its index and protocol chunk size");
195
+ }
196
+ if (header.offset + header.plaintext_length > header.total_size) {
197
+ throw new FileFrameError("invalid_header", "Chunk extends past the declared transfer size");
198
+ }
199
+ const expectedPayloadLength = header.protection === "grant_aead_v1"
200
+ ? header.plaintext_length + 16
201
+ : header.plaintext_length;
202
+ if (payloadLength !== expectedPayloadLength) {
203
+ throw new FileFrameError("invalid_length", "Payload length does not match the protected plaintext length");
204
+ }
205
+ }
206
+ function expectedFileFramePayloadLength(header) {
207
+ return header.plaintext_length + (header.protection === "grant_aead_v1" ? 16 : 0);
208
+ }
209
+ /** Prefix and canonical header authenticated by the file chunk AEAD profile. */
210
+ export function fileFrameAuthenticatedData(kind, input) {
211
+ const header = canonicalHeader(input);
212
+ const payloadLength = expectedFileFramePayloadLength(header);
213
+ validateFrameSemantics(kind, header, payloadLength);
214
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
215
+ if (headerBytes.byteLength > MAX_FILE_FRAME_HEADER_BYTES) {
216
+ throw new FileFrameError("limit_exceeded", "File frame header exceeds the protocol limit");
217
+ }
218
+ const output = new Uint8Array(FILE_FRAME_PREFIX_BYTES + headerBytes.byteLength);
219
+ output.set(FILE_FRAME_MAGIC_BYTES, 0);
220
+ const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
221
+ view.setUint8(4, FILE_FRAME_VERSION);
222
+ view.setUint8(5, frameKindCode(kind));
223
+ view.setUint16(6, FILE_FRAME_FLAGS, false);
224
+ view.setUint32(8, headerBytes.byteLength, false);
225
+ view.setUint32(12, payloadLength, false);
226
+ output.set(headerBytes, FILE_FRAME_PREFIX_BYTES);
227
+ return output;
228
+ }
229
+ export function encodeFileFrame(frame) {
230
+ const header = canonicalHeader(frame.header);
231
+ validateFrameSemantics(frame.kind, header, frame.payload.byteLength);
232
+ if (frame.payload.byteLength > MAX_FILE_CHUNK_BYTES + 16) {
233
+ throw new FileFrameError("limit_exceeded", "File frame payload exceeds the protocol limit");
234
+ }
235
+ const authenticatedData = fileFrameAuthenticatedData(frame.kind, header);
236
+ const output = new Uint8Array(authenticatedData.byteLength + frame.payload.byteLength);
237
+ output.set(authenticatedData, 0);
238
+ output.set(frame.payload, authenticatedData.byteLength);
239
+ return output;
240
+ }
241
+ export function decodeFileFrame(bytes, limits = {}) {
242
+ if (bytes.byteLength < FILE_FRAME_PREFIX_BYTES) {
243
+ throw new FileFrameError("invalid_length", "File frame is shorter than its fixed prefix");
244
+ }
245
+ if (!FILE_FRAME_MAGIC_BYTES.every((byte, index) => bytes[index] === byte)) {
246
+ throw new FileFrameError("invalid_magic", "File frame magic is not MDBF");
247
+ }
248
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
249
+ const version = view.getUint8(4);
250
+ if (version !== FILE_FRAME_VERSION) {
251
+ throw new FileFrameError("unsupported_version", `Unsupported file frame version ${version}`);
252
+ }
253
+ const kind = frameKindFromCode(view.getUint8(5));
254
+ if (view.getUint16(6, false) !== FILE_FRAME_FLAGS) {
255
+ throw new FileFrameError("unsupported_flags", "File frame uses unsupported flags");
256
+ }
257
+ const headerLength = view.getUint32(8, false);
258
+ const payloadLength = view.getUint32(12, false);
259
+ const maxHeaderBytes = limits.maxHeaderBytes ?? MAX_FILE_FRAME_HEADER_BYTES;
260
+ const maxPayloadBytes = limits.maxPayloadBytes ?? MAX_FILE_CHUNK_BYTES + 16;
261
+ if (headerLength === 0) {
262
+ throw new FileFrameError("invalid_length", "File frame header must not be empty");
263
+ }
264
+ if (headerLength > maxHeaderBytes || payloadLength > maxPayloadBytes) {
265
+ throw new FileFrameError("limit_exceeded", "File frame exceeds the configured decode limits");
266
+ }
267
+ const expectedLength = FILE_FRAME_PREFIX_BYTES + headerLength + payloadLength;
268
+ if (!Number.isSafeInteger(expectedLength) || expectedLength !== bytes.byteLength) {
269
+ throw new FileFrameError("invalid_length", "File frame lengths do not match the supplied bytes");
270
+ }
271
+ const headerEnd = FILE_FRAME_PREFIX_BYTES + headerLength;
272
+ const header = parseHeader(bytes.subarray(FILE_FRAME_PREFIX_BYTES, headerEnd));
273
+ validateFrameSemantics(kind, header, payloadLength);
274
+ return { kind, header, payload: bytes.slice(headerEnd) };
275
+ }
276
+ export function encodeRelayFileFrame(frame) {
277
+ const header = canonicalRelayHeader(frame.header);
278
+ validateRelayFrame(frame.kind, header, frame.payload.byteLength);
279
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
280
+ if (headerBytes.byteLength > MAX_RELAY_FILE_HEADER_BYTES
281
+ || frame.payload.byteLength > MAX_RELAY_FILE_PAYLOAD_BYTES) {
282
+ throw new RelayFileFrameError("limit_exceeded", "Relay file frame exceeds protocol limits");
283
+ }
284
+ const output = new Uint8Array(RELAY_FILE_PREFIX_BYTES + headerBytes.byteLength + frame.payload.byteLength);
285
+ output.set(RELAY_FILE_MAGIC_BYTES, 0);
286
+ const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
287
+ view.setUint8(4, RELAY_FILE_FRAME_VERSION);
288
+ view.setUint8(5, relayFileKindCode(frame.kind));
289
+ view.setUint16(6, RELAY_FILE_FRAME_FLAGS, false);
290
+ view.setUint32(8, headerBytes.byteLength, false);
291
+ view.setUint32(12, frame.payload.byteLength, false);
292
+ output.set(headerBytes, RELAY_FILE_PREFIX_BYTES);
293
+ output.set(frame.payload, RELAY_FILE_PREFIX_BYTES + headerBytes.byteLength);
294
+ return output;
295
+ }
296
+ export function decodeRelayFileFrame(bytes) {
297
+ if (bytes.byteLength < RELAY_FILE_PREFIX_BYTES) {
298
+ throw new RelayFileFrameError("invalid_length", "Relay file frame is shorter than its prefix");
299
+ }
300
+ if (!RELAY_FILE_MAGIC_BYTES.every((byte, index) => bytes[index] === byte)) {
301
+ throw new RelayFileFrameError("invalid_magic", "Relay file frame magic is not MDBR");
302
+ }
303
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
304
+ if (view.getUint8(4) !== RELAY_FILE_FRAME_VERSION) {
305
+ throw new RelayFileFrameError("unsupported_version", "Unsupported relay file frame version");
306
+ }
307
+ const kind = relayFileKindFromCode(view.getUint8(5));
308
+ if (view.getUint16(6, false) !== RELAY_FILE_FRAME_FLAGS) {
309
+ throw new RelayFileFrameError("unsupported_flags", "Relay file frame uses unsupported flags");
310
+ }
311
+ const headerLength = view.getUint32(8, false);
312
+ const payloadLength = view.getUint32(12, false);
313
+ if (headerLength === 0) {
314
+ throw new RelayFileFrameError("invalid_length", "Relay file frame header must not be empty");
315
+ }
316
+ if (headerLength > MAX_RELAY_FILE_HEADER_BYTES
317
+ || payloadLength > MAX_RELAY_FILE_PAYLOAD_BYTES) {
318
+ throw new RelayFileFrameError("limit_exceeded", "Relay file frame exceeds protocol limits");
319
+ }
320
+ const expectedLength = RELAY_FILE_PREFIX_BYTES + headerLength + payloadLength;
321
+ if (!Number.isSafeInteger(expectedLength) || expectedLength !== bytes.byteLength) {
322
+ throw new RelayFileFrameError("invalid_length", "Relay file frame lengths do not match its bytes");
323
+ }
324
+ const headerEnd = RELAY_FILE_PREFIX_BYTES + headerLength;
325
+ const headerBytes = bytes.subarray(RELAY_FILE_PREFIX_BYTES, headerEnd);
326
+ let source;
327
+ let parsed;
328
+ try {
329
+ source = new TextDecoder("utf-8", { fatal: true }).decode(headerBytes);
330
+ parsed = JSON.parse(source);
331
+ }
332
+ catch {
333
+ throw new RelayFileFrameError("invalid_header", "Relay file frame header is not valid JSON");
334
+ }
335
+ const header = parseRelayHeader(parsed);
336
+ if (JSON.stringify(header) !== source) {
337
+ throw new RelayFileFrameError("invalid_header", "Relay file frame header is not canonical JSON");
338
+ }
339
+ validateRelayFrame(kind, header, payloadLength);
340
+ return { kind, header, payload: bytes.slice(headerEnd) };
341
+ }
342
+ function canonicalRelayHeader(header) {
343
+ if (header.protocol_version !== RELAY_FILE_PROTOCOL_VERSION) {
344
+ throw new RelayFileFrameError("unsupported_version", "Unsupported relay file protocol");
345
+ }
346
+ if (!relayFileKinds.includes(header.type)) {
347
+ throw new RelayFileFrameError("invalid_kind", "Unknown relay file message type");
348
+ }
349
+ for (const [name, value] of [
350
+ ["request_id", header.request_id],
351
+ ["grant_id", header.grant_id],
352
+ ["transfer_id", header.transfer_id]
353
+ ]) {
354
+ if (!UUID_PATTERN.test(value)) {
355
+ throw new RelayFileFrameError("invalid_header", `Relay file header ${name} must be a UUID`);
356
+ }
357
+ }
358
+ if (!Number.isSafeInteger(header.chunk_index) || header.chunk_index < 0) {
359
+ throw new RelayFileFrameError("invalid_header", "Relay file chunk index is invalid");
360
+ }
361
+ return {
362
+ protocol_version: RELAY_FILE_PROTOCOL_VERSION,
363
+ type: header.type,
364
+ request_id: header.request_id,
365
+ grant_id: header.grant_id,
366
+ transfer_id: header.transfer_id,
367
+ chunk_index: header.chunk_index
368
+ };
369
+ }
370
+ function parseRelayHeader(value) {
371
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
372
+ throw new RelayFileFrameError("invalid_header", "Relay file header must be an object");
373
+ }
374
+ const object = value;
375
+ const allowed = new Set([
376
+ "protocol_version", "type", "request_id", "grant_id", "transfer_id", "chunk_index"
377
+ ]);
378
+ if (Object.keys(object).some((key) => !allowed.has(key))) {
379
+ throw new RelayFileFrameError("invalid_header", "Relay file header contains an unknown field");
380
+ }
381
+ return canonicalRelayHeader({
382
+ protocol_version: object.protocol_version,
383
+ type: object.type,
384
+ request_id: object.request_id,
385
+ grant_id: object.grant_id,
386
+ transfer_id: object.transfer_id,
387
+ chunk_index: object.chunk_index
388
+ });
389
+ }
390
+ const relayFileKinds = [
391
+ "upload_chunk",
392
+ "upload_acknowledged",
393
+ "download_request",
394
+ "download_chunk",
395
+ "rejected"
396
+ ];
397
+ function relayFileKindCode(kind) {
398
+ const index = relayFileKinds.indexOf(kind);
399
+ if (index < 0)
400
+ throw new RelayFileFrameError("invalid_kind", "Unknown relay file frame kind");
401
+ return index + 1;
402
+ }
403
+ function relayFileKindFromCode(code) {
404
+ const kind = relayFileKinds[code - 1];
405
+ if (!kind)
406
+ throw new RelayFileFrameError("invalid_kind", `Unknown relay file frame kind ${code}`);
407
+ return kind;
408
+ }
409
+ function validateRelayFrame(kind, header, payloadLength) {
410
+ if (kind !== header.type) {
411
+ throw new RelayFileFrameError("invalid_header", "Relay file kind does not match its header");
412
+ }
413
+ const carriesPayload = kind === "upload_chunk" || kind === "download_chunk";
414
+ if (carriesPayload !== (payloadLength > 0)) {
415
+ throw new RelayFileFrameError("invalid_length", "Relay file payload does not match its kind");
416
+ }
417
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import type { ConnectProblem } from "./connect-problems.generated.js";
2
+ import type { ApplicationFileRequirement, CollectionFileDescriptor, FileCapability } from "./files.js";
3
+ export * from "./connect-problems.generated.js";
4
+ export * from "./files.js";
1
5
  export declare const CONTROL_PROTOCOL_VERSION: 1;
2
6
  export declare const ENCRYPTED_RELAY_PROTOCOL_VERSION: 1;
3
7
  export declare const LOOPBACK_PROTOCOL_VERSION: 1;
@@ -5,8 +9,9 @@ export declare const DEFAULT_LOOPBACK_PORT: 28485;
5
9
  export declare const RELAY_ENCRYPTION_SUITE: "P256-HKDF-SHA256-AES256GCM";
6
10
  export declare const SYNC_PROTOCOL_VERSION: 1;
7
11
  export declare const CONTRACT_SETUP_CAPABILITY: "contract-setup-v1";
12
+ export declare const FILE_RELAY_CAPABILITY: "file-relay-v1";
8
13
  export declare const RELAY_REQUIRED_CAPABILITIES: readonly ["authorization-activation", "encrypted-relay", "policy-ack"];
9
- export declare const RELAY_CAPABILITIES: readonly ["authorization-activation", "encrypted-relay", "policy-ack", "contract-setup-v1"];
14
+ export declare const RELAY_CAPABILITIES: readonly ["authorization-activation", "encrypted-relay", "policy-ack", "contract-setup-v1", "file-relay-v1"];
10
15
  export declare const AUTHORITY_PROOF_VERSION: 1;
11
16
  export declare const AUTHORITY_PROOF_ALGORITHM: "P256-SHA256";
12
17
  export declare const AUTHORITY_PROOF_DOMAIN: "mdbase-authority-request-proof-v1";
@@ -29,6 +34,7 @@ export declare const CONNECT_SCHEMA_IDS: {
29
34
  readonly eventActionInterop: "https://mdbase.dev/schemas/interop/v0.1/profile.schema.json";
30
35
  readonly protocol: "https://mdbase.dev/connect/schemas/connect-protocol.v1.json";
31
36
  readonly encryptedRelay: "https://mdbase.dev/connect/schemas/encrypted-relay.v1.json";
37
+ readonly files: "https://mdbase.dev/connect/schemas/files.v1.json";
32
38
  readonly sync: "https://mdbase.dev/connect/schemas/sync.v1.json";
33
39
  };
34
40
  export type CollectionOperation = "describe" | "changes" | "read" | "query" | "list_views" | "execute_view" | "read_view_source" | "create_view_source" | "update_view_source" | "delete_view_source" | "validate" | "create" | "update" | "delete" | "rename" | "read_type" | "create_type" | "update_type" | "install_type_pack" | "list_timers" | "put_timer" | "cancel_timer" | "reconcile_timers" | "sync";
@@ -143,6 +149,8 @@ export interface ApplicationRequirements {
143
149
  access?: "contract" | "full_collection";
144
150
  /** Restrict authorization to durable provider-backed collections. */
145
151
  collection_kind?: "hosted";
152
+ /** First-class non-Markdown file access requested independently of records. */
153
+ files?: ApplicationFileRequirement;
146
154
  }
147
155
  export interface TypePackManifestResource {
148
156
  kind: "contract" | "type" | "schema";
@@ -217,17 +225,19 @@ export interface RelayOperationRequest {
217
225
  operation: CollectionOperation;
218
226
  input: unknown;
219
227
  }
220
- export interface RelayOperationResponse {
228
+ export type RelayOperationResponse = {
221
229
  type: "operation_response";
222
230
  protocol_version: 1;
223
231
  request_id: string;
224
- ok: boolean;
225
- result?: unknown;
226
- error?: {
227
- code: string;
228
- message: string;
229
- };
230
- }
232
+ ok: true;
233
+ result: unknown;
234
+ } | {
235
+ type: "operation_response";
236
+ protocol_version: 1;
237
+ request_id: string;
238
+ ok: false;
239
+ problem: ConnectProblem;
240
+ };
231
241
  export interface MdbaseOperationRequest<Input = unknown> {
232
242
  protocol_version: 1;
233
243
  request_id: string;
@@ -242,11 +252,7 @@ export type MdbaseOperationResponse<Result = unknown> = {
242
252
  protocol_version: 1;
243
253
  request_id: string;
244
254
  ok: false;
245
- error: {
246
- code: string;
247
- message: string;
248
- details?: unknown;
249
- };
255
+ problem: ConnectProblem;
250
256
  };
251
257
  export interface GrantPolicy {
252
258
  id: string;
@@ -268,6 +274,7 @@ export interface GrantPolicy {
268
274
  notification_criteria?: NotificationCriterion[];
269
275
  created_at: string;
270
276
  encryption?: GrantEncryption;
277
+ file_capability?: FileCapability;
271
278
  }
272
279
  export interface GrantEncryption {
273
280
  protocol_version: 1;
@@ -287,12 +294,14 @@ export interface EncryptedRelayEnvelope {
287
294
  application_id: string;
288
295
  connector_id: string;
289
296
  collection_id: string;
290
- operation: CollectionOperation;
297
+ operation: EncryptedRelayOperation;
291
298
  scope_epoch: number;
292
299
  key_id: string;
293
300
  counter: string;
294
301
  ciphertext: string;
295
302
  }
303
+ /** Encrypted control namespaces share grant authentication but not record permissions. */
304
+ export type EncryptedRelayOperation = CollectionOperation | "file_control";
296
305
  export interface SyncRecord<Frontmatter extends JsonObject = JsonObject> {
297
306
  record_id: string;
298
307
  path: string;
@@ -327,6 +336,7 @@ export interface AuthoritySnapshot<Frontmatter extends JsonObject = JsonObject>
327
336
  manifest_digest: string;
328
337
  resources: SyncCollectionResources;
329
338
  records: Array<AuthoritySnapshotRecord<Frontmatter>>;
339
+ files: CollectionFileDescriptor[];
330
340
  }
331
341
  export interface AuthorityImportManifest {
332
342
  protocol_version: 1;
@@ -336,6 +346,8 @@ export interface AuthorityImportManifest {
336
346
  manifest_digest: string;
337
347
  resources: SyncCollectionResources;
338
348
  record_count: number;
349
+ file_count: number;
350
+ files: CollectionFileDescriptor[];
339
351
  }
340
352
  export interface AuthorityImportRecord {
341
353
  record_id: string;
@@ -350,6 +362,7 @@ export interface AuthorityImportSnapshot {
350
362
  manifest_digest: string;
351
363
  resources: SyncCollectionResources;
352
364
  records: AuthorityImportRecord[];
365
+ files: CollectionFileDescriptor[];
353
366
  }
354
367
  export interface AuthorityImportRecordPage {
355
368
  protocol_version: 1;
@@ -380,6 +393,16 @@ export interface SyncSnapshotRecord<Frontmatter extends JsonObject = JsonObject>
380
393
  /** Exact bytes whose SHA-256 revision and parsed metadata match this record. */
381
394
  document: string;
382
395
  }
396
+ /** Manifest-only snapshot page. File bytes are fetched by digest via the file data plane. */
397
+ export interface SyncFileSnapshotPage {
398
+ protocol_version: 1;
399
+ type: "file_snapshot_page";
400
+ snapshot_id: string;
401
+ scope_epoch: number;
402
+ cursor: number;
403
+ files: CollectionFileDescriptor[];
404
+ next_page?: string;
405
+ }
383
406
  export type SyncChange<Frontmatter extends JsonObject = JsonObject> = {
384
407
  sequence: number;
385
408
  type: "put";
@@ -390,6 +413,16 @@ export type SyncChange<Frontmatter extends JsonObject = JsonObject> = {
390
413
  record_id: string;
391
414
  previous_path: string;
392
415
  revision: string;
416
+ } | {
417
+ sequence: number;
418
+ type: "file_put";
419
+ file: CollectionFileDescriptor;
420
+ } | {
421
+ sequence: number;
422
+ type: "file_remove";
423
+ file_id: string;
424
+ previous_path: string;
425
+ revision: string;
393
426
  };
394
427
  export interface SyncChangesPage<Frontmatter extends JsonObject = JsonObject> {
395
428
  protocol_version: 1;
@@ -411,6 +444,31 @@ export interface SyncMutation {
411
444
  created_at: string;
412
445
  causal_predecessor?: string;
413
446
  }
447
+ interface SyncFileMutationBase {
448
+ mutation_id: string;
449
+ replica_id: string;
450
+ scope_epoch: number;
451
+ file_id: string;
452
+ created_at: string;
453
+ causal_predecessor?: string;
454
+ }
455
+ export type SyncFileMutation = (SyncFileMutationBase & {
456
+ operation: "file_put";
457
+ base_revision?: string;
458
+ path: string;
459
+ transfer_id: string;
460
+ content_digest: `sha256:${string}`;
461
+ size: number;
462
+ media_type?: string;
463
+ }) | (SyncFileMutationBase & {
464
+ operation: "file_move";
465
+ base_revision: string;
466
+ path: string;
467
+ update_references: boolean;
468
+ }) | (SyncFileMutationBase & {
469
+ operation: "file_delete";
470
+ base_revision: string;
471
+ });
414
472
  export interface SyncConflict<Frontmatter extends JsonObject = JsonObject> {
415
473
  record_id: string;
416
474
  mutation: SyncMutation;
@@ -434,6 +492,29 @@ export type SyncMutationReceipt<Frontmatter extends JsonObject = JsonObject> = {
434
492
  message: string;
435
493
  };
436
494
  };
495
+ export interface SyncFileConflict {
496
+ file_id: string;
497
+ mutation: SyncFileMutation;
498
+ current?: CollectionFileDescriptor;
499
+ current_revision?: string;
500
+ }
501
+ export type SyncFileMutationReceipt = {
502
+ mutation_id: string;
503
+ status: "file_applied" | "file_previously_applied";
504
+ sequence: number;
505
+ file?: CollectionFileDescriptor;
506
+ } | {
507
+ mutation_id: string;
508
+ status: "file_conflicted";
509
+ conflict: SyncFileConflict;
510
+ } | {
511
+ mutation_id: string;
512
+ status: "file_rejected";
513
+ error: {
514
+ code: string;
515
+ message: string;
516
+ };
517
+ };
437
518
  export type EncryptedRelayOperationRequest = EncryptedRelayEnvelope & {
438
519
  type: "encrypted_operation_request";
439
520
  };
@@ -516,6 +597,7 @@ export interface ConnectorCollection {
516
597
  }
517
598
  export type JsonObject = Record<string, unknown>;
518
599
  export interface MdbaseDiagnostic {
600
+ [key: string]: unknown;
519
601
  severity: "error" | "warning" | "info";
520
602
  code: string;
521
603
  message: string;
@@ -753,4 +835,3 @@ export interface CollectionChangesPage {
753
835
  has_more: boolean;
754
836
  reset: boolean;
755
837
  }
756
- export {};
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ export * from "./connect-problems.generated.js";
2
+ export * from "./files.js";
1
3
  export const CONTROL_PROTOCOL_VERSION = 1;
2
4
  export const ENCRYPTED_RELAY_PROTOCOL_VERSION = 1;
3
5
  export const LOOPBACK_PROTOCOL_VERSION = 1;
@@ -5,6 +7,7 @@ export const DEFAULT_LOOPBACK_PORT = 28_485;
5
7
  export const RELAY_ENCRYPTION_SUITE = "P256-HKDF-SHA256-AES256GCM";
6
8
  export const SYNC_PROTOCOL_VERSION = 1;
7
9
  export const CONTRACT_SETUP_CAPABILITY = "contract-setup-v1";
10
+ export const FILE_RELAY_CAPABILITY = "file-relay-v1";
8
11
  export const RELAY_REQUIRED_CAPABILITIES = [
9
12
  "authorization-activation",
10
13
  "encrypted-relay",
@@ -12,7 +15,8 @@ export const RELAY_REQUIRED_CAPABILITIES = [
12
15
  ];
13
16
  export const RELAY_CAPABILITIES = [
14
17
  ...RELAY_REQUIRED_CAPABILITIES,
15
- CONTRACT_SETUP_CAPABILITY
18
+ CONTRACT_SETUP_CAPABILITY,
19
+ FILE_RELAY_CAPABILITY
16
20
  ];
17
21
  export const AUTHORITY_PROOF_VERSION = 1;
18
22
  export const AUTHORITY_PROOF_ALGORITHM = "P256-SHA256";
@@ -53,5 +57,6 @@ export const CONNECT_SCHEMA_IDS = {
53
57
  eventActionInterop: "https://mdbase.dev/schemas/interop/v0.1/profile.schema.json",
54
58
  protocol: "https://mdbase.dev/connect/schemas/connect-protocol.v1.json",
55
59
  encryptedRelay: "https://mdbase.dev/connect/schemas/encrypted-relay.v1.json",
60
+ files: "https://mdbase.dev/connect/schemas/files.v1.json",
56
61
  sync: "https://mdbase.dev/connect/schemas/sync.v1.json"
57
62
  };