@mdbase-dev/connect 0.1.0-beta.27 → 0.1.0-beta.28
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 +43 -30
- package/dist/application-identity.d.ts +32 -3
- package/dist/application-identity.js +104 -12
- package/dist/application-session.d.ts +120 -0
- package/dist/application-session.js +281 -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/capabilities.d.ts +23 -0
- package/dist/capabilities.js +112 -0
- package/dist/connect-options.d.ts +3 -0
- package/dist/connect.d.ts +8 -0
- package/dist/connect.js +73 -102
- package/dist/connection-types.d.ts +7 -0
- package/dist/connection.d.ts +1 -13
- package/dist/connection.js +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/internal-types.d.ts +0 -1
- package/dist/mdbase-connect.d.ts +6 -2
- package/dist/mdbase-connect.js +10 -3
- package/dist/outcomes.d.ts +3 -3
- package/dist/outcomes.js +4 -2
- package/dist/runtime-utils.d.ts +2 -0
- package/dist/runtime-utils.js +23 -0
- package/dist/selection.d.ts +3 -3
- package/dist/session.d.ts +2 -2
- package/package.json +2 -2
- package/dist/first-contact-presentation.d.ts +0 -5
- package/dist/first-contact-presentation.js +0 -51
- package/dist/first-contact.d.ts +0 -14
- package/dist/first-contact.js +0 -145
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384-
|
|
1
|
+
sha384-F1O89BbCjA89Uv9ARtbs7XDJ4AITt+2tUQE574UdH73CaBSOZPhgI9Z8UdC8AN5P
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ApplicationCapabilityId, type ApplicationCapabilityRequirements, type MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import type { MdbaseConnectionInfo } from "./connection-types.js";
|
|
3
|
+
export type MdbaseCapabilityState = "available" | "requires_authorization" | "requires_setup" | "temporarily_unavailable" | "unsupported";
|
|
4
|
+
export interface MdbaseCapabilityEvidence {
|
|
5
|
+
source: "application" | "authorization" | "authority" | "runtime";
|
|
6
|
+
fact: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MdbaseEffectiveCapability {
|
|
9
|
+
id: ApplicationCapabilityId;
|
|
10
|
+
requirement: "required" | "optional";
|
|
11
|
+
state: MdbaseCapabilityState;
|
|
12
|
+
operations: string[];
|
|
13
|
+
missingOperations: string[];
|
|
14
|
+
evidence: MdbaseCapabilityEvidence[];
|
|
15
|
+
reason?: string;
|
|
16
|
+
details?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface MdbaseEffectiveCapabilities {
|
|
19
|
+
contractVersion: 1;
|
|
20
|
+
values: Partial<Record<ApplicationCapabilityId, MdbaseEffectiveCapability>>;
|
|
21
|
+
requiredAvailable: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function effectiveCapabilities(requirements: ApplicationCapabilityRequirements, manifest: MdbaseAppManifest, connection: MdbaseConnectionInfo): MdbaseEffectiveCapabilities;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { capabilityOperations } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
export function effectiveCapabilities(requirements, manifest, connection) {
|
|
3
|
+
const required = new Set(requirements.required);
|
|
4
|
+
const declared = [...new Set([...requirements.required, ...(requirements.optional ?? [])])];
|
|
5
|
+
const values = {};
|
|
6
|
+
for (const id of declared) {
|
|
7
|
+
values[id] = effectiveCapability(id, required.has(id), manifest, connection);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
contractVersion: 1,
|
|
11
|
+
values,
|
|
12
|
+
requiredAvailable: requirements.required.every((id) => values[id]?.state === "available")
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function effectiveCapability(id, required, manifest, connection) {
|
|
16
|
+
const operations = capabilityOperations(id);
|
|
17
|
+
const missingOperations = operations.filter((operation) => !connection.operations.includes(operation));
|
|
18
|
+
const base = {
|
|
19
|
+
id,
|
|
20
|
+
requirement: required ? "required" : "optional",
|
|
21
|
+
operations,
|
|
22
|
+
missingOperations,
|
|
23
|
+
evidence: [{
|
|
24
|
+
source: "application",
|
|
25
|
+
fact: `${required ? "Required" : "Optional"} in capability contract v1.`
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
if (missingOperations.length > 0) {
|
|
29
|
+
return {
|
|
30
|
+
...base,
|
|
31
|
+
state: "requires_authorization",
|
|
32
|
+
evidence: [...base.evidence, {
|
|
33
|
+
source: "authorization",
|
|
34
|
+
fact: `Missing ${missingOperations.join(", ")}.`
|
|
35
|
+
}],
|
|
36
|
+
reason: "The current grant does not include every operation for this capability."
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (id === "definitions.contracts.current") {
|
|
40
|
+
const approved = new Set(connection.scope.contracts.map(({ id, version, digest }) => `${id}@${version}:${digest}`));
|
|
41
|
+
const missingContracts = (manifest.requirements?.contracts ?? []).filter(({ id, version, digest }) => !approved.has(`${id}@${version}:${digest}`));
|
|
42
|
+
if (missingContracts.length > 0) {
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
state: "requires_authorization",
|
|
46
|
+
reason: "The current grant does not approve the exact required contract definitions.",
|
|
47
|
+
evidence: [...base.evidence, {
|
|
48
|
+
source: "authorization",
|
|
49
|
+
fact: `Missing ${missingContracts.map(({ id, version }) => `${id}@${version}`).join(", ")}.`
|
|
50
|
+
}],
|
|
51
|
+
details: { missingContracts }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (id.startsWith("files.")) {
|
|
56
|
+
const action = id.slice("files.".length);
|
|
57
|
+
if (!connection.fileCapability?.actions.includes(action)) {
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
state: connection.fileCapability ? "requires_authorization" : "unsupported",
|
|
61
|
+
reason: connection.fileCapability
|
|
62
|
+
? `The current file grant does not include ${action}.`
|
|
63
|
+
: "This connection has no file capability.",
|
|
64
|
+
evidence: [...base.evidence, {
|
|
65
|
+
source: "authorization",
|
|
66
|
+
fact: connection.fileCapability
|
|
67
|
+
? `Granted file actions: ${connection.fileCapability.actions.join(", ")}.`
|
|
68
|
+
: "No file capability was granted."
|
|
69
|
+
}]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (id === "notifications.background-delivery"
|
|
74
|
+
&& (manifest.notifications?.criteria.length ?? 0) === 0) {
|
|
75
|
+
return {
|
|
76
|
+
...base,
|
|
77
|
+
state: "requires_setup",
|
|
78
|
+
reason: "The application manifest declares no notification criteria.",
|
|
79
|
+
evidence: [...base.evidence, {
|
|
80
|
+
source: "application",
|
|
81
|
+
fact: "No authority notification criteria are declared."
|
|
82
|
+
}]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...base,
|
|
87
|
+
state: "available",
|
|
88
|
+
evidence: [...base.evidence, {
|
|
89
|
+
source: "authorization",
|
|
90
|
+
fact: operations.length > 0
|
|
91
|
+
? `Granted operations: ${operations.join(", ")}.`
|
|
92
|
+
: "No record operation is required."
|
|
93
|
+
}, {
|
|
94
|
+
source: "authority",
|
|
95
|
+
fact: connection.authority.kind === "hosted"
|
|
96
|
+
? "Backed by a durable hosted mdbase authority."
|
|
97
|
+
: "Backed by a user-operated mdbase connector."
|
|
98
|
+
}],
|
|
99
|
+
...(id === "sync.offline-replica"
|
|
100
|
+
? {
|
|
101
|
+
details: {
|
|
102
|
+
durability: "device",
|
|
103
|
+
writes: "queued",
|
|
104
|
+
authority: connection.authority.kind
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
: {}),
|
|
108
|
+
...(id === "notifications.background-delivery"
|
|
109
|
+
? { details: { delivery: "authority", payload: "opaque" } }
|
|
110
|
+
: {})
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JsonObject, MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import type { GrantKeyStore } from "./crypto.js";
|
|
3
|
+
import type { ApplicationIdentityStore } from "./application-identity.js";
|
|
3
4
|
export interface MdbaseConnectOptions {
|
|
4
5
|
serverUrl: string;
|
|
5
6
|
/**
|
|
@@ -12,6 +13,8 @@ export interface MdbaseConnectOptions {
|
|
|
12
13
|
/** Encrypted relay is required by default for newly authorized grants. */
|
|
13
14
|
relayEncryption?: "required" | "disabled";
|
|
14
15
|
keyStore?: GrantKeyStore;
|
|
16
|
+
/** Persistent installation signing identity; separate from disposable grant keys. */
|
|
17
|
+
identityStore?: ApplicationIdentityStore;
|
|
15
18
|
/** Prefer same-computer connector access when the browser permits it. */
|
|
16
19
|
directAccess?: "auto" | "disabled";
|
|
17
20
|
/** Loopback origin override for development and automated testing. */
|
package/dist/connect.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JsonObject, MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { type ApplicationIdentityStore } from "./application-identity.js";
|
|
2
3
|
import { MdbaseConnection, type MdbaseAuthorizationOutcome, type MdbaseAuthorizationResult, type MdbaseAuthorizeOptions, type MdbaseConnectEnvironment } from "./connection.js";
|
|
3
4
|
import type { MdbaseConnectOptions } from "./connect-options.js";
|
|
4
5
|
import type { MdbaseConnectionInfo } from "./connection-types.js";
|
|
@@ -13,11 +14,13 @@ export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
|
|
|
13
14
|
readonly storage: Storage;
|
|
14
15
|
readonly relayEncryption: "required" | "disabled";
|
|
15
16
|
readonly keyStore: GrantKeyStore;
|
|
17
|
+
readonly identityStore: ApplicationIdentityStore;
|
|
16
18
|
readonly directAccessMode: "auto" | "disabled";
|
|
17
19
|
readonly loopbackUrl: string;
|
|
18
20
|
readonly navigate?: (url: string) => void | Promise<void>;
|
|
19
21
|
readonly credentialStorage: MdbaseConnectEnvironment["credentialStorage"];
|
|
20
22
|
private application;
|
|
23
|
+
private manifestPromise;
|
|
21
24
|
private readonly completionPromises;
|
|
22
25
|
private readonly connectionCache;
|
|
23
26
|
private readonly listeners;
|
|
@@ -25,6 +28,7 @@ export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
|
|
|
25
28
|
constructor(options: MdbaseConnectOptions);
|
|
26
29
|
environment(): MdbaseConnectEnvironment;
|
|
27
30
|
register(): Promise<Application>;
|
|
31
|
+
manifestDeclaration(): Promise<MdbaseAppManifest>;
|
|
28
32
|
private loadManifest;
|
|
29
33
|
authorize(options?: MdbaseAuthorizeOptions): Promise<MdbaseAuthorizationOutcome<Frontmatter>>;
|
|
30
34
|
private authorizeWeb;
|
|
@@ -36,6 +40,10 @@ export declare class MdbaseConnectInternals<Frontmatter extends JsonObject> {
|
|
|
36
40
|
unavailableReason(collectionId: string): MdbaseUnavailableReason | null;
|
|
37
41
|
onConnectionsChange(listener: (connections: MdbaseConnectionInfo[]) => void): () => void;
|
|
38
42
|
storeTokenResponse(body: any, clientId: string, keyHandle?: string): StoredToken;
|
|
43
|
+
forgetConnectorIdentity(connectorId: string): void;
|
|
44
|
+
private pinConnectorIdentity;
|
|
45
|
+
private connectorPinKey;
|
|
46
|
+
private loadApplicationIdentity;
|
|
39
47
|
removeToken(collectionId: string, keyHandle?: string, reason?: MdbaseUnavailableReason): void;
|
|
40
48
|
tokenKey(collectionId: string): string;
|
|
41
49
|
pendingMutationKey(collectionId: string): string;
|
package/dist/connect.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { APPLICATION_AUTHORIZATION_PROTOCOL_VERSION, DEFAULT_LOOPBACK_PORT, ENCRYPTED_RELAY_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
|
-
import { applicationInstallationId, signApplicationAuthorization } from "./application-identity.js";
|
|
4
|
+
import { ApplicationIdentityStoreError, IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, applicationIdentity, applicationInstallationId, signApplicationAuthorization } from "./application-identity.js";
|
|
5
5
|
import { MdbaseConnection } from "./connection.js";
|
|
6
6
|
import { IndexedDbGrantKeyStore, MemoryGrantKeyStore, validateGrantEncryption } from "./crypto.js";
|
|
7
|
-
import { applicationIdentity, presentFirstContact } from "./first-contact-presentation.js";
|
|
8
7
|
import { connectError, serverConnectError } from "./errors.js";
|
|
9
8
|
import { DEFAULT_OPERATIONS } from "./internal-types.js";
|
|
10
9
|
import { uniqueOperations } from "./operation-helpers.js";
|
|
11
|
-
import { MemoryStorage, apiError, canonicalLoopbackUrl, connectFetch, createPkce, defaultManifestSource, defaultRedirectUri, defaultStorage, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseGrantScope, parseStored, stripTrailingSlash, validFileCapability, validAuthorityTokenResponse } from "./runtime-utils.js";
|
|
10
|
+
import { MemoryStorage, apiError, applicationStorageOrigin, canonicalLoopbackUrl, collectionIdFromTokenKey, connectFetch, createPkce, defaultManifestSource, defaultRedirectUri, defaultStorage, isOpaquePortableManifest, manifestStorageFingerprint, oauthErrorCode, parseDeviceAuthorization, parseGrantScope, parseStored, stripTrailingSlash, validFileCapability, validAuthorityTokenResponse } from "./runtime-utils.js";
|
|
12
11
|
export class MdbaseConnectInternals {
|
|
13
12
|
serverUrl;
|
|
14
13
|
manifest;
|
|
@@ -17,11 +16,13 @@ export class MdbaseConnectInternals {
|
|
|
17
16
|
storage;
|
|
18
17
|
relayEncryption;
|
|
19
18
|
keyStore;
|
|
19
|
+
identityStore;
|
|
20
20
|
directAccessMode;
|
|
21
21
|
loopbackUrl;
|
|
22
22
|
navigate;
|
|
23
23
|
credentialStorage;
|
|
24
24
|
application = null;
|
|
25
|
+
manifestPromise = null;
|
|
25
26
|
completionPromises = new Map();
|
|
26
27
|
connectionCache = new Map();
|
|
27
28
|
listeners = new Set();
|
|
@@ -41,7 +42,10 @@ export class MdbaseConnectInternals {
|
|
|
41
42
|
this.storage = options.storage ?? defaultStorage(opaquePortable);
|
|
42
43
|
this.relayEncryption = options.relayEncryption ?? "required";
|
|
43
44
|
this.keyStore = options.keyStore ?? (opaquePortable ? new MemoryGrantKeyStore() : new IndexedDbGrantKeyStore());
|
|
44
|
-
this.
|
|
45
|
+
this.identityStore = options.identityStore ?? (opaquePortable
|
|
46
|
+
? new MemoryApplicationIdentityStore()
|
|
47
|
+
: new IndexedDbApplicationIdentityStore());
|
|
48
|
+
this.credentialStorage = options.storage || options.keyStore || options.identityStore
|
|
45
49
|
? "custom"
|
|
46
50
|
: this.storage instanceof MemoryStorage ? "memory" : "persistent";
|
|
47
51
|
this.directAccessMode = options.directAccess ?? "auto";
|
|
@@ -75,7 +79,7 @@ export class MdbaseConnectInternals {
|
|
|
75
79
|
const response = await connectFetch(`${this.serverUrl}/v1/apps/register`, {
|
|
76
80
|
method: "POST",
|
|
77
81
|
headers: { "content-type": "application/json" },
|
|
78
|
-
body: JSON.stringify({ manifest: await this.
|
|
82
|
+
body: JSON.stringify({ manifest: await this.manifestDeclaration() })
|
|
79
83
|
}, "temporarily_unavailable", "Application registration is temporarily unavailable.");
|
|
80
84
|
const body = await response.json();
|
|
81
85
|
if (!response.ok)
|
|
@@ -83,6 +87,12 @@ export class MdbaseConnectInternals {
|
|
|
83
87
|
this.application = body.application;
|
|
84
88
|
return this.application;
|
|
85
89
|
}
|
|
90
|
+
manifestDeclaration() {
|
|
91
|
+
if (this.manifestPromise)
|
|
92
|
+
return this.manifestPromise;
|
|
93
|
+
this.manifestPromise = this.loadManifest();
|
|
94
|
+
return this.manifestPromise;
|
|
95
|
+
}
|
|
86
96
|
async loadManifest() {
|
|
87
97
|
if (typeof this.manifest !== "string")
|
|
88
98
|
return this.manifest;
|
|
@@ -112,7 +122,8 @@ export class MdbaseConnectInternals {
|
|
|
112
122
|
}
|
|
113
123
|
const portableDeclared = typeof this.manifest !== "string"
|
|
114
124
|
&& this.manifest.distribution === "portable";
|
|
115
|
-
const popup =
|
|
125
|
+
const popup = portableDeclared
|
|
126
|
+
&& !options.openVerification
|
|
116
127
|
&& !this.navigate
|
|
117
128
|
&& typeof window !== "undefined"
|
|
118
129
|
? window.open("", "mdbase-connect-authorization", "popup,width=620,height=760")
|
|
@@ -130,9 +141,10 @@ export class MdbaseConnectInternals {
|
|
|
130
141
|
popup?.close();
|
|
131
142
|
return this.authorizePortable(application, options, options.openVerification ? null : popup);
|
|
132
143
|
}
|
|
133
|
-
|
|
144
|
+
popup?.close();
|
|
145
|
+
return this.authorizeWeb(application, options);
|
|
134
146
|
}
|
|
135
|
-
async authorizeWeb(application, options
|
|
147
|
+
async authorizeWeb(application, options) {
|
|
136
148
|
const { verifier, challenge } = await createPkce();
|
|
137
149
|
const state = randomBase64Url(24);
|
|
138
150
|
const authorizationId = crypto.randomUUID();
|
|
@@ -141,16 +153,22 @@ export class MdbaseConnectInternals {
|
|
|
141
153
|
: undefined;
|
|
142
154
|
const keyHandle = `grant:${application.id}:${state}`;
|
|
143
155
|
const grantKey = await this.keyStore.create(keyHandle);
|
|
144
|
-
|
|
156
|
+
let installation;
|
|
157
|
+
try {
|
|
158
|
+
installation = await this.loadApplicationIdentity(application);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
await this.keyStore.delete(keyHandle);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
145
164
|
const operations = uniqueOperations(options.operations ?? DEFAULT_OPERATIONS);
|
|
146
165
|
const issuedAt = new Date();
|
|
147
166
|
const proof = await signApplicationAuthorization({
|
|
148
|
-
protocol_version:
|
|
167
|
+
protocol_version: APPLICATION_AUTHORIZATION_PROTOCOL_VERSION,
|
|
149
168
|
authorization_id: authorizationId,
|
|
150
169
|
application_id: application.id,
|
|
151
170
|
application_manifest_digest: application.manifest_digest,
|
|
152
171
|
application_installation_id: await applicationInstallationId(installation),
|
|
153
|
-
installation_agreement_public_key: installation.agreementPublicKey,
|
|
154
172
|
installation_signing_public_key: installation.signingPublicKey,
|
|
155
173
|
grant_agreement_public_key: grantKey.agreementPublicKey,
|
|
156
174
|
grant_signing_public_key: grantKey.signingPublicKey,
|
|
@@ -177,7 +195,6 @@ export class MdbaseConnectInternals {
|
|
|
177
195
|
collectionId: targetCollectionId,
|
|
178
196
|
returnTo: options.returnTo,
|
|
179
197
|
keyHandle,
|
|
180
|
-
installationKeyHandle: installation.handle,
|
|
181
198
|
authorizationId,
|
|
182
199
|
applicationAgreementPublicKey: grantKey.agreementPublicKey,
|
|
183
200
|
applicationSigningPublicKey: grantKey.signingPublicKey
|
|
@@ -206,71 +223,19 @@ export class MdbaseConnectInternals {
|
|
|
206
223
|
if (typeof body.authorization_uri !== "string"
|
|
207
224
|
|| body.authorization_id !== authorizationId
|
|
208
225
|
|| !Number.isInteger(body.expires_in)
|
|
209
|
-
||
|
|
210
|
-
|
|
211
|
-
|| body.interval <= 0) {
|
|
212
|
-
throw connectError("invalid_device_authorization_response", "The authorization service returned an invalid polling response.");
|
|
226
|
+
|| body.expires_in <= 0) {
|
|
227
|
+
throw connectError("invalid_device_authorization_response", "The authorization service returned an invalid authorization response.");
|
|
213
228
|
}
|
|
214
229
|
if (this.navigate)
|
|
215
230
|
await this.navigate(body.authorization_uri);
|
|
216
|
-
else if (
|
|
217
|
-
|
|
231
|
+
else if (typeof location !== "undefined")
|
|
232
|
+
location.assign(body.authorization_uri);
|
|
218
233
|
else {
|
|
219
|
-
throw connectError("browser_required", "
|
|
220
|
-
}
|
|
221
|
-
const expiresAt = Date.now() + body.expires_in * 1_000;
|
|
222
|
-
let intervalSeconds = body.interval;
|
|
223
|
-
let shownFirstContact = "";
|
|
224
|
-
while (Date.now() < expiresAt) {
|
|
225
|
-
await abortableDelay(intervalSeconds * 1_000, options.signal);
|
|
226
|
-
if (options.signal?.aborted) {
|
|
227
|
-
throw connectError("authorization_cancelled", "Application authorization was cancelled.");
|
|
228
|
-
}
|
|
229
|
-
let statusResponse;
|
|
230
|
-
try {
|
|
231
|
-
statusResponse = await fetch(`${this.serverUrl}/oauth/authorization_status`, {
|
|
232
|
-
method: "POST",
|
|
233
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
234
|
-
body: new URLSearchParams({
|
|
235
|
-
client_id: application.id,
|
|
236
|
-
authorization_id: authorizationId,
|
|
237
|
-
code_verifier: verifier
|
|
238
|
-
}),
|
|
239
|
-
signal: options.signal
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
catch (cause) {
|
|
243
|
-
if (options.signal?.aborted) {
|
|
244
|
-
throw connectError("authorization_cancelled", "Application authorization was cancelled.", { cause });
|
|
245
|
-
}
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const statusBody = await statusResponse.json();
|
|
249
|
-
if (!statusResponse.ok) {
|
|
250
|
-
const code = oauthErrorCode(statusBody);
|
|
251
|
-
if (code === "authorization_pending") {
|
|
252
|
-
if (statusBody.first_contact) {
|
|
253
|
-
shownFirstContact = await presentFirstContact(statusBody.first_contact, application, installation, options, shownFirstContact);
|
|
254
|
-
}
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (code === "slow_down") {
|
|
258
|
-
intervalSeconds += 2;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
throw apiError(statusBody, "token_exchange_failed", "Application authorization could not be completed.", statusResponse.status);
|
|
262
|
-
}
|
|
263
|
-
if (typeof statusBody.authorization_redirect !== "string") {
|
|
264
|
-
throw connectError("invalid_token_response", "The authorization service returned an invalid completion response.");
|
|
265
|
-
}
|
|
266
|
-
const result = await this.performAuthorizationCompletion(statusBody.authorization_redirect, state);
|
|
267
|
-
popup?.close();
|
|
268
|
-
return { kind: "connected", ...result };
|
|
234
|
+
throw connectError("browser_required", "Authorization navigation requires a browser environment.");
|
|
269
235
|
}
|
|
270
|
-
|
|
236
|
+
return { kind: "redirecting" };
|
|
271
237
|
}
|
|
272
238
|
catch (error) {
|
|
273
|
-
popup?.close();
|
|
274
239
|
this.storage.removeItem(this.pendingKey(state));
|
|
275
240
|
await this.keyStore.delete(keyHandle);
|
|
276
241
|
if (options.signal?.aborted) {
|
|
@@ -287,17 +252,24 @@ export class MdbaseConnectInternals {
|
|
|
287
252
|
const { verifier, challenge } = await createPkce();
|
|
288
253
|
const keyHandle = `grant:${application.id}:${randomBase64Url(24)}`;
|
|
289
254
|
const grantKey = await this.keyStore.create(keyHandle);
|
|
290
|
-
|
|
255
|
+
let installation;
|
|
256
|
+
try {
|
|
257
|
+
installation = await this.loadApplicationIdentity(application);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
popup?.close();
|
|
261
|
+
await this.keyStore.delete(keyHandle);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
291
264
|
const operations = uniqueOperations(options.operations ?? DEFAULT_OPERATIONS);
|
|
292
265
|
const authorizationId = crypto.randomUUID();
|
|
293
266
|
const issuedAt = new Date();
|
|
294
267
|
const proof = await signApplicationAuthorization({
|
|
295
|
-
protocol_version:
|
|
268
|
+
protocol_version: APPLICATION_AUTHORIZATION_PROTOCOL_VERSION,
|
|
296
269
|
authorization_id: authorizationId,
|
|
297
270
|
application_id: application.id,
|
|
298
271
|
application_manifest_digest: application.manifest_digest,
|
|
299
272
|
application_installation_id: await applicationInstallationId(installation),
|
|
300
|
-
installation_agreement_public_key: installation.agreementPublicKey,
|
|
301
273
|
installation_signing_public_key: installation.signingPublicKey,
|
|
302
274
|
grant_agreement_public_key: grantKey.agreementPublicKey,
|
|
303
275
|
grant_signing_public_key: grantKey.signingPublicKey,
|
|
@@ -380,7 +352,6 @@ export class MdbaseConnectInternals {
|
|
|
380
352
|
});
|
|
381
353
|
}
|
|
382
354
|
let intervalSeconds = authorization.intervalSeconds;
|
|
383
|
-
let shownFirstContact = "";
|
|
384
355
|
try {
|
|
385
356
|
while (Date.now() < authorization.expiresAt) {
|
|
386
357
|
await abortableDelay(intervalSeconds * 1_000, options.signal);
|
|
@@ -411,9 +382,6 @@ export class MdbaseConnectInternals {
|
|
|
411
382
|
if (!tokenResponse.ok) {
|
|
412
383
|
const code = oauthErrorCode(tokenBody);
|
|
413
384
|
if (code === "authorization_pending") {
|
|
414
|
-
if (tokenBody.first_contact) {
|
|
415
|
-
shownFirstContact = await presentFirstContact(tokenBody.first_contact, application, installation, options, shownFirstContact);
|
|
416
|
-
}
|
|
417
385
|
continue;
|
|
418
386
|
}
|
|
419
387
|
if (code === "slow_down") {
|
|
@@ -598,6 +566,7 @@ export class MdbaseConnectInternals {
|
|
|
598
566
|
catch {
|
|
599
567
|
throw connectError("invalid_token_response", "Authorization returned an invalid encrypted relay binding.");
|
|
600
568
|
}
|
|
569
|
+
this.pinConnectorIdentity(body.encryption.connector_id, body.encryption.connector_agreement_public_key);
|
|
601
570
|
if (body.encryption.collection_id !== collectionId
|
|
602
571
|
|| typeof body.grant_id !== "string"
|
|
603
572
|
|| body.grant_id.length === 0) {
|
|
@@ -655,6 +624,31 @@ export class MdbaseConnectInternals {
|
|
|
655
624
|
this.emitConnections();
|
|
656
625
|
return token;
|
|
657
626
|
}
|
|
627
|
+
forgetConnectorIdentity(connectorId) {
|
|
628
|
+
this.storage.removeItem(this.connectorPinKey(connectorId));
|
|
629
|
+
}
|
|
630
|
+
pinConnectorIdentity(connectorId, publicKey) {
|
|
631
|
+
const key = this.connectorPinKey(connectorId);
|
|
632
|
+
const previous = parseStored(this.storage.getItem(key));
|
|
633
|
+
if (previous && (previous.version !== 1 || previous.publicKey !== publicKey)) {
|
|
634
|
+
throw connectError("connector_identity_changed", "The connector identity changed since this application first connected. Forget the saved connector identity only after verifying the computer, then authorize again.");
|
|
635
|
+
}
|
|
636
|
+
if (!previous)
|
|
637
|
+
this.storage.setItem(key, JSON.stringify({ version: 1, publicKey }));
|
|
638
|
+
}
|
|
639
|
+
connectorPinKey(connectorId) {
|
|
640
|
+
return `mdbase-connect:${this.serverUrl}:connector-pin:${connectorId}`;
|
|
641
|
+
}
|
|
642
|
+
async loadApplicationIdentity(application) {
|
|
643
|
+
try {
|
|
644
|
+
return await applicationIdentity(this.identityStore, this.serverUrl, application);
|
|
645
|
+
}
|
|
646
|
+
catch (cause) {
|
|
647
|
+
throw connectError("application_identity_unavailable", cause instanceof ApplicationIdentityStoreError
|
|
648
|
+
? cause.message
|
|
649
|
+
: "The application installation identity is unavailable.", { cause });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
658
652
|
removeToken(collectionId, keyHandle, reason = "authorization_lost") {
|
|
659
653
|
this.invalidatedConnections.set(collectionId, reason);
|
|
660
654
|
if (keyHandle)
|
|
@@ -684,25 +678,7 @@ export class MdbaseConnectInternals {
|
|
|
684
678
|
return `mdbase-connect:direct:${this.defaultApplicationOrigin()}`;
|
|
685
679
|
}
|
|
686
680
|
defaultApplicationOrigin() {
|
|
687
|
-
|
|
688
|
-
|| this.application?.distribution === "portable") {
|
|
689
|
-
return "null";
|
|
690
|
-
}
|
|
691
|
-
const redirect = new URL(this.redirectUri);
|
|
692
|
-
if (["http:", "https:"].includes(redirect.protocol))
|
|
693
|
-
return redirect.origin;
|
|
694
|
-
if (typeof location !== "undefined")
|
|
695
|
-
return location.origin;
|
|
696
|
-
if (this.manifest
|
|
697
|
-
&& typeof this.manifest !== "string") {
|
|
698
|
-
return new URL(this.manifest.homepage).origin;
|
|
699
|
-
}
|
|
700
|
-
try {
|
|
701
|
-
return new URL(this.manifestSource).origin;
|
|
702
|
-
}
|
|
703
|
-
catch {
|
|
704
|
-
return "";
|
|
705
|
-
}
|
|
681
|
+
return applicationStorageOrigin(this.manifest, this.manifestSource, this.redirectUri, this.application?.distribution === "portable");
|
|
706
682
|
}
|
|
707
683
|
pendingKey(state) {
|
|
708
684
|
return `${this.storagePrefix()}:pending:${state}`;
|
|
@@ -732,8 +708,3 @@ export class MdbaseConnectInternals {
|
|
|
732
708
|
listener(connections);
|
|
733
709
|
}
|
|
734
710
|
}
|
|
735
|
-
function collectionIdFromTokenKey(key) {
|
|
736
|
-
const marker = ":token:";
|
|
737
|
-
const index = key.lastIndexOf(marker);
|
|
738
|
-
return index < 0 ? "" : key.slice(index + marker.length);
|
|
739
|
-
}
|
|
@@ -7,6 +7,13 @@ export interface MdbaseConnectionInfo {
|
|
|
7
7
|
operations: CollectionOperation[];
|
|
8
8
|
scope: GrantScope;
|
|
9
9
|
fileCapability?: FileCapability;
|
|
10
|
+
authority: {
|
|
11
|
+
kind: "hosted";
|
|
12
|
+
durability: "provider";
|
|
13
|
+
} | {
|
|
14
|
+
kind: "connector";
|
|
15
|
+
durability: "computer";
|
|
16
|
+
};
|
|
10
17
|
route: MdbaseConnectionRoute;
|
|
11
18
|
directAccess: DirectAccessStatus;
|
|
12
19
|
}
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CollectionChange, CollectionChangesPage, CollectionDescription, CollectionOperation,
|
|
1
|
+
import type { CollectionChange, CollectionChangesPage, CollectionDescription, CollectionOperation, CollectionTypeDocument, CreateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, FileCapability, GrantScope, JsonObject, ReadViewSourceInput, RecordDocument, SavedViewExecution, SavedViewList, SavedViewSourceDocument, ApplyTypePackInput, AssessTypePackInput, TypePackApplyResult, TypePackAssessment, UpdateViewSourceInput } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import { type ConnectionTransportInternals } from "./connection-transport.js";
|
|
3
3
|
import type { DirectAccessStatus, MdbaseAuthorizationCapabilities, MdbaseConnectionInfo, MdbaseConnectionRoute, MdbaseSyncConnection } from "./connection-types.js";
|
|
4
4
|
import type { GrantKeyStore } from "./crypto.js";
|
|
@@ -22,11 +22,6 @@ export interface MdbaseAuthorizeOptions {
|
|
|
22
22
|
returnTo?: string;
|
|
23
23
|
/** Receives the short code even when the SDK also opens the approval page. */
|
|
24
24
|
onDeviceCode?: (authorization: MdbaseDeviceAuthorization) => void;
|
|
25
|
-
/**
|
|
26
|
-
* Displays the independently derived first-contact code. Keep it visible
|
|
27
|
-
* while the user compares and accepts the same code on the connector.
|
|
28
|
-
*/
|
|
29
|
-
onFirstContact?: (challenge: MdbaseFirstContactChallenge) => void | Promise<void>;
|
|
30
25
|
/** Replace the default popup for a downloaded application's approval page. */
|
|
31
26
|
openVerification?: (authorization: MdbaseDeviceAuthorization) => void | Promise<void>;
|
|
32
27
|
/** Stop polling and discard the unapproved, in-memory key. */
|
|
@@ -36,16 +31,9 @@ export interface MdbaseConnectionAuthorizeOptions {
|
|
|
36
31
|
operations?: CollectionOperation[];
|
|
37
32
|
returnTo?: string;
|
|
38
33
|
onDeviceCode?: (authorization: MdbaseDeviceAuthorization) => void;
|
|
39
|
-
onFirstContact?: (challenge: MdbaseFirstContactChallenge) => void | Promise<void>;
|
|
40
34
|
openVerification?: (authorization: MdbaseDeviceAuthorization) => void | Promise<void>;
|
|
41
35
|
signal?: AbortSignal;
|
|
42
36
|
}
|
|
43
|
-
export interface MdbaseFirstContactChallenge {
|
|
44
|
-
binding: FirstContactBinding;
|
|
45
|
-
authenticationString: string;
|
|
46
|
-
applicationId: string;
|
|
47
|
-
connectorId: string;
|
|
48
|
-
}
|
|
49
37
|
export interface MdbaseConnectEnvironment {
|
|
50
38
|
distribution: "web" | "portable";
|
|
51
39
|
applicationOrigin: string;
|
package/dist/connection.js
CHANGED
|
@@ -74,6 +74,9 @@ export class MdbaseConnection {
|
|
|
74
74
|
operations: [...token.operations],
|
|
75
75
|
scope: token.scope,
|
|
76
76
|
...(token.fileCapability ? { fileCapability: token.fileCapability } : {}),
|
|
77
|
+
authority: token.authority
|
|
78
|
+
? { kind: "hosted", durability: "provider" }
|
|
79
|
+
: { kind: "connector", durability: "computer" },
|
|
77
80
|
route: this.transport.route,
|
|
78
81
|
directAccess: this.transport.directAccess
|
|
79
82
|
} : null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
export { decryptRelayResponse, encryptRelayRequest, IndexedDbGrantKeyStore, MemoryGrantKeyStore, RelayCryptoError, type RelayBinding, type GrantKeyRecord, type GrantKeyStore } from "./crypto.js";
|
|
2
2
|
export { FileTransferCryptoError, GrantFileTransferCipher, type FileTransferBinding } from "./file-crypto.js";
|
|
3
|
-
export {
|
|
4
|
-
export { applicationInstallationId, authorizationSigningMessage, signApplicationAuthorization } from "./application-identity.js";
|
|
3
|
+
export { ApplicationIdentityStoreError, IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, applicationInstallationId, authorizationSigningMessage, signApplicationAuthorization, type ApplicationIdentity, type ApplicationIdentityStore } from "./application-identity.js";
|
|
5
4
|
export * from "./authorization-types.js";
|
|
5
|
+
export * from "./application-session.js";
|
|
6
6
|
export * from "./collection-client.js";
|
|
7
7
|
export * from "./connect-options.js";
|
|
8
8
|
export * from "./mdbase-connect.js";
|
|
9
9
|
export * from "./connection.js";
|
|
10
10
|
export * from "./connection-types.js";
|
|
11
|
+
export * from "./capabilities.js";
|
|
11
12
|
export * from "./errors.js";
|
|
12
13
|
export * from "./files.js";
|
|
13
14
|
export * from "./notifications.js";
|
|
14
15
|
export * from "./outcomes.js";
|
|
15
16
|
export * from "./operation-types.js";
|
|
16
17
|
export * from "./selection.js";
|
|
17
|
-
export
|
|
18
|
+
export type { MdbaseUnavailableReason } from "./session.js";
|
|
18
19
|
export { createPkce } from "./runtime-utils.js";
|
|
19
|
-
export type { ApplicationProvisions, ApplicationRequirements, ApplicationNotifications, ApplicationAuthorizationBinding, ApplicationAuthorizationProof, CollectionChange, CollectionChangesPage, CollectionContractDescriptor, CollectionDescription, CollectionFileMetadata, CollectionOperation as MdbaseOperation, CollectionTypeDescriptor, CollectionTypeDocument, ConnectOperationOutcome, ConnectProblem, ConnectProblemCategory, ConnectProblemCode, ConnectProblemDetailsByCode, ConnectRecoveryAction, ContractRequirement, DataContractViewIdentity, GrantScope,
|
|
20
|
+
export type { ApplicationProvisions, ApplicationCapabilityId, ApplicationCapabilityRequirements, ApplicationRequirements, ApplicationNotifications, ApplicationAuthorizationBinding, ApplicationAuthorizationProof, CollectionChange, CollectionChangesPage, CollectionContractDescriptor, CollectionDescription, CollectionFileMetadata, FileAction, CollectionOperation as MdbaseOperation, CollectionTypeDescriptor, CollectionTypeDocument, ConnectOperationOutcome, ConnectProblem, ConnectProblemCategory, ConnectProblemCode, ConnectProblemDetailsByCode, ConnectRecoveryAction, ContractRequirement, DataContractViewIdentity, GrantScope, JsonObject, MdbaseAppManifest, NotificationCriterion, MdbaseDiagnostic, MdbaseOperationEnvelope, KnownConnectProblem, RecordDocument, QueryRecord, SavedNamedView, SavedViewDocument, SavedViewExecution, SavedViewList, SavedViewPresentation, SavedViewProperty, SavedViewSource, SavedViewSourceDocument, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, TypePackManifest, TypePackManifestResource, TypePackApplyResult, TypePackAssessment, TypePackReceipt, AssessTypePackInput, ApplyTypePackInput, TypePackProvision, TypePackResourceDiff, TypePackSourceResource, UnknownConnectProblem } from "@mdbase-dev/connect-protocol";
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
export { decryptRelayResponse, encryptRelayRequest, IndexedDbGrantKeyStore, MemoryGrantKeyStore, RelayCryptoError } from "./crypto.js";
|
|
2
2
|
export { FileTransferCryptoError, GrantFileTransferCipher } from "./file-crypto.js";
|
|
3
|
-
export {
|
|
4
|
-
export { applicationInstallationId, authorizationSigningMessage, signApplicationAuthorization } from "./application-identity.js";
|
|
3
|
+
export { ApplicationIdentityStoreError, IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, applicationInstallationId, authorizationSigningMessage, signApplicationAuthorization } from "./application-identity.js";
|
|
5
4
|
export * from "./authorization-types.js";
|
|
5
|
+
export * from "./application-session.js";
|
|
6
6
|
export * from "./collection-client.js";
|
|
7
7
|
export * from "./connect-options.js";
|
|
8
8
|
export * from "./mdbase-connect.js";
|
|
9
9
|
export * from "./connection.js";
|
|
10
10
|
export * from "./connection-types.js";
|
|
11
|
+
export * from "./capabilities.js";
|
|
11
12
|
export * from "./errors.js";
|
|
12
13
|
export * from "./files.js";
|
|
13
14
|
export * from "./notifications.js";
|
|
14
15
|
export * from "./outcomes.js";
|
|
15
16
|
export * from "./operation-types.js";
|
|
16
17
|
export * from "./selection.js";
|
|
17
|
-
export * from "./session.js";
|
|
18
18
|
export { createPkce } from "./runtime-utils.js";
|
package/dist/internal-types.d.ts
CHANGED