@mdbase-dev/connect 0.1.0-beta.108 → 0.1.0-beta.110

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-wpc0R7K2M2u/v8bIQcBgnveqMOK02ergpDhAolUQ2yEXNgyXH7U7pLkhlzyvmKZs
1
+ sha384-87GfQ8HHeR0B68yJLi01ycH5DHMGUf0jbXDsLhhpaYD8jpF/qvol4yQtP7hdPExh
package/dist/connect.js CHANGED
@@ -5,7 +5,6 @@ import { authorizationCallbackState } from "./authorization-url.js";
5
5
  import { randomBase64Url } from "./base64.js";
6
6
  import { ApplicationIdentityStoreError, IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, applicationIdentity, applicationInstallationId, signApplicationAuthorization } from "./application-identity.js";
7
7
  import { MdbaseConnection } from "./connection.js";
8
- import { addConnectionId, connectionIds, removeConnectionId } from "./connection-index.js";
9
8
  import { assertFreshV2AuthorizationSupport, authorizationAbort, declarationIdFromFamilyIdentity } from "./connect-authorization-helpers.js";
10
9
  import { IndexedDbGrantKeyStore, MemoryGrantKeyStore } from "./crypto.js";
11
10
  import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
@@ -13,7 +12,7 @@ import { prepareAuthorizationWindow, publishPopupAuthorizationCompletion, waitFo
13
12
  import { pendingRecoveryTransports, removePendingMutations } from "./pending-recovery-transports.js";
14
13
  import { GrantKeyLeaseRegistry } from "./grant-key-leases.js";
15
14
  import { resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
16
- import { MemoryStorage, apiError, applicationStorageOrigin, canonicalLoopbackUrl, collectionIdFromTokenKey, connectFetch, createPkce, defaultStorage, decodeJsonResponse, isOpaquePortableManifest, resolveManifestSource, oauthErrorCode, parseDeviceAuthorization, parseStored, stripTrailingSlash, validAuthorityTokenResponse } from "./runtime-utils.js";
15
+ import { MemoryStorage, addConnectionId, connectionIds, removeConnectionId, apiError, applicationStorageOrigin, canonicalLoopbackUrl, collectionIdFromTokenKey, connectFetch, createPkce, defaultStorage, decodeJsonResponse, isOpaquePortableManifest, resolveManifestSource, oauthErrorCode, parseDeviceAuthorization, parseStored, stripTrailingSlash, validAuthorityTokenResponse } from "./runtime-utils.js";
17
16
  import { storedTokenFromResponse } from "./token-response.js";
18
17
  import { deleteUnusedGrantKey, planRetiredGrantKeys, storedTokenKeyHandles } from "./retired-grant-keys.js";
19
18
  import { SharedOperationMap } from "./shared-operation.js";
@@ -1,5 +1,12 @@
1
1
  import type { CollectionOperation, FileCapability, GrantScope, JsonObject, SyncChangesPage, SyncMutation, SyncMutationReceipt, SyncSession, SyncSnapshotPage } from "@mdbase-dev/connect-protocol";
2
2
  import type { ConnectRequestOptions } from "./operation-types.js";
3
+ export interface MdbaseDeviceAuthorization {
4
+ userCode: string;
5
+ verificationUri: string;
6
+ verificationUriComplete: string;
7
+ expiresAt: number;
8
+ intervalSeconds: number;
9
+ }
3
10
  export type MdbaseConnectionRoute = "remote" | "direct" | "relay";
4
11
  export type DirectAccessStatus = "disabled" | "permission_required" | "checking" | "available" | "unavailable" | "denied";
5
12
  export interface MdbaseConnectionInfo {
@@ -8,7 +8,7 @@ import { type Application } from "./internal-types.js";
8
8
  import type { MdbaseNativeNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNotificationRegistrationOptions } from "./notifications.js";
9
9
  import type { ChangesInput, CollectionChangesPage, CollectionDescription, CollectionSetupApplyResult, CollectionSetupAssessment, ApplyCollectionSetupInput, ApplyTypePackInput, AssessCollectionSetupInput, AssessTypePackInput, CreateInput, CreateTypeInput, CreateViewSourceInput, DeleteInput, DeletePreflightResult, DeleteProgressOptions, DeleteResult, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, PendingMutation, QueryAllOptions, QueryInput, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadTypeInput, ReadViewSourceInput, RenameInput, RenamePreflightResult, RenameProgressOptions, RenameResult, RecordDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateInput, UpdateTypeInput, UpdateViewSourceInput, MdbaseWatchSubscription, WatchInput } from "./operation-types.js";
10
10
  import { type AuthorizationProblemCode, type CollectionChangesProblemCode, type CollectionDescriptionProblemCode, type CollectionMutationProblemCode, type CollectionQueryProblemCode, type CollectionReadProblemCode, type CollectionTypeProblemCode, type ConnectOutcome, type DirectAccessProblemCode, type NotificationProblemCode, type RegistrationProblemCode } from "./outcomes.js";
11
- import type { MdbaseDeviceAuthorization } from "./authorization-types.js";
11
+ import type { MdbaseDeviceAuthorization } from "./connection-types.js";
12
12
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
13
13
  export type MdbaseAuthorizationTarget = {
14
14
  kind: "choose";
@@ -174,7 +174,11 @@ export function requirePreparedPart(part, transferId, partIndex, offset, content
174
174
  || part.part_index !== partIndex
175
175
  || part.offset !== offset
176
176
  || part.content_length !== contentLength
177
+ || typeof part.method !== "string"
177
178
  || part.method.toUpperCase() !== method
179
+ || !part.headers || typeof part.headers !== "object" || Array.isArray(part.headers)
180
+ || Object.values(part.headers).some((value) => typeof value !== "string")
181
+ || typeof part.expires_at !== "string" || !Number.isFinite(Date.parse(part.expires_at))
178
182
  || url.protocol !== "https:"
179
183
  && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname))
180
184
  || url.username
package/dist/files.js CHANGED
@@ -144,21 +144,36 @@ export class MdbaseFileClient {
144
144
  const partCount = session.strategy.kind === "object_put"
145
145
  ? 1
146
146
  : Math.ceil(size / partSize);
147
- const status = await this.request("GET", `transfers/${encodeURIComponent(transferId)}`, undefined, options.signal);
148
- requireTransferStatus(status, session);
149
- if (status.state !== "open" && status.state !== "committed") {
150
- throw connectError("invalid_operation_response", `The authority cannot resume a ${status.state} file transfer.`);
147
+ // Open replays its ORIGINAL receipt, not fresh progress. A still-valid
148
+ // create-only capability can nevertheless be reused safely: PUT either
149
+ // creates the candidate or returns 412, then commit verifies/replays it.
150
+ let bootstrap = session.prepared_upload_part;
151
+ if (bootstrap !== undefined) {
152
+ requirePreparedPart(bootstrap, transferId, 0, 0, size, "PUT");
153
+ if (session.strategy.kind !== "object_put"
154
+ || browserObjectHeaders(bootstrap.headers).get("if-none-match") !== "*") {
155
+ throw connectError("invalid_operation_response", "Upload bootstrap must be a create-only single PUT.");
156
+ }
157
+ if (Date.parse(bootstrap.expires_at) <= Date.now() + 60_000)
158
+ bootstrap = undefined;
159
+ }
160
+ const status = bootstrap ? undefined : await this.request("GET", `transfers/${encodeURIComponent(transferId)}`, undefined, options.signal);
161
+ if (status) {
162
+ requireTransferStatus(status, session);
163
+ if (status.state !== "open" && status.state !== "committed") {
164
+ throw connectError("invalid_operation_response", `The authority cannot resume a ${status.state} file transfer.`);
165
+ }
151
166
  }
152
167
  prepareSource?.(partSize);
153
- if (!framed && status.received.length === partCount) {
168
+ if (!framed && status?.received.length === partCount) {
154
169
  const replay = await this.tryReplayUploadCommit(transferId, session.strategy.kind === "object_multipart" ? status.uploaded_parts : undefined, options.signal);
155
170
  if (replay) {
156
171
  committed = true;
157
172
  return clientFileDescriptor(replay.file);
158
173
  }
159
174
  }
160
- const received = new Set(status.received);
161
- const uploadedParts = new Map(status.uploaded_parts.map((part) => [part.part_number - 1, part]));
175
+ const received = new Set(status?.received ?? []);
176
+ const uploadedParts = new Map((status?.uploaded_parts ?? []).map((part) => [part.part_number - 1, part]));
162
177
  let transferredBytes = [...received].reduce((total, index) => total + chunkLength(size, partSize, index), 0);
163
178
  const parts = await mapConcurrent(partCount, concurrency, async (partIndex) => {
164
179
  const offset = partIndex * partSize;
@@ -171,7 +186,7 @@ export class MdbaseFileClient {
171
186
  return uploadedParts.get(partIndex) ?? null;
172
187
  const uploaded = framed
173
188
  ? await retryChunk(async () => this.framed.uploadChunk(session, partIndex, await uploadBodyBytes(part), options.signal), options.signal).then(() => null)
174
- : await this.uploadPart(transferId, partIndex, offset, part, length, session.strategy.kind === "object_multipart", options.signal);
189
+ : await this.uploadPart(transferId, partIndex, offset, part, length, session.strategy.kind === "object_multipart", options.signal, bootstrap ? { session, part: bootstrap } : undefined);
175
190
  transferredBytes += length;
176
191
  options.onProgress?.({
177
192
  phase: "uploading",
@@ -472,11 +487,24 @@ export class MdbaseFileClient {
472
487
  revision: receipt.revision
473
488
  };
474
489
  }
475
- async uploadPart(transferId, partIndex, offset, body, contentLength, requireEtag, signal) {
490
+ async uploadPart(transferId, partIndex, offset, body, contentLength, requireEtag, signal, bootstrap) {
476
491
  for (let attempt = 1; attempt <= MAX_OBJECT_ATTEMPTS; attempt += 1) {
477
492
  throwIfAborted(signal);
478
493
  try {
479
- const prepared = await this.request("POST", `uploads/${encodeURIComponent(transferId)}/parts`, {
494
+ const reuseBootstrap = attempt === 1 && bootstrap
495
+ && Date.parse(bootstrap.part.expires_at) > Date.now() + 60_000;
496
+ if (bootstrap && !reuseBootstrap) {
497
+ // A slow byte source, clock change, or ambiguous PUT can outlive the
498
+ // capability. Recover through fresh progress, never a stale receipt.
499
+ const status = await this.request("GET", `transfers/${encodeURIComponent(transferId)}`, undefined, signal);
500
+ requireTransferStatus(status, bootstrap.session);
501
+ if (status.state !== "open" && status.state !== "committed") {
502
+ throw connectError("invalid_operation_response", `The authority cannot resume a ${status.state} file transfer.`);
503
+ }
504
+ if (status.received.includes(partIndex))
505
+ return { part_number: partIndex + 1, etag: "" };
506
+ }
507
+ const prepared = reuseBootstrap ? bootstrap.part : await this.request("POST", `uploads/${encodeURIComponent(transferId)}/parts`, {
480
508
  protocol_version: FILE_PROTOCOL_VERSION,
481
509
  type: "prepare_file_upload_part",
482
510
  transfer_id: transferId,
@@ -484,13 +512,21 @@ export class MdbaseFileClient {
484
512
  content_length: contentLength
485
513
  }, signal);
486
514
  requirePreparedPart(prepared, transferId, partIndex, offset, contentLength, "PUT");
515
+ const requestHeaders = browserObjectHeaders(prepared.headers);
487
516
  const response = await fetch(prepared.url, {
488
517
  method: prepared.method,
489
- headers: browserObjectHeaders(prepared.headers),
518
+ headers: requestHeaders,
490
519
  body,
491
520
  redirect: "error",
492
521
  signal
493
522
  });
523
+ // A lost successful create-only PUT can be retried after the object
524
+ // already exists. 412 is not proof of success: commit must still verify
525
+ // the authoritative object's digest/length and publish its receipt.
526
+ const existingCandidate = response.status === 412 && !requireEtag
527
+ && requestHeaders.get("if-none-match") === "*";
528
+ if (existingCandidate)
529
+ return { part_number: partIndex + 1, etag: "" };
494
530
  if (!response.ok)
495
531
  throw new Error(`Object upload failed with HTTP ${response.status}.`);
496
532
  const etag = response.headers.get("etag");
package/dist/index.d.ts CHANGED
@@ -1,11 +1,10 @@
1
- export type { MdbaseDeviceAuthorization } from "./authorization-types.js";
2
1
  export { MdbaseApplicationSession, MdbaseMemoryVerificationStore } from "./application-session.js";
3
2
  export type { MdbaseApplicationSessionOptions, MdbaseApplicationVerificationStore, MdbaseDefinitionUpdate, MdbaseCollectionSetupUpdate, MdbaseApplicationSessionSnapshot } from "./application-session.js";
4
3
  export type { MdbaseConnectOptions, MdbaseConnectTimeouts, MdbaseFrontmatter } from "./connect-options.js";
5
4
  export { MdbaseConnect } from "./mdbase-connect.js";
6
5
  export { MdbaseConnection } from "./connection.js";
7
6
  export type { MdbaseAuthorizationTarget, MdbaseAuthorizeOptions, MdbaseConnectionAuthorizeOptions, MdbaseConnectEnvironment, MdbaseAuthorizationResult, MdbaseAuthorizationOutcome } from "./connection.js";
8
- export type { MdbaseConnectionRoute, DirectAccessStatus, MdbaseConnectionInfo, MdbaseAuthorizationCapabilities, MdbaseSyncTransport, MdbaseSyncConnection } from "./connection-types.js";
7
+ export type { MdbaseConnectionRoute, DirectAccessStatus, MdbaseConnectionInfo, MdbaseDeviceAuthorization, MdbaseAuthorizationCapabilities, MdbaseSyncTransport, MdbaseSyncConnection } from "./connection-types.js";
9
8
  export { effectiveCapabilities } from "./capabilities.js";
10
9
  export type { MdbaseStructuredReadiness } from "./structured-readiness.js";
11
10
  export type { MdbaseCapabilityState, MdbaseCapabilityEvidence, MdbaseEffectiveCapability, MdbaseEffectiveCapabilities } from "./capabilities.js";
@@ -2,7 +2,10 @@ import type { FileCapability, GrantEncryption, GrantScope } from "@mdbase-dev/co
2
2
  import type { MdbaseApplicationManifest as MdbaseAppManifest } from "./application-contract.js";
3
3
  import { MdbaseConnectError } from "./errors.js";
4
4
  import type { StoredToken } from "./internal-types.js";
5
- import type { MdbaseDeviceAuthorization } from "./authorization-types.js";
5
+ import type { MdbaseDeviceAuthorization } from "./connection-types.js";
6
+ export declare function connectionIds(storage: Storage, storagePrefix: string): string[];
7
+ export declare function addConnectionId(storage: Storage, storagePrefix: string, collectionId: string): void;
8
+ export declare function removeConnectionId(storage: Storage, storagePrefix: string, collectionId: string): void;
6
9
  /** Resolve bundled declarations and their stable storage identity without loading URLs. */
7
10
  export declare function resolveManifestSource(input?: MdbaseAppManifest | string | URL, redirect?: string | URL): {
8
11
  manifest: MdbaseAppManifest | string;
@@ -2,6 +2,24 @@ import { validateGrantEncryption } from "./crypto.js";
2
2
  import { connectError, serverConnectError } from "./errors.js";
3
3
  import { canonicalJson } from "./operation-helpers.js";
4
4
  import { bytesToBase64Url, randomBase64Url } from "./base64.js";
5
+ export function connectionIds(storage, storagePrefix) {
6
+ const index = parseStored(storage.getItem(`${storagePrefix}:connections`));
7
+ return index?.version === 1 && Array.isArray(index.collectionIds)
8
+ && index.collectionIds.every((collectionId) => typeof collectionId === "string")
9
+ ? index.collectionIds
10
+ : [];
11
+ }
12
+ export function addConnectionId(storage, storagePrefix, collectionId) {
13
+ writeConnectionIds(storage, storagePrefix, [...connectionIds(storage, storagePrefix), collectionId]);
14
+ }
15
+ export function removeConnectionId(storage, storagePrefix, collectionId) {
16
+ writeConnectionIds(storage, storagePrefix, connectionIds(storage, storagePrefix).filter((id) => id !== collectionId));
17
+ }
18
+ function writeConnectionIds(storage, storagePrefix, ids) {
19
+ storage.setItem(`${storagePrefix}:connections`, JSON.stringify({
20
+ version: 1, collectionIds: [...new Set(ids)]
21
+ }));
22
+ }
5
23
  /** Resolve bundled declarations and their stable storage identity without loading URLs. */
6
24
  export function resolveManifestSource(input, redirect) {
7
25
  const manifest = input instanceof URL ? input.href : input ?? defaultManifestSource();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdbase-dev/connect",
3
- "version": "0.1.0-beta.108",
3
+ "version": "0.1.0-beta.110",
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.108"
36
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.110"
37
37
  },
38
38
  "devDependencies": {
39
39
  "esbuild": "^0.28.1",
@@ -1,7 +0,0 @@
1
- export interface MdbaseDeviceAuthorization {
2
- userCode: string;
3
- verificationUri: string;
4
- verificationUriComplete: string;
5
- expiresAt: number;
6
- intervalSeconds: number;
7
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,3 +0,0 @@
1
- export declare function connectionIds(storage: Storage, storagePrefix: string): string[];
2
- export declare function addConnectionId(storage: Storage, storagePrefix: string, collectionId: string): void;
3
- export declare function removeConnectionId(storage: Storage, storagePrefix: string, collectionId: string): void;
@@ -1,20 +0,0 @@
1
- import { parseStored } from "./runtime-utils.js";
2
- export function connectionIds(storage, storagePrefix) {
3
- const index = parseStored(storage.getItem(`${storagePrefix}:connections`));
4
- return index?.version === 1 && Array.isArray(index.collectionIds)
5
- && index.collectionIds.every((collectionId) => typeof collectionId === "string")
6
- ? index.collectionIds
7
- : [];
8
- }
9
- export function addConnectionId(storage, storagePrefix, collectionId) {
10
- writeConnectionIds(storage, storagePrefix, [...connectionIds(storage, storagePrefix), collectionId]);
11
- }
12
- export function removeConnectionId(storage, storagePrefix, collectionId) {
13
- writeConnectionIds(storage, storagePrefix, connectionIds(storage, storagePrefix).filter((id) => id !== collectionId));
14
- }
15
- function writeConnectionIds(storage, storagePrefix, ids) {
16
- storage.setItem(`${storagePrefix}:connections`, JSON.stringify({
17
- version: 1,
18
- collectionIds: [...new Set(ids)]
19
- }));
20
- }