@mdbase-dev/connect 0.1.0-beta.21 → 0.1.0-beta.23
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 +108 -38
- package/dist/authorization-url.d.ts +2 -1
- package/dist/authorization-url.js +5 -7
- 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/collection-client.d.ts +34 -31
- package/dist/collection-client.js +107 -61
- package/dist/connect.d.ts +4 -3
- package/dist/connect.js +53 -38
- package/dist/connection-file-transport.d.ts +31 -0
- package/dist/connection-file-transport.js +42 -0
- package/dist/connection-notifications.js +21 -21
- package/dist/connection-transport.d.ts +2 -0
- package/dist/connection-transport.js +95 -37
- package/dist/connection-types.d.ts +2 -1
- package/dist/connection.d.ts +49 -45
- package/dist/connection.js +73 -33
- package/dist/crypto.d.ts +5 -6
- package/dist/crypto.js +13 -13
- package/dist/errors.d.ts +38 -24
- package/dist/errors.js +130 -71
- package/dist/file-crypto.d.ts +25 -0
- package/dist/file-crypto.js +132 -0
- package/dist/file-sha256.d.ts +12 -0
- package/dist/file-sha256.js +143 -0
- package/dist/file-stream-source.d.ts +17 -0
- package/dist/file-stream-source.js +157 -0
- package/dist/files.d.ts +91 -0
- package/dist/files.js +644 -0
- package/dist/hosted-file-request.d.ts +6 -0
- package/dist/hosted-file-request.js +63 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/internal-types.d.ts +3 -1
- package/dist/local-file-transport.d.ts +30 -0
- package/dist/local-file-transport.js +367 -0
- package/dist/notifications.js +5 -5
- package/dist/operation-helpers.d.ts +2 -2
- package/dist/operation-helpers.js +22 -18
- package/dist/operation-types.d.ts +3 -4
- package/dist/outcomes.d.ts +51 -0
- package/dist/outcomes.js +168 -0
- package/dist/runtime-utils.d.ts +6 -1
- package/dist/runtime-utils.js +90 -11
- package/dist/selection.js +3 -3
- package/dist/session.d.ts +9 -8
- package/dist/session.js +31 -20
- package/package.json +2 -2
package/dist/connect.js
CHANGED
|
@@ -3,21 +3,22 @@ import { abortableDelay } from "./async.js";
|
|
|
3
3
|
import { randomBase64Url } from "./base64.js";
|
|
4
4
|
import { MdbaseConnection } from "./connection.js";
|
|
5
5
|
import { IndexedDbGrantKeyStore, MemoryGrantKeyStore, validateGrantEncryption } from "./crypto.js";
|
|
6
|
-
import {
|
|
6
|
+
import { connectError, serverConnectError } from "./errors.js";
|
|
7
7
|
import { DEFAULT_OPERATIONS } from "./internal-types.js";
|
|
8
8
|
import { uniqueOperations } from "./operation-helpers.js";
|
|
9
|
+
import { AUTHORIZATION_PROBLEM_CODES, REGISTRATION_PROBLEM_CODES, captureConnectOutcome } from "./outcomes.js";
|
|
9
10
|
import { MdbaseSession } from "./session.js";
|
|
10
|
-
import { MemoryStorage, apiError, canonicalLoopbackUrl, createPkce, defaultCallbackUrl, defaultManifestSource, defaultRedirectUri, defaultStorage, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseGrantScope, parseStored, stripTrailingSlash, validAuthorityTokenResponse } from "./runtime-utils.js";
|
|
11
|
+
import { MemoryStorage, apiError, canonicalLoopbackUrl, connectFetch, createPkce, defaultCallbackUrl, defaultManifestSource, defaultRedirectUri, defaultStorage, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseGrantScope, parseStored, stripTrailingSlash, validFileCapability, validAuthorityTokenResponse } from "./runtime-utils.js";
|
|
11
12
|
export class MdbaseConnect {
|
|
12
13
|
internals;
|
|
13
14
|
constructor(options) {
|
|
14
15
|
this.internals = new MdbaseConnectInternals(options);
|
|
15
16
|
}
|
|
16
17
|
register() {
|
|
17
|
-
return this.internals.register();
|
|
18
|
+
return captureConnectOutcome(() => this.internals.register(), REGISTRATION_PROBLEM_CODES);
|
|
18
19
|
}
|
|
19
20
|
authorize(options = {}) {
|
|
20
|
-
return this.internals.authorize(options);
|
|
21
|
+
return captureConnectOutcome(() => this.internals.authorize(options), AUTHORIZATION_PROBLEM_CODES);
|
|
21
22
|
}
|
|
22
23
|
createSession(options) {
|
|
23
24
|
return new MdbaseSession(this, options);
|
|
@@ -25,8 +26,8 @@ export class MdbaseConnect {
|
|
|
25
26
|
environment() {
|
|
26
27
|
return this.internals.environment();
|
|
27
28
|
}
|
|
28
|
-
completeAuthorization(callbackUrl
|
|
29
|
-
return this.internals.completeAuthorization(callbackUrl);
|
|
29
|
+
completeAuthorization(callbackUrl) {
|
|
30
|
+
return captureConnectOutcome(() => this.internals.completeAuthorization(callbackUrl ?? defaultCallbackUrl()), AUTHORIZATION_PROBLEM_CODES);
|
|
30
31
|
}
|
|
31
32
|
connections() {
|
|
32
33
|
return this.internals.connections();
|
|
@@ -109,11 +110,11 @@ class MdbaseConnectInternals {
|
|
|
109
110
|
async register() {
|
|
110
111
|
if (this.application)
|
|
111
112
|
return this.application;
|
|
112
|
-
const response = await
|
|
113
|
+
const response = await connectFetch(`${this.serverUrl}/v1/apps/register`, {
|
|
113
114
|
method: "POST",
|
|
114
115
|
headers: { "content-type": "application/json" },
|
|
115
116
|
body: JSON.stringify({ manifest: await this.loadManifest() })
|
|
116
|
-
});
|
|
117
|
+
}, "temporarily_unavailable", "Application registration is temporarily unavailable.");
|
|
117
118
|
const body = await response.json();
|
|
118
119
|
if (!response.ok)
|
|
119
120
|
throw apiError(body, "discovery_failed", "Application discovery failed.", response.status);
|
|
@@ -131,21 +132,21 @@ class MdbaseConnectInternals {
|
|
|
131
132
|
});
|
|
132
133
|
}
|
|
133
134
|
catch (cause) {
|
|
134
|
-
throw
|
|
135
|
+
throw connectError("manifest_load_failed", "The bundled application declaration could not be loaded.", { cause });
|
|
135
136
|
}
|
|
136
137
|
if (!response.ok) {
|
|
137
|
-
throw
|
|
138
|
+
throw connectError("manifest_load_failed", `The bundled application declaration returned HTTP ${response.status}.`, { status: response.status });
|
|
138
139
|
}
|
|
139
140
|
try {
|
|
140
141
|
return await response.json();
|
|
141
142
|
}
|
|
142
143
|
catch (cause) {
|
|
143
|
-
throw
|
|
144
|
+
throw connectError("invalid_application_manifest", "The bundled application declaration is not valid JSON.", { cause });
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
async authorize(options = {}) {
|
|
147
148
|
if (typeof location === "undefined" && !this.navigate && !options.openVerification) {
|
|
148
|
-
throw
|
|
149
|
+
throw connectError("browser_required", "Authorization navigation requires a browser environment.");
|
|
149
150
|
}
|
|
150
151
|
const portableDeclared = typeof this.manifest !== "string"
|
|
151
152
|
&& this.manifest.distribution === "portable";
|
|
@@ -213,7 +214,7 @@ class MdbaseConnectInternals {
|
|
|
213
214
|
async authorizePortable(application, options, popup) {
|
|
214
215
|
if (this.relayEncryption !== "required") {
|
|
215
216
|
popup?.close();
|
|
216
|
-
throw
|
|
217
|
+
throw connectError("encryption_required", "Downloaded applications require encrypted relay authorization.");
|
|
217
218
|
}
|
|
218
219
|
const { verifier, challenge } = await createPkce();
|
|
219
220
|
const keyHandle = `grant:${application.id}:${randomBase64Url(24)}`;
|
|
@@ -242,7 +243,7 @@ class MdbaseConnectInternals {
|
|
|
242
243
|
catch (cause) {
|
|
243
244
|
popup?.close();
|
|
244
245
|
await this.keyStore.delete(keyHandle);
|
|
245
|
-
throw
|
|
246
|
+
throw connectError("device_authorization_failed", "Downloaded application authorization could not be started.", { cause });
|
|
246
247
|
}
|
|
247
248
|
if (!response.ok) {
|
|
248
249
|
popup?.close();
|
|
@@ -272,14 +273,22 @@ class MdbaseConnectInternals {
|
|
|
272
273
|
}
|
|
273
274
|
else {
|
|
274
275
|
await this.keyStore.delete(keyHandle);
|
|
275
|
-
throw
|
|
276
|
+
throw connectError("approval_window_blocked", "The approval window was blocked. Show the provided verification link and code, then try again.", {
|
|
277
|
+
details: {
|
|
278
|
+
user_code: authorization.userCode,
|
|
279
|
+
verification_uri: authorization.verificationUri,
|
|
280
|
+
verification_uri_complete: authorization.verificationUriComplete,
|
|
281
|
+
expires_at: authorization.expiresAt,
|
|
282
|
+
interval_seconds: authorization.intervalSeconds
|
|
283
|
+
}
|
|
284
|
+
});
|
|
276
285
|
}
|
|
277
286
|
let intervalSeconds = authorization.intervalSeconds;
|
|
278
287
|
try {
|
|
279
288
|
while (Date.now() < authorization.expiresAt) {
|
|
280
289
|
await abortableDelay(intervalSeconds * 1_000, options.signal);
|
|
281
290
|
if (options.signal?.aborted) {
|
|
282
|
-
throw
|
|
291
|
+
throw connectError("authorization_cancelled", "Downloaded application authorization was cancelled.");
|
|
283
292
|
}
|
|
284
293
|
let tokenResponse;
|
|
285
294
|
try {
|
|
@@ -297,7 +306,7 @@ class MdbaseConnectInternals {
|
|
|
297
306
|
}
|
|
298
307
|
catch (cause) {
|
|
299
308
|
if (options.signal?.aborted) {
|
|
300
|
-
throw
|
|
309
|
+
throw connectError("authorization_cancelled", "Downloaded application authorization was cancelled.", { cause });
|
|
301
310
|
}
|
|
302
311
|
continue;
|
|
303
312
|
}
|
|
@@ -319,22 +328,22 @@ class MdbaseConnectInternals {
|
|
|
319
328
|
&& tokenBody.encryption.application_agreement_public_key
|
|
320
329
|
=== grantKey.agreementPublicKey;
|
|
321
330
|
if (tokenBody.application_origin !== "null") {
|
|
322
|
-
throw
|
|
331
|
+
throw connectError("invalid_token_response", "Authorization did not bind the portable grant to its opaque application origin.");
|
|
323
332
|
}
|
|
324
333
|
if (tokenBody.authority
|
|
325
334
|
&& (!authority
|
|
326
335
|
|| tokenBody.encryption != null
|
|
327
336
|
|| tokenBody.authority.proof_public_key !== grantKey.signingPublicKey)) {
|
|
328
|
-
throw
|
|
337
|
+
throw connectError("invalid_token_response", "Authorization returned a remote authority capability that is not bound to this portable grant key.");
|
|
329
338
|
}
|
|
330
339
|
if (!tokenBody.authority && !localEncryption) {
|
|
331
|
-
throw
|
|
340
|
+
throw connectError("encryption_required", "Authorization did not establish the expected key-bound local portable grant.");
|
|
332
341
|
}
|
|
333
342
|
const token = this.storeTokenResponse(tokenBody, application.id, keyHandle);
|
|
334
343
|
if (options.target?.kind === "collection"
|
|
335
344
|
&& options.target.collectionId !== token.collectionId) {
|
|
336
345
|
this.removeToken(token.collectionId, token.keyHandle);
|
|
337
|
-
throw
|
|
346
|
+
throw connectError("collection_mismatch", "The approved collection does not match the collection requested by this link.");
|
|
338
347
|
}
|
|
339
348
|
popup?.close();
|
|
340
349
|
return {
|
|
@@ -343,7 +352,7 @@ class MdbaseConnectInternals {
|
|
|
343
352
|
...(options.returnTo ? { returnTo: options.returnTo } : {})
|
|
344
353
|
};
|
|
345
354
|
}
|
|
346
|
-
throw
|
|
355
|
+
throw connectError("expired_token", "The downloaded application authorization code expired.");
|
|
347
356
|
}
|
|
348
357
|
catch (error) {
|
|
349
358
|
popup?.close();
|
|
@@ -354,7 +363,7 @@ class MdbaseConnectInternals {
|
|
|
354
363
|
completeAuthorization(callbackUrl) {
|
|
355
364
|
const state = new URL(callbackUrl).searchParams.get("state");
|
|
356
365
|
if (!state) {
|
|
357
|
-
return Promise.reject(
|
|
366
|
+
return Promise.reject(connectError("invalid_callback", "Authorization callback is missing its state."));
|
|
358
367
|
}
|
|
359
368
|
const existing = this.completionPromises.get(state);
|
|
360
369
|
if (existing)
|
|
@@ -373,18 +382,18 @@ class MdbaseConnectInternals {
|
|
|
373
382
|
const pendingKey = this.pendingKey(state);
|
|
374
383
|
const pending = parseStored(this.storage.getItem(pendingKey));
|
|
375
384
|
if (!pending || pending.version !== 1 || state !== pending.state) {
|
|
376
|
-
throw
|
|
385
|
+
throw connectError("invalid_callback", "Authorization callback is missing or does not match this browser session.");
|
|
377
386
|
}
|
|
378
387
|
if (callback.searchParams.has("error")) {
|
|
379
388
|
if (pending.keyHandle)
|
|
380
389
|
await this.keyStore.delete(pending.keyHandle);
|
|
381
390
|
this.storage.removeItem(pendingKey);
|
|
382
|
-
throw
|
|
391
|
+
throw serverConnectError(callback.searchParams.get("error") ?? "access_denied", callback.searchParams.get("error_description") ?? "Collection access was not approved.", { details: pending.returnTo ? { return_to: pending.returnTo } : undefined });
|
|
383
392
|
}
|
|
384
393
|
if (!code) {
|
|
385
|
-
throw
|
|
394
|
+
throw connectError("invalid_callback", "Authorization callback is missing its code.");
|
|
386
395
|
}
|
|
387
|
-
const response = await
|
|
396
|
+
const response = await connectFetch(`${this.serverUrl}/oauth/token`, {
|
|
388
397
|
method: "POST",
|
|
389
398
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
390
399
|
body: new URLSearchParams({
|
|
@@ -394,7 +403,7 @@ class MdbaseConnectInternals {
|
|
|
394
403
|
redirect_uri: pending.redirectUri,
|
|
395
404
|
code_verifier: pending.verifier
|
|
396
405
|
})
|
|
397
|
-
});
|
|
406
|
+
}, "temporarily_unavailable", "Authorization completion is temporarily unavailable.");
|
|
398
407
|
const body = await response.json();
|
|
399
408
|
if (!response.ok)
|
|
400
409
|
throw apiError(body, "token_exchange_failed", "Authorization could not be completed.", response.status);
|
|
@@ -405,7 +414,7 @@ class MdbaseConnectInternals {
|
|
|
405
414
|
if (pending.keyHandle)
|
|
406
415
|
await this.keyStore.delete(pending.keyHandle);
|
|
407
416
|
this.storage.removeItem(pendingKey);
|
|
408
|
-
throw
|
|
417
|
+
throw connectError("encryption_required", "Authorization did not establish the required encrypted relay grant.");
|
|
409
418
|
}
|
|
410
419
|
if (body.authority?.proof_public_key) {
|
|
411
420
|
if (!pending.keyHandle
|
|
@@ -414,7 +423,7 @@ class MdbaseConnectInternals {
|
|
|
414
423
|
if (pending.keyHandle)
|
|
415
424
|
await this.keyStore.delete(pending.keyHandle);
|
|
416
425
|
this.storage.removeItem(pendingKey);
|
|
417
|
-
throw
|
|
426
|
+
throw connectError("invalid_token_response", "Authorization returned a remote authority capability bound to another application key.");
|
|
418
427
|
}
|
|
419
428
|
}
|
|
420
429
|
else if (body.authority && pending.keyHandle) {
|
|
@@ -424,7 +433,7 @@ class MdbaseConnectInternals {
|
|
|
424
433
|
if (pending.collectionId && pending.collectionId !== token.collectionId) {
|
|
425
434
|
this.removeToken(token.collectionId, token.keyHandle);
|
|
426
435
|
this.storage.removeItem(pendingKey);
|
|
427
|
-
throw
|
|
436
|
+
throw connectError("collection_mismatch", "The approved collection does not match the collection requested by this link.");
|
|
428
437
|
}
|
|
429
438
|
this.storage.removeItem(pendingKey);
|
|
430
439
|
return {
|
|
@@ -462,29 +471,33 @@ class MdbaseConnectInternals {
|
|
|
462
471
|
storeTokenResponse(body, clientId, keyHandle) {
|
|
463
472
|
const collectionId = body.collection_id;
|
|
464
473
|
if (typeof collectionId !== "string") {
|
|
465
|
-
throw
|
|
474
|
+
throw connectError("invalid_token_response", "Authorization returned no collection ID.");
|
|
466
475
|
}
|
|
467
476
|
const scope = parseGrantScope(body.scope);
|
|
468
477
|
if (!scope) {
|
|
469
|
-
throw
|
|
478
|
+
throw connectError("invalid_token_response", "Authorization returned no valid collection scope.");
|
|
470
479
|
}
|
|
471
480
|
if (body.authority && !validAuthorityTokenResponse(body.authority, collectionId)) {
|
|
472
|
-
throw
|
|
481
|
+
throw connectError("invalid_token_response", "Authorization returned an invalid remote authority capability.");
|
|
473
482
|
}
|
|
474
483
|
if (body.authority && body.encryption) {
|
|
475
|
-
throw
|
|
484
|
+
throw connectError("invalid_token_response", "Authorization returned conflicting collection transports.");
|
|
485
|
+
}
|
|
486
|
+
if (body.file_capability !== null && body.file_capability !== undefined
|
|
487
|
+
&& !validFileCapability(body.file_capability)) {
|
|
488
|
+
throw connectError("invalid_token_response", "Authorization returned an invalid file capability.");
|
|
476
489
|
}
|
|
477
490
|
if (body.encryption) {
|
|
478
491
|
try {
|
|
479
492
|
validateGrantEncryption(body.encryption);
|
|
480
493
|
}
|
|
481
494
|
catch {
|
|
482
|
-
throw
|
|
495
|
+
throw connectError("invalid_token_response", "Authorization returned an invalid encrypted relay binding.");
|
|
483
496
|
}
|
|
484
497
|
if (body.encryption.collection_id !== collectionId
|
|
485
498
|
|| typeof body.grant_id !== "string"
|
|
486
499
|
|| body.grant_id.length === 0) {
|
|
487
|
-
throw
|
|
500
|
+
throw connectError("invalid_token_response", "Authorization returned an encrypted relay binding for another grant.");
|
|
488
501
|
}
|
|
489
502
|
}
|
|
490
503
|
const previous = parseStored(this.storage.getItem(this.tokenKey(collectionId)));
|
|
@@ -498,7 +511,7 @@ class MdbaseConnectInternals {
|
|
|
498
511
|
!== body.encryption.connector_agreement_public_key
|
|
499
512
|
|| previous.encryption.application_agreement_public_key
|
|
500
513
|
!== body.encryption.application_agreement_public_key)) {
|
|
501
|
-
throw
|
|
514
|
+
throw connectError("connector_identity_changed", "The connector identity changed during authorization renewal. Reauthorize before sending collection data.");
|
|
502
515
|
}
|
|
503
516
|
if (previous?.keyHandle && previous.keyHandle !== keyHandle) {
|
|
504
517
|
void this.keyStore.delete(previous.keyHandle).catch(() => undefined);
|
|
@@ -518,12 +531,14 @@ class MdbaseConnectInternals {
|
|
|
518
531
|
: undefined,
|
|
519
532
|
grantId: body.grant_id,
|
|
520
533
|
encryption: body.encryption ?? undefined,
|
|
534
|
+
fileCapability: body.file_capability ?? undefined,
|
|
521
535
|
applicationOrigin: body.application_origin ?? this.defaultApplicationOrigin(),
|
|
522
536
|
keyHandle,
|
|
523
537
|
savedAt: Date.now(),
|
|
524
538
|
authority: body.authority ? {
|
|
525
539
|
operationsUrl: body.authority.operations_url,
|
|
526
540
|
syncUrl: body.authority.sync_url,
|
|
541
|
+
filesUrl: body.authority.files_url,
|
|
527
542
|
replicaId: body.authority.replica_id,
|
|
528
543
|
accessToken: body.authority.access_token,
|
|
529
544
|
proofPublicKey: body.authority.proof_public_key
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FileTransferSession } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import type { GrantKeyStore } from "./crypto.js";
|
|
3
|
+
import type { StoredToken } from "./internal-types.js";
|
|
4
|
+
type ProofHeaders = (token: StoredToken, method: "GET" | "POST" | "DELETE", url: string, body: string | undefined, credential: string) => Promise<Record<string, string>>;
|
|
5
|
+
export interface ConnectionFileTransportOptions {
|
|
6
|
+
keyStore: GrantKeyStore;
|
|
7
|
+
serverUrl: string;
|
|
8
|
+
loopbackUrl: string;
|
|
9
|
+
authorizedToken(): Promise<StoredToken | null>;
|
|
10
|
+
refreshAuthorization(): Promise<StoredToken>;
|
|
11
|
+
shouldAttemptDirect(token: StoredToken): Promise<boolean>;
|
|
12
|
+
onDirectAvailable(): void;
|
|
13
|
+
onDirectUnavailable(): void;
|
|
14
|
+
onRelayAvailable(): void;
|
|
15
|
+
authorityProofHeaders: ProofHeaders;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Owns every transport concern specific to files. ConnectionTransport only
|
|
19
|
+
* supplies authorization, routing, and proof-signing policy.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ConnectionFileTransport {
|
|
22
|
+
private readonly options;
|
|
23
|
+
private readonly local;
|
|
24
|
+
constructor(options: ConnectionFileTransportOptions);
|
|
25
|
+
control<Result>(method: "GET" | "POST" | "DELETE", path?: string, input?: unknown, signal?: AbortSignal): Promise<Result>;
|
|
26
|
+
uploadChunk(session: FileTransferSession, chunkIndex: number, bytes: Uint8Array, signal?: AbortSignal): Promise<void>;
|
|
27
|
+
downloadChunk(session: FileTransferSession, chunkIndex: number, signal?: AbortSignal): Promise<Uint8Array>;
|
|
28
|
+
downloadHostedPart(session: FileTransferSession, partIndex: number, expectedLength: number, signal?: AbortSignal): Promise<ReadableStream<Uint8Array>>;
|
|
29
|
+
private requireFileToken;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { connectError } from "./errors.js";
|
|
2
|
+
import { performHostedFilePartRequest, performHostedFileRequest } from "./hosted-file-request.js";
|
|
3
|
+
import { LocalFileTransport } from "./local-file-transport.js";
|
|
4
|
+
/**
|
|
5
|
+
* Owns every transport concern specific to files. ConnectionTransport only
|
|
6
|
+
* supplies authorization, routing, and proof-signing policy.
|
|
7
|
+
*/
|
|
8
|
+
export class ConnectionFileTransport {
|
|
9
|
+
options;
|
|
10
|
+
local;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.local = new LocalFileTransport(options);
|
|
14
|
+
}
|
|
15
|
+
async control(method, path = "", input, signal) {
|
|
16
|
+
const token = await this.requireFileToken();
|
|
17
|
+
if (!token.authority) {
|
|
18
|
+
return this.local.control(token, method, path, input, signal);
|
|
19
|
+
}
|
|
20
|
+
return performHostedFileRequest(token, method, path, input, signal, this.options.refreshAuthorization, this.options.authorityProofHeaders);
|
|
21
|
+
}
|
|
22
|
+
uploadChunk(session, chunkIndex, bytes, signal) {
|
|
23
|
+
return this.local.uploadChunk(session, chunkIndex, bytes, signal);
|
|
24
|
+
}
|
|
25
|
+
downloadChunk(session, chunkIndex, signal) {
|
|
26
|
+
return this.local.downloadChunk(session, chunkIndex, signal);
|
|
27
|
+
}
|
|
28
|
+
async downloadHostedPart(session, partIndex, expectedLength, signal) {
|
|
29
|
+
const token = await this.requireFileToken();
|
|
30
|
+
if (!token.authority) {
|
|
31
|
+
throw connectError("not_remote_authority", "Hosted file delivery requires a remote authority endpoint.");
|
|
32
|
+
}
|
|
33
|
+
return performHostedFilePartRequest(token, `downloads/${encodeURIComponent(session.transfer_id)}/parts/${partIndex}`, expectedLength, signal, this.options.refreshAuthorization, this.options.authorityProofHeaders);
|
|
34
|
+
}
|
|
35
|
+
async requireFileToken() {
|
|
36
|
+
const token = await this.options.authorizedToken();
|
|
37
|
+
if (!token?.fileCapability) {
|
|
38
|
+
throw connectError("not_authorized", "This connection has no file capability.");
|
|
39
|
+
}
|
|
40
|
+
return token;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { apiError, parseStored } from "./runtime-utils.js";
|
|
1
|
+
import { connectError } from "./errors.js";
|
|
2
|
+
import { apiError, connectFetch, parseStored } from "./runtime-utils.js";
|
|
3
3
|
import { base64UrlBytes, randomBase64Url } from "./base64.js";
|
|
4
4
|
export class ConnectionNotifications {
|
|
5
5
|
context;
|
|
@@ -9,19 +9,19 @@ export class ConnectionNotifications {
|
|
|
9
9
|
async registerNotifications(options) {
|
|
10
10
|
const token = await this.context.authorizedToken();
|
|
11
11
|
if (!token) {
|
|
12
|
-
throw
|
|
12
|
+
throw connectError("not_authorized", "Connect this application before enabling notifications.");
|
|
13
13
|
}
|
|
14
14
|
const application = await this.context.register();
|
|
15
15
|
const declared = application.notifications?.criteria.map((criterion) => criterion.id) ?? [];
|
|
16
16
|
const criteria = [...new Set(options.criteria ?? declared)];
|
|
17
17
|
const undeclared = criteria.find((criterion) => !declared.includes(criterion));
|
|
18
18
|
if (undeclared) {
|
|
19
|
-
throw
|
|
19
|
+
throw connectError("notification_criterion_not_declared", `The application manifest does not declare notification criterion ${undeclared}.`);
|
|
20
20
|
}
|
|
21
21
|
if (criteria.length === 0) {
|
|
22
|
-
throw
|
|
22
|
+
throw connectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
|
|
23
23
|
}
|
|
24
|
-
const keyResponse = await
|
|
24
|
+
const keyResponse = await connectFetch(`${this.context.serverUrl}/v1/notifications/vapid-public-key`, undefined, "notifications_unavailable", "Push notifications are unavailable.");
|
|
25
25
|
const keyBody = await keyResponse.json();
|
|
26
26
|
if (!keyResponse.ok) {
|
|
27
27
|
throw apiError(keyBody, "notifications_unavailable", "Push notifications are unavailable.", keyResponse.status);
|
|
@@ -35,13 +35,13 @@ export class ConnectionNotifications {
|
|
|
35
35
|
}
|
|
36
36
|
const serialized = pushSubscription.toJSON();
|
|
37
37
|
if (!serialized.endpoint || !serialized.keys?.p256dh || !serialized.keys.auth) {
|
|
38
|
-
throw
|
|
38
|
+
throw connectError("invalid_push_subscription", "The browser returned an incomplete push subscription.");
|
|
39
39
|
}
|
|
40
40
|
const previous = parseStored(this.context.storage.getItem(this.context.notificationKey()));
|
|
41
41
|
const installationId = options.installationId
|
|
42
42
|
?? previous?.installationId
|
|
43
43
|
?? randomBase64Url(24);
|
|
44
|
-
const channelResponse = await
|
|
44
|
+
const channelResponse = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels`, {
|
|
45
45
|
method: "POST",
|
|
46
46
|
headers: {
|
|
47
47
|
authorization: `Bearer ${token.accessToken}`,
|
|
@@ -56,7 +56,7 @@ export class ConnectionNotifications {
|
|
|
56
56
|
keys: serialized.keys
|
|
57
57
|
}
|
|
58
58
|
})
|
|
59
|
-
});
|
|
59
|
+
}, "notification_registration_failed", "Could not register push notifications.");
|
|
60
60
|
const channelBody = await channelResponse.json();
|
|
61
61
|
if (!channelResponse.ok) {
|
|
62
62
|
throw apiError(channelBody, "notification_registration_failed", "Could not register push notifications.", channelResponse.status);
|
|
@@ -84,27 +84,27 @@ export class ConnectionNotifications {
|
|
|
84
84
|
async registerNativeNotifications(options) {
|
|
85
85
|
const token = await this.context.authorizedToken();
|
|
86
86
|
if (!token) {
|
|
87
|
-
throw
|
|
87
|
+
throw connectError("not_authorized", "Connect this application before enabling notifications.");
|
|
88
88
|
}
|
|
89
89
|
const application = await this.context.register();
|
|
90
90
|
if (application.notifications?.native_delivery?.mode !== "managed_fcm") {
|
|
91
|
-
throw
|
|
91
|
+
throw connectError("managed_fcm_not_declared", "This application does not declare Connect-managed native notifications.");
|
|
92
92
|
}
|
|
93
93
|
const declared = application.notifications.criteria.map((criterion) => criterion.id);
|
|
94
94
|
const criteria = [...new Set(options.criteria ?? declared)];
|
|
95
95
|
const undeclared = criteria.find((criterion) => !declared.includes(criterion));
|
|
96
96
|
if (undeclared) {
|
|
97
|
-
throw
|
|
97
|
+
throw connectError("notification_criterion_not_declared", `The application manifest does not declare notification criterion ${undeclared}.`);
|
|
98
98
|
}
|
|
99
99
|
if (criteria.length === 0) {
|
|
100
|
-
throw
|
|
100
|
+
throw connectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
|
|
101
101
|
}
|
|
102
102
|
const storageKey = this.context.notificationKey("fcm");
|
|
103
103
|
const previous = parseStored(this.context.storage.getItem(storageKey));
|
|
104
104
|
const installationId = options.installationId
|
|
105
105
|
?? previous?.installationId
|
|
106
106
|
?? randomBase64Url(24);
|
|
107
|
-
const response = await
|
|
107
|
+
const response = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels`, {
|
|
108
108
|
method: "POST",
|
|
109
109
|
headers: {
|
|
110
110
|
authorization: `Bearer ${token.accessToken}`,
|
|
@@ -116,7 +116,7 @@ export class ConnectionNotifications {
|
|
|
116
116
|
transport: "fcm",
|
|
117
117
|
token: options.token
|
|
118
118
|
})
|
|
119
|
-
});
|
|
119
|
+
}, "notification_registration_failed", "Could not register native notifications.");
|
|
120
120
|
const body = await response.json();
|
|
121
121
|
if (!response.ok) {
|
|
122
122
|
throw apiError(body, "notification_registration_failed", "Could not register native notifications.", response.status);
|
|
@@ -139,7 +139,7 @@ export class ConnectionNotifications {
|
|
|
139
139
|
const registration = parseStored(this.context.storage.getItem(storageKey));
|
|
140
140
|
const token = await this.context.authorizedToken();
|
|
141
141
|
if (registration?.channelId && !token) {
|
|
142
|
-
throw
|
|
142
|
+
throw connectError("not_authorized", "Reconnect this application before disabling native notifications.");
|
|
143
143
|
}
|
|
144
144
|
if (registration?.channelId && token) {
|
|
145
145
|
await this.deleteNotificationChannel(registration.channelId, token.accessToken);
|
|
@@ -150,13 +150,13 @@ export class ConnectionNotifications {
|
|
|
150
150
|
const registration = parseStored(this.context.storage.getItem(this.context.notificationKey()));
|
|
151
151
|
const token = await this.context.authorizedToken();
|
|
152
152
|
if (registration?.channelId && !token) {
|
|
153
|
-
throw
|
|
153
|
+
throw connectError("not_authorized", "Reconnect this application before disabling push notifications.");
|
|
154
154
|
}
|
|
155
155
|
if (registration?.channelId && token) {
|
|
156
|
-
const response = await
|
|
156
|
+
const response = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels/${registration.channelId}`, {
|
|
157
157
|
method: "DELETE",
|
|
158
158
|
headers: { authorization: `Bearer ${token.accessToken}` }
|
|
159
|
-
});
|
|
159
|
+
}, "notification_unregistration_failed", "Could not unregister push notifications.");
|
|
160
160
|
if (!response.ok && response.status !== 404) {
|
|
161
161
|
const body = await response.json();
|
|
162
162
|
throw apiError(body, "notification_unregistration_failed", "Could not unregister push notifications.", response.status);
|
|
@@ -167,10 +167,10 @@ export class ConnectionNotifications {
|
|
|
167
167
|
await subscription?.unsubscribe();
|
|
168
168
|
}
|
|
169
169
|
async deleteNotificationChannel(channelId, accessToken) {
|
|
170
|
-
const response = await
|
|
170
|
+
const response = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels/${channelId}`, {
|
|
171
171
|
method: "DELETE",
|
|
172
172
|
headers: { authorization: `Bearer ${accessToken}` }
|
|
173
|
-
});
|
|
173
|
+
}, "notification_unregistration_failed", "Could not unregister push notifications.");
|
|
174
174
|
if (!response.ok && response.status !== 404) {
|
|
175
175
|
const body = await response.json();
|
|
176
176
|
throw apiError(body, "notification_unregistration_failed", "Could not unregister push notifications.", response.status);
|
|
@@ -3,6 +3,7 @@ import { type GrantKeyStore } from "./crypto.js";
|
|
|
3
3
|
import type { StoredToken } from "./internal-types.js";
|
|
4
4
|
import type { DirectAccessStatus, MdbaseConnectionRoute } from "./connection-types.js";
|
|
5
5
|
import type { OperationRequestOptions, PendingMutationSummary } from "./operation-types.js";
|
|
6
|
+
import { ConnectionFileTransport } from "./connection-file-transport.js";
|
|
6
7
|
export interface ConnectionTransportInternals {
|
|
7
8
|
readonly relayEncryption: "required" | "disabled";
|
|
8
9
|
removeToken(collectionId: string, keyHandle?: string, reason?: "not_authorized" | "authorization_lost" | "invalid_stored_grant"): void;
|
|
@@ -31,6 +32,7 @@ export declare class ConnectionTransport {
|
|
|
31
32
|
private readonly collectionId;
|
|
32
33
|
private readonly internals;
|
|
33
34
|
private readonly onChange;
|
|
35
|
+
readonly files: ConnectionFileTransport;
|
|
34
36
|
private refreshPromise;
|
|
35
37
|
private directStatus;
|
|
36
38
|
private currentRoute;
|