@mdbase-dev/connect 0.1.0-beta.30 → 0.1.0-beta.32
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 +75 -19
- package/dist/advanced.d.ts +7 -0
- package/dist/advanced.js +4 -0
- package/dist/application-session.d.ts +10 -5
- package/dist/application-session.js +32 -16
- package/dist/async.js +11 -5
- package/dist/authority-proof.d.ts +3 -0
- package/dist/authority-proof.js +24 -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/browser.d.ts +3 -0
- package/dist/browser.js +5 -0
- package/dist/capabilities.js +2 -1
- package/dist/collection-client.d.ts +28 -28
- package/dist/collection-client.js +42 -42
- package/dist/connect-options.d.ts +12 -4
- package/dist/connect.d.ts +12 -4
- package/dist/connect.js +105 -104
- package/dist/connection-file-transport.d.ts +2 -2
- package/dist/connection-file-transport.js +4 -4
- package/dist/connection-notifications.d.ts +10 -4
- package/dist/connection-notifications.js +41 -23
- package/dist/connection-transport.d.ts +19 -15
- package/dist/connection-transport.js +202 -202
- package/dist/connection-types.d.ts +5 -4
- package/dist/connection.d.ts +43 -43
- package/dist/connection.js +221 -77
- package/dist/crypto-entry.d.ts +4 -0
- package/dist/crypto-entry.js +4 -0
- package/dist/crypto.js +4 -4
- package/dist/external-store.d.ts +10 -0
- package/dist/external-store.js +10 -0
- package/dist/file-transfer-internals.d.ts +22 -0
- package/dist/file-transfer-internals.js +186 -0
- package/dist/files.d.ts +16 -12
- package/dist/files.js +52 -206
- package/dist/hosted-file-request.js +3 -3
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -5
- package/dist/internal-types.d.ts +8 -1
- package/dist/local-file-transport.d.ts +2 -2
- package/dist/local-file-transport.js +13 -13
- package/dist/mdbase-connect.d.ts +6 -4
- package/dist/mdbase-connect.js +9 -8
- package/dist/notifications.d.ts +3 -2
- package/dist/operation-helpers.d.ts +3 -3
- package/dist/operation-helpers.js +12 -29
- package/dist/operation-types.d.ts +33 -8
- package/dist/outcomes.d.ts +15 -15
- package/dist/outcomes.js +13 -1
- package/dist/pending-mutation-store.d.ts +13 -0
- package/dist/pending-mutation-store.js +64 -0
- package/dist/request-budget.d.ts +27 -0
- package/dist/request-budget.js +95 -0
- package/dist/runtime-utils.d.ts +2 -0
- package/dist/runtime-utils.js +19 -0
- package/dist/session.d.ts +4 -3
- package/dist/session.js +4 -4
- package/dist/stored-token.d.ts +10 -0
- package/dist/stored-token.js +60 -0
- package/dist/token-response.d.ts +11 -0
- package/dist/token-response.js +77 -0
- package/package.json +12 -2
package/dist/connect.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { APPLICATION_AUTHORIZATION_PROTOCOL_VERSION, DEFAULT_LOOPBACK_PORT,
|
|
1
|
+
import { APPLICATION_AUTHORIZATION_PROTOCOL_VERSION, authorizationContractRequirements, DEFAULT_LOOPBACK_PORT, GRANT_ENCRYPTION_PROTOCOL_VERSION, RELAY_ENCRYPTION_SUITE } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import { abortableDelay } from "./async.js";
|
|
3
3
|
import { randomBase64Url } from "./base64.js";
|
|
4
4
|
import { ApplicationIdentityStoreError, IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, applicationIdentity, applicationInstallationId, signApplicationAuthorization } from "./application-identity.js";
|
|
5
5
|
import { MdbaseConnection } from "./connection.js";
|
|
6
|
-
import { IndexedDbGrantKeyStore, MemoryGrantKeyStore
|
|
7
|
-
import { connectError, serverConnectError } from "./errors.js";
|
|
6
|
+
import { IndexedDbGrantKeyStore, MemoryGrantKeyStore } from "./crypto.js";
|
|
7
|
+
import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
|
|
8
8
|
import { DEFAULT_OPERATIONS } from "./internal-types.js";
|
|
9
9
|
import { uniqueOperations } from "./operation-helpers.js";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
|
|
11
|
+
import { MemoryStorage, apiError, applicationStorageOrigin, canonicalLoopbackUrl, collectionIdFromTokenKey, connectFetch, createPkce, defaultManifestSource, defaultRedirectUri, defaultStorage, decodeJsonResponse, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseStored, stripTrailingSlash, validAuthorityTokenResponse } from "./runtime-utils.js";
|
|
12
|
+
import { storedTokenFromResponse } from "./token-response.js";
|
|
11
13
|
export class MdbaseConnectInternals {
|
|
12
14
|
serverUrl;
|
|
13
15
|
manifest;
|
|
@@ -20,6 +22,7 @@ export class MdbaseConnectInternals {
|
|
|
20
22
|
directAccessMode;
|
|
21
23
|
loopbackUrl;
|
|
22
24
|
navigate;
|
|
25
|
+
timeouts;
|
|
23
26
|
credentialStorage;
|
|
24
27
|
application = null;
|
|
25
28
|
manifestPromise = null;
|
|
@@ -28,17 +31,19 @@ export class MdbaseConnectInternals {
|
|
|
28
31
|
listeners = new Set();
|
|
29
32
|
invalidatedConnections = new Map();
|
|
30
33
|
constructor(options) {
|
|
31
|
-
this.serverUrl = stripTrailingSlash(options.serverUrl);
|
|
32
|
-
this.manifest = options.manifest
|
|
34
|
+
this.serverUrl = stripTrailingSlash(String(options.serverUrl));
|
|
35
|
+
this.manifest = options.manifest instanceof URL
|
|
36
|
+
? options.manifest.href
|
|
37
|
+
: options.manifest ?? defaultManifestSource();
|
|
33
38
|
this.manifestSource = typeof this.manifest === "string"
|
|
34
39
|
? this.manifest
|
|
35
40
|
: this.manifest.distribution === "portable"
|
|
36
41
|
? `bundle:${this.manifest.id}:${manifestStorageFingerprint(this.manifest)}`
|
|
37
42
|
: `bundle:${this.manifest.id}`;
|
|
38
43
|
const opaquePortable = isOpaquePortableManifest(this.manifest);
|
|
39
|
-
this.redirectUri = options.redirectUri
|
|
44
|
+
this.redirectUri = options.redirectUri === undefined ? (typeof this.manifest !== "string" && this.manifest.distribution === "portable"
|
|
40
45
|
? ""
|
|
41
|
-
: defaultRedirectUri());
|
|
46
|
+
: defaultRedirectUri()) : String(options.redirectUri);
|
|
42
47
|
this.storage = options.storage ?? defaultStorage(opaquePortable);
|
|
43
48
|
this.relayEncryption = options.relayEncryption ?? "required";
|
|
44
49
|
this.keyStore = options.keyStore ?? (opaquePortable ? new MemoryGrantKeyStore() : new IndexedDbGrantKeyStore());
|
|
@@ -49,8 +54,9 @@ export class MdbaseConnectInternals {
|
|
|
49
54
|
? "custom"
|
|
50
55
|
: this.storage instanceof MemoryStorage ? "memory" : "persistent";
|
|
51
56
|
this.directAccessMode = options.directAccess ?? "auto";
|
|
52
|
-
this.loopbackUrl = canonicalLoopbackUrl(options.loopbackUrl ?? `http://127.0.0.1:${DEFAULT_LOOPBACK_PORT}`);
|
|
57
|
+
this.loopbackUrl = canonicalLoopbackUrl(String(options.loopbackUrl ?? `http://127.0.0.1:${DEFAULT_LOOPBACK_PORT}`));
|
|
53
58
|
this.navigate = options.navigate;
|
|
59
|
+
this.timeouts = resolveConnectTimeouts(options.timeouts);
|
|
54
60
|
if (typeof window !== "undefined" && this.storage === window.localStorage) {
|
|
55
61
|
window.addEventListener("storage", (event) => {
|
|
56
62
|
if (event.storageArea !== this.storage || !event.key?.startsWith(this.storagePrefix()))
|
|
@@ -73,34 +79,45 @@ export class MdbaseConnectInternals {
|
|
|
73
79
|
credentialStorage: this.credentialStorage
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
|
-
|
|
82
|
+
register(options = {}) {
|
|
83
|
+
return withRequestBudget(options, this.timeouts.requestMs, (budget) => this.registerWithinBudget(budget.signal));
|
|
84
|
+
}
|
|
85
|
+
async registerWithinBudget(signal) {
|
|
77
86
|
if (this.application)
|
|
78
87
|
return this.application;
|
|
79
88
|
const response = await connectFetch(`${this.serverUrl}/v1/apps/register`, {
|
|
80
89
|
method: "POST",
|
|
81
90
|
headers: { "content-type": "application/json" },
|
|
82
|
-
body: JSON.stringify({ manifest: await this.manifestDeclaration() })
|
|
91
|
+
body: JSON.stringify({ manifest: await this.manifestDeclaration({ signal, timeoutMs: null }) }),
|
|
92
|
+
signal
|
|
83
93
|
}, "temporarily_unavailable", "Application registration is temporarily unavailable.");
|
|
84
|
-
const body = await response.
|
|
94
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "Connect returned an invalid application registration response.");
|
|
85
95
|
if (!response.ok)
|
|
86
96
|
throw apiError(body, "discovery_failed", "Application discovery failed.", response.status);
|
|
87
97
|
this.application = body.application;
|
|
88
98
|
return this.application;
|
|
89
99
|
}
|
|
90
|
-
manifestDeclaration() {
|
|
100
|
+
manifestDeclaration(options = {}) {
|
|
91
101
|
if (this.manifestPromise)
|
|
92
102
|
return this.manifestPromise;
|
|
93
|
-
this.
|
|
103
|
+
const loading = withRequestBudget(options, this.timeouts.requestMs, (budget) => this.loadManifest(budget.signal));
|
|
104
|
+
const shared = loading.catch((error) => {
|
|
105
|
+
if (this.manifestPromise === shared)
|
|
106
|
+
this.manifestPromise = null;
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
109
|
+
this.manifestPromise = shared;
|
|
94
110
|
return this.manifestPromise;
|
|
95
111
|
}
|
|
96
|
-
async loadManifest() {
|
|
112
|
+
async loadManifest(signal) {
|
|
97
113
|
if (typeof this.manifest !== "string")
|
|
98
114
|
return this.manifest;
|
|
99
115
|
const source = this.manifest;
|
|
100
116
|
let response;
|
|
101
117
|
try {
|
|
102
118
|
response = await fetch(source, {
|
|
103
|
-
headers: { accept: "application/json" }
|
|
119
|
+
headers: { accept: "application/json" },
|
|
120
|
+
signal
|
|
104
121
|
});
|
|
105
122
|
}
|
|
106
123
|
catch (cause) {
|
|
@@ -110,7 +127,7 @@ export class MdbaseConnectInternals {
|
|
|
110
127
|
throw connectError("manifest_load_failed", `The bundled application declaration returned HTTP ${response.status}.`, { status: response.status });
|
|
111
128
|
}
|
|
112
129
|
try {
|
|
113
|
-
return await response.
|
|
130
|
+
return await decodeJsonResponse(response, "invalid_application_manifest", "The bundled application declaration is not valid JSON.");
|
|
114
131
|
}
|
|
115
132
|
catch (cause) {
|
|
116
133
|
throw connectError("invalid_application_manifest", "The bundled application declaration is not valid JSON.", {
|
|
@@ -127,6 +144,9 @@ export class MdbaseConnectInternals {
|
|
|
127
144
|
}
|
|
128
145
|
}
|
|
129
146
|
async authorize(options = {}) {
|
|
147
|
+
return withRequestBudget(options, this.timeouts.requestMs, (budget) => this.authorizeWithinBudget({ ...options, signal: budget.signal, timeoutMs: null }));
|
|
148
|
+
}
|
|
149
|
+
async authorizeWithinBudget(options) {
|
|
130
150
|
if (typeof location === "undefined" && !this.navigate && !options.openVerification) {
|
|
131
151
|
throw connectError("browser_required", "Authorization navigation requires a browser environment.");
|
|
132
152
|
}
|
|
@@ -140,7 +160,7 @@ export class MdbaseConnectInternals {
|
|
|
140
160
|
: null;
|
|
141
161
|
let application;
|
|
142
162
|
try {
|
|
143
|
-
application = await this.
|
|
163
|
+
application = await this.registerWithinBudget(options.signal);
|
|
144
164
|
}
|
|
145
165
|
catch (error) {
|
|
146
166
|
popup?.close();
|
|
@@ -189,6 +209,7 @@ export class MdbaseConnectInternals {
|
|
|
189
209
|
redirect_uri: this.redirectUri,
|
|
190
210
|
state,
|
|
191
211
|
code_challenge: challenge,
|
|
212
|
+
contracts: authorizationContractRequirements(operations, application.requirements?.files),
|
|
192
213
|
requested_operations: operations,
|
|
193
214
|
...(application.requirements?.files
|
|
194
215
|
? { requested_files: application.requirements.files }
|
|
@@ -226,7 +247,7 @@ export class MdbaseConnectInternals {
|
|
|
226
247
|
}),
|
|
227
248
|
signal: options.signal
|
|
228
249
|
});
|
|
229
|
-
const body = await response.
|
|
250
|
+
const body = await decodeJsonResponse(response, "invalid_device_authorization_response", "The authorization service returned an invalid authorization response.");
|
|
230
251
|
if (!response.ok) {
|
|
231
252
|
throw apiError(body, "device_authorization_failed", "Application authorization could not be started.", response.status);
|
|
232
253
|
}
|
|
@@ -249,7 +270,7 @@ export class MdbaseConnectInternals {
|
|
|
249
270
|
this.storage.removeItem(this.pendingKey(state));
|
|
250
271
|
await this.keyStore.delete(keyHandle);
|
|
251
272
|
if (options.signal?.aborted) {
|
|
252
|
-
throw
|
|
273
|
+
throw authorizationAbort(options.signal, "Application authorization was cancelled.", error);
|
|
253
274
|
}
|
|
254
275
|
throw error;
|
|
255
276
|
}
|
|
@@ -288,6 +309,7 @@ export class MdbaseConnectInternals {
|
|
|
288
309
|
issued_at: issuedAt.toISOString(),
|
|
289
310
|
expires_at: new Date(issuedAt.getTime() + 10 * 60 * 1_000).toISOString(),
|
|
290
311
|
code_challenge: challenge,
|
|
312
|
+
contracts: authorizationContractRequirements(operations, application.requirements?.files),
|
|
291
313
|
requested_operations: operations,
|
|
292
314
|
...(application.requirements?.files
|
|
293
315
|
? { requested_files: application.requirements.files }
|
|
@@ -311,9 +333,10 @@ export class MdbaseConnectInternals {
|
|
|
311
333
|
code_challenge: challenge,
|
|
312
334
|
code_challenge_method: "S256",
|
|
313
335
|
application_authorization: JSON.stringify(proof)
|
|
314
|
-
})
|
|
336
|
+
}),
|
|
337
|
+
signal: options.signal
|
|
315
338
|
});
|
|
316
|
-
body = await response.
|
|
339
|
+
body = await decodeJsonResponse(response, "invalid_device_authorization_response", "The authorization service returned an invalid device authorization response.");
|
|
317
340
|
}
|
|
318
341
|
catch (cause) {
|
|
319
342
|
popup?.close();
|
|
@@ -333,7 +356,7 @@ export class MdbaseConnectInternals {
|
|
|
333
356
|
popup?.close();
|
|
334
357
|
await this.keyStore.delete(keyHandle);
|
|
335
358
|
if (options.signal?.aborted) {
|
|
336
|
-
throw
|
|
359
|
+
throw authorizationAbort(options.signal, "Downloaded application authorization was cancelled.", error);
|
|
337
360
|
}
|
|
338
361
|
throw error;
|
|
339
362
|
}
|
|
@@ -366,7 +389,7 @@ export class MdbaseConnectInternals {
|
|
|
366
389
|
while (Date.now() < authorization.expiresAt) {
|
|
367
390
|
await abortableDelay(intervalSeconds * 1_000, options.signal);
|
|
368
391
|
if (options.signal?.aborted) {
|
|
369
|
-
throw
|
|
392
|
+
throw authorizationAbort(options.signal, "Downloaded application authorization was cancelled.");
|
|
370
393
|
}
|
|
371
394
|
let tokenResponse;
|
|
372
395
|
try {
|
|
@@ -384,11 +407,11 @@ export class MdbaseConnectInternals {
|
|
|
384
407
|
}
|
|
385
408
|
catch (cause) {
|
|
386
409
|
if (options.signal?.aborted) {
|
|
387
|
-
throw
|
|
410
|
+
throw authorizationAbort(options.signal, "Downloaded application authorization was cancelled.", cause);
|
|
388
411
|
}
|
|
389
412
|
continue;
|
|
390
413
|
}
|
|
391
|
-
const tokenBody = await tokenResponse.
|
|
414
|
+
const tokenBody = await decodeJsonResponse(tokenResponse, "invalid_token_response", "The authorization service returned an invalid token response.");
|
|
392
415
|
if (!tokenResponse.ok) {
|
|
393
416
|
const code = oauthErrorCode(tokenBody);
|
|
394
417
|
if (code === "authorization_pending") {
|
|
@@ -402,7 +425,7 @@ export class MdbaseConnectInternals {
|
|
|
402
425
|
}
|
|
403
426
|
const authority = validAuthorityTokenResponse(tokenBody.authority, tokenBody.collection_id);
|
|
404
427
|
const localEncryption = tokenBody.encryption
|
|
405
|
-
&& tokenBody.encryption.protocol_version ===
|
|
428
|
+
&& tokenBody.encryption.protocol_version === GRANT_ENCRYPTION_PROTOCOL_VERSION
|
|
406
429
|
&& tokenBody.encryption.suite === RELAY_ENCRYPTION_SUITE
|
|
407
430
|
&& tokenBody.encryption.application_agreement_public_key
|
|
408
431
|
=== grantKey.agreementPublicKey;
|
|
@@ -437,12 +460,15 @@ export class MdbaseConnectInternals {
|
|
|
437
460
|
popup?.close();
|
|
438
461
|
await this.keyStore.delete(keyHandle);
|
|
439
462
|
if (options.signal?.aborted) {
|
|
440
|
-
throw
|
|
463
|
+
throw authorizationAbort(options.signal, "Downloaded application authorization was cancelled.", error);
|
|
441
464
|
}
|
|
442
465
|
throw error;
|
|
443
466
|
}
|
|
444
467
|
}
|
|
445
|
-
completeAuthorization(callbackUrl) {
|
|
468
|
+
completeAuthorization(callbackUrl, options = {}) {
|
|
469
|
+
return withRequestBudget(options, this.timeouts.requestMs, (budget) => this.completeAuthorizationWithinBudget(callbackUrl, budget.signal));
|
|
470
|
+
}
|
|
471
|
+
completeAuthorizationWithinBudget(callbackUrl, signal) {
|
|
446
472
|
const state = new URL(callbackUrl).searchParams.get("state");
|
|
447
473
|
if (!state) {
|
|
448
474
|
return Promise.reject(connectError("invalid_callback", "Authorization callback is missing its state."));
|
|
@@ -450,7 +476,7 @@ export class MdbaseConnectInternals {
|
|
|
450
476
|
const existing = this.completionPromises.get(state);
|
|
451
477
|
if (existing)
|
|
452
478
|
return existing;
|
|
453
|
-
const completion = this.performAuthorizationCompletion(callbackUrl, state);
|
|
479
|
+
const completion = this.performAuthorizationCompletion(callbackUrl, state, signal);
|
|
454
480
|
const shared = completion.finally(() => {
|
|
455
481
|
if (this.completionPromises.get(state) === shared)
|
|
456
482
|
this.completionPromises.delete(state);
|
|
@@ -458,7 +484,7 @@ export class MdbaseConnectInternals {
|
|
|
458
484
|
this.completionPromises.set(state, shared);
|
|
459
485
|
return shared;
|
|
460
486
|
}
|
|
461
|
-
async performAuthorizationCompletion(callbackUrl, state) {
|
|
487
|
+
async performAuthorizationCompletion(callbackUrl, state, signal) {
|
|
462
488
|
const callback = new URL(callbackUrl);
|
|
463
489
|
const code = callback.searchParams.get("code");
|
|
464
490
|
const pendingKey = this.pendingKey(state);
|
|
@@ -484,9 +510,10 @@ export class MdbaseConnectInternals {
|
|
|
484
510
|
client_id: pending.clientId,
|
|
485
511
|
redirect_uri: pending.redirectUri,
|
|
486
512
|
code_verifier: pending.verifier
|
|
487
|
-
})
|
|
513
|
+
}),
|
|
514
|
+
signal
|
|
488
515
|
}, "temporarily_unavailable", "Authorization completion is temporarily unavailable.");
|
|
489
|
-
const body = await response.
|
|
516
|
+
const body = await decodeJsonResponse(response, "invalid_token_response", "The authorization service returned an invalid token response.");
|
|
490
517
|
if (!response.ok)
|
|
491
518
|
throw apiError(body, "token_exchange_failed", "Authorization could not be completed.", response.status);
|
|
492
519
|
if (pending.relayEncryption === "required" && !body.authority && (!body.encryption
|
|
@@ -555,78 +582,18 @@ export class MdbaseConnectInternals {
|
|
|
555
582
|
if (typeof collectionId !== "string") {
|
|
556
583
|
throw connectError("invalid_token_response", "Authorization returned no collection ID.");
|
|
557
584
|
}
|
|
558
|
-
const scope = parseGrantScope(body.scope);
|
|
559
|
-
if (!scope) {
|
|
560
|
-
throw connectError("invalid_token_response", "Authorization returned no valid collection scope.");
|
|
561
|
-
}
|
|
562
|
-
if (body.authority && !validAuthorityTokenResponse(body.authority, collectionId)) {
|
|
563
|
-
throw connectError("invalid_token_response", "Authorization returned an invalid remote authority capability.");
|
|
564
|
-
}
|
|
565
|
-
if (body.authority && body.encryption) {
|
|
566
|
-
throw connectError("invalid_token_response", "Authorization returned conflicting collection transports.");
|
|
567
|
-
}
|
|
568
|
-
if (body.file_capability !== null && body.file_capability !== undefined
|
|
569
|
-
&& !validFileCapability(body.file_capability)) {
|
|
570
|
-
throw connectError("invalid_token_response", "Authorization returned an invalid file capability.");
|
|
571
|
-
}
|
|
572
|
-
if (body.encryption) {
|
|
573
|
-
try {
|
|
574
|
-
validateGrantEncryption(body.encryption);
|
|
575
|
-
}
|
|
576
|
-
catch {
|
|
577
|
-
throw connectError("invalid_token_response", "Authorization returned an invalid encrypted relay binding.");
|
|
578
|
-
}
|
|
579
|
-
this.pinConnectorIdentity(body.encryption.connector_id, body.encryption.connector_agreement_public_key);
|
|
580
|
-
if (body.encryption.collection_id !== collectionId
|
|
581
|
-
|| typeof body.grant_id !== "string"
|
|
582
|
-
|| body.grant_id.length === 0) {
|
|
583
|
-
throw connectError("invalid_token_response", "Authorization returned an encrypted relay binding for another grant.");
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
585
|
const previous = parseStored(this.storage.getItem(this.tokenKey(collectionId)));
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|| previous.encryption.application_agreement_public_key
|
|
596
|
-
!== body.encryption.application_agreement_public_key)) {
|
|
597
|
-
throw connectError("connector_identity_changed", "The connector identity changed during authorization renewal. Reauthorize before sending collection data.");
|
|
598
|
-
}
|
|
586
|
+
const token = storedTokenFromResponse({
|
|
587
|
+
body,
|
|
588
|
+
clientId,
|
|
589
|
+
keyHandle,
|
|
590
|
+
previous,
|
|
591
|
+
defaultApplicationOrigin: this.defaultApplicationOrigin(),
|
|
592
|
+
pinConnectorIdentity: (connectorId, publicKey) => this.pinConnectorIdentity(connectorId, publicKey)
|
|
593
|
+
});
|
|
599
594
|
if (previous?.keyHandle && previous.keyHandle !== keyHandle) {
|
|
600
595
|
void this.keyStore.delete(previous.keyHandle).catch(() => undefined);
|
|
601
596
|
}
|
|
602
|
-
const token = {
|
|
603
|
-
version: 1,
|
|
604
|
-
accessToken: body.access_token,
|
|
605
|
-
refreshToken: body.refresh_token,
|
|
606
|
-
clientId,
|
|
607
|
-
collectionId,
|
|
608
|
-
collectionName: body.collection_name ?? `Collection ${collectionId.slice(0, 8)}`,
|
|
609
|
-
operations: body.operations,
|
|
610
|
-
scope,
|
|
611
|
-
expiresAt: Date.now() + body.expires_in * 1_000,
|
|
612
|
-
refreshExpiresAt: body.refresh_expires_in
|
|
613
|
-
? Date.now() + body.refresh_expires_in * 1_000
|
|
614
|
-
: undefined,
|
|
615
|
-
grantId: body.grant_id,
|
|
616
|
-
encryption: body.encryption ?? undefined,
|
|
617
|
-
fileCapability: body.file_capability ?? undefined,
|
|
618
|
-
applicationOrigin: body.application_origin ?? this.defaultApplicationOrigin(),
|
|
619
|
-
keyHandle,
|
|
620
|
-
savedAt: Date.now(),
|
|
621
|
-
authority: body.authority ? {
|
|
622
|
-
operationsUrl: body.authority.operations_url,
|
|
623
|
-
syncUrl: body.authority.sync_url,
|
|
624
|
-
filesUrl: body.authority.files_url,
|
|
625
|
-
replicaId: body.authority.replica_id,
|
|
626
|
-
accessToken: body.authority.access_token,
|
|
627
|
-
proofPublicKey: body.authority.proof_public_key
|
|
628
|
-
} : undefined
|
|
629
|
-
};
|
|
630
597
|
this.storage.setItem(this.tokenKey(collectionId), JSON.stringify(token));
|
|
631
598
|
this.addConnectionId(collectionId);
|
|
632
599
|
this.invalidatedConnections.delete(collectionId);
|
|
@@ -659,12 +626,14 @@ export class MdbaseConnectInternals {
|
|
|
659
626
|
: "The application installation identity is unavailable.", { cause });
|
|
660
627
|
}
|
|
661
628
|
}
|
|
662
|
-
removeToken(collectionId, keyHandle, reason = "authorization_lost") {
|
|
629
|
+
removeToken(collectionId, keyHandle, reason = "authorization_lost", discardPending = false) {
|
|
663
630
|
this.invalidatedConnections.set(collectionId, reason);
|
|
664
|
-
if (keyHandle)
|
|
631
|
+
if (keyHandle && (discardPending || !this.pendingMutationsUseKey(collectionId, keyHandle))) {
|
|
665
632
|
void this.keyStore.delete(keyHandle).catch(() => undefined);
|
|
633
|
+
}
|
|
666
634
|
this.storage.removeItem(this.tokenKey(collectionId));
|
|
667
|
-
|
|
635
|
+
if (discardPending)
|
|
636
|
+
this.removePendingMutations(collectionId);
|
|
668
637
|
for (const transport of ["web_push", "fcm"]) {
|
|
669
638
|
this.storage.removeItem(this.notificationKey(collectionId, transport));
|
|
670
639
|
}
|
|
@@ -681,6 +650,33 @@ export class MdbaseConnectInternals {
|
|
|
681
650
|
pendingMutationKey(collectionId) {
|
|
682
651
|
return `${this.storagePrefix()}:pending-mutation:${collectionId}`;
|
|
683
652
|
}
|
|
653
|
+
removePendingMutations(collectionId) {
|
|
654
|
+
const baseKey = this.pendingMutationKey(collectionId);
|
|
655
|
+
const keys = [baseKey];
|
|
656
|
+
for (let index = 0; index < this.storage.length; index += 1) {
|
|
657
|
+
const key = this.storage.key(index);
|
|
658
|
+
if (key?.startsWith(`${baseKey}:`))
|
|
659
|
+
keys.push(key);
|
|
660
|
+
}
|
|
661
|
+
for (const key of keys) {
|
|
662
|
+
const pending = parseStored(this.storage.getItem(key));
|
|
663
|
+
if (pending?.keyHandle)
|
|
664
|
+
void this.keyStore.delete(pending.keyHandle).catch(() => undefined);
|
|
665
|
+
this.storage.removeItem(key);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
pendingMutationsUseKey(collectionId, keyHandle) {
|
|
669
|
+
const baseKey = this.pendingMutationKey(collectionId);
|
|
670
|
+
for (let index = 0; index < this.storage.length; index += 1) {
|
|
671
|
+
const key = this.storage.key(index);
|
|
672
|
+
if (key !== baseKey && !key?.startsWith(`${baseKey}:`))
|
|
673
|
+
continue;
|
|
674
|
+
const pending = parseStored(this.storage.getItem(key));
|
|
675
|
+
if (pending?.keyHandle === keyHandle)
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
684
680
|
notificationKey(collectionId, transport = "web_push") {
|
|
685
681
|
return `${this.storagePrefix()}:notifications:${collectionId}:${transport}`;
|
|
686
682
|
}
|
|
@@ -718,3 +714,8 @@ export class MdbaseConnectInternals {
|
|
|
718
714
|
listener(connections);
|
|
719
715
|
}
|
|
720
716
|
}
|
|
717
|
+
function authorizationAbort(signal, message, cause) {
|
|
718
|
+
if (signal.reason instanceof MdbaseConnectError)
|
|
719
|
+
return signal.reason;
|
|
720
|
+
return connectError("authorization_cancelled", message, { cause });
|
|
721
|
+
}
|
|
@@ -6,8 +6,8 @@ export interface ConnectionFileTransportOptions {
|
|
|
6
6
|
keyStore: GrantKeyStore;
|
|
7
7
|
serverUrl: string;
|
|
8
8
|
loopbackUrl: string;
|
|
9
|
-
authorizedToken(): Promise<StoredToken | null>;
|
|
10
|
-
refreshAuthorization(): Promise<StoredToken>;
|
|
9
|
+
authorizedToken(signal?: AbortSignal): Promise<StoredToken | null>;
|
|
10
|
+
refreshAuthorization(signal?: AbortSignal): Promise<StoredToken>;
|
|
11
11
|
shouldAttemptDirect(token: StoredToken): Promise<boolean>;
|
|
12
12
|
onDirectAvailable(): void;
|
|
13
13
|
onDirectUnavailable(): void;
|
|
@@ -13,7 +13,7 @@ export class ConnectionFileTransport {
|
|
|
13
13
|
this.local = new LocalFileTransport(options);
|
|
14
14
|
}
|
|
15
15
|
async control(method, path = "", input, signal) {
|
|
16
|
-
const token = await this.requireFileToken();
|
|
16
|
+
const token = await this.requireFileToken(signal);
|
|
17
17
|
if (!token.authority) {
|
|
18
18
|
return this.local.control(token, method, path, input, signal);
|
|
19
19
|
}
|
|
@@ -26,14 +26,14 @@ export class ConnectionFileTransport {
|
|
|
26
26
|
return this.local.downloadChunk(session, chunkIndex, signal);
|
|
27
27
|
}
|
|
28
28
|
async downloadHostedPart(session, partIndex, expectedLength, signal) {
|
|
29
|
-
const token = await this.requireFileToken();
|
|
29
|
+
const token = await this.requireFileToken(signal);
|
|
30
30
|
if (!token.authority) {
|
|
31
31
|
throw connectError("not_remote_authority", "Hosted file delivery requires a remote authority endpoint.");
|
|
32
32
|
}
|
|
33
33
|
return performHostedFilePartRequest(token, `downloads/${encodeURIComponent(session.transfer_id)}/parts/${partIndex}`, expectedLength, signal, this.options.refreshAuthorization, this.options.authorityProofHeaders);
|
|
34
34
|
}
|
|
35
|
-
async requireFileToken() {
|
|
36
|
-
const token = await this.options.authorizedToken();
|
|
35
|
+
async requireFileToken(signal) {
|
|
36
|
+
const token = await this.options.authorizedToken(signal);
|
|
37
37
|
if (!token?.fileCapability) {
|
|
38
38
|
throw connectError("not_authorized", "This connection has no file capability.");
|
|
39
39
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import type { Application } from "./internal-types.js";
|
|
2
2
|
import type { StoredToken } from "./internal-types.js";
|
|
3
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
3
4
|
import type { MdbaseNativeNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNotificationRegistrationOptions } from "./notifications.js";
|
|
4
5
|
export interface ConnectionNotificationContext {
|
|
5
6
|
serverUrl: string;
|
|
6
7
|
storage: Storage;
|
|
7
|
-
authorizedToken(): Promise<StoredToken | null>;
|
|
8
|
-
register(): Promise<Application>;
|
|
8
|
+
authorizedToken(signal?: AbortSignal): Promise<StoredToken | null>;
|
|
9
|
+
register(signal?: AbortSignal): Promise<Application>;
|
|
9
10
|
notificationKey(transport?: "web_push" | "fcm"): string;
|
|
11
|
+
requestTimeoutMs: number | null;
|
|
10
12
|
}
|
|
11
13
|
export declare class ConnectionNotifications {
|
|
12
14
|
private readonly context;
|
|
13
15
|
constructor(context: ConnectionNotificationContext);
|
|
14
16
|
registerNotifications(options: MdbaseNotificationRegistrationOptions): Promise<MdbaseNotificationRegistration>;
|
|
17
|
+
private registerNotificationsWithinBudget;
|
|
15
18
|
/**
|
|
16
19
|
* Register an iOS or Android installation for Connect-managed FCM.
|
|
17
20
|
*
|
|
@@ -19,7 +22,10 @@ export declare class ConnectionNotifications {
|
|
|
19
22
|
* same installation whenever Firebase refreshes its token.
|
|
20
23
|
*/
|
|
21
24
|
registerNativeNotifications(options: MdbaseNativeNotificationRegistrationOptions): Promise<MdbaseNativeNotificationRegistration>;
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
private registerNativeNotificationsWithinBudget;
|
|
26
|
+
unregisterNativeNotifications(options?: ConnectRequestOptions): Promise<void>;
|
|
27
|
+
private unregisterNativeNotificationsWithinBudget;
|
|
28
|
+
unregisterNotifications(serviceWorker?: ServiceWorkerRegistration, options?: ConnectRequestOptions): Promise<void>;
|
|
29
|
+
private unregisterNotificationsWithinBudget;
|
|
24
30
|
private deleteNotificationChannel;
|
|
25
31
|
}
|