@mdbase-dev/connect 0.1.0-beta.32 → 0.1.0-beta.34

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 CHANGED
@@ -7,6 +7,13 @@ applications. See the [portable application guide](../../docs/portable-apps.md)
7
7
  for the v1 manifest, device-code flow, version-pinned CDN URL, SRI metadata,
8
8
  and `file://` storage boundary.
9
9
 
10
+ Ordinary applications install only `@mdbase-dev/connect`. Add
11
+ `@mdbase-dev/connect-dev` for declaration/type-pack authoring,
12
+ `@mdbase-dev/connect-testing` for supported test fixtures,
13
+ `@mdbase-dev/connect-sync` for an offline mirror, or the Pickle package only
14
+ when the application uses that feature. Wire and authority implementations use
15
+ `@mdbase-dev/connect-protocol`; ordinary application code does not.
16
+
10
17
  The complete developer guide covers
11
18
  [setup](https://mdbase.dev/sdk/quickstart/),
12
19
  [manifests and contracts](https://mdbase.dev/sdk/manifest/),
@@ -33,7 +40,8 @@ const session = mdbase.application({
33
40
  });
34
41
  const unsubscribeSession = session.subscribe(() => render(session.getSnapshot()));
35
42
 
36
- const started = await session.start();
43
+ const startup = new AbortController();
44
+ const started = await session.start({ signal: startup.signal, timeoutMs: 20_000 });
37
45
  if (!started.ok) {
38
46
  renderProblem(started.problem);
39
47
  return;
@@ -56,10 +64,15 @@ if (!queried.ok) {
56
64
  return;
57
65
  }
58
66
  const workouts = queried.value;
67
+ const current = await connection.read({ path: workouts.results[0].path });
68
+ if (!current.ok) {
69
+ renderProblem(current.problem);
70
+ return;
71
+ }
59
72
  const updated = await connection.update({
60
- path: "workouts/monday.md",
73
+ path: current.value.path,
61
74
  patch: { completed: true },
62
- if_revision: workouts.results[0].revision
75
+ ifRevision: current.value.revision
63
76
  });
64
77
  if (!updated.ok) renderProblem(updated.problem);
65
78
 
@@ -85,6 +98,12 @@ reserved for programming errors and broken SDK invariants. See
85
98
  [typed outcomes and recovery](../../docs/sdk-outcomes.md) for the complete
86
99
  problem model, setup failures, mutation uncertainty, and UI guidance.
87
100
 
101
+ SDK call options, operation inputs, results, descriptors, and progress events
102
+ use camelCase. The client translates canonical snake_case protocol payloads at
103
+ the authority boundary. Versioned JSON documents such as `MdbaseAppManifest`
104
+ and `TypePackProvision`, problem detail objects, diagnostics, and user-owned
105
+ frontmatter retain the exact field names defined by their own schemas.
106
+
88
107
  React applications can use the same session without introducing another state
89
108
  owner:
90
109
 
@@ -132,16 +151,16 @@ versioned capabilities in their manifest; they never maintain a parallel array
132
151
  of protocol operations. Use `getSnapshot()` and `subscribe()` directly or
133
152
  through your framework's external-store integration.
134
153
 
135
- The session distinguishes `authorization_required`, `checking_definitions`,
136
- `definition_review_required`, `ready`, `unavailable`, and `blocked`. Definition
154
+ The session distinguishes `authorization_required`, `checking_setup`,
155
+ `setup_review_required`, `ready`, `unavailable`, and `blocked`. Setup
137
156
  inspection is read-only. If an update is needed, render the supplied plan and
138
157
  apply the exact assessment the user reviewed:
139
158
 
140
159
  ```ts
141
160
  const snapshot = session.getSnapshot();
142
- if (snapshot.status === "definition_review_required") {
143
- renderDefinitionChanges(snapshot.updates);
144
- const applied = await session.applyDefinitionUpdates();
161
+ if (snapshot.status === "setup_review_required") {
162
+ renderCollectionSetup(snapshot.update);
163
+ const applied = await session.applyCollectionSetup({ timeoutMs: 30_000 });
145
164
  if (!applied.ok) renderProblem(applied.problem);
146
165
  }
147
166
  ```
@@ -267,10 +286,10 @@ one-shot reminders at the collection authority:
267
286
  ```ts
268
287
  await connection.reconcileTimers({
269
288
  namespace: "workout-reminders",
270
- criterion_id: "workout.reminder",
289
+ criterionId: "workout.reminder",
271
290
  timers: [{
272
291
  id: "workout-42",
273
- fire_at: new Date("2026-07-25T10:00:00Z").toISOString()
292
+ fireAt: new Date("2026-07-25T10:00:00Z").toISOString()
274
293
  }]
275
294
  });
276
295
  ```
@@ -311,7 +330,7 @@ if (!current.ok) return renderProblem(current.problem);
311
330
  const updatedType = await connection.updateType({
312
331
  path: current.value.path,
313
332
  document: current.value.document.replace("version: 1", "version: 2"),
314
- if_revision: current.value.revision
333
+ ifRevision: current.value.revision
315
334
  });
316
335
  if (!updatedType.ok) renderProblem(updatedType.problem);
317
336
  ```
@@ -329,15 +348,15 @@ const provision = await response.json();
329
348
  const installedBy = "dev.example.catalog";
330
349
  const assessment = await connection.assessTypePack({
331
350
  provision,
332
- installed_by: installedBy
351
+ installedBy
333
352
  });
334
353
  if (!assessment.ok) return renderProblem(assessment.problem);
335
354
 
336
355
  renderDefinitionChanges(assessment.value);
337
356
  const applied = await connection.applyTypePack({
338
357
  provision,
339
- installed_by: installedBy,
340
- expected_assessment_digest: assessment.value.assessment_digest
358
+ installedBy,
359
+ expectedAssessmentDigest: assessment.value.assessmentDigest
341
360
  });
342
361
  if (!applied.ok) return renderProblem(applied.problem);
343
362
  ```
@@ -467,10 +486,13 @@ is not granted collection-wide type-management access.
467
486
 
468
487
  The SDK returns typed outcomes, carries successful mdbase diagnostics alongside
469
488
  the value, carries revision tokens in typed record results, and accepts
470
- `if_revision` on mutations. `describe()` exposes
489
+ `ifRevision` on mutations. `describe()` exposes
471
490
  JSON Schemas, portable type definitions, canonical collection settings, and
472
491
  first-class data contracts. `watch()` resumes from a local collection cursor;
473
492
  the Connect server does not store the change feed.
493
+ `queryAll()` treats `timeoutMs` as one deadline for the complete paginated
494
+ query. The caller-driven `queryPages()` iterator instead accepts
495
+ `pageTimeoutMs`, an independent budget for each page, plus a lifetime `signal`.
474
496
  `preflightRename()` and `preflightDelete()` run the canonical collection
475
497
  operation without changing records or advancing the change cursor, so an app
476
498
  can show authoritative reference impact before asking for confirmation.
@@ -2,6 +2,8 @@
2
2
  export { MdbaseCollectionClient } from "./collection-client.js";
3
3
  export type { MdbaseCollectionTransport } from "./operation-types.js";
4
4
  export { createPkce } from "./runtime-utils.js";
5
+ export { connectError, connectProblem, operationProblem } from "./errors.js";
6
+ export { connectFailure, connectSuccess } from "./outcomes.js";
5
7
  export type { MdbaseApplicationVerificationStore, MdbaseApplicationSessionConnect } from "./application-session.js";
6
8
  export { MdbaseSession } from "./session.js";
7
9
  export type { MdbaseSessionConnect, MdbaseSessionOptions } from "./session.js";
package/dist/advanced.js CHANGED
@@ -1,4 +1,6 @@
1
1
  /** Low-level construction and test seams. Ordinary applications use the root package. */
2
2
  export { MdbaseCollectionClient } from "./collection-client.js";
3
3
  export { createPkce } from "./runtime-utils.js";
4
+ export { connectError, connectProblem, operationProblem } from "./errors.js";
5
+ export { connectFailure, connectSuccess } from "./outcomes.js";
4
6
  export { MdbaseSession } from "./session.js";
@@ -1,13 +1,15 @@
1
- import { type ApplicationCapabilityId, type JsonObject, type MdbaseAppManifest, type TypePackAssessment } from "@mdbase-dev/connect-protocol";
1
+ import { type ApplicationCapabilityId, type JsonObject, type MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
2
2
  import { type MdbaseEffectiveCapabilities } from "./capabilities.js";
3
3
  import type { MdbaseAuthorizationOutcome, MdbaseAuthorizeOptions, MdbaseConnection } from "./connection.js";
4
4
  import type { MdbaseConnectionInfo } from "./connection-types.js";
5
5
  import { type ConnectOutcome, type CollectionTypeProblemCode, type AuthorizationProblemCode, type RegistrationProblemCode, type SessionProblemCode } from "./outcomes.js";
6
6
  import type { MdbaseApplicationSelection, MdbaseSelectionHistory } from "./selection.js";
7
- import type { ConnectRequestOptions } from "./operation-types.js";
7
+ import type { CollectionSetupAssessment, ConfigurationSetupAssessment, ConnectRequestOptions, TypePackAssessment } from "./operation-types.js";
8
8
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
9
+ import type { Application } from "./internal-types.js";
9
10
  import { type MdbaseSessionConnect, type MdbaseUnavailableReason } from "./session.js";
10
11
  export interface MdbaseApplicationSessionConnect<Frontmatter extends JsonObject = JsonObject> extends MdbaseSessionConnect<Frontmatter> {
12
+ register(options?: ConnectRequestOptions): Promise<ConnectOutcome<Application, RegistrationProblemCode>>;
11
13
  manifest(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAppManifest, RegistrationProblemCode>>;
12
14
  }
13
15
  export interface MdbaseApplicationSessionOptions {
@@ -35,7 +37,18 @@ export interface MdbaseDefinitionUpdate {
35
37
  currentVersion?: string;
36
38
  desiredVersion: string;
37
39
  resources: TypePackAssessment["resources"];
38
- contractSetups: TypePackAssessment["contract_setups"];
40
+ contractSetups: TypePackAssessment["contractSetups"];
41
+ canApply: boolean;
42
+ reason: string;
43
+ }
44
+ export interface MdbaseCollectionSetupUpdate {
45
+ status: CollectionSetupAssessment["status"];
46
+ applicable: boolean;
47
+ assessmentDigest: string;
48
+ collectionRevision: string;
49
+ provisionDigest: string;
50
+ configuration: ConfigurationSetupAssessment[];
51
+ typePacks: MdbaseDefinitionUpdate[];
39
52
  canApply: boolean;
40
53
  reason: string;
41
54
  }
@@ -54,10 +67,10 @@ export type MdbaseApplicationSessionSnapshot = {
54
67
  } | ({
55
68
  status: "authorization_required";
56
69
  } & ApplicationSessionContext) | ({
57
- status: "checking_definitions";
70
+ status: "checking_setup";
58
71
  } & ApplicationSessionContext) | ({
59
- status: "definition_review_required";
60
- updates: MdbaseDefinitionUpdate[];
72
+ status: "setup_review_required";
73
+ update: MdbaseCollectionSetupUpdate;
61
74
  } & ApplicationSessionContext) | ({
62
75
  status: "ready";
63
76
  verification: "cached" | "verified";
@@ -77,7 +90,7 @@ export type MdbaseApplicationSessionSnapshot = {
77
90
  /**
78
91
  * The application-level lifecycle boundary. It derives authorization from the
79
92
  * bundled manifest, exposes semantic capabilities, and never mutates collection
80
- * definitions without an explicit `applyDefinitionUpdates` call.
93
+ * setup without an explicit `applyCollectionSetup` call.
81
94
  */
82
95
  export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = JsonObject> {
83
96
  private readonly connect;
@@ -87,14 +100,19 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
87
100
  private readonly verificationStore;
88
101
  private snapshot;
89
102
  private manifest;
103
+ private application;
90
104
  private base;
91
105
  private stopBase?;
92
- private reviewEntries;
106
+ private setupAssessment;
93
107
  private verificationGeneration;
108
+ private lifecycleGeneration;
109
+ private startOperation;
94
110
  constructor(connect: MdbaseApplicationSessionConnect<Frontmatter>, options: MdbaseApplicationSessionOptions, timeouts?: ResolvedConnectTimeouts);
95
111
  start(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, SessionProblemCode>>;
112
+ private beginStart;
96
113
  private startWithinBudget;
97
114
  destroy(): void;
115
+ private cleanupBase;
98
116
  getSnapshot(): MdbaseApplicationSessionSnapshot;
99
117
  subscribe(listener: () => void): () => void;
100
118
  connection(): MdbaseConnection<Frontmatter> | null;
@@ -110,16 +128,18 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
110
128
  }, options?: Omit<MdbaseAuthorizeOptions, "operations" | "returnTo" | "target">): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
111
129
  handleAuthorizationCallback(callbackUrl: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
112
130
  completeAuthorization(callbackUrl?: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
113
- ensureCapabilities(capabilities: ApplicationCapabilityId[]): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter> | {
131
+ ensureCapabilities(capabilities: ApplicationCapabilityId[], options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter> | {
114
132
  kind: "unchanged";
115
133
  connection: MdbaseConnection<Frontmatter>;
116
134
  }, SessionProblemCode>>;
117
- applyDefinitionUpdates(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, CollectionTypeProblemCode | "collection_not_ready">>;
135
+ applyCollectionSetup(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, CollectionTypeProblemCode | "collection_not_ready">>;
118
136
  private refresh;
119
- private verifyDefinitions;
137
+ private verifySetup;
138
+ private collectionSetupInput;
120
139
  private context;
121
140
  private publish;
122
141
  private requireBase;
123
142
  private requireManifest;
143
+ private requireApplication;
124
144
  }
125
145
  export {};
@@ -3,7 +3,7 @@ import { capabilityOperations, operationsForApplicationCapabilities } from "@mdb
3
3
  import { effectiveCapabilities } from "./capabilities.js";
4
4
  import { connectProblem } from "./errors.js";
5
5
  import { connectFailure, connectSuccess } from "./outcomes.js";
6
- import { resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
6
+ import { createRequestBudget, requestAbortReason, resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
7
7
  import { defaultCallbackUrl } from "./runtime-utils.js";
8
8
  import { MdbaseSession } from "./session.js";
9
9
  export class MdbaseMemoryVerificationStore {
@@ -21,7 +21,7 @@ export class MdbaseMemoryVerificationStore {
21
21
  /**
22
22
  * The application-level lifecycle boundary. It derives authorization from the
23
23
  * bundled manifest, exposes semantic capabilities, and never mutates collection
24
- * definitions without an explicit `applyDefinitionUpdates` call.
24
+ * setup without an explicit `applyCollectionSetup` call.
25
25
  */
26
26
  export class MdbaseApplicationSession {
27
27
  connect;
@@ -31,10 +31,13 @@ export class MdbaseApplicationSession {
31
31
  verificationStore;
32
32
  snapshot = { status: "opening", connections: [] };
33
33
  manifest = null;
34
+ application = null;
34
35
  base = null;
35
36
  stopBase;
36
- reviewEntries = [];
37
+ setupAssessment = null;
37
38
  verificationGeneration = 0;
39
+ lifecycleGeneration = 0;
40
+ startOperation = null;
38
41
  constructor(connect, options, timeouts = resolveConnectTimeouts()) {
39
42
  this.connect = connect;
40
43
  this.options = options;
@@ -42,9 +45,42 @@ export class MdbaseApplicationSession {
42
45
  this.verificationStore = options.verificationStore ?? defaultVerificationStore();
43
46
  }
44
47
  start(options) {
45
- return withRequestBudget(options, this.timeouts.watchStartMs, (budget) => this.startWithinBudget({ signal: budget.signal, timeoutMs: null }));
48
+ if (this.base && !this.startOperation)
49
+ return Promise.resolve(connectSuccess(this.snapshot));
50
+ const operation = this.startOperation ?? this.beginStart();
51
+ operation.waiters += 1;
52
+ return withRequestBudget(options, this.timeouts.watchStartMs, () => operation.promise)
53
+ .finally(() => {
54
+ operation.waiters -= 1;
55
+ if (operation.waiters === 0 && this.startOperation === operation) {
56
+ operation.controller.abort();
57
+ this.startOperation = null;
58
+ this.lifecycleGeneration += 1;
59
+ }
60
+ });
61
+ }
62
+ beginStart() {
63
+ this.snapshot = { status: "opening", connections: [] };
64
+ const controller = new AbortController();
65
+ const generation = ++this.lifecycleGeneration;
66
+ const operation = {
67
+ promise: this.startWithinBudget({ signal: controller.signal, timeoutMs: null }, generation),
68
+ controller,
69
+ waiters: 0,
70
+ generation
71
+ };
72
+ this.startOperation = operation;
73
+ const settled = () => {
74
+ if (this.startOperation === operation)
75
+ this.startOperation = null;
76
+ };
77
+ operation.promise.then(settled, settled);
78
+ return operation;
46
79
  }
47
- async startWithinBudget(options) {
80
+ async startWithinBudget(options, generation) {
81
+ const registration = await this.connect.register(options);
82
+ if (!registration.ok)
83
+ return registration;
48
84
  const manifest = await this.connect.manifest(options);
49
85
  if (!manifest.ok)
50
86
  return manifest;
@@ -61,24 +97,60 @@ export class MdbaseApplicationSession {
61
97
  }
62
98
  }));
63
99
  }
100
+ if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
101
+ throw requestAbortReason(options.signal ?? new AbortController().signal);
102
+ }
103
+ this.application = registration.value;
64
104
  this.manifest = manifest.value;
65
- this.base = new MdbaseSession(this.connect, {
105
+ const base = new MdbaseSession(this.connect, {
66
106
  selection: this.options.selection,
67
107
  autoSelect: this.options.autoSelect,
68
108
  operations: operationsForSession(capabilities)
69
109
  });
70
- this.stopBase = this.base.subscribe(() => this.refresh());
71
- const started = await this.base.start(options);
72
- if (!started.ok)
73
- return started;
74
- await this.refresh(true, options);
75
- return connectSuccess(this.snapshot);
110
+ this.base = base;
111
+ this.stopBase = base.subscribe(() => {
112
+ if (this.base === base)
113
+ void this.refresh();
114
+ });
115
+ try {
116
+ const started = await base.start(options);
117
+ if (!started.ok) {
118
+ this.cleanupBase(base);
119
+ return started;
120
+ }
121
+ if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
122
+ throw requestAbortReason(options.signal ?? new AbortController().signal);
123
+ }
124
+ await this.refresh(true, options);
125
+ if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
126
+ throw requestAbortReason(options.signal ?? new AbortController().signal);
127
+ }
128
+ return connectSuccess(this.snapshot);
129
+ }
130
+ catch (error) {
131
+ this.cleanupBase(base);
132
+ throw error;
133
+ }
76
134
  }
77
135
  destroy() {
136
+ this.lifecycleGeneration += 1;
78
137
  this.verificationGeneration += 1;
138
+ this.startOperation?.controller.abort();
139
+ this.startOperation = null;
140
+ if (this.base)
141
+ this.cleanupBase(this.base);
142
+ this.application = null;
143
+ this.manifest = null;
144
+ this.setupAssessment = null;
145
+ this.snapshot = { status: "opening", connections: [] };
146
+ this.listeners.clear();
147
+ }
148
+ cleanupBase(base) {
149
+ if (this.base !== base)
150
+ return;
79
151
  this.stopBase?.();
80
152
  this.stopBase = undefined;
81
- this.base?.destroy();
153
+ base.destroy();
82
154
  this.base = null;
83
155
  }
84
156
  getSnapshot() {
@@ -110,7 +182,7 @@ export class MdbaseApplicationSession {
110
182
  completeAuthorization(callbackUrl, options) {
111
183
  return this.requireBase().handleAuthorizationCallback(callbackUrl === undefined ? defaultCallbackUrl() : String(callbackUrl), options);
112
184
  }
113
- ensureCapabilities(capabilities) {
185
+ ensureCapabilities(capabilities, options) {
114
186
  const manifestRequirements = this.requireManifest().requirements?.capabilities;
115
187
  const declared = new Set([
116
188
  ...(manifestRequirements?.required ?? []),
@@ -131,35 +203,42 @@ export class MdbaseApplicationSession {
131
203
  }
132
204
  })));
133
205
  }
134
- return this.requireBase().ensureOperations(operationsForIds(capabilities));
206
+ return this.requireBase().ensureOperations(operationsForIds(capabilities), options);
135
207
  }
136
- async applyDefinitionUpdates(options) {
137
- if (this.snapshot.status !== "definition_review_required" || this.reviewEntries.length === 0) {
138
- return connectFailure(connectProblem("collection_not_ready", "There are no reviewed definition updates to apply."));
208
+ async applyCollectionSetup(options) {
209
+ if (this.snapshot.status !== "setup_review_required" || !this.setupAssessment) {
210
+ return connectFailure(connectProblem("collection_not_ready", "There is no reviewed collection setup to apply."));
139
211
  }
140
- if (this.reviewEntries.some(({ assessment }) => !assessment.applicable)) {
141
- return connectFailure(connectProblem("collection_not_ready", "Resolve the definition conflicts before applying this update."));
212
+ if (!this.setupAssessment.applicable) {
213
+ return connectFailure(connectProblem("collection_not_ready", "Resolve the collection setup conflicts before applying this update."));
142
214
  }
143
215
  const connection = this.connection();
144
216
  if (!connection) {
145
217
  return connectFailure(connectProblem("collection_not_ready", "The collection is not ready."));
146
218
  }
147
- for (const { provision, assessment } of this.reviewEntries) {
148
- const input = {
149
- provision,
150
- installed_by: this.requireManifest().id,
151
- expected_assessment_digest: assessment.assessment_digest,
152
- ...(assessment.status === "downgrade" ? { allow_downgrade: true } : {})
153
- };
154
- const applied = options
155
- ? await connection.applyTypePack(input, options)
156
- : await connection.applyTypePack(input);
219
+ const assessment = this.setupAssessment;
220
+ const input = {
221
+ ...this.collectionSetupInput(),
222
+ expectedAssessmentDigest: assessment.assessmentDigest,
223
+ expectedCollectionRevision: assessment.collectionRevision,
224
+ expectedProvisionDigest: assessment.provisionDigest,
225
+ allowTypePackDowngrades: assessment.typePacks
226
+ .filter((pack) => pack.status === "downgrade")
227
+ .map((pack) => pack.desired.id)
228
+ };
229
+ const budget = createRequestBudget(options, this.timeouts.requestMs);
230
+ const requestOptions = { signal: budget.signal, timeoutMs: null };
231
+ try {
232
+ const applied = await connection.applyCollectionSetup(input, requestOptions);
157
233
  if (!applied.ok)
158
234
  return applied;
235
+ this.setupAssessment = null;
236
+ await this.verifySetup(this.context(connection), ++this.verificationGeneration, requestOptions);
237
+ return connectSuccess(this.snapshot);
238
+ }
239
+ finally {
240
+ budget.dispose();
159
241
  }
160
- this.reviewEntries = [];
161
- await this.verifyDefinitions(this.context(connection), ++this.verificationGeneration, options);
162
- return connectSuccess(this.snapshot);
163
242
  }
164
243
  async refresh(awaitVerification = false, options) {
165
244
  if (!this.base || !this.manifest)
@@ -167,7 +246,7 @@ export class MdbaseApplicationSession {
167
246
  const current = this.base.getSnapshot();
168
247
  this.verificationGeneration += 1;
169
248
  const generation = this.verificationGeneration;
170
- this.reviewEntries = [];
249
+ this.setupAssessment = null;
171
250
  if (current.status === "unselected") {
172
251
  this.publish({ status: "unselected", connections: current.connections });
173
252
  return;
@@ -181,12 +260,9 @@ export class MdbaseApplicationSession {
181
260
  this.publish({ status: "authorization_required", ...context });
182
261
  return;
183
262
  }
184
- const managesDefinitions = this.manifest.requirements?.capabilities?.required
185
- .includes("definitions.type-pack.apply") ?? false;
186
- const provisions = managesDefinitions
187
- ? this.manifest.provisions?.type_packs ?? []
188
- : [];
189
- if (provisions.length === 0) {
263
+ const managesSetup = this.manifest.requirements?.capabilities?.required
264
+ .includes("collection.setup.apply") ?? false;
265
+ if (!managesSetup) {
190
266
  this.publish({ status: "ready", verification: "verified", ...context });
191
267
  return;
192
268
  }
@@ -195,46 +271,34 @@ export class MdbaseApplicationSession {
195
271
  this.publish({ status: "ready", verification: "cached", ...context });
196
272
  }
197
273
  else {
198
- this.publish({ status: "checking_definitions", ...context });
274
+ this.publish({ status: "checking_setup", ...context });
199
275
  }
200
- const verification = this.verifyDefinitions(context, generation, options);
276
+ const verification = this.verifySetup(context, generation, options);
201
277
  if (awaitVerification)
202
278
  await verification;
203
279
  }
204
- async verifyDefinitions(context, generation, options) {
280
+ async verifySetup(context, generation, options) {
205
281
  const connection = this.connection();
206
282
  const manifest = this.requireManifest();
207
283
  if (!connection || connection.collectionId !== context.collectionId)
208
284
  return;
209
- const entries = [];
210
- const managesDefinitions = manifest.requirements?.capabilities?.required
211
- .includes("definitions.type-pack.apply") ?? false;
212
- for (const provision of managesDefinitions ? manifest.provisions?.type_packs ?? [] : []) {
213
- const input = {
214
- provision,
215
- installed_by: manifest.id
216
- };
217
- const outcome = options
218
- ? await connection.assessTypePack(input, options)
219
- : await connection.assessTypePack(input);
220
- if (generation !== this.verificationGeneration)
221
- return;
222
- if (!outcome.ok) {
223
- this.publish({ status: "blocked", problem: outcome.problem, ...context });
224
- return;
225
- }
226
- if (outcome.value.status !== "current") {
227
- entries.push({ provision, assessment: outcome.value });
228
- }
285
+ const outcome = options
286
+ ? await connection.assessCollectionSetup(this.collectionSetupInput(), options)
287
+ : await connection.assessCollectionSetup(this.collectionSetupInput());
288
+ if (generation !== this.verificationGeneration)
289
+ return;
290
+ if (!outcome.ok) {
291
+ this.publish({ status: "blocked", problem: outcome.problem, ...context });
292
+ return;
229
293
  }
230
294
  if (generation !== this.verificationGeneration)
231
295
  return;
232
- if (entries.length > 0) {
233
- this.reviewEntries = entries;
296
+ if (outcome.value.status !== "current") {
297
+ this.setupAssessment = outcome.value;
234
298
  this.verificationStore.remove(verificationKey(manifest, context.collectionId));
235
299
  this.publish({
236
- status: "definition_review_required",
237
- updates: entries.map(definitionUpdate),
300
+ status: "setup_review_required",
301
+ update: collectionSetupUpdate(outcome.value, manifest.provisions?.type_packs ?? []),
238
302
  ...context
239
303
  });
240
304
  return;
@@ -242,6 +306,21 @@ export class MdbaseApplicationSession {
242
306
  this.verificationStore.write(verificationKey(manifest, context.collectionId), verificationValue(manifest));
243
307
  this.publish({ status: "ready", verification: "verified", ...context });
244
308
  }
309
+ collectionSetupInput() {
310
+ const manifest = this.requireManifest();
311
+ const application = this.requireApplication();
312
+ return {
313
+ applicationId: declarationIdFromFamilyIdentity(application.family_identity),
314
+ declarationDigest: `sha256:${application.manifest_digest}`,
315
+ requirements: {
316
+ configuration: manifest.requirements?.configuration ?? []
317
+ },
318
+ provisions: {
319
+ configuration: manifest.provisions?.configuration ?? [],
320
+ typePacks: manifest.provisions?.type_packs ?? []
321
+ }
322
+ };
323
+ }
245
324
  context(connection) {
246
325
  const info = connection.info();
247
326
  return {
@@ -268,6 +347,11 @@ export class MdbaseApplicationSession {
268
347
  throw new Error("Start the application session before using it.");
269
348
  return this.manifest;
270
349
  }
350
+ requireApplication() {
351
+ if (!this.application)
352
+ throw new Error("Start the application session before using it.");
353
+ return this.application;
354
+ }
271
355
  }
272
356
  function operationsForSession(requirements) {
273
357
  return operationsForApplicationCapabilities(requirements);
@@ -276,8 +360,7 @@ function operationsForIds(capabilities) {
276
360
  const definitions = capabilities.flatMap((id) => capabilityOperations(id));
277
361
  return [...new Set(definitions)];
278
362
  }
279
- function definitionUpdate(entry) {
280
- const { assessment } = entry;
363
+ function definitionUpdate(provision, assessment) {
281
364
  const statusReason = assessment.status === "conflict"
282
365
  ? "Existing user-owned resources conflict with this application definition pack."
283
366
  : assessment.status === "install"
@@ -285,27 +368,59 @@ function definitionUpdate(entry) {
285
368
  : `This collection has a ${assessment.status} definition change to review.`;
286
369
  return {
287
370
  id: assessment.desired.id,
288
- name: entry.provision.manifest.name ?? assessment.desired.id,
371
+ name: provision.manifest.name ?? assessment.desired.id,
289
372
  status: assessment.status,
290
373
  applicable: assessment.applicable,
291
- assessmentDigest: assessment.assessment_digest,
374
+ assessmentDigest: assessment.assessmentDigest,
292
375
  ...(assessment.current ? { currentVersion: assessment.current.version } : {}),
293
376
  desiredVersion: assessment.desired.version,
294
377
  resources: assessment.resources,
295
- contractSetups: assessment.contract_setups,
378
+ contractSetups: assessment.contractSetups,
296
379
  canApply: assessment.applicable,
297
380
  reason: statusReason
298
381
  };
299
382
  }
383
+ function collectionSetupUpdate(assessment, provisions) {
384
+ const typePacks = assessment.typePacks.map((pack) => {
385
+ const provision = provisions.find((candidate) => candidate.manifest.id === pack.desired.id);
386
+ if (!provision) {
387
+ throw new Error(`Collection setup returned undeclared type pack '${pack.desired.id}'.`);
388
+ }
389
+ return definitionUpdate(provision, pack);
390
+ });
391
+ const configurationChanges = assessment.configuration.filter((entry) => entry.action !== "current").length;
392
+ const reason = assessment.status === "conflict"
393
+ ? "Existing collection policy conflicts with this application's required setup."
394
+ : `This application requires ${configurationChanges} configuration change${configurationChanges === 1 ? "" : "s"} and ${typePacks.length} definition update${typePacks.length === 1 ? "" : "s"}.`;
395
+ return {
396
+ status: assessment.status,
397
+ applicable: assessment.applicable,
398
+ assessmentDigest: assessment.assessmentDigest,
399
+ collectionRevision: assessment.collectionRevision,
400
+ provisionDigest: assessment.provisionDigest,
401
+ configuration: assessment.configuration,
402
+ typePacks,
403
+ canApply: assessment.applicable,
404
+ reason
405
+ };
406
+ }
300
407
  function verificationKey(manifest, collectionId) {
301
408
  return `mdbase-application-session:v1:${manifest.id}:${collectionId}`;
302
409
  }
303
410
  function verificationValue(manifest) {
304
411
  return JSON.stringify({
305
412
  capabilities: manifest.requirements?.capabilities,
306
- provisions: manifest.provisions?.type_packs ?? []
413
+ requirements: manifest.requirements?.configuration ?? [],
414
+ provisions: manifest.provisions ?? {}
307
415
  });
308
416
  }
417
+ function declarationIdFromFamilyIdentity(familyIdentity) {
418
+ const prefix = "bundle:";
419
+ if (!familyIdentity.startsWith(prefix) || familyIdentity.length === prefix.length) {
420
+ throw new Error("The registered application has no valid declaration identity.");
421
+ }
422
+ return familyIdentity.slice(prefix.length);
423
+ }
309
424
  function defaultVerificationStore() {
310
425
  if (typeof localStorage !== "undefined") {
311
426
  return {