@mdbase-dev/connect 0.1.0-beta.56 → 0.1.0-beta.57

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.
@@ -1 +1 @@
1
- sha384-fZeP3Z5L7qaOmLPO0ST72DyQgwJCq0XaamP4LFUHOTWxA2MTeF4eerxdIfULZkEq
1
+ sha384-4Lyda0FLrTi1k549p7spkiOUvz9jT9v7sm+BUgVC4uwPVgyZgY8o6aJlDd+H45xY
package/dist/connect.d.ts CHANGED
@@ -53,8 +53,6 @@ export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
53
53
  removeToken(collectionId: string, keyHandle?: string, reason?: MdbaseUnavailableReason, discardPending?: boolean): void;
54
54
  tokenKey(collectionId: string): string;
55
55
  pendingMutationKey(collectionId: string): string;
56
- private removePendingMutations;
57
- private pendingMutationsUseKey;
58
56
  notificationKey(collectionId: string, transport?: "web_push" | "fcm"): string;
59
57
  directPreferenceKey(): string;
60
58
  defaultApplicationOrigin(): string;
package/dist/connect.js CHANGED
@@ -8,6 +8,7 @@ import { IndexedDbGrantKeyStore, MemoryGrantKeyStore } from "./crypto.js";
8
8
  import { connectError, serverConnectError } from "./errors.js";
9
9
  import { DEFAULT_OPERATIONS } from "./internal-types.js";
10
10
  import { uniqueOperations } from "./operation-helpers.js";
11
+ import { pendingMutationsUseKey, pendingRecoveryTransports, removePendingMutations } from "./pending-recovery-transports.js";
11
12
  import { resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
12
13
  import { MemoryStorage, apiError, applicationStorageOrigin, canonicalLoopbackUrl, collectionIdFromTokenKey, connectFetch, createPkce, defaultManifestSource, defaultRedirectUri, defaultStorage, decodeJsonResponse, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseStored, stripTrailingSlash, validAuthorityTokenResponse } from "./runtime-utils.js";
13
14
  import { storedTokenFromResponse } from "./token-response.js";
@@ -211,7 +212,7 @@ export class MdbaseConnectInternals {
211
212
  redirect_uri: this.redirectUri,
212
213
  state,
213
214
  code_challenge: challenge,
214
- contracts: authorizationContractRequirements(operations, application.requirements?.files),
215
+ contracts: authorizationContractRequirements(operations, application.requirements?.files, pendingRecoveryTransports(this.storage, this.storagePrefix(), operations, targetCollectionId)),
215
216
  requested_operations: operations,
216
217
  ...(application.requirements?.files
217
218
  ? { requested_files: application.requirements.files }
@@ -312,7 +313,7 @@ export class MdbaseConnectInternals {
312
313
  issued_at: issuedAt.toISOString(),
313
314
  expires_at: new Date(issuedAt.getTime() + 10 * 60 * 1_000).toISOString(),
314
315
  code_challenge: challenge,
315
- contracts: authorizationContractRequirements(operations, application.requirements?.files),
316
+ contracts: authorizationContractRequirements(operations, application.requirements?.files, pendingRecoveryTransports(this.storage, this.storagePrefix(), operations, options.target?.kind === "collection" ? options.target.collectionId : undefined)),
316
317
  requested_operations: operations,
317
318
  ...(application.requirements?.files
318
319
  ? { requested_files: application.requirements.files }
@@ -631,12 +632,14 @@ export class MdbaseConnectInternals {
631
632
  }
632
633
  removeToken(collectionId, keyHandle, reason = "authorization_lost", discardPending = false) {
633
634
  this.invalidatedConnections.set(collectionId, reason);
634
- if (keyHandle && (discardPending || !this.pendingMutationsUseKey(collectionId, keyHandle))) {
635
+ if (keyHandle && (discardPending || !pendingMutationsUseKey(this.storage, this.pendingMutationKey(collectionId), keyHandle))) {
635
636
  void this.keyStore.delete(keyHandle).catch(() => undefined);
636
637
  }
637
638
  this.storage.removeItem(this.tokenKey(collectionId));
638
639
  if (discardPending)
639
- this.removePendingMutations(collectionId);
640
+ removePendingMutations(this.storage, this.pendingMutationKey(collectionId), (pendingKeyHandle) => {
641
+ void this.keyStore.delete(pendingKeyHandle).catch(() => undefined);
642
+ });
640
643
  for (const transport of ["web_push", "fcm"]) {
641
644
  this.storage.removeItem(this.notificationKey(collectionId, transport));
642
645
  }
@@ -653,33 +656,6 @@ export class MdbaseConnectInternals {
653
656
  pendingMutationKey(collectionId) {
654
657
  return `${this.storagePrefix()}:pending-mutation:${collectionId}`;
655
658
  }
656
- removePendingMutations(collectionId) {
657
- const baseKey = this.pendingMutationKey(collectionId);
658
- const keys = [baseKey];
659
- for (let index = 0; index < this.storage.length; index += 1) {
660
- const key = this.storage.key(index);
661
- if (key?.startsWith(`${baseKey}:`))
662
- keys.push(key);
663
- }
664
- for (const key of keys) {
665
- const pending = parseStored(this.storage.getItem(key));
666
- if (pending?.keyHandle)
667
- void this.keyStore.delete(pending.keyHandle).catch(() => undefined);
668
- this.storage.removeItem(key);
669
- }
670
- }
671
- pendingMutationsUseKey(collectionId, keyHandle) {
672
- const baseKey = this.pendingMutationKey(collectionId);
673
- for (let index = 0; index < this.storage.length; index += 1) {
674
- const key = this.storage.key(index);
675
- if (key !== baseKey && !key?.startsWith(`${baseKey}:`))
676
- continue;
677
- const pending = parseStored(this.storage.getItem(key));
678
- if (pending?.keyHandle === keyHandle)
679
- return true;
680
- }
681
- return false;
682
- }
683
659
  notificationKey(collectionId, transport = "web_push") {
684
660
  return `${this.storagePrefix()}:notifications:${collectionId}:${transport}`;
685
661
  }
@@ -266,7 +266,9 @@ export class ConnectionTransport {
266
266
  throw error;
267
267
  }
268
268
  }
269
- if (body?.protocol_version !== OPERATION_TRANSPORT_PROTOCOL_VERSION
269
+ const expectedProtocol = attempt.pendingMutationRecord?.request?.protocol_version
270
+ ?? OPERATION_TRANSPORT_PROTOCOL_VERSION;
271
+ if (body?.protocol_version !== expectedProtocol
270
272
  || body?.request_id !== attempt.requestId) {
271
273
  if (attempt.pendingMutation)
272
274
  throw unknownMutationOutcome(attempt.requestId, new Error("Operation response protocol or request ID did not match."));
package/dist/crypto.js CHANGED
@@ -161,8 +161,14 @@ export async function encryptRelayRequest(store, handle, binding, operation, inp
161
161
  validateGrantEncryption(binding.encryption);
162
162
  const record = await requireAgreementKey(store, handle, binding.encryption.application_agreement_public_key);
163
163
  const counter = await store.nextCounter(handle);
164
- const metadata = { binding, requestId, operation, counter };
165
- const key = await deriveDirectionalKey(record.agreementPrivateKey, binding, "request");
164
+ const metadata = {
165
+ binding,
166
+ protocolVersion: OPERATION_TRANSPORT_PROTOCOL_VERSION,
167
+ requestId,
168
+ operation,
169
+ counter
170
+ };
171
+ const key = await deriveDirectionalKey(record.agreementPrivateKey, binding, metadata.protocolVersion, "request");
166
172
  const plaintext = new TextEncoder().encode(JSON.stringify(input ?? {}));
167
173
  const ciphertext = await crypto.subtle.encrypt({
168
174
  name: "AES-GCM",
@@ -183,11 +189,12 @@ export async function decryptRelayResponse(store, handle, binding, request, resp
183
189
  const record = await requireAgreementKey(store, handle, binding.encryption.application_agreement_public_key);
184
190
  const metadata = {
185
191
  binding,
192
+ protocolVersion: request.protocol_version,
186
193
  requestId: request.request_id,
187
194
  operation: request.operation,
188
195
  counter: request.counter
189
196
  };
190
- const key = await deriveDirectionalKey(record.agreementPrivateKey, binding, "response");
197
+ const key = await deriveDirectionalKey(record.agreementPrivateKey, binding, request.protocol_version, "response");
191
198
  try {
192
199
  const plaintext = await crypto.subtle.decrypt({
193
200
  name: "AES-GCM",
@@ -246,10 +253,10 @@ async function requireSigningKey(store, handle, expectedPublicKey) {
246
253
  }
247
254
  return record;
248
255
  }
249
- async function deriveDirectionalKey(privateKey, binding, direction) {
256
+ async function deriveDirectionalKey(privateKey, binding, protocolVersion, direction) {
250
257
  const shared = await deriveP256SharedSecret(privateKey, binding.encryption.connector_agreement_public_key);
251
258
  const material = await crypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
252
- const contextBytes = new TextEncoder().encode(context(binding));
259
+ const contextBytes = new TextEncoder().encode(context(binding, protocolVersion));
253
260
  const salt = await crypto.subtle.digest("SHA-256", contextBytes);
254
261
  return crypto.subtle.deriveKey({
255
262
  name: "HKDF",
@@ -268,11 +275,11 @@ export async function deriveP256SharedSecret(privateKey, peerPublicKey) {
268
275
  throw new RelayCryptoError("invalid_public_key", "The connector relay public key is invalid.");
269
276
  }
270
277
  }
271
- function context(binding) {
278
+ function context(binding, protocolVersion) {
272
279
  const value = binding.encryption;
273
280
  return [
274
281
  "mdbase-connect",
275
- OPERATION_TRANSPORT_PROTOCOL_VERSION,
282
+ protocolVersion,
276
283
  value.suite,
277
284
  binding.grantId,
278
285
  binding.applicationId,
@@ -283,12 +290,18 @@ function context(binding) {
283
290
  ].join("|");
284
291
  }
285
292
  function aad(metadata, direction) {
286
- return [context(metadata.binding), metadata.requestId, direction, metadata.operation, metadata.counter].join("|");
293
+ return [
294
+ context(metadata.binding, metadata.protocolVersion),
295
+ metadata.requestId,
296
+ direction,
297
+ metadata.operation,
298
+ metadata.counter
299
+ ].join("|");
287
300
  }
288
301
  function envelopeMetadata(metadata) {
289
302
  const encryption = metadata.binding.encryption;
290
303
  return {
291
- protocol_version: OPERATION_TRANSPORT_PROTOCOL_VERSION,
304
+ protocol_version: metadata.protocolVersion,
292
305
  suite: RELAY_ENCRYPTION_SUITE,
293
306
  request_id: metadata.requestId,
294
307
  grant_id: metadata.binding.grantId,
@@ -0,0 +1,4 @@
1
+ import type { CollectionOperation, OperationTransportProtocolVersion } from "@mdbase-dev/connect-protocol";
2
+ export declare function pendingRecoveryTransports(storage: Storage, storagePrefix: string, operations: readonly CollectionOperation[], collectionId?: string): OperationTransportProtocolVersion[];
3
+ export declare function pendingMutationsUseKey(storage: Storage, baseKey: string, keyHandle: string): boolean;
4
+ export declare function removePendingMutations(storage: Storage, baseKey: string, removeKey: (keyHandle: string) => void): void;
@@ -0,0 +1,48 @@
1
+ import { isSupportedOperationTransport, OPERATION_TRANSPORT_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
2
+ import { parseStored } from "./runtime-utils.js";
3
+ export function pendingRecoveryTransports(storage, storagePrefix, operations, collectionId) {
4
+ const prefix = `${storagePrefix}:pending-mutation:`;
5
+ const versions = new Set();
6
+ for (let index = 0; index < storage.length; index += 1) {
7
+ const key = storage.key(index);
8
+ if (!key?.startsWith(prefix))
9
+ continue;
10
+ const pending = parseStored(storage.getItem(key));
11
+ if (!pending
12
+ || !operations.includes(pending.operation)
13
+ || (collectionId && pending.collectionId !== collectionId))
14
+ continue;
15
+ const version = pending.envelope?.protocol_version ?? pending.request?.protocol_version;
16
+ if (version !== undefined
17
+ && version !== OPERATION_TRANSPORT_PROTOCOL_VERSION
18
+ && isSupportedOperationTransport(version)) {
19
+ versions.add(version);
20
+ }
21
+ }
22
+ return [...versions].sort((left, right) => right - left);
23
+ }
24
+ export function pendingMutationsUseKey(storage, baseKey, keyHandle) {
25
+ for (let index = 0; index < storage.length; index += 1) {
26
+ const key = storage.key(index);
27
+ if (key !== baseKey && !key?.startsWith(`${baseKey}:`))
28
+ continue;
29
+ const pending = parseStored(storage.getItem(key));
30
+ if (pending?.keyHandle === keyHandle)
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ export function removePendingMutations(storage, baseKey, removeKey) {
36
+ const keys = [baseKey];
37
+ for (let index = 0; index < storage.length; index += 1) {
38
+ const key = storage.key(index);
39
+ if (key?.startsWith(`${baseKey}:`))
40
+ keys.push(key);
41
+ }
42
+ for (const key of keys) {
43
+ const pending = parseStored(storage.getItem(key));
44
+ if (pending?.keyHandle)
45
+ removeKey(pending.keyHandle);
46
+ storage.removeItem(key);
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdbase-dev/connect",
3
- "version": "0.1.0-beta.56",
3
+ "version": "0.1.0-beta.57",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "README.md"
34
34
  ],
35
35
  "dependencies": {
36
- "@mdbase-dev/connect-protocol": "0.1.0-beta.56"
36
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.57"
37
37
  },
38
38
  "devDependencies": {
39
39
  "esbuild": "^0.28.1",