@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.
@@ -1 +1 @@
1
- sha384-DjJkmk65xgvpJONlTzVuGLRLNCDOhbOdcG4xoq+le2zjOf+cYDEiN4Ctdl+HyrCL
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 { FIRST_CONTACT_PROTOCOL_VERSION, DEFAULT_LOOPBACK_PORT, ENCRYPTED_RELAY_PROTOCOL_VERSION, RELAY_ENCRYPTION_SUITE } from "@mdbase-dev/connect-protocol";
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.credentialStorage = options.storage || options.keyStore
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.loadManifest() })
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.", { cause });
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 = !(portableDeclared && options.openVerification)
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
- return this.authorizeWeb(application, options, popup);
154
+ popup?.close();
155
+ return this.authorizeWeb(application, options);
134
156
  }
135
- async authorizeWeb(application, options, popup) {
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
- const installation = await applicationIdentity(this.keyStore, this.serverUrl, application);
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: FIRST_CONTACT_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
- || !Number.isInteger(body.interval)
210
- || body.expires_in <= 0
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 (popup)
217
- popup.location.href = body.authorization_uri;
241
+ else if (typeof location !== "undefined")
242
+ location.assign(body.authorization_uri);
218
243
  else {
219
- throw connectError("browser_required", "The approval window was blocked. Allow popups for this application and try again.");
244
+ throw connectError("browser_required", "Authorization navigation requires a browser environment.");
220
245
  }
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 };
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
- const installation = await applicationIdentity(this.keyStore, this.serverUrl, application);
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: FIRST_CONTACT_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
- if ((typeof this.manifest !== "string" && this.manifest.distribution === "portable")
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
  }
@@ -1,4 +1,4 @@
1
- import type { CollectionChange, CollectionChangesPage, CollectionDescription, CollectionOperation, FirstContactBinding, CollectionTypeDocument, CreateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, FileCapability, GrantScope, JsonObject, ReadViewSourceInput, RecordDocument, SavedViewExecution, SavedViewList, SavedViewSourceDocument, ApplyTypePackInput, AssessTypePackInput, TypePackApplyResult, TypePackAssessment, UpdateViewSourceInput } from "@mdbase-dev/connect-protocol";
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;
@@ -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 { deriveFirstContactSas, FirstContactCryptoError, type FirstContactRole } from "./first-contact.js";
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 * from "./session.js";
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, FirstContactBinding, 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";
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 { deriveFirstContactSas, FirstContactCryptoError } from "./first-contact.js";
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";
@@ -20,7 +20,6 @@ export interface StoredAuthorization {
20
20
  collectionId?: string;
21
21
  returnTo?: string;
22
22
  keyHandle?: string;
23
- installationKeyHandle?: string;
24
23
  authorizationId?: string;
25
24
  applicationAgreementPublicKey?: string;
26
25
  applicationSigningPublicKey?: string;