@mdbase-dev/connect 0.1.0-beta.21

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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +405 -0
  3. package/dist/async.d.ts +1 -0
  4. package/dist/async.js +13 -0
  5. package/dist/authorization-types.d.ts +7 -0
  6. package/dist/authorization-types.js +1 -0
  7. package/dist/authorization-url.d.ts +3 -0
  8. package/dist/authorization-url.js +27 -0
  9. package/dist/base64.d.ts +3 -0
  10. package/dist/base64.js +21 -0
  11. package/dist/browser/integrity.json +4 -0
  12. package/dist/browser/mdbase-connect.min.js +3 -0
  13. package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
  14. package/dist/collection-client.d.ts +57 -0
  15. package/dist/collection-client.js +214 -0
  16. package/dist/connect.d.ts +39 -0
  17. package/dist/connect.js +620 -0
  18. package/dist/connection-notifications.d.ts +25 -0
  19. package/dist/connection-notifications.js +179 -0
  20. package/dist/connection-transport.d.ts +80 -0
  21. package/dist/connection-transport.js +670 -0
  22. package/dist/connection-types.d.ts +29 -0
  23. package/dist/connection-types.js +1 -0
  24. package/dist/connection.d.ts +154 -0
  25. package/dist/connection.js +371 -0
  26. package/dist/crypto.d.ts +64 -0
  27. package/dist/crypto.js +391 -0
  28. package/dist/errors.d.ts +31 -0
  29. package/dist/errors.js +83 -0
  30. package/dist/index.d.ts +13 -0
  31. package/dist/index.js +12 -0
  32. package/dist/internal-types.d.ts +70 -0
  33. package/dist/internal-types.js +1 -0
  34. package/dist/notifications.d.ts +47 -0
  35. package/dist/notifications.js +52 -0
  36. package/dist/operation-helpers.d.ts +24 -0
  37. package/dist/operation-helpers.js +129 -0
  38. package/dist/operation-types.d.ts +254 -0
  39. package/dist/operation-types.js +1 -0
  40. package/dist/runtime-utils.d.ts +33 -0
  41. package/dist/runtime-utils.js +224 -0
  42. package/dist/selection.d.ts +48 -0
  43. package/dist/selection.js +112 -0
  44. package/dist/session.d.ts +76 -0
  45. package/dist/session.js +271 -0
  46. package/package.json +41 -0
package/dist/crypto.js ADDED
@@ -0,0 +1,391 @@
1
+ import { ENCRYPTED_RELAY_PROTOCOL_VERSION, AUTHORITY_PROOF_DOMAIN, AUTHORITY_PROOF_HEADERS, AUTHORITY_PROOF_VERSION, RELAY_ENCRYPTION_SUITE } 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()) {
161
+ validateGrantEncryption(binding.encryption);
162
+ const record = await requireAgreementKey(store, handle, binding.encryption.application_agreement_public_key);
163
+ const counter = await store.nextCounter(handle);
164
+ const metadata = { binding, requestId, operation, counter };
165
+ const key = await deriveDirectionalKey(record.agreementPrivateKey, binding, "request");
166
+ const plaintext = new TextEncoder().encode(JSON.stringify(input ?? {}));
167
+ const ciphertext = await crypto.subtle.encrypt({
168
+ name: "AES-GCM",
169
+ iv: nonce(counter),
170
+ additionalData: new TextEncoder().encode(aad(metadata, "request")),
171
+ tagLength: 128
172
+ }, key, plaintext);
173
+ return {
174
+ type: "encrypted_operation_request",
175
+ ...envelopeMetadata(metadata),
176
+ ciphertext: bytesToBase64Url(new Uint8Array(ciphertext))
177
+ };
178
+ }
179
+ export async function decryptRelayResponse(store, handle, binding, request, response) {
180
+ if (!sameEnvelopeMetadata(request, response)) {
181
+ throw new RelayCryptoError("invalid_encrypted_response", "Encrypted response metadata does not match its request.");
182
+ }
183
+ const record = await requireAgreementKey(store, handle, binding.encryption.application_agreement_public_key);
184
+ const metadata = {
185
+ binding,
186
+ requestId: request.request_id,
187
+ operation: request.operation,
188
+ counter: request.counter
189
+ };
190
+ const key = await deriveDirectionalKey(record.agreementPrivateKey, binding, "response");
191
+ try {
192
+ const plaintext = await crypto.subtle.decrypt({
193
+ name: "AES-GCM",
194
+ iv: nonce(request.counter),
195
+ additionalData: new TextEncoder().encode(aad(metadata, "response")),
196
+ tagLength: 128
197
+ }, key, toArrayBuffer(base64UrlToBytes(response.ciphertext)));
198
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
199
+ if (value?.ok === true && "result" in value)
200
+ return value;
201
+ if (value?.ok === false
202
+ && typeof value.error?.code === "string"
203
+ && typeof value.error?.message === "string")
204
+ return value;
205
+ throw new Error("invalid response body");
206
+ }
207
+ catch (error) {
208
+ if (error instanceof RelayCryptoError)
209
+ throw error;
210
+ throw new RelayCryptoError("encrypted_response_rejected", "The encrypted connector response could not be authenticated.");
211
+ }
212
+ }
213
+ export class RelayCryptoError extends Error {
214
+ code;
215
+ constructor(code, message) {
216
+ super(message);
217
+ this.code = code;
218
+ }
219
+ }
220
+ async function generateGrantKey(handle) {
221
+ const agreement = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
222
+ const agreementPublicKey = bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("raw", agreement.publicKey)));
223
+ const agreementPkcs8 = await crypto.subtle.exportKey("pkcs8", agreement.privateKey);
224
+ const agreementPrivateKey = await crypto.subtle.importKey("pkcs8", agreementPkcs8, { name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
225
+ const signing = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
226
+ const signingPublicKey = bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("raw", signing.publicKey)));
227
+ const signingPkcs8 = await crypto.subtle.exportKey("pkcs8", signing.privateKey);
228
+ const signingPrivateKey = await crypto.subtle.importKey("pkcs8", signingPkcs8, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
229
+ return {
230
+ handle,
231
+ agreementPrivateKey,
232
+ agreementPublicKey,
233
+ signingPrivateKey,
234
+ signingPublicKey
235
+ };
236
+ }
237
+ async function requireAgreementKey(store, handle, expectedPublicKey) {
238
+ const record = await store.get(handle);
239
+ if (!record || record.agreementPublicKey !== expectedPublicKey) {
240
+ throw new RelayCryptoError("missing_grant_key", "The encrypted grant key is unavailable or does not match the grant.");
241
+ }
242
+ return record;
243
+ }
244
+ async function requireSigningKey(store, handle, expectedPublicKey) {
245
+ const record = await store.get(handle);
246
+ if (!record || record.signingPublicKey !== expectedPublicKey) {
247
+ throw new RelayCryptoError("missing_grant_key", "The remote authority signing key is unavailable or does not match the grant.");
248
+ }
249
+ return record;
250
+ }
251
+ async function deriveDirectionalKey(privateKey, binding, direction) {
252
+ let peer;
253
+ let shared;
254
+ try {
255
+ peer = await crypto.subtle.importKey("raw", toArrayBuffer(p256PublicKey(binding.encryption.connector_agreement_public_key)), { name: "ECDH", namedCurve: "P-256" }, false, []);
256
+ shared = await crypto.subtle.deriveBits({ name: "ECDH", public: peer }, privateKey, 256);
257
+ }
258
+ catch {
259
+ throw new RelayCryptoError("invalid_public_key", "The connector relay public key is invalid.");
260
+ }
261
+ const material = await crypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
262
+ const contextBytes = new TextEncoder().encode(context(binding));
263
+ const salt = await crypto.subtle.digest("SHA-256", contextBytes);
264
+ return crypto.subtle.deriveKey({
265
+ name: "HKDF",
266
+ hash: "SHA-256",
267
+ salt,
268
+ info: new TextEncoder().encode(direction === "request" ? REQUEST_INFO : RESPONSE_INFO)
269
+ }, material, { name: "AES-GCM", length: 256 }, false, direction === "request" ? ["encrypt"] : ["decrypt"]);
270
+ }
271
+ function context(binding) {
272
+ const value = binding.encryption;
273
+ return [
274
+ "mdbase-connect",
275
+ ENCRYPTED_RELAY_PROTOCOL_VERSION,
276
+ value.suite,
277
+ binding.grantId,
278
+ binding.applicationId,
279
+ value.connector_id,
280
+ value.collection_id,
281
+ value.scope_epoch,
282
+ value.key_id
283
+ ].join("|");
284
+ }
285
+ function aad(metadata, direction) {
286
+ return [context(metadata.binding), metadata.requestId, direction, metadata.operation, metadata.counter].join("|");
287
+ }
288
+ function envelopeMetadata(metadata) {
289
+ const encryption = metadata.binding.encryption;
290
+ return {
291
+ protocol_version: ENCRYPTED_RELAY_PROTOCOL_VERSION,
292
+ suite: RELAY_ENCRYPTION_SUITE,
293
+ request_id: metadata.requestId,
294
+ grant_id: metadata.binding.grantId,
295
+ application_id: metadata.binding.applicationId,
296
+ connector_id: encryption.connector_id,
297
+ collection_id: encryption.collection_id,
298
+ operation: metadata.operation,
299
+ scope_epoch: encryption.scope_epoch,
300
+ key_id: encryption.key_id,
301
+ counter: metadata.counter
302
+ };
303
+ }
304
+ function sameEnvelopeMetadata(request, response) {
305
+ return response.type === "encrypted_operation_response"
306
+ && typeof response.ciphertext === "string"
307
+ && request.protocol_version === response.protocol_version
308
+ && request.suite === response.suite
309
+ && request.request_id === response.request_id
310
+ && request.grant_id === response.grant_id
311
+ && request.application_id === response.application_id
312
+ && request.connector_id === response.connector_id
313
+ && request.collection_id === response.collection_id
314
+ && request.operation === response.operation
315
+ && request.scope_epoch === response.scope_epoch
316
+ && request.key_id === response.key_id
317
+ && request.counter === response.counter;
318
+ }
319
+ export function validateGrantEncryption(encryption) {
320
+ if (encryption.protocol_version !== ENCRYPTED_RELAY_PROTOCOL_VERSION
321
+ || encryption.suite !== RELAY_ENCRYPTION_SUITE
322
+ || !Number.isSafeInteger(encryption.scope_epoch)
323
+ || encryption.scope_epoch <= 0
324
+ || typeof encryption.key_id !== "string"
325
+ || encryption.key_id.length === 0
326
+ || encryption.key_id.length > 200
327
+ || encryption.key_id.includes("|")) {
328
+ throw new RelayCryptoError("unsupported_encryption", "The grant uses an unsupported relay encryption profile.");
329
+ }
330
+ if (typeof encryption.connector_id !== "string"
331
+ || !UUID_PATTERN.test(encryption.connector_id)
332
+ || typeof encryption.collection_id !== "string"
333
+ || !UUID_PATTERN.test(encryption.collection_id)) {
334
+ throw new RelayCryptoError("unsupported_encryption", "The grant uses invalid encrypted relay identities.");
335
+ }
336
+ p256PublicKey(encryption.application_agreement_public_key);
337
+ p256PublicKey(encryption.connector_agreement_public_key);
338
+ }
339
+ function nonce(counter) {
340
+ const value = BigInt(counter);
341
+ if (value <= 0n || value > MAX_U64 || value.toString() !== counter) {
342
+ throw new RelayCryptoError("invalid_counter", "The encrypted relay counter is invalid.");
343
+ }
344
+ const result = new Uint8Array(new ArrayBuffer(12));
345
+ const view = new DataView(result.buffer);
346
+ view.setBigUint64(4, value, false);
347
+ return result;
348
+ }
349
+ function bytesToBase64Url(bytes) {
350
+ let binary = "";
351
+ for (const byte of bytes)
352
+ binary += String.fromCharCode(byte);
353
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
354
+ }
355
+ async function sha256Base64Url(value) {
356
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
357
+ return bytesToBase64Url(new Uint8Array(digest));
358
+ }
359
+ function base64UrlToBytes(value) {
360
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
361
+ throw new RelayCryptoError("invalid_base64", "Encrypted relay data is malformed.");
362
+ const padding = "=".repeat((4 - value.length % 4) % 4);
363
+ const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/") + padding);
364
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
365
+ }
366
+ function p256PublicKey(value) {
367
+ const bytes = base64UrlToBytes(value);
368
+ if (bytes.length !== 65 || bytes[0] !== 4) {
369
+ throw new RelayCryptoError("invalid_public_key", "Encrypted relay public keys must be uncompressed P-256 points.");
370
+ }
371
+ return bytes;
372
+ }
373
+ function toArrayBuffer(value) {
374
+ return Uint8Array.from(value).buffer;
375
+ }
376
+ function idbRequest(request) {
377
+ return new Promise((resolve, reject) => {
378
+ request.onerror = () => reject(request.error);
379
+ request.onsuccess = () => resolve(request.result);
380
+ });
381
+ }
382
+ function idbWrite(database, operation) {
383
+ return new Promise((resolve, reject) => {
384
+ const transaction = database.transaction(STORE_NAME, "readwrite");
385
+ const request = operation(transaction.objectStore(STORE_NAME));
386
+ request.onerror = () => reject(request.error);
387
+ transaction.onerror = () => reject(transaction.error);
388
+ transaction.onabort = () => reject(transaction.error ?? new Error("Grant key transaction aborted."));
389
+ transaction.oncomplete = () => resolve();
390
+ });
391
+ }
@@ -0,0 +1,31 @@
1
+ import type { MdbaseDiagnostic, MdbaseOperationEnvelope } from "@mdbase-dev/connect-protocol";
2
+ export type MdbaseRecoveryAction = "retry" | "reauthorize" | "refresh" | "resolve_outcome" | "fix_request" | "none";
3
+ export interface MdbaseConnectErrorOptions {
4
+ status?: number;
5
+ retryable?: boolean;
6
+ requiresAuthorization?: boolean;
7
+ outcomeUnknown?: boolean;
8
+ recovery?: MdbaseRecoveryAction;
9
+ details?: unknown;
10
+ cause?: unknown;
11
+ }
12
+ export declare class MdbaseConnectError extends Error {
13
+ readonly code: string;
14
+ readonly status?: number;
15
+ readonly retryable: boolean;
16
+ readonly requiresAuthorization: boolean;
17
+ readonly outcomeUnknown: boolean;
18
+ readonly recovery: MdbaseRecoveryAction;
19
+ readonly details?: unknown;
20
+ constructor(code: string, message: string, options?: MdbaseConnectErrorOptions);
21
+ }
22
+ export declare class MdbaseOperationValidationError<Result = unknown> extends Error {
23
+ readonly diagnostics: MdbaseDiagnostic[];
24
+ readonly result: Result;
25
+ readonly code = "operation_invalid";
26
+ constructor(diagnostics: MdbaseDiagnostic[], result: Result);
27
+ }
28
+ /** Return a valid operation result or throw while preserving every diagnostic. */
29
+ export declare function unwrapOperation<Result>(envelope: MdbaseOperationEnvelope<Result>): Result;
30
+ /** True only when repeating a read/poll is safe without asking the user. */
31
+ export declare function isRetryableConnectError(error: unknown): boolean;
package/dist/errors.js ADDED
@@ -0,0 +1,83 @@
1
+ export class MdbaseConnectError extends Error {
2
+ code;
3
+ status;
4
+ retryable;
5
+ requiresAuthorization;
6
+ outcomeUnknown;
7
+ recovery;
8
+ details;
9
+ constructor(code, message, options = {}) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = "MdbaseConnectError";
13
+ const classification = classifyConnectError(code, options.status);
14
+ this.status = options.status;
15
+ this.retryable = options.retryable ?? classification.retryable;
16
+ this.requiresAuthorization = options.requiresAuthorization ?? classification.requiresAuthorization;
17
+ this.outcomeUnknown = options.outcomeUnknown ?? classification.outcomeUnknown;
18
+ this.recovery = options.recovery ?? classification.recovery;
19
+ this.details = options.details;
20
+ if (options.cause !== undefined)
21
+ Object.defineProperty(this, "cause", { value: options.cause, configurable: true });
22
+ }
23
+ }
24
+ export class MdbaseOperationValidationError extends Error {
25
+ diagnostics;
26
+ result;
27
+ code = "operation_invalid";
28
+ constructor(diagnostics, result) {
29
+ super(diagnostics.filter((item) => item.severity === "error").map((item) => item.message).join(" ")
30
+ || diagnostics.map((item) => item.message).join(" ")
31
+ || "The collection rejected this operation.");
32
+ this.diagnostics = diagnostics;
33
+ this.result = result;
34
+ this.name = "MdbaseOperationValidationError";
35
+ }
36
+ }
37
+ /** Return a valid operation result or throw while preserving every diagnostic. */
38
+ export function unwrapOperation(envelope) {
39
+ if (!envelope.valid)
40
+ throw new MdbaseOperationValidationError(envelope.diagnostics, envelope.result);
41
+ return envelope.result;
42
+ }
43
+ /** True only when repeating a read/poll is safe without asking the user. */
44
+ export function isRetryableConnectError(error) {
45
+ if (error instanceof MdbaseConnectError)
46
+ return error.retryable && !error.outcomeUnknown;
47
+ return error instanceof TypeError;
48
+ }
49
+ function classifyConnectError(code, status) {
50
+ const authorizationCodes = new Set([
51
+ "authorization_expired",
52
+ "direct_operation_rejected",
53
+ "encryption_required",
54
+ "insufficient_access",
55
+ "missing_grant_key",
56
+ "not_authorized",
57
+ "relay_authorization_expired"
58
+ ]);
59
+ const outcomeUnknown = code === "direct_outcome_unknown" || code === "pending_mutation_unresolved";
60
+ const requiresAuthorization = authorizationCodes.has(code) || status === 401;
61
+ const retryableCodes = new Set([
62
+ "connector_offline",
63
+ "discovery_failed",
64
+ "relay_unavailable",
65
+ "sync_failed",
66
+ "temporarily_unavailable",
67
+ "timeout"
68
+ ]);
69
+ const retryableStatus = status === 408 || status === 425 || status === 429 || (status !== undefined && status >= 500);
70
+ const retryable = !outcomeUnknown && !requiresAuthorization && (retryableCodes.has(code) || retryableStatus);
71
+ const recovery = outcomeUnknown
72
+ ? "resolve_outcome"
73
+ : requiresAuthorization
74
+ ? "reauthorize"
75
+ : code === "change_cursor_reset"
76
+ ? "refresh"
77
+ : retryable
78
+ ? "retry"
79
+ : status !== undefined && status >= 400 && status < 500
80
+ ? "fix_request"
81
+ : "none";
82
+ return { retryable, requiresAuthorization, outcomeUnknown, recovery };
83
+ }
@@ -0,0 +1,13 @@
1
+ export { decryptRelayResponse, encryptRelayRequest, IndexedDbGrantKeyStore, MemoryGrantKeyStore, RelayCryptoError, type RelayBinding, type GrantKeyRecord, type GrantKeyStore } from "./crypto.js";
2
+ export * from "./authorization-types.js";
3
+ export * from "./collection-client.js";
4
+ export * from "./connect.js";
5
+ export * from "./connection.js";
6
+ export * from "./connection-types.js";
7
+ export * from "./errors.js";
8
+ export * from "./notifications.js";
9
+ export * from "./operation-types.js";
10
+ export * from "./selection.js";
11
+ export * from "./session.js";
12
+ export { createPkce } from "./runtime-utils.js";
13
+ export type { ApplicationProvisions, ApplicationRequirements, ApplicationNotifications, CollectionChange, CollectionChangesPage, CollectionContractDescriptor, CollectionDescription, CollectionFileMetadata, CollectionOperation as MdbaseOperation, CollectionTypeDescriptor, CollectionTypeDocument, ContractRequirement, DataContractViewIdentity, GrantScope, JsonObject, MdbaseAppManifest, NotificationCriterion, MdbaseDiagnostic, MdbaseOperationEnvelope, RecordDocument, QueryRecord, SavedNamedView, SavedViewDocument, SavedViewExecution, SavedViewList, SavedViewPresentation, SavedViewProperty, SavedViewSource, SavedViewSourceDocument, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, TypePackManifest, TypePackManifestResource, TypePackInstallResult, TypePackProvision, TypePackResourceDiff, TypePackSourceResource } from "@mdbase-dev/connect-protocol";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export { decryptRelayResponse, encryptRelayRequest, IndexedDbGrantKeyStore, MemoryGrantKeyStore, RelayCryptoError } from "./crypto.js";
2
+ export * from "./authorization-types.js";
3
+ export * from "./collection-client.js";
4
+ export * from "./connect.js";
5
+ export * from "./connection.js";
6
+ export * from "./connection-types.js";
7
+ export * from "./errors.js";
8
+ export * from "./notifications.js";
9
+ export * from "./operation-types.js";
10
+ export * from "./selection.js";
11
+ export * from "./session.js";
12
+ export { createPkce } from "./runtime-utils.js";
@@ -0,0 +1,70 @@
1
+ import type { ApplicationNotifications, CollectionOperation, EncryptedRelayOperationRequest, GrantEncryption, GrantScope } from "@mdbase-dev/connect-protocol";
2
+ import { encryptRelayRequest } from "./crypto.js";
3
+ export interface Application {
4
+ id: string;
5
+ name: string;
6
+ distribution?: "web" | "portable";
7
+ homepage?: string;
8
+ project_url?: string;
9
+ notifications?: ApplicationNotifications;
10
+ }
11
+ export interface StoredAuthorization {
12
+ version: 1;
13
+ verifier: string;
14
+ state: string;
15
+ clientId: string;
16
+ redirectUri: string;
17
+ relayEncryption: "required" | "disabled";
18
+ collectionId?: string;
19
+ returnTo?: string;
20
+ keyHandle?: string;
21
+ applicationAgreementPublicKey?: string;
22
+ applicationSigningPublicKey?: string;
23
+ }
24
+ export interface StoredToken {
25
+ version: 1;
26
+ accessToken: string;
27
+ refreshToken?: string;
28
+ clientId: string;
29
+ collectionId: string;
30
+ collectionName: string;
31
+ operations: CollectionOperation[];
32
+ scope: GrantScope;
33
+ expiresAt: number;
34
+ refreshExpiresAt?: number;
35
+ grantId?: string;
36
+ encryption?: GrantEncryption;
37
+ applicationOrigin?: string;
38
+ keyHandle?: string;
39
+ savedAt: number;
40
+ authority?: {
41
+ operationsUrl: string;
42
+ syncUrl: string;
43
+ replicaId: string;
44
+ accessToken: string;
45
+ proofPublicKey?: string;
46
+ };
47
+ }
48
+ export interface StoredConnectionIndex {
49
+ version: 1;
50
+ collectionIds: string[];
51
+ }
52
+ export interface PendingMutation {
53
+ collectionId: string;
54
+ grantId?: string;
55
+ keyId?: string;
56
+ operation: CollectionOperation;
57
+ inputFingerprint: string;
58
+ requestId: string;
59
+ envelope?: EncryptedRelayOperationRequest;
60
+ createdAt: number;
61
+ }
62
+ export interface OperationAttempt {
63
+ response: Response;
64
+ requestId: string;
65
+ encryptedRequest?: Awaited<ReturnType<typeof encryptRelayRequest>>;
66
+ directDeliveryUncertain?: boolean;
67
+ pendingMutation?: boolean;
68
+ resumingMutation?: boolean;
69
+ }
70
+ export declare const DEFAULT_OPERATIONS: CollectionOperation[];
@@ -0,0 +1 @@
1
+ export const DEFAULT_OPERATIONS = ["describe", "changes", "read", "query"];
@@ -0,0 +1,47 @@
1
+ export interface MdbaseNotificationRegistrationOptions {
2
+ serviceWorker: ServiceWorkerRegistration;
3
+ /** Manifest criterion IDs to enable. Omit to enable every declared criterion. */
4
+ criteria?: string[];
5
+ /** Stable per-installation ID. The SDK persists one when omitted. */
6
+ installationId?: string;
7
+ }
8
+ export interface MdbaseNotificationRegistration {
9
+ channelId: string;
10
+ installationId: string;
11
+ criteria: string[];
12
+ }
13
+ export interface MdbaseNativeNotificationRegistrationOptions {
14
+ /** Current FCM registration token. Refresh by calling this method again. */
15
+ token: string;
16
+ /** Manifest criterion IDs to enable. Omit to enable every declared criterion. */
17
+ criteria?: string[];
18
+ /** Stable per-installation ID. The SDK persists one when omitted. */
19
+ installationId?: string;
20
+ }
21
+ export interface MdbaseNativeNotificationRegistration extends MdbaseNotificationRegistration {
22
+ transport: "fcm";
23
+ }
24
+ export interface MdbaseNativeNotificationData {
25
+ type: "mdbase.notification";
26
+ version: 1;
27
+ signal_id: string;
28
+ criterion_id: string;
29
+ cursor: string;
30
+ }
31
+ export interface MdbasePushPayload {
32
+ type: "mdbase.notification";
33
+ version: 1;
34
+ signal_id: string;
35
+ criterion_id: string;
36
+ cursor: string;
37
+ presentation: {
38
+ title: string;
39
+ body?: string;
40
+ tag?: string;
41
+ };
42
+ }
43
+ export declare function parseMdbasePushPayload(value: unknown): MdbasePushPayload;
44
+ /** Parse the string-valued data attached to an APNs/FCM notification. */
45
+ export declare function parseMdbaseNativeNotificationData(value: unknown): MdbaseNativeNotificationData;
46
+ /** Display a validated mdbase push from a service worker `push` handler. */
47
+ export declare function showMdbasePushNotification(registration: Pick<ServiceWorkerRegistration, "showNotification">, value: unknown): Promise<void>;