@mdbase-dev/connect 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 (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +696 -0
  3. package/dist/advanced.d.ts +9 -0
  4. package/dist/advanced.js +6 -0
  5. package/dist/application-contract.d.ts +22 -0
  6. package/dist/application-contract.js +75 -0
  7. package/dist/application-identity.d.ts +34 -0
  8. package/dist/application-identity.js +148 -0
  9. package/dist/application-session.d.ts +167 -0
  10. package/dist/application-session.js +722 -0
  11. package/dist/async.d.ts +1 -0
  12. package/dist/async.js +19 -0
  13. package/dist/authority-proof.d.ts +3 -0
  14. package/dist/authority-proof.js +24 -0
  15. package/dist/authority-sync-request.d.ts +3 -0
  16. package/dist/authority-sync-request.js +20 -0
  17. package/dist/authorization-refresh.d.ts +14 -0
  18. package/dist/authorization-refresh.js +41 -0
  19. package/dist/authorization-types.d.ts +7 -0
  20. package/dist/authorization-types.js +1 -0
  21. package/dist/authorization-url.d.ts +5 -0
  22. package/dist/authorization-url.js +34 -0
  23. package/dist/base64.d.ts +3 -0
  24. package/dist/base64.js +21 -0
  25. package/dist/browser/integrity.json +4 -0
  26. package/dist/browser/mdbase-connect.min.js +3 -0
  27. package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
  28. package/dist/browser.d.ts +3 -0
  29. package/dist/browser.js +5 -0
  30. package/dist/capabilities.d.ts +24 -0
  31. package/dist/capabilities.js +91 -0
  32. package/dist/collection-client.d.ts +66 -0
  33. package/dist/collection-client.js +686 -0
  34. package/dist/connect-authorization-helpers.d.ts +4 -0
  35. package/dist/connect-authorization-helpers.js +25 -0
  36. package/dist/connect-options.d.ts +35 -0
  37. package/dist/connect-options.js +1 -0
  38. package/dist/connect.d.ts +66 -0
  39. package/dist/connect.js +711 -0
  40. package/dist/connection-file-transport.d.ts +34 -0
  41. package/dist/connection-file-transport.js +85 -0
  42. package/dist/connection-index.d.ts +3 -0
  43. package/dist/connection-index.js +20 -0
  44. package/dist/connection-notifications.d.ts +31 -0
  45. package/dist/connection-notifications.js +197 -0
  46. package/dist/connection-transport-internals.d.ts +24 -0
  47. package/dist/connection-transport-internals.js +1 -0
  48. package/dist/connection-transport.d.ts +64 -0
  49. package/dist/connection-transport.js +725 -0
  50. package/dist/connection-types.d.ts +38 -0
  51. package/dist/connection-types.js +1 -0
  52. package/dist/connection.d.ts +175 -0
  53. package/dist/connection.js +606 -0
  54. package/dist/crypto-entry.d.ts +7 -0
  55. package/dist/crypto-entry.js +4 -0
  56. package/dist/crypto.d.ts +63 -0
  57. package/dist/crypto.js +409 -0
  58. package/dist/direct-access.d.ts +3 -0
  59. package/dist/direct-access.js +29 -0
  60. package/dist/errors.d.ts +45 -0
  61. package/dist/errors.js +142 -0
  62. package/dist/external-store.d.ts +10 -0
  63. package/dist/external-store.js +10 -0
  64. package/dist/file-crypto.d.ts +25 -0
  65. package/dist/file-crypto.js +132 -0
  66. package/dist/file-sha256.d.ts +12 -0
  67. package/dist/file-sha256.js +143 -0
  68. package/dist/file-stream-source.d.ts +17 -0
  69. package/dist/file-stream-source.js +157 -0
  70. package/dist/file-transfer-internals.d.ts +23 -0
  71. package/dist/file-transfer-internals.js +228 -0
  72. package/dist/files.d.ts +115 -0
  73. package/dist/files.js +563 -0
  74. package/dist/fresh-encrypted-request.d.ts +4 -0
  75. package/dist/fresh-encrypted-request.js +28 -0
  76. package/dist/grant-key-leases.d.ts +24 -0
  77. package/dist/grant-key-leases.js +144 -0
  78. package/dist/hosted-file-request.d.ts +6 -0
  79. package/dist/hosted-file-request.js +63 -0
  80. package/dist/hosted-operation-response.d.ts +5 -0
  81. package/dist/hosted-operation-response.js +26 -0
  82. package/dist/index.d.ts +27 -0
  83. package/dist/index.js +10 -0
  84. package/dist/internal-types.d.ts +100 -0
  85. package/dist/internal-types.js +1 -0
  86. package/dist/local-file-transport.d.ts +37 -0
  87. package/dist/local-file-transport.js +457 -0
  88. package/dist/mdbase-connect.d.ts +28 -0
  89. package/dist/mdbase-connect.js +53 -0
  90. package/dist/notifications.d.ts +48 -0
  91. package/dist/notifications.js +52 -0
  92. package/dist/operation-helpers.d.ts +27 -0
  93. package/dist/operation-helpers.js +144 -0
  94. package/dist/operation-types.d.ts +707 -0
  95. package/dist/operation-types.js +1 -0
  96. package/dist/outcomes.d.ts +51 -0
  97. package/dist/outcomes.js +194 -0
  98. package/dist/pending-mutation-store.d.ts +13 -0
  99. package/dist/pending-mutation-store.js +64 -0
  100. package/dist/pending-recovery-transports.d.ts +5 -0
  101. package/dist/pending-recovery-transports.js +52 -0
  102. package/dist/popup-authorization.d.ts +31 -0
  103. package/dist/popup-authorization.js +100 -0
  104. package/dist/query-pagination.d.ts +6 -0
  105. package/dist/query-pagination.js +119 -0
  106. package/dist/request-budget.d.ts +34 -0
  107. package/dist/request-budget.js +104 -0
  108. package/dist/request-coordinator.d.ts +29 -0
  109. package/dist/request-coordinator.js +213 -0
  110. package/dist/retired-grant-keys.d.ts +9 -0
  111. package/dist/retired-grant-keys.js +56 -0
  112. package/dist/runtime-utils.d.ts +52 -0
  113. package/dist/runtime-utils.js +368 -0
  114. package/dist/saved-view-pagination.d.ts +6 -0
  115. package/dist/saved-view-pagination.js +76 -0
  116. package/dist/selection.d.ts +48 -0
  117. package/dist/selection.js +112 -0
  118. package/dist/session.d.ts +114 -0
  119. package/dist/session.js +569 -0
  120. package/dist/shared-operation.d.ts +7 -0
  121. package/dist/shared-operation.js +41 -0
  122. package/dist/stored-token.d.ts +10 -0
  123. package/dist/stored-token.js +64 -0
  124. package/dist/structured-readiness.d.ts +77 -0
  125. package/dist/structured-readiness.js +126 -0
  126. package/dist/token-response.d.ts +12 -0
  127. package/dist/token-response.js +81 -0
  128. package/dist/transient-retry.d.ts +22 -0
  129. package/dist/transient-retry.js +59 -0
  130. package/dist/watch-policy.d.ts +8 -0
  131. package/dist/watch-policy.js +12 -0
  132. package/package.json +51 -0
package/dist/crypto.js ADDED
@@ -0,0 +1,409 @@
1
+ import { OPERATION_TRANSPORT_PROTOCOL_VERSION, GRANT_ENCRYPTION_PROTOCOL_VERSION, AUTHORITY_PROOF_DOMAIN, AUTHORITY_PROOF_HEADERS, AUTHORITY_PROOF_VERSION, RELAY_ENCRYPTION_SUITE, isConnectProblem } from "@mdbase-dev/connect-protocol";
2
+ const DATABASE_NAME = "mdbase-connect-keys";
3
+ const STORE_NAME = "grant-keys";
4
+ const MAX_U64 = 18446744073709551615n;
5
+ const REQUEST_INFO = "mdbase-connect relay request key v1";
6
+ const RESPONSE_INFO = "mdbase-connect relay response key v1";
7
+ 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;
8
+ /** Origin-scoped storage for non-extractable grant keys and atomic counters. */
9
+ export class IndexedDbGrantKeyStore {
10
+ database = null;
11
+ async create(handle) {
12
+ const record = await generateGrantKey(handle);
13
+ const database = await this.open();
14
+ await idbWrite(database, (store) => store.add({ ...record, counter: "0" }));
15
+ return record;
16
+ }
17
+ async get(handle) {
18
+ const database = await this.open();
19
+ const value = await idbRequest(database.transaction(STORE_NAME).objectStore(STORE_NAME).get(handle));
20
+ return value ? {
21
+ handle: value.handle,
22
+ agreementPrivateKey: value.agreementPrivateKey,
23
+ agreementPublicKey: value.agreementPublicKey,
24
+ signingPrivateKey: value.signingPrivateKey,
25
+ signingPublicKey: value.signingPublicKey
26
+ } : null;
27
+ }
28
+ async nextCounter(handle) {
29
+ const database = await this.open();
30
+ return new Promise((resolve, reject) => {
31
+ const transaction = database.transaction(STORE_NAME, "readwrite");
32
+ const store = transaction.objectStore(STORE_NAME);
33
+ const request = store.get(handle);
34
+ let next = null;
35
+ request.onerror = () => reject(request.error);
36
+ request.onsuccess = () => {
37
+ const record = request.result;
38
+ if (!record) {
39
+ transaction.abort();
40
+ reject(new RelayCryptoError("missing_grant_key", "The encrypted grant key is unavailable."));
41
+ return;
42
+ }
43
+ const counter = BigInt(record.counter) + 1n;
44
+ if (counter > MAX_U64) {
45
+ transaction.abort();
46
+ reject(new RelayCryptoError("counter_exhausted", "The encrypted grant must be authorized again."));
47
+ return;
48
+ }
49
+ next = counter.toString();
50
+ store.put({ ...record, counter: next });
51
+ };
52
+ transaction.onerror = () => reject(transaction.error);
53
+ transaction.onabort = () => reject(transaction.error ?? new Error("Key counter transaction aborted."));
54
+ transaction.oncomplete = () => {
55
+ if (next)
56
+ resolve(next);
57
+ };
58
+ });
59
+ }
60
+ async delete(handle) {
61
+ const database = await this.open();
62
+ await idbWrite(database, (store) => store.delete(handle));
63
+ }
64
+ open() {
65
+ if (typeof indexedDB === "undefined") {
66
+ throw new RelayCryptoError("key_storage_unavailable", "Encrypted relay authorization requires browser IndexedDB or an injected grant key store.");
67
+ }
68
+ if (!this.database) {
69
+ this.database = new Promise((resolve, reject) => {
70
+ const request = indexedDB.open(DATABASE_NAME, 1);
71
+ request.onupgradeneeded = () => {
72
+ if (!request.result.objectStoreNames.contains(STORE_NAME)) {
73
+ request.result.createObjectStore(STORE_NAME, { keyPath: "handle" });
74
+ }
75
+ };
76
+ request.onerror = () => reject(request.error);
77
+ request.onsuccess = () => resolve(request.result);
78
+ });
79
+ }
80
+ return this.database;
81
+ }
82
+ }
83
+ /** Deterministic process-local storage for tests, CLIs, and non-browser adapters. */
84
+ export class MemoryGrantKeyStore {
85
+ records = new Map();
86
+ sequence = Promise.resolve();
87
+ async create(handle) {
88
+ if (this.records.has(handle))
89
+ throw new Error(`Grant key already exists: ${handle}`);
90
+ const record = await generateGrantKey(handle);
91
+ this.records.set(handle, { ...record, counter: "0" });
92
+ return record;
93
+ }
94
+ async get(handle) {
95
+ const value = this.records.get(handle);
96
+ return value ? {
97
+ handle: value.handle,
98
+ agreementPrivateKey: value.agreementPrivateKey,
99
+ agreementPublicKey: value.agreementPublicKey,
100
+ signingPrivateKey: value.signingPrivateKey,
101
+ signingPublicKey: value.signingPublicKey
102
+ } : null;
103
+ }
104
+ nextCounter(handle) {
105
+ let resolve;
106
+ let reject;
107
+ const result = new Promise((res, rej) => { resolve = res; reject = rej; });
108
+ this.sequence = this.sequence.then(() => {
109
+ const record = this.records.get(handle);
110
+ if (!record)
111
+ throw new RelayCryptoError("missing_grant_key", "The encrypted grant key is unavailable.");
112
+ const counter = BigInt(record.counter) + 1n;
113
+ if (counter > MAX_U64)
114
+ throw new RelayCryptoError("counter_exhausted", "The encrypted grant must be authorized again.");
115
+ record.counter = counter.toString();
116
+ resolve(record.counter);
117
+ }).catch((error) => {
118
+ reject(error);
119
+ });
120
+ return result;
121
+ }
122
+ async delete(handle) {
123
+ this.records.delete(handle);
124
+ }
125
+ }
126
+ /** Sign one remote-authority or token-refresh request with the approved grant key. */
127
+ export async function signAuthorityRequest(store, handle, expectedPublicKey, input) {
128
+ const record = await requireSigningKey(store, handle, expectedPublicKey);
129
+ const timestamp = input.timestamp ?? Math.floor(Date.now() / 1_000);
130
+ const nonce = input.nonce ?? crypto.randomUUID();
131
+ const message = await authorityProofMessage({ ...input, timestamp, nonce });
132
+ const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, record.signingPrivateKey, new TextEncoder().encode(message));
133
+ return {
134
+ [AUTHORITY_PROOF_HEADERS.version]: String(AUTHORITY_PROOF_VERSION),
135
+ [AUTHORITY_PROOF_HEADERS.timestamp]: String(timestamp),
136
+ [AUTHORITY_PROOF_HEADERS.nonce]: nonce,
137
+ [AUTHORITY_PROOF_HEADERS.signature]: bytesToBase64Url(new Uint8Array(signature))
138
+ };
139
+ }
140
+ export async function authorityProofMessage(input) {
141
+ const method = input.method.toUpperCase();
142
+ const bodyHash = await sha256Base64Url(input.body ?? "");
143
+ const credentialHash = await sha256Base64Url(input.credential);
144
+ for (const value of [method, input.target, String(input.timestamp), input.nonce]) {
145
+ if (!value || value.includes("\n") || value.includes("\r")) {
146
+ throw new RelayCryptoError("invalid_proof_input", "Authority request proof metadata is invalid.");
147
+ }
148
+ }
149
+ return [
150
+ AUTHORITY_PROOF_DOMAIN,
151
+ AUTHORITY_PROOF_VERSION,
152
+ method,
153
+ input.target,
154
+ bodyHash,
155
+ credentialHash,
156
+ input.timestamp,
157
+ input.nonce
158
+ ].join("\n");
159
+ }
160
+ export async function encryptRelayRequest(store, handle, binding, operation, input, requestId = crypto.randomUUID(), deadlineUnixMs) {
161
+ validateGrantEncryption(binding.encryption);
162
+ if (deadlineUnixMs !== undefined
163
+ && (!Number.isSafeInteger(deadlineUnixMs) || deadlineUnixMs <= 0)) {
164
+ throw new RelayCryptoError("invalid_deadline", "The operation deadline is invalid.");
165
+ }
166
+ const record = await requireAgreementKey(store, handle, binding.encryption.application_agreement_public_key);
167
+ const counter = await store.nextCounter(handle);
168
+ const metadata = {
169
+ binding,
170
+ protocolVersion: OPERATION_TRANSPORT_PROTOCOL_VERSION,
171
+ requestId,
172
+ operation,
173
+ counter
174
+ };
175
+ const key = await deriveDirectionalKey(record.agreementPrivateKey, binding, metadata.protocolVersion, "request");
176
+ const plaintext = new TextEncoder().encode(JSON.stringify(input ?? {}));
177
+ const ciphertext = await crypto.subtle.encrypt({
178
+ name: "AES-GCM",
179
+ iv: nonce(counter),
180
+ additionalData: new TextEncoder().encode(aad(metadata, "request")),
181
+ tagLength: 128
182
+ }, key, plaintext);
183
+ return {
184
+ type: "encrypted_operation_request",
185
+ ...envelopeMetadata(metadata),
186
+ ...(deadlineUnixMs === undefined ? {} : { deadline_unix_ms: deadlineUnixMs }),
187
+ ciphertext: bytesToBase64Url(new Uint8Array(ciphertext))
188
+ };
189
+ }
190
+ export async function decryptRelayResponse(store, handle, binding, request, response) {
191
+ if (!sameEnvelopeMetadata(request, response)) {
192
+ throw new RelayCryptoError("invalid_encrypted_response", "Encrypted response metadata does not match its request.");
193
+ }
194
+ const record = await requireAgreementKey(store, handle, binding.encryption.application_agreement_public_key);
195
+ const metadata = {
196
+ binding,
197
+ protocolVersion: request.protocol_version,
198
+ requestId: request.request_id,
199
+ operation: request.operation,
200
+ counter: request.counter
201
+ };
202
+ const key = await deriveDirectionalKey(record.agreementPrivateKey, binding, request.protocol_version, "response");
203
+ try {
204
+ const plaintext = await crypto.subtle.decrypt({
205
+ name: "AES-GCM",
206
+ iv: nonce(request.counter),
207
+ additionalData: new TextEncoder().encode(aad(metadata, "response")),
208
+ tagLength: 128
209
+ }, key, toArrayBuffer(base64UrlToBytes(response.ciphertext)));
210
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
211
+ if (value?.ok === true && "result" in value)
212
+ return value;
213
+ if (value?.ok === false && isConnectProblem(value.problem))
214
+ return value;
215
+ throw new Error("invalid response body");
216
+ }
217
+ catch (error) {
218
+ if (error instanceof RelayCryptoError)
219
+ throw error;
220
+ throw new RelayCryptoError("encrypted_response_rejected", "The encrypted connector response could not be authenticated.");
221
+ }
222
+ }
223
+ export class RelayCryptoError extends Error {
224
+ code;
225
+ constructor(code, message) {
226
+ super(message);
227
+ this.code = code;
228
+ }
229
+ }
230
+ async function generateGrantKey(handle) {
231
+ const agreement = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
232
+ const agreementPublicKey = bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("raw", agreement.publicKey)));
233
+ const agreementPkcs8 = await crypto.subtle.exportKey("pkcs8", agreement.privateKey);
234
+ const agreementPrivateKey = await crypto.subtle.importKey("pkcs8", agreementPkcs8, { name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
235
+ const signing = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
236
+ const signingPublicKey = bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("raw", signing.publicKey)));
237
+ const signingPkcs8 = await crypto.subtle.exportKey("pkcs8", signing.privateKey);
238
+ const signingPrivateKey = await crypto.subtle.importKey("pkcs8", signingPkcs8, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
239
+ return {
240
+ handle,
241
+ agreementPrivateKey,
242
+ agreementPublicKey,
243
+ signingPrivateKey,
244
+ signingPublicKey
245
+ };
246
+ }
247
+ async function requireAgreementKey(store, handle, expectedPublicKey) {
248
+ const record = await store.get(handle);
249
+ if (!record || record.agreementPublicKey !== expectedPublicKey) {
250
+ throw new RelayCryptoError("missing_grant_key", "The encrypted grant key is unavailable or does not match the grant.");
251
+ }
252
+ return record;
253
+ }
254
+ async function requireSigningKey(store, handle, expectedPublicKey) {
255
+ const record = await store.get(handle);
256
+ if (!record || record.signingPublicKey !== expectedPublicKey) {
257
+ throw new RelayCryptoError("missing_grant_key", "The remote authority signing key is unavailable or does not match the grant.");
258
+ }
259
+ return record;
260
+ }
261
+ async function deriveDirectionalKey(privateKey, binding, protocolVersion, direction) {
262
+ const shared = await deriveP256SharedSecret(privateKey, binding.encryption.connector_agreement_public_key);
263
+ const material = await crypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
264
+ const contextBytes = new TextEncoder().encode(context(binding, protocolVersion));
265
+ const salt = await crypto.subtle.digest("SHA-256", contextBytes);
266
+ return crypto.subtle.deriveKey({
267
+ name: "HKDF",
268
+ hash: "SHA-256",
269
+ salt,
270
+ info: new TextEncoder().encode(direction === "request" ? REQUEST_INFO : RESPONSE_INFO)
271
+ }, material, { name: "AES-GCM", length: 256 }, false, direction === "request" ? ["encrypt"] : ["decrypt"]);
272
+ }
273
+ /** @internal Shared ECDH primitive used by the independently keyed file data plane. */
274
+ export async function deriveP256SharedSecret(privateKey, peerPublicKey) {
275
+ try {
276
+ const peer = await crypto.subtle.importKey("raw", toArrayBuffer(p256PublicKey(peerPublicKey)), { name: "ECDH", namedCurve: "P-256" }, false, []);
277
+ return await crypto.subtle.deriveBits({ name: "ECDH", public: peer }, privateKey, 256);
278
+ }
279
+ catch {
280
+ throw new RelayCryptoError("invalid_public_key", "The connector relay public key is invalid.");
281
+ }
282
+ }
283
+ function context(binding, protocolVersion) {
284
+ const value = binding.encryption;
285
+ return [
286
+ "mdbase-connect",
287
+ protocolVersion,
288
+ value.suite,
289
+ binding.grantId,
290
+ binding.applicationId,
291
+ value.connector_id,
292
+ value.collection_id,
293
+ value.scope_epoch,
294
+ value.key_id
295
+ ].join("|");
296
+ }
297
+ function aad(metadata, direction) {
298
+ return [
299
+ context(metadata.binding, metadata.protocolVersion),
300
+ metadata.requestId,
301
+ direction,
302
+ metadata.operation,
303
+ metadata.counter
304
+ ].join("|");
305
+ }
306
+ function envelopeMetadata(metadata) {
307
+ const encryption = metadata.binding.encryption;
308
+ return {
309
+ protocol_version: metadata.protocolVersion,
310
+ suite: RELAY_ENCRYPTION_SUITE,
311
+ request_id: metadata.requestId,
312
+ grant_id: metadata.binding.grantId,
313
+ application_id: metadata.binding.applicationId,
314
+ connector_id: encryption.connector_id,
315
+ collection_id: encryption.collection_id,
316
+ operation: metadata.operation,
317
+ scope_epoch: encryption.scope_epoch,
318
+ key_id: encryption.key_id,
319
+ counter: metadata.counter
320
+ };
321
+ }
322
+ function sameEnvelopeMetadata(request, response) {
323
+ return response.type === "encrypted_operation_response"
324
+ && typeof response.ciphertext === "string"
325
+ && request.protocol_version === response.protocol_version
326
+ && request.suite === response.suite
327
+ && request.request_id === response.request_id
328
+ && request.grant_id === response.grant_id
329
+ && request.application_id === response.application_id
330
+ && request.connector_id === response.connector_id
331
+ && request.collection_id === response.collection_id
332
+ && request.operation === response.operation
333
+ && request.scope_epoch === response.scope_epoch
334
+ && request.key_id === response.key_id
335
+ && request.counter === response.counter;
336
+ }
337
+ export function validateGrantEncryption(encryption) {
338
+ if (encryption.protocol_version !== GRANT_ENCRYPTION_PROTOCOL_VERSION
339
+ || encryption.suite !== RELAY_ENCRYPTION_SUITE
340
+ || !Number.isSafeInteger(encryption.scope_epoch)
341
+ || encryption.scope_epoch <= 0
342
+ || typeof encryption.key_id !== "string"
343
+ || encryption.key_id.length === 0
344
+ || encryption.key_id.length > 200
345
+ || encryption.key_id.includes("|")) {
346
+ throw new RelayCryptoError("unsupported_encryption", "The grant uses an unsupported relay encryption profile.");
347
+ }
348
+ if (typeof encryption.connector_id !== "string"
349
+ || !UUID_PATTERN.test(encryption.connector_id)
350
+ || typeof encryption.collection_id !== "string"
351
+ || !UUID_PATTERN.test(encryption.collection_id)) {
352
+ throw new RelayCryptoError("unsupported_encryption", "The grant uses invalid encrypted relay identities.");
353
+ }
354
+ p256PublicKey(encryption.application_agreement_public_key);
355
+ p256PublicKey(encryption.connector_agreement_public_key);
356
+ }
357
+ function nonce(counter) {
358
+ const value = BigInt(counter);
359
+ if (value <= 0n || value > MAX_U64 || value.toString() !== counter) {
360
+ throw new RelayCryptoError("invalid_counter", "The encrypted relay counter is invalid.");
361
+ }
362
+ const result = new Uint8Array(new ArrayBuffer(12));
363
+ const view = new DataView(result.buffer);
364
+ view.setBigUint64(4, value, false);
365
+ return result;
366
+ }
367
+ function bytesToBase64Url(bytes) {
368
+ let binary = "";
369
+ for (const byte of bytes)
370
+ binary += String.fromCharCode(byte);
371
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
372
+ }
373
+ async function sha256Base64Url(value) {
374
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
375
+ return bytesToBase64Url(new Uint8Array(digest));
376
+ }
377
+ function base64UrlToBytes(value) {
378
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
379
+ throw new RelayCryptoError("invalid_base64", "Encrypted relay data is malformed.");
380
+ const padding = "=".repeat((4 - value.length % 4) % 4);
381
+ const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/") + padding);
382
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
383
+ }
384
+ function p256PublicKey(value) {
385
+ const bytes = base64UrlToBytes(value);
386
+ if (bytes.length !== 65 || bytes[0] !== 4) {
387
+ throw new RelayCryptoError("invalid_public_key", "Encrypted relay public keys must be uncompressed P-256 points.");
388
+ }
389
+ return bytes;
390
+ }
391
+ function toArrayBuffer(value) {
392
+ return Uint8Array.from(value).buffer;
393
+ }
394
+ function idbRequest(request) {
395
+ return new Promise((resolve, reject) => {
396
+ request.onerror = () => reject(request.error);
397
+ request.onsuccess = () => resolve(request.result);
398
+ });
399
+ }
400
+ function idbWrite(database, operation) {
401
+ return new Promise((resolve, reject) => {
402
+ const transaction = database.transaction(STORE_NAME, "readwrite");
403
+ const request = operation(transaction.objectStore(STORE_NAME));
404
+ request.onerror = () => reject(request.error);
405
+ transaction.onerror = () => reject(transaction.error);
406
+ transaction.onabort = () => reject(transaction.error ?? new Error("Grant key transaction aborted."));
407
+ transaction.oncomplete = () => resolve();
408
+ });
409
+ }
@@ -0,0 +1,3 @@
1
+ import type { StoredToken } from "./internal-types.js";
2
+ export declare function tokenSupportsDirectAccess(token: StoredToken | null, mode: "auto" | "disabled"): boolean;
3
+ export declare function probeLoopbackAccess(loopbackUrl: string, expectedOperationProtocol: number, signal?: AbortSignal): Promise<boolean>;
@@ -0,0 +1,29 @@
1
+ import { loopbackRequest } from "./operation-helpers.js";
2
+ import { decodeJsonResponse } from "./runtime-utils.js";
3
+ export function tokenSupportsDirectAccess(token, mode) {
4
+ if (!token || token.authority || !token.encryption || !token.grantId || !token.keyHandle) {
5
+ return false;
6
+ }
7
+ if (mode === "disabled")
8
+ return false;
9
+ return typeof location === "undefined"
10
+ || !token.applicationOrigin
11
+ || token.applicationOrigin === location.origin;
12
+ }
13
+ export async function probeLoopbackAccess(loopbackUrl, expectedOperationProtocol, signal) {
14
+ try {
15
+ const response = await fetch(`${loopbackUrl}/v1/ready`, loopbackRequest({
16
+ method: "GET",
17
+ cache: "no-store",
18
+ signal
19
+ }));
20
+ const body = await decodeJsonResponse(response, "invalid_operation_response", "The connector returned an invalid readiness response.").catch(() => null);
21
+ return response.ok
22
+ && body?.service === "mdbase-connect"
23
+ && body?.loopback_protocol_version === 1
24
+ && body?.operation_transport_protocol_version === expectedOperationProtocol;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
@@ -0,0 +1,45 @@
1
+ import { type ConnectOperationOutcome, type ConnectProblem, type ConnectProblemCode, type ConnectProblemDetailsByCode, type KnownConnectProblem, type MdbaseOperationEnvelope } from "@mdbase-dev/connect-protocol";
2
+ interface ProblemOptions {
3
+ operationOutcome?: ConnectOperationOutcome;
4
+ traceId?: string;
5
+ status?: number;
6
+ cause?: unknown;
7
+ }
8
+ type ProblemOptionsFor<Code extends ConnectProblemCode> = ConnectProblemDetailsByCode[Code] extends undefined ? ProblemOptions & {
9
+ details?: never;
10
+ } : {} extends ConnectProblemDetailsByCode[Code] ? ProblemOptions & {
11
+ details?: ConnectProblemDetailsByCode[Code];
12
+ } : ProblemOptions & {
13
+ details: ConnectProblemDetailsByCode[Code];
14
+ };
15
+ type ProblemArguments<Code extends ConnectProblemCode> = ConnectProblemDetailsByCode[Code] extends undefined ? [options?: ProblemOptionsFor<Code>] : {} extends ConnectProblemDetailsByCode[Code] ? [options?: ProblemOptionsFor<Code>] : [options: ProblemOptionsFor<Code>];
16
+ export interface ConnectErrorContext {
17
+ status?: number;
18
+ cause?: unknown;
19
+ }
20
+ /** Internal carrier used while adapting throwing transports into public outcomes. */
21
+ export declare class MdbaseConnectError extends Error {
22
+ readonly problem: ConnectProblem;
23
+ readonly code: ConnectProblem["code"];
24
+ readonly status?: number;
25
+ constructor(problem: ConnectProblem, context?: ConnectErrorContext);
26
+ get retryable(): boolean;
27
+ get requiresAuthorization(): boolean;
28
+ get outcomeUnknown(): boolean;
29
+ get recovery(): "choose_collection" | "contact_support" | "fix_request" | "none" | "reauthorize" | "refresh" | "repair_collection" | "resolve_conflict" | "resolve_outcome" | "resume_connector_access" | "retry" | "upgrade_application" | "upgrade_collection" | "upgrade_connector";
30
+ get details(): unknown;
31
+ }
32
+ export declare function connectProblem<Code extends ConnectProblemCode>(code: Code, message: string, ...[options]: ProblemArguments<Code>): KnownConnectProblem<Code>;
33
+ export declare function unknownConnectProblem(serverCode: string, message: string, options?: ProblemOptions & {
34
+ details?: unknown;
35
+ }): ConnectProblem;
36
+ export declare function connectError<Code extends ConnectProblemCode>(code: Code, message: string, ...args: ProblemArguments<Code>): MdbaseConnectError;
37
+ export declare function serverConnectError(serverCode: string, message: string, options?: ProblemOptions & {
38
+ details?: unknown;
39
+ status?: number;
40
+ cause?: unknown;
41
+ }): MdbaseConnectError;
42
+ export declare function operationProblem<Result>(envelope: MdbaseOperationEnvelope<Result>): KnownConnectProblem<"collection_configuration_invalid" | "collection_invalid" | "collection_type_registry_invalid" | "collection_version_unsupported" | "operation_invalid">;
43
+ /** True only when repeating an operation is safe without asking the user. */
44
+ export declare function isRetryableConnectError(error: unknown): boolean;
45
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,142 @@
1
+ import { CONNECT_PROBLEM_CATALOG, CONNECT_PROBLEM_VERSION, normalizeConnectProblem } from "@mdbase-dev/connect-protocol";
2
+ /** Internal carrier used while adapting throwing transports into public outcomes. */
3
+ export class MdbaseConnectError extends Error {
4
+ problem;
5
+ code;
6
+ status;
7
+ constructor(problem, context = {}) {
8
+ super(problem.message, context.cause === undefined ? undefined : { cause: context.cause });
9
+ this.problem = problem;
10
+ this.name = "MdbaseConnectError";
11
+ this.code = problem.code;
12
+ this.status = context.status;
13
+ }
14
+ get retryable() {
15
+ return this.problem.recovery === "retry"
16
+ && this.problem.operation_outcome !== "unknown";
17
+ }
18
+ get requiresAuthorization() {
19
+ return this.problem.category === "authorization";
20
+ }
21
+ get outcomeUnknown() {
22
+ return this.problem.operation_outcome === "unknown";
23
+ }
24
+ get recovery() {
25
+ return this.problem.recovery;
26
+ }
27
+ get details() {
28
+ return this.problem.details;
29
+ }
30
+ }
31
+ export function connectProblem(code, message, ...[options = {}]) {
32
+ const definition = CONNECT_PROBLEM_CATALOG[code];
33
+ return {
34
+ problem_version: CONNECT_PROBLEM_VERSION,
35
+ code,
36
+ category: definition.category,
37
+ recovery: definition.recovery,
38
+ message,
39
+ ...(options.details === undefined ? {} : { details: options.details }),
40
+ ...(options.operationOutcome === undefined
41
+ ? {}
42
+ : { operation_outcome: options.operationOutcome }),
43
+ ...(options.traceId === undefined ? {} : { trace_id: options.traceId })
44
+ };
45
+ }
46
+ export function unknownConnectProblem(serverCode, message, options = {}) {
47
+ return normalizeConnectProblem(serverCode, message, {
48
+ details: options.details,
49
+ operation_outcome: options.operationOutcome,
50
+ trace_id: options.traceId
51
+ });
52
+ }
53
+ export function connectError(code, message, ...args) {
54
+ const options = args[0];
55
+ return new MdbaseConnectError(connectProblem(code, message, ...args), {
56
+ status: options?.status,
57
+ cause: options?.cause
58
+ });
59
+ }
60
+ export function serverConnectError(serverCode, message, options = {}) {
61
+ const { status, cause, ...problemOptions } = options;
62
+ return new MdbaseConnectError(normalizeConnectProblem(serverCode, message, {
63
+ details: problemOptions.details,
64
+ operation_outcome: problemOptions.operationOutcome,
65
+ trace_id: problemOptions.traceId
66
+ }), { status, cause });
67
+ }
68
+ export function operationProblem(envelope) {
69
+ const code = diagnosticProblemCode(envelope.diagnostics);
70
+ const message = diagnosticMessage(envelope.diagnostics);
71
+ if (code === "collection_version_unsupported") {
72
+ const versions = diagnosticVersions(envelope.diagnostics);
73
+ return connectProblem(code, message, {
74
+ operationOutcome: "rejected",
75
+ details: versions
76
+ });
77
+ }
78
+ if (code !== "operation_invalid") {
79
+ return connectProblem(code, message, {
80
+ operationOutcome: "rejected",
81
+ details: { diagnostics: envelope.diagnostics }
82
+ });
83
+ }
84
+ return connectProblem(code, message, {
85
+ operationOutcome: "rejected",
86
+ details: {
87
+ diagnostics: envelope.diagnostics,
88
+ partial_result: envelope.result
89
+ }
90
+ });
91
+ }
92
+ function diagnosticProblemCode(diagnostics) {
93
+ const codes = new Set(diagnostics
94
+ .filter((diagnostic) => diagnostic.severity === "error")
95
+ .map((diagnostic) => diagnostic.code));
96
+ if (["migration_required", "migration_lossy", "unsupported_version"]
97
+ .some((code) => codes.has(code)))
98
+ return "collection_version_unsupported";
99
+ if (["invalid_config", "invalid_configuration", "missing_config", "missing_spec_version"]
100
+ .some((code) => codes.has(code)))
101
+ return "collection_configuration_invalid";
102
+ if ([
103
+ "duplicate_type",
104
+ "invalid_contract_definition",
105
+ "invalid_data_contract",
106
+ "invalid_type_definition",
107
+ "invalid_type_path"
108
+ ].some((code) => codes.has(code)))
109
+ return "collection_type_registry_invalid";
110
+ if (["collection_invalid", "collection_open_failed"]
111
+ .some((code) => codes.has(code)))
112
+ return "collection_invalid";
113
+ return "operation_invalid";
114
+ }
115
+ function diagnosticVersions(diagnostics) {
116
+ const source = diagnostics
117
+ .map((diagnostic) => diagnostic.details)
118
+ .find((details) => isRecord(details));
119
+ return {
120
+ current_version: typeof source?.current_version === "string"
121
+ ? source.current_version
122
+ : "0.2.x",
123
+ required_version: typeof source?.required_version === "string"
124
+ ? source.required_version
125
+ : "0.3.0"
126
+ };
127
+ }
128
+ function isRecord(value) {
129
+ return value !== null && typeof value === "object" && !Array.isArray(value);
130
+ }
131
+ /** True only when repeating an operation is safe without asking the user. */
132
+ export function isRetryableConnectError(error) {
133
+ return error instanceof MdbaseConnectError && error.retryable;
134
+ }
135
+ function diagnosticMessage(diagnostics) {
136
+ return diagnostics
137
+ .filter((item) => item.severity === "error")
138
+ .map((item) => item.message)
139
+ .join(" ")
140
+ || diagnostics.map((item) => item.message).join(" ")
141
+ || "The collection rejected this operation.";
142
+ }
@@ -0,0 +1,10 @@
1
+ /** Minimal framework-neutral shape consumed by React's useSyncExternalStore. */
2
+ export interface MdbaseExternalStore<Snapshot> {
3
+ subscribe(listener: () => void): () => void;
4
+ getSnapshot(): Snapshot;
5
+ }
6
+ /**
7
+ * Adapt an existing SDK session without introducing another cache or state
8
+ * owner. The stable object can be passed directly to useSyncExternalStore.
9
+ */
10
+ export declare function externalStore<Snapshot>(source: MdbaseExternalStore<Snapshot>): MdbaseExternalStore<Snapshot>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Adapt an existing SDK session without introducing another cache or state
3
+ * owner. The stable object can be passed directly to useSyncExternalStore.
4
+ */
5
+ export function externalStore(source) {
6
+ return {
7
+ subscribe: (listener) => source.subscribe(listener),
8
+ getSnapshot: () => source.getSnapshot()
9
+ };
10
+ }