@mdbase-dev/connect 0.1.0-beta.26 → 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.
@@ -1 +1 @@
1
- sha384-Rejwyx0zN9iv54K0UaKMWs09sQLXcxnOBtKT574KVKlC7aR4FZ7CqpPRMd7mKlNl
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,4 +1,4 @@
1
- import type { CollectionChange, CollectionChangesPage, CollectionDescription, CollectionOperation, CollectionTypeDocument, CreateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, JsonObject, ReadViewSourceInput, RecordDocument, SavedViewExecution, SavedViewList, SavedViewSourceDocument, TypePackInstallResult, TypePackProvision, UpdateViewSourceInput } from "@mdbase-dev/connect-protocol";
1
+ import type { CollectionChange, CollectionChangesPage, CollectionDescription, CollectionOperation, CollectionTypeDocument, ApplyTypePackInput, AssessTypePackInput, CreateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, JsonObject, ReadViewSourceInput, RecordDocument, SavedViewExecution, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateViewSourceInput } from "@mdbase-dev/connect-protocol";
2
2
  import { type CollectionChangesProblemCode, type CollectionDescriptionProblemCode, type CollectionMutationProblemCode, type CollectionQueryProblemCode, type CollectionReadProblemCode, type CollectionTypeProblemCode, type ConnectOutcome } from "./outcomes.js";
3
3
  import type { ChangesInput, CreateInput, CreateTypeInput, DeleteInput, DeletePreflightResult, DeleteResult, MdbaseCollectionTransport, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, OperationRequestOptions, QueryInput, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadTypeInput, RenameInput, RenamePreflightResult, RenameResult, UpdateInput, UpdateTypeInput, WatchOptions } from "./operation-types.js";
4
4
  /**
@@ -33,7 +33,8 @@ export declare class MdbaseCollectionClient<Frontmatter extends JsonObject = Jso
33
33
  readType(input: ReadTypeInput): Promise<ConnectOutcome<CollectionTypeDocument, CollectionTypeProblemCode>>;
34
34
  createType(input: CreateTypeInput): Promise<ConnectOutcome<CollectionTypeDocument, CollectionTypeProblemCode>>;
35
35
  updateType(input: UpdateTypeInput): Promise<ConnectOutcome<CollectionTypeDocument, CollectionTypeProblemCode>>;
36
- installTypePack(input: TypePackProvision): Promise<ConnectOutcome<TypePackInstallResult, CollectionTypeProblemCode>>;
36
+ assessTypePack(input: AssessTypePackInput): Promise<ConnectOutcome<TypePackAssessment, CollectionTypeProblemCode>>;
37
+ applyTypePack(input: ApplyTypePackInput): Promise<ConnectOutcome<TypePackApplyResult, CollectionTypeProblemCode>>;
37
38
  listTimers(namespace: string): Promise<ConnectOutcome<MdbaseTimerList, CollectionReadProblemCode>>;
38
39
  putTimer(input: {
39
40
  namespace: string;
@@ -143,8 +143,11 @@ export class MdbaseCollectionClient {
143
143
  updateType(input) {
144
144
  return this.envelopeOperation("update_type", input, COLLECTION_TYPE_PROBLEM_CODES);
145
145
  }
146
- installTypePack(input) {
147
- return this.envelopeOperation("install_type_pack", input, COLLECTION_TYPE_PROBLEM_CODES);
146
+ assessTypePack(input) {
147
+ return this.envelopeOperation("assess_type_pack", input, COLLECTION_TYPE_PROBLEM_CODES);
148
+ }
149
+ applyTypePack(input) {
150
+ return this.envelopeOperation("apply_type_pack", input, COLLECTION_TYPE_PROBLEM_CODES);
148
151
  }
149
152
  listTimers(namespace) {
150
153
  return this.rawOperation("list_timers", { namespace }, COLLECTION_READ_PROBLEM_CODES);
@@ -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;
@@ -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 = !(portableDeclared && options.openVerification)
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
- return this.authorizeWeb(application, options, popup);
144
+ popup?.close();
145
+ return this.authorizeWeb(application, options);
134
146
  }
135
- async authorizeWeb(application, options, popup) {
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
- const installation = await applicationIdentity(this.keyStore, this.serverUrl, application);
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: FIRST_CONTACT_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
- || !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.");
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 (popup)
217
- popup.location.href = body.authorization_uri;
231
+ else if (typeof location !== "undefined")
232
+ location.assign(body.authorization_uri);
218
233
  else {
219
- throw connectError("browser_required", "The approval window was blocked. Allow popups for this application and try again.");
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
- throw connectError("expired_token", "The application authorization expired.");
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
- const installation = await applicationIdentity(this.keyStore, this.serverUrl, application);
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: FIRST_CONTACT_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
- 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
- }
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
  }
@@ -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, TypePackInstallResult, TypePackProvision, 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;
@@ -143,7 +131,8 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
143
131
  readType(input: ReadTypeInput): Promise<ConnectOutcome<CollectionTypeDocument, CollectionTypeProblemCode>>;
144
132
  createType(input: CreateTypeInput): Promise<ConnectOutcome<CollectionTypeDocument, CollectionTypeProblemCode>>;
145
133
  updateType(input: UpdateTypeInput): Promise<ConnectOutcome<CollectionTypeDocument, CollectionTypeProblemCode>>;
146
- installTypePack(input: TypePackProvision): Promise<ConnectOutcome<TypePackInstallResult, CollectionTypeProblemCode>>;
134
+ assessTypePack(input: AssessTypePackInput): Promise<ConnectOutcome<TypePackAssessment, CollectionTypeProblemCode>>;
135
+ applyTypePack(input: ApplyTypePackInput): Promise<ConnectOutcome<TypePackApplyResult, CollectionTypeProblemCode>>;
147
136
  listTimers(namespace: string): Promise<ConnectOutcome<MdbaseTimerList, CollectionReadProblemCode>>;
148
137
  putTimer(input: {
149
138
  namespace: 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;
@@ -382,8 +385,11 @@ export class MdbaseConnection {
382
385
  updateType(input) {
383
386
  return this.collectionClient.updateType(input);
384
387
  }
385
- installTypePack(input) {
386
- return this.collectionClient.installTypePack(input);
388
+ assessTypePack(input) {
389
+ return this.collectionClient.assessTypePack(input);
390
+ }
391
+ applyTypePack(input) {
392
+ return this.collectionClient.applyTypePack(input);
387
393
  }
388
394
  listTimers(namespace) {
389
395
  return this.collectionClient.listTimers(namespace);