@mdbase-dev/connect 0.1.0-beta.108 → 0.1.0-beta.111
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.
- package/README.md +14 -0
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +3 -3
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/connect.js +1 -2
- package/dist/connection-transport.js +11 -5
- package/dist/connection-types.d.ts +7 -0
- package/dist/connection.d.ts +1 -1
- package/dist/direct-access.js +13 -8
- package/dist/file-transfer-internals.js +4 -0
- package/dist/files.js +47 -11
- package/dist/index.d.ts +1 -2
- package/dist/local-file-transport.js +4 -4
- package/dist/operation-helpers.d.ts +1 -1
- package/dist/operation-helpers.js +7 -2
- package/dist/runtime-utils.d.ts +4 -1
- package/dist/runtime-utils.js +18 -0
- package/package.json +2 -2
- package/dist/authorization-types.d.ts +0 -7
- package/dist/authorization-types.js +0 -1
- package/dist/connection-index.d.ts +0 -3
- package/dist/connection-index.js +0 -20
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384
|
|
1
|
+
sha384-/62JQssTDGRQGVQwcO5KLWO7BOqrDVxB0ne307QlEFVO3/MzYMBJfjmbz/OdCRWP
|
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";
|
|
@@ -5,7 +5,7 @@ import { ConnectionFileTransport } from "./connection-file-transport.js";
|
|
|
5
5
|
import { authorityProofHeaders } from "./authority-proof.js";
|
|
6
6
|
import { performAuthorizationRefresh } from "./authorization-refresh.js";
|
|
7
7
|
import { PendingMutationStore } from "./pending-mutation-store.js";
|
|
8
|
-
import {
|
|
8
|
+
import { directFallbackResponse, encryptedOperationError, fetchOperationRequest, isMutation, localNetworkPermission, loopbackRequest, operationFingerprint, operationTransportError, sameAuthorization, throwIfCancelled, unknownMutationOutcome, withOperationDeadline } from "./operation-helpers.js";
|
|
9
9
|
import { apiError, decodeJsonResponse, parseStored, } from "./runtime-utils.js";
|
|
10
10
|
import { readStoredToken } from "./stored-token.js";
|
|
11
11
|
import { requestOptionsWithinBudget, withCooperativeRequestBudget, withRequestBudget } from "./request-budget.js";
|
|
@@ -467,7 +467,7 @@ export class ConnectionTransport {
|
|
|
467
467
|
body: JSON.stringify(encryptedRequest),
|
|
468
468
|
signal: options.signal
|
|
469
469
|
}));
|
|
470
|
-
if (!
|
|
470
|
+
if (!await directFallbackResponse(response)
|
|
471
471
|
|| await isExplicitConnectorBusyResponse(response)) {
|
|
472
472
|
if (response.ok) {
|
|
473
473
|
this.markDirectAvailable();
|
|
@@ -603,9 +603,15 @@ export class ConnectionTransport {
|
|
|
603
603
|
}
|
|
604
604
|
async probeDirectAccess(signal) {
|
|
605
605
|
this.setDirectStatus("checking");
|
|
606
|
-
|
|
607
|
-
this.
|
|
608
|
-
|
|
606
|
+
try {
|
|
607
|
+
if (await probeLoopbackAccess(this.loopbackUrl, OPERATION_TRANSPORT_PROTOCOL_VERSION, signal)) {
|
|
608
|
+
this.markDirectAvailable();
|
|
609
|
+
return "available";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
this.markDirectUnavailable();
|
|
614
|
+
throw error;
|
|
609
615
|
}
|
|
610
616
|
if ((await localNetworkPermission()) === "denied")
|
|
611
617
|
return this.setDirectStatus("denied");
|
|
@@ -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 {
|
package/dist/connection.d.ts
CHANGED
|
@@ -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 "./
|
|
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";
|
package/dist/direct-access.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loopbackRequest } from "./operation-helpers.js";
|
|
2
|
-
import { decodeJsonResponse } from "./runtime-utils.js";
|
|
2
|
+
import { decodeJsonResponse, portableApplicationOrigin } from "./runtime-utils.js";
|
|
3
|
+
import { serverConnectError } from "./errors.js";
|
|
3
4
|
export function tokenSupportsDirectAccess(token, mode) {
|
|
4
5
|
if (!token || token.authority || !token.encryption || !token.grantId || !token.keyHandle) {
|
|
5
6
|
return false;
|
|
@@ -8,22 +9,26 @@ export function tokenSupportsDirectAccess(token, mode) {
|
|
|
8
9
|
return false;
|
|
9
10
|
return typeof location === "undefined"
|
|
10
11
|
|| !token.applicationOrigin
|
|
11
|
-
|| token.applicationOrigin ===
|
|
12
|
+
|| token.applicationOrigin === portableApplicationOrigin();
|
|
12
13
|
}
|
|
13
14
|
export async function probeLoopbackAccess(loopbackUrl, expectedOperationProtocol, signal) {
|
|
15
|
+
let response;
|
|
14
16
|
try {
|
|
15
|
-
|
|
17
|
+
response = await fetch(`${loopbackUrl}/v1/ready`, loopbackRequest({
|
|
16
18
|
method: "GET",
|
|
17
19
|
cache: "no-store",
|
|
18
20
|
signal
|
|
19
21
|
}));
|
|
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
22
|
}
|
|
26
23
|
catch {
|
|
27
24
|
return false;
|
|
28
25
|
}
|
|
26
|
+
if (response.status === 403) {
|
|
27
|
+
throw serverConnectError("operation_failed", "The connector rejected direct access from this application. Relay access remains available.", { status: response.status });
|
|
28
|
+
}
|
|
29
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "The connector returned an invalid readiness response.").catch(() => null);
|
|
30
|
+
return response.ok
|
|
31
|
+
&& body?.service === "mdbase-connect"
|
|
32
|
+
&& body?.loopback_protocol_version === 1
|
|
33
|
+
&& body?.operation_transport_protocol_version === expectedOperationProtocol;
|
|
29
34
|
}
|
|
@@ -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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
161
|
-
const uploadedParts = new Map(status
|
|
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
|
|
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:
|
|
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";
|
|
@@ -3,7 +3,7 @@ import { decryptRelayResponse, encryptRelayRequest } from "./crypto.js";
|
|
|
3
3
|
import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
|
|
4
4
|
import { GrantFileTransferCipher } from "./file-crypto.js";
|
|
5
5
|
import { GrantKeyLeaseSet, retainCurrentGrantToken } from "./grant-key-leases.js";
|
|
6
|
-
import {
|
|
6
|
+
import { directFallbackResponse, loopbackRequest } from "./operation-helpers.js";
|
|
7
7
|
import { apiError, decodeJsonResponse } from "./runtime-utils.js";
|
|
8
8
|
import { isConnectorBusy, isExplicitConnectorBusyResponse, MAX_CONNECTOR_BUSY_RETRIES, waitForConnectorAvailability } from "./transient-retry.js";
|
|
9
9
|
/** Keeps grant-bound file crypto and loopback framing out of the operation transport. */
|
|
@@ -42,7 +42,7 @@ export class LocalFileTransport {
|
|
|
42
42
|
throw error;
|
|
43
43
|
this.options.onDirectUnavailable();
|
|
44
44
|
}
|
|
45
|
-
if (response && (!
|
|
45
|
+
if (response && (!await directFallbackResponse(response)
|
|
46
46
|
|| await isExplicitConnectorBusyResponse(response))) {
|
|
47
47
|
if (response.ok)
|
|
48
48
|
this.options.onDirectAvailable();
|
|
@@ -134,7 +134,7 @@ export class LocalFileTransport {
|
|
|
134
134
|
throw error;
|
|
135
135
|
this.options.onDirectUnavailable();
|
|
136
136
|
}
|
|
137
|
-
if (response && (!
|
|
137
|
+
if (response && (!await directFallbackResponse(response)
|
|
138
138
|
|| await isExplicitConnectorBusyResponse(response))) {
|
|
139
139
|
if (response.ok)
|
|
140
140
|
this.options.onDirectAvailable();
|
|
@@ -180,7 +180,7 @@ export class LocalFileTransport {
|
|
|
180
180
|
throw error;
|
|
181
181
|
this.options.onDirectUnavailable();
|
|
182
182
|
}
|
|
183
|
-
if (response && (!
|
|
183
|
+
if (response && (!await directFallbackResponse(response)
|
|
184
184
|
|| await isExplicitConnectorBusyResponse(response))) {
|
|
185
185
|
if (response.ok)
|
|
186
186
|
this.options.onDirectAvailable();
|
|
@@ -7,7 +7,7 @@ export type LoopbackRequestInit = RequestInit & {
|
|
|
7
7
|
};
|
|
8
8
|
export declare function loopbackRequest(init: RequestInit): LoopbackRequestInit;
|
|
9
9
|
export declare function localNetworkPermission(): Promise<PermissionState | null>;
|
|
10
|
-
export declare function
|
|
10
|
+
export declare function directFallbackResponse(response: Response): Promise<boolean>;
|
|
11
11
|
export declare function isMutation(operation: CollectionOperation, input?: unknown): boolean;
|
|
12
12
|
export declare function withOperationDeadline(request: EncryptedRelayOperationRequest, deadlineUnixMs?: number): EncryptedRelayOperationRequest;
|
|
13
13
|
export declare function uniqueOperations(operations: CollectionOperation[]): CollectionOperation[];
|
|
@@ -16,8 +16,13 @@ export async function localNetworkPermission() {
|
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
export function
|
|
20
|
-
|
|
19
|
+
export async function directFallbackResponse(response) {
|
|
20
|
+
const { status } = response;
|
|
21
|
+
if (status === 404 || status === 405 || status === 426 || status >= 500)
|
|
22
|
+
return true;
|
|
23
|
+
// Loopback rejects browser requests before execution with an empty 403.
|
|
24
|
+
// Structured grant/policy rejections must remain terminal, not route failures.
|
|
25
|
+
return status === 403 && (await response.clone().text()).trim() === "";
|
|
21
26
|
}
|
|
22
27
|
export function isMutation(operation, input) {
|
|
23
28
|
return isMutatingOperation(operation, input);
|
package/dist/runtime-utils.d.ts
CHANGED
|
@@ -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 "./
|
|
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;
|
package/dist/runtime-utils.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.0-beta.111",
|
|
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.
|
|
36
|
+
"@mdbase-dev/connect-protocol": "0.1.0-beta.111"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"esbuild": "^0.28.1",
|
|
@@ -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;
|
package/dist/connection-index.js
DELETED
|
@@ -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
|
-
}
|