@mdbase-dev/connect-protocol 0.1.0-beta.100

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/dist/application-authorization.d.ts +32 -0
  3. package/dist/application-authorization.js +207 -0
  4. package/dist/capabilities.d.ts +73 -0
  5. package/dist/capabilities.js +92 -0
  6. package/dist/collection-setup.d.ts +90 -0
  7. package/dist/collection-setup.js +1 -0
  8. package/dist/compatibility.d.ts +37 -0
  9. package/dist/compatibility.js +65 -0
  10. package/dist/connect-problems.generated.d.ts +1388 -0
  11. package/dist/connect-problems.generated.js +574 -0
  12. package/dist/files.d.ts +269 -0
  13. package/dist/files.js +426 -0
  14. package/dist/index.d.ts +878 -0
  15. package/dist/index.js +94 -0
  16. package/dist/manifest.d.ts +62 -0
  17. package/dist/manifest.js +527 -0
  18. package/dist/mutation-fingerprint.d.ts +6 -0
  19. package/dist/mutation-fingerprint.js +116 -0
  20. package/dist/operations.d.ts +15 -0
  21. package/dist/operations.js +194 -0
  22. package/dist/type-packs.d.ts +133 -0
  23. package/dist/type-packs.js +30 -0
  24. package/package.json +42 -0
  25. package/schemas/application-capability-catalog.v1.json +40 -0
  26. package/schemas/application-capability-catalog.v2.json +68 -0
  27. package/schemas/catalog.json +17 -0
  28. package/schemas/connect-problem.v1.schema.json +5132 -0
  29. package/schemas/connect-problems.v1.catalog.json +351 -0
  30. package/schemas/connect-protocol.v1.schema.json +800 -0
  31. package/schemas/data-contract.schema.json +163 -0
  32. package/schemas/encrypted-relay.v1.schema.json +63 -0
  33. package/schemas/encrypted-relay.v2.schema.json +63 -0
  34. package/schemas/encrypted-relay.v3.schema.json +64 -0
  35. package/schemas/files.v1.schema.json +507 -0
  36. package/schemas/interop/v0.1/action-cancellation.schema.json +6 -0
  37. package/schemas/interop/v0.1/action-invocation.schema.json +6 -0
  38. package/schemas/interop/v0.1/action-outcome.schema.json +6 -0
  39. package/schemas/interop/v0.1/action-provider-declaration.schema.json +6 -0
  40. package/schemas/interop/v0.1/action-request.schema.json +6 -0
  41. package/schemas/interop/v0.1/conformance-claim.schema.json +6 -0
  42. package/schemas/interop/v0.1/event-source-declaration.schema.json +6 -0
  43. package/schemas/interop/v0.1/event.schema.json +6 -0
  44. package/schemas/interop/v0.1/profile.schema.json +456 -0
  45. package/schemas/mdbase-app.legacy-v1.schema.json +405 -0
  46. package/schemas/mdbase-app.schema.json +381 -0
  47. package/schemas/mdbase-query.v0.3.schema.json +171 -0
  48. package/schemas/notification-webhook.v1.schema.json +56 -0
  49. package/schemas/operation-catalog.v1.json +42 -0
  50. package/schemas/operation-mutation-classification.v1.json +537 -0
  51. package/schemas/sync.v1.schema.json +483 -0
@@ -0,0 +1,269 @@
1
+ /** Frozen predecessor file declaration; never translate this into v2 intent. */
2
+ export interface LegacyApplicationFileRequirement {
3
+ actions: FileAction[];
4
+ scope: FileScope;
5
+ }
6
+ export declare const FILE_PROTOCOL_VERSION: 1;
7
+ export declare const FILE_TRANSFER_PROTOCOL_VERSION: 1;
8
+ export declare const DEFAULT_FILE_CHUNK_BYTES: number;
9
+ export declare const MAX_FILE_CHUNK_BYTES: number;
10
+ export declare const MAX_FILE_FRAME_HEADER_BYTES: number;
11
+ export declare const FILE_FRAME_PREFIX_BYTES = 16;
12
+ export declare const FILE_FRAME_MAGIC: "MDBF";
13
+ export declare const MAX_FILE_FRAME_BYTES: number;
14
+ export declare const RELAY_FILE_PROTOCOL_VERSION: 1;
15
+ export declare const RELAY_FILE_PREFIX_BYTES = 16;
16
+ export declare const MAX_RELAY_FILE_HEADER_BYTES = 1024;
17
+ export declare const MAX_RELAY_FILE_PAYLOAD_BYTES: number;
18
+ export declare const RELAY_FILE_MAGIC: "MDBR";
19
+ export type FileMediaClass = "image" | "audio" | "video" | "pdf" | "other";
20
+ /** Device-local projection. Folder exclusions apply to Markdown and files. */
21
+ export interface SelectiveSyncPolicy {
22
+ file_classes: FileMediaClass[];
23
+ excluded_folders: string[];
24
+ }
25
+ export type FileAction = "list" | "read" | "add" | "replace" | "move" | "delete";
26
+ export type FileTransferDirection = "upload" | "download";
27
+ export type FileTransferProtection = "grant_aead_v1" | "transport_tls";
28
+ export type FileTransferState = "open" | "committed" | "aborted" | "expired";
29
+ export type FileTransferStrategy = {
30
+ kind: "framed_chunks";
31
+ chunk_size: number;
32
+ } | {
33
+ kind: "object_put";
34
+ } | {
35
+ kind: "object_multipart";
36
+ part_size: number;
37
+ } | {
38
+ kind: "object_ranges";
39
+ part_size: number;
40
+ };
41
+ export type FileFrameKind = "upload_chunk" | "download_chunk";
42
+ export type RelayFileKind = "upload_chunk" | "upload_acknowledged" | "download_request" | "download_chunk" | "rejected";
43
+ export type FileScope = {
44
+ kind: "selected_folders";
45
+ folders: string[];
46
+ } | {
47
+ kind: "collection";
48
+ };
49
+ /** File intent declared by an application before an authority is selected. */
50
+ export interface ApplicationFileRequirement {
51
+ required: FileAction[];
52
+ optional?: FileAction[];
53
+ scope: FileScope;
54
+ }
55
+ /** Exact file actions signed into an authorization request. */
56
+ export interface ApplicationFileRequest {
57
+ actions: FileAction[];
58
+ scope: FileScope;
59
+ }
60
+ export declare function applicationFileRequest(requirement: ApplicationFileRequirement, options?: {
61
+ includeOptional?: boolean;
62
+ }): ApplicationFileRequest;
63
+ export interface FileCapability {
64
+ kind: "files";
65
+ protocol_version: 1;
66
+ actions: FileAction[];
67
+ scope: FileScope;
68
+ }
69
+ export interface CollectionFileDescriptor {
70
+ file_id: string;
71
+ path: string;
72
+ revision: string;
73
+ content_digest: `sha256:${string}`;
74
+ size: number;
75
+ media_type?: string;
76
+ media_class: FileMediaClass;
77
+ modified_at: string;
78
+ }
79
+ export interface ListFilesRequest {
80
+ protocol_version: 1;
81
+ type: "list_files";
82
+ folder?: string;
83
+ /** Opaque authority-issued continuation cursor. */
84
+ after?: string;
85
+ limit?: number;
86
+ }
87
+ export interface ListFilesPage {
88
+ protocol_version: 1;
89
+ type: "files_page";
90
+ files: CollectionFileDescriptor[];
91
+ /** Opaque continuation cursor; pass it back unchanged as `after`. */
92
+ next?: string;
93
+ }
94
+ export interface OpenFileUploadRequest {
95
+ protocol_version: 1;
96
+ type: "open_file_upload";
97
+ transfer_id: string;
98
+ path: string;
99
+ size: number;
100
+ content_digest: `sha256:${string}`;
101
+ media_type?: string;
102
+ if_revision?: string;
103
+ }
104
+ export interface OpenAuthorityImportFileUploadRequest {
105
+ protocol_version: 1;
106
+ type: "open_authority_import_file_upload";
107
+ transfer_id: string;
108
+ file_id: string;
109
+ }
110
+ export interface OpenFileDownloadRequest {
111
+ protocol_version: 1;
112
+ type: "open_file_download";
113
+ transfer_id: string;
114
+ file_id: string;
115
+ revision?: string;
116
+ }
117
+ export interface MoveFileRequest {
118
+ protocol_version: 1;
119
+ type: "move_file";
120
+ mutation_id: string;
121
+ file_id: string;
122
+ if_revision: string;
123
+ from_path: string;
124
+ path: string;
125
+ update_references: boolean;
126
+ }
127
+ export interface MoveFileReceipt {
128
+ protocol_version: 1;
129
+ type: "file_moved";
130
+ mutation_id: string;
131
+ file: CollectionFileDescriptor;
132
+ }
133
+ export interface DeleteFileRequest {
134
+ protocol_version: 1;
135
+ type: "delete_file";
136
+ mutation_id: string;
137
+ file_id: string;
138
+ if_revision: string;
139
+ path: string;
140
+ }
141
+ export interface DeleteFileReceipt {
142
+ protocol_version: 1;
143
+ type: "file_deleted";
144
+ mutation_id: string;
145
+ file_id: string;
146
+ previous_path: string;
147
+ revision: string;
148
+ }
149
+ export interface FileTransferSession {
150
+ protocol_version: 1;
151
+ type: "file_transfer";
152
+ transfer_id: string;
153
+ direction: FileTransferDirection;
154
+ protection: FileTransferProtection;
155
+ strategy: FileTransferStrategy;
156
+ total_size: number;
157
+ expires_at: string;
158
+ received: number[];
159
+ /** R2/S3 part receipts for bandwidth-efficient multipart resume. */
160
+ uploaded_parts?: UploadedFilePart[];
161
+ }
162
+ export interface FileTransferStatus {
163
+ protocol_version: 1;
164
+ type: "file_transfer_status";
165
+ transfer_id: string;
166
+ state: FileTransferState;
167
+ received: number[];
168
+ received_bytes: number;
169
+ /** Object-store receipts for the currently durable multipart parts. */
170
+ uploaded_parts: UploadedFilePart[];
171
+ }
172
+ export interface PrepareFileUploadPartRequest {
173
+ protocol_version: 1;
174
+ type: "prepare_file_upload_part";
175
+ transfer_id: string;
176
+ part_number: number;
177
+ content_length: number;
178
+ }
179
+ export interface PreparedFilePart {
180
+ protocol_version: 1;
181
+ type: "file_part";
182
+ transfer_id: string;
183
+ part_index: number;
184
+ offset: number;
185
+ content_length: number;
186
+ method: string;
187
+ url: string;
188
+ headers: Record<string, string>;
189
+ expires_at: string;
190
+ }
191
+ export interface UploadedFilePart {
192
+ part_number: number;
193
+ etag: string;
194
+ }
195
+ export interface CommitFileUploadRequest {
196
+ protocol_version: 1;
197
+ type: "commit_file_upload";
198
+ transfer_id: string;
199
+ parts?: UploadedFilePart[];
200
+ }
201
+ export interface CommitFileUploadReceipt {
202
+ protocol_version: 1;
203
+ type: "file_upload_committed";
204
+ transfer_id: string;
205
+ file: CollectionFileDescriptor;
206
+ }
207
+ export interface AbortFileTransferRequest {
208
+ protocol_version: 1;
209
+ type: "abort_file_transfer";
210
+ transfer_id: string;
211
+ }
212
+ export interface GetFileTransferStatusRequest {
213
+ protocol_version: 1;
214
+ type: "get_file_transfer_status";
215
+ transfer_id: string;
216
+ }
217
+ export interface FileFrameHeader {
218
+ protocol_version: 1;
219
+ protection: FileTransferProtection;
220
+ grant_id: string;
221
+ authority_id: string;
222
+ collection_id: string;
223
+ transfer_id: string;
224
+ direction: FileTransferDirection;
225
+ chunk_size: number;
226
+ chunk_index: number;
227
+ offset: number;
228
+ plaintext_length: number;
229
+ total_size: number;
230
+ scope_epoch: number;
231
+ key_id?: string;
232
+ }
233
+ export interface FileFrame {
234
+ kind: FileFrameKind;
235
+ header: FileFrameHeader;
236
+ payload: Uint8Array;
237
+ }
238
+ export interface RelayFileHeader {
239
+ protocol_version: 1;
240
+ type: RelayFileKind;
241
+ request_id: string;
242
+ grant_id: string;
243
+ transfer_id: string;
244
+ chunk_index: number;
245
+ }
246
+ export interface RelayFileFrame {
247
+ kind: RelayFileKind;
248
+ header: RelayFileHeader;
249
+ payload: Uint8Array;
250
+ }
251
+ export interface FileFrameDecodeLimits {
252
+ maxHeaderBytes?: number;
253
+ maxPayloadBytes?: number;
254
+ }
255
+ export type FileFrameErrorCode = "invalid_magic" | "unsupported_version" | "invalid_kind" | "unsupported_flags" | "invalid_length" | "invalid_header" | "limit_exceeded";
256
+ export declare class FileFrameError extends Error {
257
+ readonly code: FileFrameErrorCode;
258
+ constructor(code: FileFrameErrorCode, message: string);
259
+ }
260
+ export declare class RelayFileFrameError extends Error {
261
+ readonly code: FileFrameErrorCode;
262
+ constructor(code: FileFrameErrorCode, message: string);
263
+ }
264
+ /** Prefix and canonical header authenticated by the file chunk AEAD profile. */
265
+ export declare function fileFrameAuthenticatedData(kind: FileFrameKind, input: FileFrameHeader): Uint8Array;
266
+ export declare function encodeFileFrame(frame: FileFrame): Uint8Array;
267
+ export declare function decodeFileFrame(bytes: Uint8Array, limits?: FileFrameDecodeLimits): FileFrame;
268
+ export declare function encodeRelayFileFrame(frame: RelayFileFrame): Uint8Array;
269
+ export declare function decodeRelayFileFrame(bytes: Uint8Array): RelayFileFrame;
package/dist/files.js ADDED
@@ -0,0 +1,426 @@
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 function applicationFileRequest(requirement, options = {}) {
25
+ return {
26
+ actions: [...new Set([
27
+ ...requirement.required,
28
+ ...(options.includeOptional === false ? [] : requirement.optional ?? [])
29
+ ])],
30
+ scope: structuredClone(requirement.scope)
31
+ };
32
+ }
33
+ export class FileFrameError extends Error {
34
+ code;
35
+ constructor(code, message) {
36
+ super(message);
37
+ this.code = code;
38
+ this.name = "FileFrameError";
39
+ }
40
+ }
41
+ export class RelayFileFrameError extends Error {
42
+ code;
43
+ constructor(code, message) {
44
+ super(message);
45
+ this.code = code;
46
+ this.name = "RelayFileFrameError";
47
+ }
48
+ }
49
+ function frameKindCode(kind) {
50
+ return kind === "upload_chunk" ? 1 : 2;
51
+ }
52
+ function frameKindFromCode(code) {
53
+ if (code === 1)
54
+ return "upload_chunk";
55
+ if (code === 2)
56
+ return "download_chunk";
57
+ throw new FileFrameError("invalid_kind", `Unknown file frame kind ${code}`);
58
+ }
59
+ function expectObject(value) {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
61
+ throw new FileFrameError("invalid_header", "File frame header must be a JSON object");
62
+ }
63
+ return value;
64
+ }
65
+ function expectString(object, name) {
66
+ const value = object[name];
67
+ if (typeof value !== "string" || value.length === 0) {
68
+ throw new FileFrameError("invalid_header", `File frame header ${name} must be a non-empty string`);
69
+ }
70
+ return value;
71
+ }
72
+ function expectUuid(object, name) {
73
+ const value = expectString(object, name);
74
+ if (!UUID_PATTERN.test(value)) {
75
+ throw new FileFrameError("invalid_header", `File frame header ${name} must be a UUID`);
76
+ }
77
+ return value;
78
+ }
79
+ function expectInteger(object, name, maximum = Number.MAX_SAFE_INTEGER) {
80
+ const value = object[name];
81
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
82
+ throw new FileFrameError("invalid_header", `File frame header ${name} is outside its allowed range`);
83
+ }
84
+ return value;
85
+ }
86
+ function canonicalHeader(header) {
87
+ if (header.protocol_version !== FILE_TRANSFER_PROTOCOL_VERSION) {
88
+ throw new FileFrameError("unsupported_version", `Unsupported file transfer protocol ${header.protocol_version}`);
89
+ }
90
+ if (header.protection !== "grant_aead_v1" && header.protection !== "transport_tls") {
91
+ throw new FileFrameError("invalid_header", "Unknown file transfer protection");
92
+ }
93
+ if (header.direction !== "upload" && header.direction !== "download") {
94
+ throw new FileFrameError("invalid_header", "Unknown file transfer direction");
95
+ }
96
+ for (const [name, value] of [
97
+ ["grant_id", header.grant_id],
98
+ ["authority_id", header.authority_id],
99
+ ["collection_id", header.collection_id],
100
+ ["transfer_id", header.transfer_id]
101
+ ]) {
102
+ if (!UUID_PATTERN.test(value)) {
103
+ throw new FileFrameError("invalid_header", `File frame header ${name} must be a UUID`);
104
+ }
105
+ }
106
+ for (const [name, value, maximum] of [
107
+ ["chunk_size", header.chunk_size, MAX_FILE_CHUNK_BYTES],
108
+ ["chunk_index", header.chunk_index, Number.MAX_SAFE_INTEGER],
109
+ ["offset", header.offset, Number.MAX_SAFE_INTEGER],
110
+ ["plaintext_length", header.plaintext_length, MAX_FILE_CHUNK_BYTES],
111
+ ["total_size", header.total_size, Number.MAX_SAFE_INTEGER]
112
+ ]) {
113
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
114
+ throw new FileFrameError("invalid_header", `File frame header ${name} is outside its allowed range`);
115
+ }
116
+ }
117
+ if (header.chunk_size < 64 * 1024) {
118
+ throw new FileFrameError("invalid_header", "File frame chunk_size is below the protocol minimum");
119
+ }
120
+ if (!Number.isSafeInteger(header.scope_epoch) || header.scope_epoch < 1) {
121
+ throw new FileFrameError("invalid_header", "File frame header scope_epoch must be a positive integer");
122
+ }
123
+ if (header.protection === "grant_aead_v1" && !header.key_id) {
124
+ throw new FileFrameError("invalid_header", "AEAD-protected file frames require key_id");
125
+ }
126
+ return {
127
+ protocol_version: FILE_TRANSFER_PROTOCOL_VERSION,
128
+ protection: header.protection,
129
+ grant_id: header.grant_id,
130
+ authority_id: header.authority_id,
131
+ collection_id: header.collection_id,
132
+ transfer_id: header.transfer_id,
133
+ direction: header.direction,
134
+ chunk_size: header.chunk_size,
135
+ chunk_index: header.chunk_index,
136
+ offset: header.offset,
137
+ plaintext_length: header.plaintext_length,
138
+ total_size: header.total_size,
139
+ scope_epoch: header.scope_epoch,
140
+ ...(header.key_id === undefined ? {} : { key_id: header.key_id })
141
+ };
142
+ }
143
+ function parseHeader(bytes) {
144
+ let source;
145
+ try {
146
+ source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
147
+ }
148
+ catch {
149
+ throw new FileFrameError("invalid_header", "File frame header is not valid UTF-8");
150
+ }
151
+ let object;
152
+ try {
153
+ object = expectObject(JSON.parse(source));
154
+ }
155
+ catch (error) {
156
+ if (error instanceof FileFrameError)
157
+ throw error;
158
+ throw new FileFrameError("invalid_header", "File frame header is not valid JSON");
159
+ }
160
+ const allowed = new Set([
161
+ "protocol_version", "protection", "grant_id", "authority_id", "collection_id",
162
+ "transfer_id", "direction", "chunk_size", "chunk_index", "offset", "plaintext_length",
163
+ "total_size", "scope_epoch", "key_id"
164
+ ]);
165
+ if (Object.keys(object).some((key) => !allowed.has(key))) {
166
+ throw new FileFrameError("invalid_header", "File frame header contains an unknown field");
167
+ }
168
+ const protocolVersion = expectInteger(object, "protocol_version");
169
+ if (protocolVersion !== FILE_TRANSFER_PROTOCOL_VERSION) {
170
+ throw new FileFrameError("unsupported_version", `Unsupported file transfer protocol ${protocolVersion}`);
171
+ }
172
+ const protection = expectString(object, "protection");
173
+ const direction = expectString(object, "direction");
174
+ const keyId = object.key_id === undefined ? undefined : expectString(object, "key_id");
175
+ const header = canonicalHeader({
176
+ protocol_version: FILE_TRANSFER_PROTOCOL_VERSION,
177
+ protection: protection,
178
+ grant_id: expectUuid(object, "grant_id"),
179
+ authority_id: expectUuid(object, "authority_id"),
180
+ collection_id: expectUuid(object, "collection_id"),
181
+ transfer_id: expectUuid(object, "transfer_id"),
182
+ direction: direction,
183
+ chunk_size: expectInteger(object, "chunk_size", MAX_FILE_CHUNK_BYTES),
184
+ chunk_index: expectInteger(object, "chunk_index"),
185
+ offset: expectInteger(object, "offset"),
186
+ plaintext_length: expectInteger(object, "plaintext_length", MAX_FILE_CHUNK_BYTES),
187
+ total_size: expectInteger(object, "total_size"),
188
+ scope_epoch: expectInteger(object, "scope_epoch"),
189
+ ...(keyId === undefined ? {} : { key_id: keyId })
190
+ });
191
+ if (JSON.stringify(header) !== source) {
192
+ throw new FileFrameError("invalid_header", "File frame header must use the canonical field order without duplicate fields or whitespace");
193
+ }
194
+ return header;
195
+ }
196
+ function validateFrameSemantics(kind, header, payloadLength) {
197
+ const expectedDirection = kind === "upload_chunk" ? "upload" : "download";
198
+ if (header.direction !== expectedDirection) {
199
+ throw new FileFrameError("invalid_header", `Frame kind ${kind} requires direction ${expectedDirection}`);
200
+ }
201
+ const expectedOffset = header.chunk_index * header.chunk_size;
202
+ if (!Number.isSafeInteger(expectedOffset) || header.offset !== expectedOffset) {
203
+ throw new FileFrameError("invalid_header", "Chunk offset does not match its index and protocol chunk size");
204
+ }
205
+ if (header.offset + header.plaintext_length > header.total_size) {
206
+ throw new FileFrameError("invalid_header", "Chunk extends past the declared transfer size");
207
+ }
208
+ const expectedPayloadLength = header.protection === "grant_aead_v1"
209
+ ? header.plaintext_length + 16
210
+ : header.plaintext_length;
211
+ if (payloadLength !== expectedPayloadLength) {
212
+ throw new FileFrameError("invalid_length", "Payload length does not match the protected plaintext length");
213
+ }
214
+ }
215
+ function expectedFileFramePayloadLength(header) {
216
+ return header.plaintext_length + (header.protection === "grant_aead_v1" ? 16 : 0);
217
+ }
218
+ /** Prefix and canonical header authenticated by the file chunk AEAD profile. */
219
+ export function fileFrameAuthenticatedData(kind, input) {
220
+ const header = canonicalHeader(input);
221
+ const payloadLength = expectedFileFramePayloadLength(header);
222
+ validateFrameSemantics(kind, header, payloadLength);
223
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
224
+ if (headerBytes.byteLength > MAX_FILE_FRAME_HEADER_BYTES) {
225
+ throw new FileFrameError("limit_exceeded", "File frame header exceeds the protocol limit");
226
+ }
227
+ const output = new Uint8Array(FILE_FRAME_PREFIX_BYTES + headerBytes.byteLength);
228
+ output.set(FILE_FRAME_MAGIC_BYTES, 0);
229
+ const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
230
+ view.setUint8(4, FILE_FRAME_VERSION);
231
+ view.setUint8(5, frameKindCode(kind));
232
+ view.setUint16(6, FILE_FRAME_FLAGS, false);
233
+ view.setUint32(8, headerBytes.byteLength, false);
234
+ view.setUint32(12, payloadLength, false);
235
+ output.set(headerBytes, FILE_FRAME_PREFIX_BYTES);
236
+ return output;
237
+ }
238
+ export function encodeFileFrame(frame) {
239
+ const header = canonicalHeader(frame.header);
240
+ validateFrameSemantics(frame.kind, header, frame.payload.byteLength);
241
+ if (frame.payload.byteLength > MAX_FILE_CHUNK_BYTES + 16) {
242
+ throw new FileFrameError("limit_exceeded", "File frame payload exceeds the protocol limit");
243
+ }
244
+ const authenticatedData = fileFrameAuthenticatedData(frame.kind, header);
245
+ const output = new Uint8Array(authenticatedData.byteLength + frame.payload.byteLength);
246
+ output.set(authenticatedData, 0);
247
+ output.set(frame.payload, authenticatedData.byteLength);
248
+ return output;
249
+ }
250
+ export function decodeFileFrame(bytes, limits = {}) {
251
+ if (bytes.byteLength < FILE_FRAME_PREFIX_BYTES) {
252
+ throw new FileFrameError("invalid_length", "File frame is shorter than its fixed prefix");
253
+ }
254
+ if (!FILE_FRAME_MAGIC_BYTES.every((byte, index) => bytes[index] === byte)) {
255
+ throw new FileFrameError("invalid_magic", "File frame magic is not MDBF");
256
+ }
257
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
258
+ const version = view.getUint8(4);
259
+ if (version !== FILE_FRAME_VERSION) {
260
+ throw new FileFrameError("unsupported_version", `Unsupported file frame version ${version}`);
261
+ }
262
+ const kind = frameKindFromCode(view.getUint8(5));
263
+ if (view.getUint16(6, false) !== FILE_FRAME_FLAGS) {
264
+ throw new FileFrameError("unsupported_flags", "File frame uses unsupported flags");
265
+ }
266
+ const headerLength = view.getUint32(8, false);
267
+ const payloadLength = view.getUint32(12, false);
268
+ const maxHeaderBytes = limits.maxHeaderBytes ?? MAX_FILE_FRAME_HEADER_BYTES;
269
+ const maxPayloadBytes = limits.maxPayloadBytes ?? MAX_FILE_CHUNK_BYTES + 16;
270
+ if (headerLength === 0) {
271
+ throw new FileFrameError("invalid_length", "File frame header must not be empty");
272
+ }
273
+ if (headerLength > maxHeaderBytes || payloadLength > maxPayloadBytes) {
274
+ throw new FileFrameError("limit_exceeded", "File frame exceeds the configured decode limits");
275
+ }
276
+ const expectedLength = FILE_FRAME_PREFIX_BYTES + headerLength + payloadLength;
277
+ if (!Number.isSafeInteger(expectedLength) || expectedLength !== bytes.byteLength) {
278
+ throw new FileFrameError("invalid_length", "File frame lengths do not match the supplied bytes");
279
+ }
280
+ const headerEnd = FILE_FRAME_PREFIX_BYTES + headerLength;
281
+ const header = parseHeader(bytes.subarray(FILE_FRAME_PREFIX_BYTES, headerEnd));
282
+ validateFrameSemantics(kind, header, payloadLength);
283
+ return { kind, header, payload: bytes.slice(headerEnd) };
284
+ }
285
+ export function encodeRelayFileFrame(frame) {
286
+ const header = canonicalRelayHeader(frame.header);
287
+ validateRelayFrame(frame.kind, header, frame.payload.byteLength);
288
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
289
+ if (headerBytes.byteLength > MAX_RELAY_FILE_HEADER_BYTES
290
+ || frame.payload.byteLength > MAX_RELAY_FILE_PAYLOAD_BYTES) {
291
+ throw new RelayFileFrameError("limit_exceeded", "Relay file frame exceeds protocol limits");
292
+ }
293
+ const output = new Uint8Array(RELAY_FILE_PREFIX_BYTES + headerBytes.byteLength + frame.payload.byteLength);
294
+ output.set(RELAY_FILE_MAGIC_BYTES, 0);
295
+ const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
296
+ view.setUint8(4, RELAY_FILE_FRAME_VERSION);
297
+ view.setUint8(5, relayFileKindCode(frame.kind));
298
+ view.setUint16(6, RELAY_FILE_FRAME_FLAGS, false);
299
+ view.setUint32(8, headerBytes.byteLength, false);
300
+ view.setUint32(12, frame.payload.byteLength, false);
301
+ output.set(headerBytes, RELAY_FILE_PREFIX_BYTES);
302
+ output.set(frame.payload, RELAY_FILE_PREFIX_BYTES + headerBytes.byteLength);
303
+ return output;
304
+ }
305
+ export function decodeRelayFileFrame(bytes) {
306
+ if (bytes.byteLength < RELAY_FILE_PREFIX_BYTES) {
307
+ throw new RelayFileFrameError("invalid_length", "Relay file frame is shorter than its prefix");
308
+ }
309
+ if (!RELAY_FILE_MAGIC_BYTES.every((byte, index) => bytes[index] === byte)) {
310
+ throw new RelayFileFrameError("invalid_magic", "Relay file frame magic is not MDBR");
311
+ }
312
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
313
+ if (view.getUint8(4) !== RELAY_FILE_FRAME_VERSION) {
314
+ throw new RelayFileFrameError("unsupported_version", "Unsupported relay file frame version");
315
+ }
316
+ const kind = relayFileKindFromCode(view.getUint8(5));
317
+ if (view.getUint16(6, false) !== RELAY_FILE_FRAME_FLAGS) {
318
+ throw new RelayFileFrameError("unsupported_flags", "Relay file frame uses unsupported flags");
319
+ }
320
+ const headerLength = view.getUint32(8, false);
321
+ const payloadLength = view.getUint32(12, false);
322
+ if (headerLength === 0) {
323
+ throw new RelayFileFrameError("invalid_length", "Relay file frame header must not be empty");
324
+ }
325
+ if (headerLength > MAX_RELAY_FILE_HEADER_BYTES
326
+ || payloadLength > MAX_RELAY_FILE_PAYLOAD_BYTES) {
327
+ throw new RelayFileFrameError("limit_exceeded", "Relay file frame exceeds protocol limits");
328
+ }
329
+ const expectedLength = RELAY_FILE_PREFIX_BYTES + headerLength + payloadLength;
330
+ if (!Number.isSafeInteger(expectedLength) || expectedLength !== bytes.byteLength) {
331
+ throw new RelayFileFrameError("invalid_length", "Relay file frame lengths do not match its bytes");
332
+ }
333
+ const headerEnd = RELAY_FILE_PREFIX_BYTES + headerLength;
334
+ const headerBytes = bytes.subarray(RELAY_FILE_PREFIX_BYTES, headerEnd);
335
+ let source;
336
+ let parsed;
337
+ try {
338
+ source = new TextDecoder("utf-8", { fatal: true }).decode(headerBytes);
339
+ parsed = JSON.parse(source);
340
+ }
341
+ catch {
342
+ throw new RelayFileFrameError("invalid_header", "Relay file frame header is not valid JSON");
343
+ }
344
+ const header = parseRelayHeader(parsed);
345
+ if (JSON.stringify(header) !== source) {
346
+ throw new RelayFileFrameError("invalid_header", "Relay file frame header is not canonical JSON");
347
+ }
348
+ validateRelayFrame(kind, header, payloadLength);
349
+ return { kind, header, payload: bytes.slice(headerEnd) };
350
+ }
351
+ function canonicalRelayHeader(header) {
352
+ if (header.protocol_version !== RELAY_FILE_PROTOCOL_VERSION) {
353
+ throw new RelayFileFrameError("unsupported_version", "Unsupported relay file protocol");
354
+ }
355
+ if (!relayFileKinds.includes(header.type)) {
356
+ throw new RelayFileFrameError("invalid_kind", "Unknown relay file message type");
357
+ }
358
+ for (const [name, value] of [
359
+ ["request_id", header.request_id],
360
+ ["grant_id", header.grant_id],
361
+ ["transfer_id", header.transfer_id]
362
+ ]) {
363
+ if (!UUID_PATTERN.test(value)) {
364
+ throw new RelayFileFrameError("invalid_header", `Relay file header ${name} must be a UUID`);
365
+ }
366
+ }
367
+ if (!Number.isSafeInteger(header.chunk_index) || header.chunk_index < 0) {
368
+ throw new RelayFileFrameError("invalid_header", "Relay file chunk index is invalid");
369
+ }
370
+ return {
371
+ protocol_version: RELAY_FILE_PROTOCOL_VERSION,
372
+ type: header.type,
373
+ request_id: header.request_id,
374
+ grant_id: header.grant_id,
375
+ transfer_id: header.transfer_id,
376
+ chunk_index: header.chunk_index
377
+ };
378
+ }
379
+ function parseRelayHeader(value) {
380
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
381
+ throw new RelayFileFrameError("invalid_header", "Relay file header must be an object");
382
+ }
383
+ const object = value;
384
+ const allowed = new Set([
385
+ "protocol_version", "type", "request_id", "grant_id", "transfer_id", "chunk_index"
386
+ ]);
387
+ if (Object.keys(object).some((key) => !allowed.has(key))) {
388
+ throw new RelayFileFrameError("invalid_header", "Relay file header contains an unknown field");
389
+ }
390
+ return canonicalRelayHeader({
391
+ protocol_version: object.protocol_version,
392
+ type: object.type,
393
+ request_id: object.request_id,
394
+ grant_id: object.grant_id,
395
+ transfer_id: object.transfer_id,
396
+ chunk_index: object.chunk_index
397
+ });
398
+ }
399
+ const relayFileKinds = [
400
+ "upload_chunk",
401
+ "upload_acknowledged",
402
+ "download_request",
403
+ "download_chunk",
404
+ "rejected"
405
+ ];
406
+ function relayFileKindCode(kind) {
407
+ const index = relayFileKinds.indexOf(kind);
408
+ if (index < 0)
409
+ throw new RelayFileFrameError("invalid_kind", "Unknown relay file frame kind");
410
+ return index + 1;
411
+ }
412
+ function relayFileKindFromCode(code) {
413
+ const kind = relayFileKinds[code - 1];
414
+ if (!kind)
415
+ throw new RelayFileFrameError("invalid_kind", `Unknown relay file frame kind ${code}`);
416
+ return kind;
417
+ }
418
+ function validateRelayFrame(kind, header, payloadLength) {
419
+ if (kind !== header.type) {
420
+ throw new RelayFileFrameError("invalid_header", "Relay file kind does not match its header");
421
+ }
422
+ const carriesPayload = kind === "upload_chunk" || kind === "download_chunk";
423
+ if (carriesPayload !== (payloadLength > 0)) {
424
+ throw new RelayFileFrameError("invalid_length", "Relay file payload does not match its kind");
425
+ }
426
+ }