@mdbase-dev/connect 0.1.0-beta.31 → 0.1.0-beta.33
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 +112 -34
- package/dist/advanced.d.ts +9 -0
- package/dist/advanced.js +6 -0
- package/dist/application-session.d.ts +39 -14
- package/dist/application-session.js +201 -70
- 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 +36 -33
- package/dist/collection-client.js +549 -104
- package/dist/connect-authorization-helpers.d.ts +3 -0
- package/dist/connect-authorization-helpers.js +13 -0
- package/dist/connect-options.d.ts +12 -4
- package/dist/connect.d.ts +12 -4
- package/dist/connect.js +102 -103
- 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 +47 -46
- package/dist/connection.js +320 -162
- package/dist/crypto-entry.d.ts +7 -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 +23 -0
- package/dist/file-transfer-internals.js +224 -0
- package/dist/files.d.ts +36 -14
- package/dist/files.js +92 -222
- package/dist/hosted-file-request.js +3 -3
- package/dist/index.d.ts +24 -19
- package/dist/index.js +10 -18
- package/dist/internal-types.d.ts +9 -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 +17 -34
- package/dist/operation-types.d.ts +435 -45
- 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 +5 -4
- package/dist/session.js +6 -5
- 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/dist/watch-policy.d.ts +8 -0
- package/dist/watch-policy.js +12 -0
- package/package.json +12 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MdbaseConnectError, connectError } from "./errors.js";
|
|
2
|
+
export function declarationIdFromFamilyIdentity(familyIdentity) {
|
|
3
|
+
const prefix = "bundle:";
|
|
4
|
+
if (!familyIdentity.startsWith(prefix) || familyIdentity.length === prefix.length) {
|
|
5
|
+
throw new Error("The registered application has no valid declaration identity.");
|
|
6
|
+
}
|
|
7
|
+
return familyIdentity.slice(prefix.length);
|
|
8
|
+
}
|
|
9
|
+
export function authorizationAbort(signal, message, cause) {
|
|
10
|
+
if (signal.reason instanceof MdbaseConnectError)
|
|
11
|
+
return signal.reason;
|
|
12
|
+
return connectError("authorization_cancelled", message, { cause });
|
|
13
|
+
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { JsonObject, MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import type { GrantKeyStore } from "./crypto.js";
|
|
3
3
|
import type { ApplicationIdentityStore } from "./application-identity.js";
|
|
4
|
+
export interface MdbaseConnectTimeouts {
|
|
5
|
+
requestMs?: number | null;
|
|
6
|
+
watchStartMs?: number | null;
|
|
7
|
+
uploadMs?: number | null;
|
|
8
|
+
syncMs?: number | null;
|
|
9
|
+
}
|
|
4
10
|
export interface MdbaseConnectOptions {
|
|
5
|
-
serverUrl: string;
|
|
11
|
+
serverUrl: string | URL;
|
|
6
12
|
/**
|
|
7
13
|
* A bundled v1 application manifest or its app-local URL. String values
|
|
8
14
|
* are loaded by this SDK and posted inline; Connect never fetches them.
|
|
9
15
|
*/
|
|
10
|
-
manifest?: MdbaseAppManifest | string;
|
|
11
|
-
redirectUri?: string;
|
|
16
|
+
manifest?: MdbaseAppManifest | string | URL;
|
|
17
|
+
redirectUri?: string | URL;
|
|
12
18
|
storage?: Storage;
|
|
13
19
|
/** Encrypted relay is required by default for newly authorized grants. */
|
|
14
20
|
relayEncryption?: "required" | "disabled";
|
|
@@ -18,8 +24,10 @@ export interface MdbaseConnectOptions {
|
|
|
18
24
|
/** Prefer same-computer connector access when the browser permits it. */
|
|
19
25
|
directAccess?: "auto" | "disabled";
|
|
20
26
|
/** Loopback origin override for development and automated testing. */
|
|
21
|
-
loopbackUrl?: string;
|
|
27
|
+
loopbackUrl?: string | URL;
|
|
22
28
|
/** Override browser navigation, for example to use a native system browser. */
|
|
23
29
|
navigate?: (url: string) => void | Promise<void>;
|
|
30
|
+
/** Workload-specific defaults. Per-call timeoutMs always wins. */
|
|
31
|
+
timeouts?: MdbaseConnectTimeouts;
|
|
24
32
|
}
|
|
25
33
|
export type MdbaseFrontmatter = JsonObject;
|
package/dist/connect.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { MdbaseConnectOptions } from "./connect-options.js";
|
|
|
5
5
|
import type { MdbaseConnectionInfo } from "./connection-types.js";
|
|
6
6
|
import { type GrantKeyStore } from "./crypto.js";
|
|
7
7
|
import { type Application, type StoredToken } from "./internal-types.js";
|
|
8
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
9
|
+
import { type ResolvedConnectTimeouts } from "./request-budget.js";
|
|
8
10
|
import type { MdbaseUnavailableReason } from "./session.js";
|
|
9
11
|
export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
|
|
10
12
|
readonly serverUrl: string;
|
|
@@ -18,6 +20,7 @@ export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
|
|
|
18
20
|
readonly directAccessMode: "auto" | "disabled";
|
|
19
21
|
readonly loopbackUrl: string;
|
|
20
22
|
readonly navigate?: (url: string) => void | Promise<void>;
|
|
23
|
+
readonly timeouts: ResolvedConnectTimeouts;
|
|
21
24
|
readonly credentialStorage: MdbaseConnectEnvironment["credentialStorage"];
|
|
22
25
|
private application;
|
|
23
26
|
private manifestPromise;
|
|
@@ -27,13 +30,16 @@ export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
|
|
|
27
30
|
private readonly invalidatedConnections;
|
|
28
31
|
constructor(options: MdbaseConnectOptions);
|
|
29
32
|
environment(): MdbaseConnectEnvironment;
|
|
30
|
-
register(): Promise<Application>;
|
|
31
|
-
|
|
33
|
+
register(options?: ConnectRequestOptions): Promise<Application>;
|
|
34
|
+
private registerWithinBudget;
|
|
35
|
+
manifestDeclaration(options?: ConnectRequestOptions): Promise<MdbaseAppManifest>;
|
|
32
36
|
private loadManifest;
|
|
33
37
|
authorize(options?: MdbaseAuthorizeOptions): Promise<MdbaseAuthorizationOutcome<Frontmatter>>;
|
|
38
|
+
private authorizeWithinBudget;
|
|
34
39
|
private authorizeWeb;
|
|
35
40
|
private authorizePortable;
|
|
36
|
-
completeAuthorization(callbackUrl: string): Promise<MdbaseAuthorizationResult<Frontmatter>>;
|
|
41
|
+
completeAuthorization(callbackUrl: string, options?: ConnectRequestOptions): Promise<MdbaseAuthorizationResult<Frontmatter>>;
|
|
42
|
+
private completeAuthorizationWithinBudget;
|
|
37
43
|
private performAuthorizationCompletion;
|
|
38
44
|
connections(): MdbaseConnectionInfo[];
|
|
39
45
|
connection(collectionId: string): MdbaseConnection<Frontmatter> | null;
|
|
@@ -44,9 +50,11 @@ export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
|
|
|
44
50
|
private pinConnectorIdentity;
|
|
45
51
|
private connectorPinKey;
|
|
46
52
|
private loadApplicationIdentity;
|
|
47
|
-
removeToken(collectionId: string, keyHandle?: string, reason?: MdbaseUnavailableReason): void;
|
|
53
|
+
removeToken(collectionId: string, keyHandle?: string, reason?: MdbaseUnavailableReason, discardPending?: boolean): void;
|
|
48
54
|
tokenKey(collectionId: string): string;
|
|
49
55
|
pendingMutationKey(collectionId: string): string;
|
|
56
|
+
private removePendingMutations;
|
|
57
|
+
private pendingMutationsUseKey;
|
|
50
58
|
notificationKey(collectionId: string, transport?: "web_push" | "fcm"): string;
|
|
51
59
|
directPreferenceKey(): string;
|
|
52
60
|
defaultApplicationOrigin(): string;
|
package/dist/connect.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
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 {
|
|
6
|
+
import { authorizationAbort, declarationIdFromFamilyIdentity } from "./connect-authorization-helpers.js";
|
|
7
|
+
import { IndexedDbGrantKeyStore, MemoryGrantKeyStore } from "./crypto.js";
|
|
7
8
|
import { connectError, serverConnectError } from "./errors.js";
|
|
8
9
|
import { DEFAULT_OPERATIONS } from "./internal-types.js";
|
|
9
10
|
import { uniqueOperations } from "./operation-helpers.js";
|
|
10
|
-
import {
|
|
11
|
+
import { resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
|
|
12
|
+
import { MemoryStorage, apiError, applicationStorageOrigin, canonicalLoopbackUrl, collectionIdFromTokenKey, connectFetch, createPkce, defaultManifestSource, defaultRedirectUri, defaultStorage, decodeJsonResponse, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseStored, stripTrailingSlash, validAuthorityTokenResponse } from "./runtime-utils.js";
|
|
13
|
+
import { storedTokenFromResponse } from "./token-response.js";
|
|
11
14
|
export class MdbaseConnectInternals {
|
|
12
15
|
serverUrl;
|
|
13
16
|
manifest;
|
|
@@ -20,6 +23,7 @@ export class MdbaseConnectInternals {
|
|
|
20
23
|
directAccessMode;
|
|
21
24
|
loopbackUrl;
|
|
22
25
|
navigate;
|
|
26
|
+
timeouts;
|
|
23
27
|
credentialStorage;
|
|
24
28
|
application = null;
|
|
25
29
|
manifestPromise = null;
|
|
@@ -28,17 +32,19 @@ export class MdbaseConnectInternals {
|
|
|
28
32
|
listeners = new Set();
|
|
29
33
|
invalidatedConnections = new Map();
|
|
30
34
|
constructor(options) {
|
|
31
|
-
this.serverUrl = stripTrailingSlash(options.serverUrl);
|
|
32
|
-
this.manifest = options.manifest
|
|
35
|
+
this.serverUrl = stripTrailingSlash(String(options.serverUrl));
|
|
36
|
+
this.manifest = options.manifest instanceof URL
|
|
37
|
+
? options.manifest.href
|
|
38
|
+
: options.manifest ?? defaultManifestSource();
|
|
33
39
|
this.manifestSource = typeof this.manifest === "string"
|
|
34
40
|
? this.manifest
|
|
35
41
|
: this.manifest.distribution === "portable"
|
|
36
42
|
? `bundle:${this.manifest.id}:${manifestStorageFingerprint(this.manifest)}`
|
|
37
43
|
: `bundle:${this.manifest.id}`;
|
|
38
44
|
const opaquePortable = isOpaquePortableManifest(this.manifest);
|
|
39
|
-
this.redirectUri = options.redirectUri
|
|
45
|
+
this.redirectUri = options.redirectUri === undefined ? (typeof this.manifest !== "string" && this.manifest.distribution === "portable"
|
|
40
46
|
? ""
|
|
41
|
-
: defaultRedirectUri());
|
|
47
|
+
: defaultRedirectUri()) : String(options.redirectUri);
|
|
42
48
|
this.storage = options.storage ?? defaultStorage(opaquePortable);
|
|
43
49
|
this.relayEncryption = options.relayEncryption ?? "required";
|
|
44
50
|
this.keyStore = options.keyStore ?? (opaquePortable ? new MemoryGrantKeyStore() : new IndexedDbGrantKeyStore());
|
|
@@ -49,8 +55,9 @@ export class MdbaseConnectInternals {
|
|
|
49
55
|
? "custom"
|
|
50
56
|
: this.storage instanceof MemoryStorage ? "memory" : "persistent";
|
|
51
57
|
this.directAccessMode = options.directAccess ?? "auto";
|
|
52
|
-
this.loopbackUrl = canonicalLoopbackUrl(options.loopbackUrl ?? `http://127.0.0.1:${DEFAULT_LOOPBACK_PORT}`);
|
|
58
|
+
this.loopbackUrl = canonicalLoopbackUrl(String(options.loopbackUrl ?? `http://127.0.0.1:${DEFAULT_LOOPBACK_PORT}`));
|
|
53
59
|
this.navigate = options.navigate;
|
|
60
|
+
this.timeouts = resolveConnectTimeouts(options.timeouts);
|
|
54
61
|
if (typeof window !== "undefined" && this.storage === window.localStorage) {
|
|
55
62
|
window.addEventListener("storage", (event) => {
|
|
56
63
|
if (event.storageArea !== this.storage || !event.key?.startsWith(this.storagePrefix()))
|
|
@@ -73,34 +80,45 @@ export class MdbaseConnectInternals {
|
|
|
73
80
|
credentialStorage: this.credentialStorage
|
|
74
81
|
};
|
|
75
82
|
}
|
|
76
|
-
|
|
83
|
+
register(options = {}) {
|
|
84
|
+
return withRequestBudget(options, this.timeouts.requestMs, (budget) => this.registerWithinBudget(budget.signal));
|
|
85
|
+
}
|
|
86
|
+
async registerWithinBudget(signal) {
|
|
77
87
|
if (this.application)
|
|
78
88
|
return this.application;
|
|
79
89
|
const response = await connectFetch(`${this.serverUrl}/v1/apps/register`, {
|
|
80
90
|
method: "POST",
|
|
81
91
|
headers: { "content-type": "application/json" },
|
|
82
|
-
body: JSON.stringify({ manifest: await this.manifestDeclaration() })
|
|
92
|
+
body: JSON.stringify({ manifest: await this.manifestDeclaration({ signal, timeoutMs: null }) }),
|
|
93
|
+
signal
|
|
83
94
|
}, "temporarily_unavailable", "Application registration is temporarily unavailable.");
|
|
84
|
-
const body = await response.
|
|
95
|
+
const body = await decodeJsonResponse(response, "invalid_operation_response", "Connect returned an invalid application registration response.");
|
|
85
96
|
if (!response.ok)
|
|
86
97
|
throw apiError(body, "discovery_failed", "Application discovery failed.", response.status);
|
|
87
98
|
this.application = body.application;
|
|
88
99
|
return this.application;
|
|
89
100
|
}
|
|
90
|
-
manifestDeclaration() {
|
|
101
|
+
manifestDeclaration(options = {}) {
|
|
91
102
|
if (this.manifestPromise)
|
|
92
103
|
return this.manifestPromise;
|
|
93
|
-
this.
|
|
104
|
+
const loading = withRequestBudget(options, this.timeouts.requestMs, (budget) => this.loadManifest(budget.signal));
|
|
105
|
+
const shared = loading.catch((error) => {
|
|
106
|
+
if (this.manifestPromise === shared)
|
|
107
|
+
this.manifestPromise = null;
|
|
108
|
+
throw error;
|
|
109
|
+
});
|
|
110
|
+
this.manifestPromise = shared;
|
|
94
111
|
return this.manifestPromise;
|
|
95
112
|
}
|
|
96
|
-
async loadManifest() {
|
|
113
|
+
async loadManifest(signal) {
|
|
97
114
|
if (typeof this.manifest !== "string")
|
|
98
115
|
return this.manifest;
|
|
99
116
|
const source = this.manifest;
|
|
100
117
|
let response;
|
|
101
118
|
try {
|
|
102
119
|
response = await fetch(source, {
|
|
103
|
-
headers: { accept: "application/json" }
|
|
120
|
+
headers: { accept: "application/json" },
|
|
121
|
+
signal
|
|
104
122
|
});
|
|
105
123
|
}
|
|
106
124
|
catch (cause) {
|
|
@@ -110,7 +128,7 @@ export class MdbaseConnectInternals {
|
|
|
110
128
|
throw connectError("manifest_load_failed", `The bundled application declaration returned HTTP ${response.status}.`, { status: response.status });
|
|
111
129
|
}
|
|
112
130
|
try {
|
|
113
|
-
return await response.
|
|
131
|
+
return await decodeJsonResponse(response, "invalid_application_manifest", "The bundled application declaration is not valid JSON.");
|
|
114
132
|
}
|
|
115
133
|
catch (cause) {
|
|
116
134
|
throw connectError("invalid_application_manifest", "The bundled application declaration is not valid JSON.", {
|
|
@@ -127,6 +145,9 @@ export class MdbaseConnectInternals {
|
|
|
127
145
|
}
|
|
128
146
|
}
|
|
129
147
|
async authorize(options = {}) {
|
|
148
|
+
return withRequestBudget(options, this.timeouts.requestMs, (budget) => this.authorizeWithinBudget({ ...options, signal: budget.signal, timeoutMs: null }));
|
|
149
|
+
}
|
|
150
|
+
async authorizeWithinBudget(options) {
|
|
130
151
|
if (typeof location === "undefined" && !this.navigate && !options.openVerification) {
|
|
131
152
|
throw connectError("browser_required", "Authorization navigation requires a browser environment.");
|
|
132
153
|
}
|
|
@@ -140,7 +161,7 @@ export class MdbaseConnectInternals {
|
|
|
140
161
|
: null;
|
|
141
162
|
let application;
|
|
142
163
|
try {
|
|
143
|
-
application = await this.
|
|
164
|
+
application = await this.registerWithinBudget(options.signal);
|
|
144
165
|
}
|
|
145
166
|
catch (error) {
|
|
146
167
|
popup?.close();
|
|
@@ -177,6 +198,7 @@ export class MdbaseConnectInternals {
|
|
|
177
198
|
protocol_version: APPLICATION_AUTHORIZATION_PROTOCOL_VERSION,
|
|
178
199
|
authorization_id: authorizationId,
|
|
179
200
|
application_id: application.id,
|
|
201
|
+
application_declaration_id: declarationIdFromFamilyIdentity(application.family_identity),
|
|
180
202
|
application_manifest_digest: application.manifest_digest,
|
|
181
203
|
application_installation_id: await applicationInstallationId(installation),
|
|
182
204
|
installation_signing_public_key: installation.signingPublicKey,
|
|
@@ -189,6 +211,7 @@ export class MdbaseConnectInternals {
|
|
|
189
211
|
redirect_uri: this.redirectUri,
|
|
190
212
|
state,
|
|
191
213
|
code_challenge: challenge,
|
|
214
|
+
contracts: authorizationContractRequirements(operations, application.requirements?.files),
|
|
192
215
|
requested_operations: operations,
|
|
193
216
|
...(application.requirements?.files
|
|
194
217
|
? { requested_files: application.requirements.files }
|
|
@@ -226,7 +249,7 @@ export class MdbaseConnectInternals {
|
|
|
226
249
|
}),
|
|
227
250
|
signal: options.signal
|
|
228
251
|
});
|
|
229
|
-
const body = await response.
|
|
252
|
+
const body = await decodeJsonResponse(response, "invalid_device_authorization_response", "The authorization service returned an invalid authorization response.");
|
|
230
253
|
if (!response.ok) {
|
|
231
254
|
throw apiError(body, "device_authorization_failed", "Application authorization could not be started.", response.status);
|
|
232
255
|
}
|
|
@@ -249,7 +272,7 @@ export class MdbaseConnectInternals {
|
|
|
249
272
|
this.storage.removeItem(this.pendingKey(state));
|
|
250
273
|
await this.keyStore.delete(keyHandle);
|
|
251
274
|
if (options.signal?.aborted) {
|
|
252
|
-
throw
|
|
275
|
+
throw authorizationAbort(options.signal, "Application authorization was cancelled.", error);
|
|
253
276
|
}
|
|
254
277
|
throw error;
|
|
255
278
|
}
|
|
@@ -278,6 +301,7 @@ export class MdbaseConnectInternals {
|
|
|
278
301
|
protocol_version: APPLICATION_AUTHORIZATION_PROTOCOL_VERSION,
|
|
279
302
|
authorization_id: authorizationId,
|
|
280
303
|
application_id: application.id,
|
|
304
|
+
application_declaration_id: declarationIdFromFamilyIdentity(application.family_identity),
|
|
281
305
|
application_manifest_digest: application.manifest_digest,
|
|
282
306
|
application_installation_id: await applicationInstallationId(installation),
|
|
283
307
|
installation_signing_public_key: installation.signingPublicKey,
|
|
@@ -288,6 +312,7 @@ export class MdbaseConnectInternals {
|
|
|
288
312
|
issued_at: issuedAt.toISOString(),
|
|
289
313
|
expires_at: new Date(issuedAt.getTime() + 10 * 60 * 1_000).toISOString(),
|
|
290
314
|
code_challenge: challenge,
|
|
315
|
+
contracts: authorizationContractRequirements(operations, application.requirements?.files),
|
|
291
316
|
requested_operations: operations,
|
|
292
317
|
...(application.requirements?.files
|
|
293
318
|
? { requested_files: application.requirements.files }
|
|
@@ -311,9 +336,10 @@ export class MdbaseConnectInternals {
|
|
|
311
336
|
code_challenge: challenge,
|
|
312
337
|
code_challenge_method: "S256",
|
|
313
338
|
application_authorization: JSON.stringify(proof)
|
|
314
|
-
})
|
|
339
|
+
}),
|
|
340
|
+
signal: options.signal
|
|
315
341
|
});
|
|
316
|
-
body = await response.
|
|
342
|
+
body = await decodeJsonResponse(response, "invalid_device_authorization_response", "The authorization service returned an invalid device authorization response.");
|
|
317
343
|
}
|
|
318
344
|
catch (cause) {
|
|
319
345
|
popup?.close();
|
|
@@ -333,7 +359,7 @@ export class MdbaseConnectInternals {
|
|
|
333
359
|
popup?.close();
|
|
334
360
|
await this.keyStore.delete(keyHandle);
|
|
335
361
|
if (options.signal?.aborted) {
|
|
336
|
-
throw
|
|
362
|
+
throw authorizationAbort(options.signal, "Downloaded application authorization was cancelled.", error);
|
|
337
363
|
}
|
|
338
364
|
throw error;
|
|
339
365
|
}
|
|
@@ -366,7 +392,7 @@ export class MdbaseConnectInternals {
|
|
|
366
392
|
while (Date.now() < authorization.expiresAt) {
|
|
367
393
|
await abortableDelay(intervalSeconds * 1_000, options.signal);
|
|
368
394
|
if (options.signal?.aborted) {
|
|
369
|
-
throw
|
|
395
|
+
throw authorizationAbort(options.signal, "Downloaded application authorization was cancelled.");
|
|
370
396
|
}
|
|
371
397
|
let tokenResponse;
|
|
372
398
|
try {
|
|
@@ -384,11 +410,11 @@ export class MdbaseConnectInternals {
|
|
|
384
410
|
}
|
|
385
411
|
catch (cause) {
|
|
386
412
|
if (options.signal?.aborted) {
|
|
387
|
-
throw
|
|
413
|
+
throw authorizationAbort(options.signal, "Downloaded application authorization was cancelled.", cause);
|
|
388
414
|
}
|
|
389
415
|
continue;
|
|
390
416
|
}
|
|
391
|
-
const tokenBody = await tokenResponse.
|
|
417
|
+
const tokenBody = await decodeJsonResponse(tokenResponse, "invalid_token_response", "The authorization service returned an invalid token response.");
|
|
392
418
|
if (!tokenResponse.ok) {
|
|
393
419
|
const code = oauthErrorCode(tokenBody);
|
|
394
420
|
if (code === "authorization_pending") {
|
|
@@ -402,7 +428,7 @@ export class MdbaseConnectInternals {
|
|
|
402
428
|
}
|
|
403
429
|
const authority = validAuthorityTokenResponse(tokenBody.authority, tokenBody.collection_id);
|
|
404
430
|
const localEncryption = tokenBody.encryption
|
|
405
|
-
&& tokenBody.encryption.protocol_version ===
|
|
431
|
+
&& tokenBody.encryption.protocol_version === GRANT_ENCRYPTION_PROTOCOL_VERSION
|
|
406
432
|
&& tokenBody.encryption.suite === RELAY_ENCRYPTION_SUITE
|
|
407
433
|
&& tokenBody.encryption.application_agreement_public_key
|
|
408
434
|
=== grantKey.agreementPublicKey;
|
|
@@ -437,12 +463,15 @@ export class MdbaseConnectInternals {
|
|
|
437
463
|
popup?.close();
|
|
438
464
|
await this.keyStore.delete(keyHandle);
|
|
439
465
|
if (options.signal?.aborted) {
|
|
440
|
-
throw
|
|
466
|
+
throw authorizationAbort(options.signal, "Downloaded application authorization was cancelled.", error);
|
|
441
467
|
}
|
|
442
468
|
throw error;
|
|
443
469
|
}
|
|
444
470
|
}
|
|
445
|
-
completeAuthorization(callbackUrl) {
|
|
471
|
+
completeAuthorization(callbackUrl, options = {}) {
|
|
472
|
+
return withRequestBudget(options, this.timeouts.requestMs, (budget) => this.completeAuthorizationWithinBudget(callbackUrl, budget.signal));
|
|
473
|
+
}
|
|
474
|
+
completeAuthorizationWithinBudget(callbackUrl, signal) {
|
|
446
475
|
const state = new URL(callbackUrl).searchParams.get("state");
|
|
447
476
|
if (!state) {
|
|
448
477
|
return Promise.reject(connectError("invalid_callback", "Authorization callback is missing its state."));
|
|
@@ -450,7 +479,7 @@ export class MdbaseConnectInternals {
|
|
|
450
479
|
const existing = this.completionPromises.get(state);
|
|
451
480
|
if (existing)
|
|
452
481
|
return existing;
|
|
453
|
-
const completion = this.performAuthorizationCompletion(callbackUrl, state);
|
|
482
|
+
const completion = this.performAuthorizationCompletion(callbackUrl, state, signal);
|
|
454
483
|
const shared = completion.finally(() => {
|
|
455
484
|
if (this.completionPromises.get(state) === shared)
|
|
456
485
|
this.completionPromises.delete(state);
|
|
@@ -458,7 +487,7 @@ export class MdbaseConnectInternals {
|
|
|
458
487
|
this.completionPromises.set(state, shared);
|
|
459
488
|
return shared;
|
|
460
489
|
}
|
|
461
|
-
async performAuthorizationCompletion(callbackUrl, state) {
|
|
490
|
+
async performAuthorizationCompletion(callbackUrl, state, signal) {
|
|
462
491
|
const callback = new URL(callbackUrl);
|
|
463
492
|
const code = callback.searchParams.get("code");
|
|
464
493
|
const pendingKey = this.pendingKey(state);
|
|
@@ -484,9 +513,10 @@ export class MdbaseConnectInternals {
|
|
|
484
513
|
client_id: pending.clientId,
|
|
485
514
|
redirect_uri: pending.redirectUri,
|
|
486
515
|
code_verifier: pending.verifier
|
|
487
|
-
})
|
|
516
|
+
}),
|
|
517
|
+
signal
|
|
488
518
|
}, "temporarily_unavailable", "Authorization completion is temporarily unavailable.");
|
|
489
|
-
const body = await response.
|
|
519
|
+
const body = await decodeJsonResponse(response, "invalid_token_response", "The authorization service returned an invalid token response.");
|
|
490
520
|
if (!response.ok)
|
|
491
521
|
throw apiError(body, "token_exchange_failed", "Authorization could not be completed.", response.status);
|
|
492
522
|
if (pending.relayEncryption === "required" && !body.authority && (!body.encryption
|
|
@@ -555,78 +585,18 @@ export class MdbaseConnectInternals {
|
|
|
555
585
|
if (typeof collectionId !== "string") {
|
|
556
586
|
throw connectError("invalid_token_response", "Authorization returned no collection ID.");
|
|
557
587
|
}
|
|
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
588
|
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
|
-
}
|
|
589
|
+
const token = storedTokenFromResponse({
|
|
590
|
+
body,
|
|
591
|
+
clientId,
|
|
592
|
+
keyHandle,
|
|
593
|
+
previous,
|
|
594
|
+
defaultApplicationOrigin: this.defaultApplicationOrigin(),
|
|
595
|
+
pinConnectorIdentity: (connectorId, publicKey) => this.pinConnectorIdentity(connectorId, publicKey)
|
|
596
|
+
});
|
|
599
597
|
if (previous?.keyHandle && previous.keyHandle !== keyHandle) {
|
|
600
598
|
void this.keyStore.delete(previous.keyHandle).catch(() => undefined);
|
|
601
599
|
}
|
|
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
600
|
this.storage.setItem(this.tokenKey(collectionId), JSON.stringify(token));
|
|
631
601
|
this.addConnectionId(collectionId);
|
|
632
602
|
this.invalidatedConnections.delete(collectionId);
|
|
@@ -659,12 +629,14 @@ export class MdbaseConnectInternals {
|
|
|
659
629
|
: "The application installation identity is unavailable.", { cause });
|
|
660
630
|
}
|
|
661
631
|
}
|
|
662
|
-
removeToken(collectionId, keyHandle, reason = "authorization_lost") {
|
|
632
|
+
removeToken(collectionId, keyHandle, reason = "authorization_lost", discardPending = false) {
|
|
663
633
|
this.invalidatedConnections.set(collectionId, reason);
|
|
664
|
-
if (keyHandle)
|
|
634
|
+
if (keyHandle && (discardPending || !this.pendingMutationsUseKey(collectionId, keyHandle))) {
|
|
665
635
|
void this.keyStore.delete(keyHandle).catch(() => undefined);
|
|
636
|
+
}
|
|
666
637
|
this.storage.removeItem(this.tokenKey(collectionId));
|
|
667
|
-
|
|
638
|
+
if (discardPending)
|
|
639
|
+
this.removePendingMutations(collectionId);
|
|
668
640
|
for (const transport of ["web_push", "fcm"]) {
|
|
669
641
|
this.storage.removeItem(this.notificationKey(collectionId, transport));
|
|
670
642
|
}
|
|
@@ -681,6 +653,33 @@ export class MdbaseConnectInternals {
|
|
|
681
653
|
pendingMutationKey(collectionId) {
|
|
682
654
|
return `${this.storagePrefix()}:pending-mutation:${collectionId}`;
|
|
683
655
|
}
|
|
656
|
+
removePendingMutations(collectionId) {
|
|
657
|
+
const baseKey = this.pendingMutationKey(collectionId);
|
|
658
|
+
const keys = [baseKey];
|
|
659
|
+
for (let index = 0; index < this.storage.length; index += 1) {
|
|
660
|
+
const key = this.storage.key(index);
|
|
661
|
+
if (key?.startsWith(`${baseKey}:`))
|
|
662
|
+
keys.push(key);
|
|
663
|
+
}
|
|
664
|
+
for (const key of keys) {
|
|
665
|
+
const pending = parseStored(this.storage.getItem(key));
|
|
666
|
+
if (pending?.keyHandle)
|
|
667
|
+
void this.keyStore.delete(pending.keyHandle).catch(() => undefined);
|
|
668
|
+
this.storage.removeItem(key);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
pendingMutationsUseKey(collectionId, keyHandle) {
|
|
672
|
+
const baseKey = this.pendingMutationKey(collectionId);
|
|
673
|
+
for (let index = 0; index < this.storage.length; index += 1) {
|
|
674
|
+
const key = this.storage.key(index);
|
|
675
|
+
if (key !== baseKey && !key?.startsWith(`${baseKey}:`))
|
|
676
|
+
continue;
|
|
677
|
+
const pending = parseStored(this.storage.getItem(key));
|
|
678
|
+
if (pending?.keyHandle === keyHandle)
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
684
683
|
notificationKey(collectionId, transport = "web_push") {
|
|
685
684
|
return `${this.storagePrefix()}:notifications:${collectionId}:${transport}`;
|
|
686
685
|
}
|
|
@@ -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
|
}
|