@mdbase-dev/connect 0.1.0-beta.27 → 0.1.0-beta.29
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 +302 -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 +84 -103
- 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-Cw/OpEwBlbmIBjDZ/JDyh1HiQoKqitRwVSPaYpNCg33rNDYZr/EdSxv5oRa4jP+I
|
|
@@ -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;
|
|
@@ -103,7 +113,17 @@ export class MdbaseConnectInternals {
|
|
|
103
113
|
return await response.json();
|
|
104
114
|
}
|
|
105
115
|
catch (cause) {
|
|
106
|
-
throw connectError("invalid_application_manifest", "The bundled application declaration is not valid JSON.", {
|
|
116
|
+
throw connectError("invalid_application_manifest", "The bundled application declaration is not valid JSON.", {
|
|
117
|
+
cause,
|
|
118
|
+
details: {
|
|
119
|
+
issues: [{
|
|
120
|
+
path: "/",
|
|
121
|
+
keyword: "json",
|
|
122
|
+
message: "must be valid JSON",
|
|
123
|
+
params: {}
|
|
124
|
+
}]
|
|
125
|
+
}
|
|
126
|
+
});
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
async authorize(options = {}) {
|
|
@@ -112,7 +132,8 @@ export class MdbaseConnectInternals {
|
|
|
112
132
|
}
|
|
113
133
|
const portableDeclared = typeof this.manifest !== "string"
|
|
114
134
|
&& this.manifest.distribution === "portable";
|
|
115
|
-
const popup =
|
|
135
|
+
const popup = portableDeclared
|
|
136
|
+
&& !options.openVerification
|
|
116
137
|
&& !this.navigate
|
|
117
138
|
&& typeof window !== "undefined"
|
|
118
139
|
? window.open("", "mdbase-connect-authorization", "popup,width=620,height=760")
|
|
@@ -130,9 +151,10 @@ export class MdbaseConnectInternals {
|
|
|
130
151
|
popup?.close();
|
|
131
152
|
return this.authorizePortable(application, options, options.openVerification ? null : popup);
|
|
132
153
|
}
|
|
133
|
-
|
|
154
|
+
popup?.close();
|
|
155
|
+
return this.authorizeWeb(application, options);
|
|
134
156
|
}
|
|
135
|
-
async authorizeWeb(application, options
|
|
157
|
+
async authorizeWeb(application, options) {
|
|
136
158
|
const { verifier, challenge } = await createPkce();
|
|
137
159
|
const state = randomBase64Url(24);
|
|
138
160
|
const authorizationId = crypto.randomUUID();
|
|
@@ -141,16 +163,22 @@ export class MdbaseConnectInternals {
|
|
|
141
163
|
: undefined;
|
|
142
164
|
const keyHandle = `grant:${application.id}:${state}`;
|
|
143
165
|
const grantKey = await this.keyStore.create(keyHandle);
|
|
144
|
-
|
|
166
|
+
let installation;
|
|
167
|
+
try {
|
|
168
|
+
installation = await this.loadApplicationIdentity(application);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
await this.keyStore.delete(keyHandle);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
145
174
|
const operations = uniqueOperations(options.operations ?? DEFAULT_OPERATIONS);
|
|
146
175
|
const issuedAt = new Date();
|
|
147
176
|
const proof = await signApplicationAuthorization({
|
|
148
|
-
protocol_version:
|
|
177
|
+
protocol_version: APPLICATION_AUTHORIZATION_PROTOCOL_VERSION,
|
|
149
178
|
authorization_id: authorizationId,
|
|
150
179
|
application_id: application.id,
|
|
151
180
|
application_manifest_digest: application.manifest_digest,
|
|
152
181
|
application_installation_id: await applicationInstallationId(installation),
|
|
153
|
-
installation_agreement_public_key: installation.agreementPublicKey,
|
|
154
182
|
installation_signing_public_key: installation.signingPublicKey,
|
|
155
183
|
grant_agreement_public_key: grantKey.agreementPublicKey,
|
|
156
184
|
grant_signing_public_key: grantKey.signingPublicKey,
|
|
@@ -177,7 +205,6 @@ export class MdbaseConnectInternals {
|
|
|
177
205
|
collectionId: targetCollectionId,
|
|
178
206
|
returnTo: options.returnTo,
|
|
179
207
|
keyHandle,
|
|
180
|
-
installationKeyHandle: installation.handle,
|
|
181
208
|
authorizationId,
|
|
182
209
|
applicationAgreementPublicKey: grantKey.agreementPublicKey,
|
|
183
210
|
applicationSigningPublicKey: grantKey.signingPublicKey
|
|
@@ -206,71 +233,19 @@ export class MdbaseConnectInternals {
|
|
|
206
233
|
if (typeof body.authorization_uri !== "string"
|
|
207
234
|
|| body.authorization_id !== authorizationId
|
|
208
235
|
|| !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.");
|
|
236
|
+
|| body.expires_in <= 0) {
|
|
237
|
+
throw connectError("invalid_device_authorization_response", "The authorization service returned an invalid authorization response.");
|
|
213
238
|
}
|
|
214
239
|
if (this.navigate)
|
|
215
240
|
await this.navigate(body.authorization_uri);
|
|
216
|
-
else if (
|
|
217
|
-
|
|
241
|
+
else if (typeof location !== "undefined")
|
|
242
|
+
location.assign(body.authorization_uri);
|
|
218
243
|
else {
|
|
219
|
-
throw connectError("browser_required", "
|
|
244
|
+
throw connectError("browser_required", "Authorization navigation requires a browser environment.");
|
|
220
245
|
}
|
|
221
|
-
|
|
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 };
|
|
269
|
-
}
|
|
270
|
-
throw connectError("expired_token", "The application authorization expired.");
|
|
246
|
+
return { kind: "redirecting" };
|
|
271
247
|
}
|
|
272
248
|
catch (error) {
|
|
273
|
-
popup?.close();
|
|
274
249
|
this.storage.removeItem(this.pendingKey(state));
|
|
275
250
|
await this.keyStore.delete(keyHandle);
|
|
276
251
|
if (options.signal?.aborted) {
|
|
@@ -287,17 +262,24 @@ export class MdbaseConnectInternals {
|
|
|
287
262
|
const { verifier, challenge } = await createPkce();
|
|
288
263
|
const keyHandle = `grant:${application.id}:${randomBase64Url(24)}`;
|
|
289
264
|
const grantKey = await this.keyStore.create(keyHandle);
|
|
290
|
-
|
|
265
|
+
let installation;
|
|
266
|
+
try {
|
|
267
|
+
installation = await this.loadApplicationIdentity(application);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
popup?.close();
|
|
271
|
+
await this.keyStore.delete(keyHandle);
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
291
274
|
const operations = uniqueOperations(options.operations ?? DEFAULT_OPERATIONS);
|
|
292
275
|
const authorizationId = crypto.randomUUID();
|
|
293
276
|
const issuedAt = new Date();
|
|
294
277
|
const proof = await signApplicationAuthorization({
|
|
295
|
-
protocol_version:
|
|
278
|
+
protocol_version: APPLICATION_AUTHORIZATION_PROTOCOL_VERSION,
|
|
296
279
|
authorization_id: authorizationId,
|
|
297
280
|
application_id: application.id,
|
|
298
281
|
application_manifest_digest: application.manifest_digest,
|
|
299
282
|
application_installation_id: await applicationInstallationId(installation),
|
|
300
|
-
installation_agreement_public_key: installation.agreementPublicKey,
|
|
301
283
|
installation_signing_public_key: installation.signingPublicKey,
|
|
302
284
|
grant_agreement_public_key: grantKey.agreementPublicKey,
|
|
303
285
|
grant_signing_public_key: grantKey.signingPublicKey,
|
|
@@ -380,7 +362,6 @@ export class MdbaseConnectInternals {
|
|
|
380
362
|
});
|
|
381
363
|
}
|
|
382
364
|
let intervalSeconds = authorization.intervalSeconds;
|
|
383
|
-
let shownFirstContact = "";
|
|
384
365
|
try {
|
|
385
366
|
while (Date.now() < authorization.expiresAt) {
|
|
386
367
|
await abortableDelay(intervalSeconds * 1_000, options.signal);
|
|
@@ -411,9 +392,6 @@ export class MdbaseConnectInternals {
|
|
|
411
392
|
if (!tokenResponse.ok) {
|
|
412
393
|
const code = oauthErrorCode(tokenBody);
|
|
413
394
|
if (code === "authorization_pending") {
|
|
414
|
-
if (tokenBody.first_contact) {
|
|
415
|
-
shownFirstContact = await presentFirstContact(tokenBody.first_contact, application, installation, options, shownFirstContact);
|
|
416
|
-
}
|
|
417
395
|
continue;
|
|
418
396
|
}
|
|
419
397
|
if (code === "slow_down") {
|
|
@@ -598,6 +576,7 @@ export class MdbaseConnectInternals {
|
|
|
598
576
|
catch {
|
|
599
577
|
throw connectError("invalid_token_response", "Authorization returned an invalid encrypted relay binding.");
|
|
600
578
|
}
|
|
579
|
+
this.pinConnectorIdentity(body.encryption.connector_id, body.encryption.connector_agreement_public_key);
|
|
601
580
|
if (body.encryption.collection_id !== collectionId
|
|
602
581
|
|| typeof body.grant_id !== "string"
|
|
603
582
|
|| body.grant_id.length === 0) {
|
|
@@ -655,6 +634,31 @@ export class MdbaseConnectInternals {
|
|
|
655
634
|
this.emitConnections();
|
|
656
635
|
return token;
|
|
657
636
|
}
|
|
637
|
+
forgetConnectorIdentity(connectorId) {
|
|
638
|
+
this.storage.removeItem(this.connectorPinKey(connectorId));
|
|
639
|
+
}
|
|
640
|
+
pinConnectorIdentity(connectorId, publicKey) {
|
|
641
|
+
const key = this.connectorPinKey(connectorId);
|
|
642
|
+
const previous = parseStored(this.storage.getItem(key));
|
|
643
|
+
if (previous && (previous.version !== 1 || previous.publicKey !== publicKey)) {
|
|
644
|
+
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.");
|
|
645
|
+
}
|
|
646
|
+
if (!previous)
|
|
647
|
+
this.storage.setItem(key, JSON.stringify({ version: 1, publicKey }));
|
|
648
|
+
}
|
|
649
|
+
connectorPinKey(connectorId) {
|
|
650
|
+
return `mdbase-connect:${this.serverUrl}:connector-pin:${connectorId}`;
|
|
651
|
+
}
|
|
652
|
+
async loadApplicationIdentity(application) {
|
|
653
|
+
try {
|
|
654
|
+
return await applicationIdentity(this.identityStore, this.serverUrl, application);
|
|
655
|
+
}
|
|
656
|
+
catch (cause) {
|
|
657
|
+
throw connectError("application_identity_unavailable", cause instanceof ApplicationIdentityStoreError
|
|
658
|
+
? cause.message
|
|
659
|
+
: "The application installation identity is unavailable.", { cause });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
658
662
|
removeToken(collectionId, keyHandle, reason = "authorization_lost") {
|
|
659
663
|
this.invalidatedConnections.set(collectionId, reason);
|
|
660
664
|
if (keyHandle)
|
|
@@ -684,25 +688,7 @@ export class MdbaseConnectInternals {
|
|
|
684
688
|
return `mdbase-connect:direct:${this.defaultApplicationOrigin()}`;
|
|
685
689
|
}
|
|
686
690
|
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
|
-
}
|
|
691
|
+
return applicationStorageOrigin(this.manifest, this.manifestSource, this.redirectUri, this.application?.distribution === "portable");
|
|
706
692
|
}
|
|
707
693
|
pendingKey(state) {
|
|
708
694
|
return `${this.storagePrefix()}:pending:${state}`;
|
|
@@ -732,8 +718,3 @@ export class MdbaseConnectInternals {
|
|
|
732
718
|
listener(connections);
|
|
733
719
|
}
|
|
734
720
|
}
|
|
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