@mdbase-dev/connect 0.1.0-beta.84 → 0.1.0-beta.85

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.
Files changed (40) hide show
  1. package/README.md +33 -6
  2. package/dist/advanced.d.ts +1 -1
  3. package/dist/application-session.d.ts +31 -10
  4. package/dist/application-session.js +278 -64
  5. package/dist/authorization-refresh.d.ts +14 -0
  6. package/dist/authorization-refresh.js +41 -0
  7. package/dist/authorization-url.d.ts +1 -0
  8. package/dist/authorization-url.js +9 -0
  9. package/dist/browser/integrity.json +1 -1
  10. package/dist/browser/mdbase-connect.min.js +3 -3
  11. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  12. package/dist/connect.d.ts +4 -1
  13. package/dist/connect.js +33 -26
  14. package/dist/connection-file-transport.d.ts +3 -0
  15. package/dist/connection-file-transport.js +59 -16
  16. package/dist/connection-transport-internals.d.ts +2 -0
  17. package/dist/connection-transport.d.ts +3 -1
  18. package/dist/connection-transport.js +83 -78
  19. package/dist/grant-key-leases.d.ts +24 -0
  20. package/dist/grant-key-leases.js +144 -0
  21. package/dist/hosted-file-request.d.ts +2 -2
  22. package/dist/hosted-file-request.js +4 -4
  23. package/dist/internal-types.d.ts +2 -0
  24. package/dist/local-file-transport.d.ts +8 -2
  25. package/dist/local-file-transport.js +71 -20
  26. package/dist/mdbase-connect.d.ts +1 -0
  27. package/dist/mdbase-connect.js +3 -0
  28. package/dist/outcomes.d.ts +12 -12
  29. package/dist/outcomes.js +6 -0
  30. package/dist/pending-recovery-transports.d.ts +1 -0
  31. package/dist/pending-recovery-transports.js +7 -3
  32. package/dist/retired-grant-keys.d.ts +9 -0
  33. package/dist/retired-grant-keys.js +56 -0
  34. package/dist/session.d.ts +36 -7
  35. package/dist/session.js +287 -34
  36. package/dist/shared-operation.d.ts +7 -0
  37. package/dist/shared-operation.js +41 -0
  38. package/dist/token-response.d.ts +2 -1
  39. package/dist/token-response.js +2 -1
  40. package/package.json +2 -2
package/README.md CHANGED
@@ -151,10 +151,19 @@ versioned capabilities in their manifest; they never maintain a parallel array
151
151
  of protocol operations. Use `getSnapshot()` and `subscribe()` directly or
152
152
  through your framework's external-store integration.
153
153
 
154
- The session distinguishes `authorization_required`, `checking_setup`,
154
+ Before and during startup the session distinguishes `not_started`, `starting`,
155
+ and `start_failed`; the last state carries the original typed problem and a
156
+ later `start()` retries. `destroyed` is terminal. Once started, the session
157
+ distinguishes `authorization_required`, `checking_setup`,
155
158
  `setup_review_required`, `ready`, `unavailable`, and `blocked`. Setup
156
- inspection is read-only. If an update is needed, render the supplied plan and
157
- apply the exact assessment the user reviewed:
159
+ inspection is read-only.
160
+ While startup owns a provisional connection, synchronous methods return the
161
+ catalogued `session_starting` outcome and cannot mutate selection or grants.
162
+ After `start_failed`, every lifecycle-dependent method returns the exact
163
+ `snapshot.problem` object.
164
+
165
+ If an update is needed, render the supplied plan and apply the exact assessment
166
+ the user reviewed:
158
167
 
159
168
  ```ts
160
169
  const snapshot = session.getSnapshot();
@@ -178,11 +187,29 @@ changes to the session. The session auto-selects only when exactly one saved
178
187
  connection exists. Switching is state-driven and must not reload the page:
179
188
 
180
189
  ```ts
181
- session.select(collectionId, { history: "replace" });
182
- session.clearSelection();
183
- session.forget(collectionId);
190
+ const selected = session.select(collectionId, { history: "replace" });
191
+ const cleared = session.clearSelection();
192
+ const forgotten = session.forget(collectionId);
184
193
  ```
185
194
 
195
+ These synchronous methods return lifecycle-aware `ConnectOutcome` values.
196
+ Explicit `forget()` and `forgetAll()` remove the saved grant, all pending
197
+ mutation recovery records for that connection, and coordinate grant-key cleanup
198
+ with other tabs and workers through Web Locks. Browsers without Web Locks retain
199
+ retired and forgotten key material in the configured key store rather than risk
200
+ deleting a key another context is using; applications that require physical key
201
+ removal must clear that store only after closing all application contexts.
202
+ Automatic authorization invalidation keeps pending recovery state so
203
+ reauthorization can resume an outcome-unknown mutation safely.
204
+ Async lifecycle-dependent methods wait for an in-progress `start()` within the
205
+ same request budget and return typed lifecycle, timeout, or cancellation
206
+ outcomes rather than throwing for session state.
207
+
208
+ If a saved grant belongs to an earlier registered identity for the same
209
+ application declaration, the snapshot is `authorization_required`. Connect
210
+ does not discard or replay that grant's pending mutations and does not open an
211
+ authorization flow automatically; call `authorize("selected")` explicitly.
212
+
186
213
  Selection validates the saved authorization before changing the URL. Explicit
187
214
  unavailable IDs remain authoritative so applications can offer exact
188
215
  reauthorization or another collection:
@@ -6,4 +6,4 @@ export { connectError, connectProblem, operationProblem } from "./errors.js";
6
6
  export { connectFailure, connectSuccess } from "./outcomes.js";
7
7
  export type { MdbaseApplicationVerificationStore, MdbaseApplicationSessionConnect } from "./application-session.js";
8
8
  export { MdbaseSession } from "./session.js";
9
- export type { MdbaseSessionConnect, MdbaseSessionOptions } from "./session.js";
9
+ export type { MdbaseSessionConnect, MdbaseSessionOptions, MdbaseSessionSnapshot } from "./session.js";
@@ -1,8 +1,8 @@
1
- import { type ApplicationCapabilityId, type JsonObject, type MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
1
+ import { type ApplicationCapabilityId, type ConnectProblem, 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
- import { type ConnectOutcome, type CollectionTypeProblemCode, type AuthorizationProblemCode, type RegistrationProblemCode, type SessionProblemCode } from "./outcomes.js";
5
+ import { type ConnectOutcome, type CollectionTypeProblemCode, type RegistrationProblemCode, type SessionProblemCode } from "./outcomes.js";
6
6
  import type { MdbaseApplicationSelection, MdbaseSelectionHistory } from "./selection.js";
7
7
  import type { CollectionSetupAssessment, ConfigurationSetupAssessment, ConnectRequestOptions, TypePackAssessment } from "./operation-types.js";
8
8
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
@@ -11,6 +11,7 @@ import { type MdbaseSessionConnect, type MdbaseUnavailableReason } from "./sessi
11
11
  export interface MdbaseApplicationSessionConnect<Frontmatter extends JsonObject = JsonObject> extends MdbaseSessionConnect<Frontmatter> {
12
12
  register(options?: ConnectRequestOptions): Promise<ConnectOutcome<Application, RegistrationProblemCode>>;
13
13
  manifest(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAppManifest, RegistrationProblemCode>>;
14
+ connectionApplicationId(collectionId: string): string | null;
14
15
  }
15
16
  export interface MdbaseApplicationSessionOptions {
16
17
  selection: MdbaseApplicationSelection;
@@ -59,7 +60,17 @@ interface ApplicationSessionContext {
59
60
  connections: MdbaseConnectionInfo[];
60
61
  }
61
62
  export type MdbaseApplicationSessionSnapshot = {
62
- status: "opening";
63
+ status: "not_started";
64
+ connections: MdbaseConnectionInfo[];
65
+ } | {
66
+ status: "starting";
67
+ connections: MdbaseConnectionInfo[];
68
+ } | {
69
+ status: "start_failed";
70
+ problem: ConnectProblem<SessionProblemCode>;
71
+ connections: MdbaseConnectionInfo[];
72
+ } | {
73
+ status: "destroyed";
63
74
  connections: MdbaseConnectionInfo[];
64
75
  } | {
65
76
  status: "unselected";
@@ -107,11 +118,16 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
107
118
  private setupTypePackAdoptions;
108
119
  private verificationGeneration;
109
120
  private lifecycleGeneration;
121
+ private readonly operationControllers;
110
122
  private startOperation;
111
123
  constructor(connect: MdbaseApplicationSessionConnect<Frontmatter>, options: MdbaseApplicationSessionOptions, timeouts?: ResolvedConnectTimeouts);
112
124
  start(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, SessionProblemCode>>;
125
+ private waitForStart;
126
+ private abandonStart;
113
127
  private beginStart;
114
128
  private startWithinBudget;
129
+ private performStartWithinBudget;
130
+ private startFailure;
115
131
  destroy(): void;
116
132
  private cleanupBase;
117
133
  getSnapshot(): MdbaseApplicationSessionSnapshot;
@@ -119,27 +135,32 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
119
135
  connection(): MdbaseConnection<Frontmatter> | null;
120
136
  select(collectionId: string, options?: {
121
137
  history?: MdbaseSelectionHistory;
122
- }): ConnectOutcome<MdbaseConnection<Frontmatter>, "unknown_collection">;
138
+ }): ConnectOutcome<MdbaseConnection<Frontmatter>, SessionProblemCode>;
123
139
  clearSelection(options?: {
124
140
  history?: MdbaseSelectionHistory;
125
- }): void;
126
- forget(collectionId: string): void;
141
+ }): ConnectOutcome<void, SessionProblemCode>;
142
+ forget(collectionId: string): ConnectOutcome<void, SessionProblemCode>;
127
143
  authorize(target: "choose" | "selected" | {
128
144
  collectionId: string;
129
145
  }, options?: Omit<MdbaseAuthorizeOptions, "operations" | "returnTo" | "target">): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
130
- handleAuthorizationCallback(callbackUrl: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
131
- completeAuthorization(callbackUrl?: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
146
+ handleAuthorizationCallback(callbackUrl: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, SessionProblemCode>>;
147
+ completeAuthorization(callbackUrl?: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, SessionProblemCode>>;
132
148
  ensureCapabilities(capabilities: ApplicationCapabilityId[], options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter> | {
133
149
  kind: "unchanged";
134
150
  connection: MdbaseConnection<Frontmatter>;
135
151
  }, SessionProblemCode>>;
136
- applyCollectionSetup(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, CollectionTypeProblemCode | "collection_not_ready">>;
152
+ applyCollectionSetup(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, CollectionTypeProblemCode | SessionProblemCode>>;
153
+ private applyCollectionSetupStarted;
137
154
  private refresh;
138
155
  private verifySetup;
139
156
  private collectionSetupInput;
140
157
  private context;
141
158
  private publish;
142
- private requireBase;
159
+ private lifecycleBase;
160
+ private withLifecycleBase;
161
+ private beginLifecycleOperation;
162
+ private lifecycleCurrent;
163
+ private destroyedOutcome;
143
164
  private requireManifest;
144
165
  private requireApplication;
145
166
  }
@@ -1,9 +1,9 @@
1
1
  import { capabilityOperations, operationsForApplicationCapabilities } from "@mdbase-dev/connect-protocol";
2
2
  /* The versioned protocol package is the sole capability-to-operation compiler. */
3
3
  import { effectiveCapabilities } from "./capabilities.js";
4
- import { connectProblem } from "./errors.js";
4
+ import { MdbaseConnectError, connectProblem } from "./errors.js";
5
5
  import { connectFailure, connectSuccess } from "./outcomes.js";
6
- import { createRequestBudget, requestAbortReason, resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
6
+ import { createRequestBudget, 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 {
@@ -29,7 +29,7 @@ export class MdbaseApplicationSession {
29
29
  timeouts;
30
30
  listeners = new Set();
31
31
  verificationStore;
32
- snapshot = { status: "opening", connections: [] };
32
+ snapshot = { status: "not_started", connections: [] };
33
33
  manifest = null;
34
34
  application = null;
35
35
  base = null;
@@ -38,6 +38,7 @@ export class MdbaseApplicationSession {
38
38
  setupTypePackAdoptions = null;
39
39
  verificationGeneration = 0;
40
40
  lifecycleGeneration = 0;
41
+ operationControllers = new Set();
41
42
  startOperation = null;
42
43
  constructor(connect, options, timeouts = resolveConnectTimeouts()) {
43
44
  this.connect = connect;
@@ -46,22 +47,47 @@ export class MdbaseApplicationSession {
46
47
  this.verificationStore = options.verificationStore ?? defaultVerificationStore();
47
48
  }
48
49
  start(options) {
50
+ if (this.snapshot.status === "destroyed") {
51
+ return Promise.resolve(connectFailure(connectProblem("session_destroyed", "This application session has been destroyed.")));
52
+ }
49
53
  if (this.base && !this.startOperation)
50
54
  return Promise.resolve(connectSuccess(this.snapshot));
51
55
  const operation = this.startOperation ?? this.beginStart();
56
+ return this.waitForStart(operation, options, this.timeouts.watchStartMs);
57
+ }
58
+ async waitForStart(operation, options, timeoutMs) {
52
59
  operation.waiters += 1;
53
- return withRequestBudget(options, this.timeouts.watchStartMs, () => operation.promise)
54
- .finally(() => {
60
+ try {
61
+ return await withRequestBudget(options, timeoutMs, () => operation.promise);
62
+ }
63
+ catch (error) {
64
+ if (error instanceof MdbaseConnectError) {
65
+ return connectFailure(error.problem);
66
+ }
67
+ throw error;
68
+ }
69
+ finally {
55
70
  operation.waiters -= 1;
56
71
  if (operation.waiters === 0 && this.startOperation === operation) {
57
- operation.controller.abort();
58
- this.startOperation = null;
59
- this.lifecycleGeneration += 1;
72
+ this.abandonStart(operation);
60
73
  }
61
- });
74
+ }
75
+ }
76
+ abandonStart(operation) {
77
+ operation.controller.abort();
78
+ this.startOperation = null;
79
+ this.lifecycleGeneration += 1;
80
+ this.verificationGeneration += 1;
81
+ if (this.base)
82
+ this.cleanupBase(this.base);
83
+ this.application = null;
84
+ this.manifest = null;
85
+ this.setupAssessment = null;
86
+ this.setupTypePackAdoptions = null;
87
+ this.publish({ status: "not_started", connections: this.connect.connections() });
62
88
  }
63
89
  beginStart() {
64
- this.snapshot = { status: "opening", connections: [] };
90
+ this.publish({ status: "starting", connections: this.connect.connections() });
65
91
  const controller = new AbortController();
66
92
  const generation = ++this.lifecycleGeneration;
67
93
  const operation = {
@@ -79,15 +105,36 @@ export class MdbaseApplicationSession {
79
105
  return operation;
80
106
  }
81
107
  async startWithinBudget(options, generation) {
108
+ try {
109
+ return await this.performStartWithinBudget(options, generation);
110
+ }
111
+ catch (error) {
112
+ if (error instanceof MdbaseConnectError) {
113
+ return this.startFailure(error.problem, generation);
114
+ }
115
+ if (generation === this.lifecycleGeneration) {
116
+ this.verificationGeneration += 1;
117
+ if (this.base)
118
+ this.cleanupBase(this.base);
119
+ this.application = null;
120
+ this.manifest = null;
121
+ this.setupAssessment = null;
122
+ this.setupTypePackAdoptions = null;
123
+ this.publish({ status: "not_started", connections: this.connect.connections() });
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+ async performStartWithinBudget(options, generation) {
82
129
  const registration = await this.connect.register(options);
83
130
  if (!registration.ok)
84
- return registration;
131
+ return this.startFailure(registration.problem, generation);
85
132
  const manifest = await this.connect.manifest(options);
86
133
  if (!manifest.ok)
87
- return manifest;
134
+ return this.startFailure(manifest.problem, generation);
88
135
  const capabilities = manifest.value.requirements?.capabilities;
89
136
  if (!capabilities) {
90
- return connectFailure(connectProblem("invalid_application_manifest", "Application sessions require a versioned semantic capability contract.", {
137
+ return this.startFailure(connectProblem("invalid_application_manifest", "Application sessions require a versioned semantic capability contract.", {
91
138
  details: {
92
139
  issues: [{
93
140
  path: "/requirements/capabilities",
@@ -96,10 +143,10 @@ export class MdbaseApplicationSession {
96
143
  params: {}
97
144
  }]
98
145
  }
99
- }));
146
+ }), generation);
100
147
  }
101
148
  if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
102
- throw requestAbortReason(options.signal ?? new AbortController().signal);
149
+ return connectFailure(connectProblem("operation_cancelled", "Application session startup was cancelled.", { operationOutcome: "not_sent" }));
103
150
  }
104
151
  this.application = registration.value;
105
152
  this.manifest = manifest.value;
@@ -113,10 +160,11 @@ export class MdbaseApplicationSession {
113
160
  const started = await base.start(options);
114
161
  if (!started.ok) {
115
162
  this.cleanupBase(base);
116
- return started;
163
+ return this.startFailure(started.problem, generation);
117
164
  }
118
165
  if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
119
- throw requestAbortReason(options.signal ?? new AbortController().signal);
166
+ this.cleanupBase(base);
167
+ return connectFailure(connectProblem("operation_cancelled", "Application session startup was cancelled.", { operationOutcome: "not_sent" }));
120
168
  }
121
169
  this.stopBase = base.subscribe(() => {
122
170
  if (this.base === base)
@@ -124,7 +172,8 @@ export class MdbaseApplicationSession {
124
172
  });
125
173
  await this.refresh(true, options);
126
174
  if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
127
- throw requestAbortReason(options.signal ?? new AbortController().signal);
175
+ this.cleanupBase(base);
176
+ return connectFailure(connectProblem("operation_cancelled", "Application session startup was cancelled.", { operationOutcome: "not_sent" }));
128
177
  }
129
178
  return connectSuccess(this.snapshot);
130
179
  }
@@ -133,18 +182,33 @@ export class MdbaseApplicationSession {
133
182
  throw error;
134
183
  }
135
184
  }
185
+ startFailure(problem, generation) {
186
+ if (generation === this.lifecycleGeneration) {
187
+ this.publish({
188
+ status: "start_failed",
189
+ problem,
190
+ connections: this.connect.connections()
191
+ });
192
+ }
193
+ return connectFailure(problem);
194
+ }
136
195
  destroy() {
196
+ if (this.snapshot.status === "destroyed")
197
+ return;
137
198
  this.lifecycleGeneration += 1;
138
199
  this.verificationGeneration += 1;
139
200
  this.startOperation?.controller.abort();
140
201
  this.startOperation = null;
202
+ for (const controller of this.operationControllers)
203
+ controller.abort();
204
+ this.operationControllers.clear();
141
205
  if (this.base)
142
206
  this.cleanupBase(this.base);
143
207
  this.application = null;
144
208
  this.manifest = null;
145
209
  this.setupAssessment = null;
146
210
  this.setupTypePackAdoptions = null;
147
- this.snapshot = { status: "opening", connections: [] };
211
+ this.publish({ status: "destroyed", connections: [] });
148
212
  this.listeners.clear();
149
213
  }
150
214
  cleanupBase(base) {
@@ -167,47 +231,100 @@ export class MdbaseApplicationSession {
167
231
  return current?.status === "ready" ? current.connection : null;
168
232
  }
169
233
  select(collectionId, options = {}) {
170
- return this.requireBase().select(collectionId, options);
234
+ const base = this.lifecycleBase();
235
+ return base.ok ? base.value.select(collectionId, options) : base;
171
236
  }
172
237
  clearSelection(options = {}) {
173
- this.requireBase().clearSelection(options);
238
+ const base = this.lifecycleBase();
239
+ if (!base.ok)
240
+ return base;
241
+ base.value.clearSelection(options);
242
+ return connectSuccess(undefined);
174
243
  }
175
244
  forget(collectionId) {
176
- this.requireBase().forget(collectionId);
245
+ const base = this.lifecycleBase();
246
+ if (!base.ok)
247
+ return base;
248
+ base.value.forget(collectionId);
249
+ return connectSuccess(undefined);
177
250
  }
178
251
  authorize(target, options = {}) {
179
- return this.requireBase().authorize(target, options);
252
+ return this.withLifecycleBase(options, async (base, requestOptions, generation) => {
253
+ const outcome = await base.authorize(target, { ...options, ...requestOptions });
254
+ if (outcome.ok && outcome.value.kind === "connected" && this.lifecycleCurrent(generation)) {
255
+ await this.refresh(true, requestOptions);
256
+ }
257
+ return outcome;
258
+ });
180
259
  }
181
260
  handleAuthorizationCallback(callbackUrl, options) {
182
- return this.requireBase().handleAuthorizationCallback(String(callbackUrl), options);
261
+ const exactUrl = String(callbackUrl);
262
+ const joinsStartupCallback = this.startOperation !== null
263
+ && this.options.selection.authorizationCallback() === exactUrl;
264
+ return this.withLifecycleBase(options, async (base, requestOptions, generation) => {
265
+ if (joinsStartupCallback) {
266
+ const current = base.getSnapshot();
267
+ if (current.status === "ready")
268
+ return connectSuccess(current.connection);
269
+ return connectFailure(connectProblem("collection_not_ready", "Authorization did not produce a ready collection."));
270
+ }
271
+ const outcome = await base.handleAuthorizationCallback(exactUrl, requestOptions);
272
+ if (outcome.ok && this.lifecycleCurrent(generation))
273
+ await this.refresh(true, requestOptions);
274
+ return outcome;
275
+ });
183
276
  }
184
277
  completeAuthorization(callbackUrl, options) {
185
- return this.requireBase().handleAuthorizationCallback(callbackUrl === undefined ? defaultCallbackUrl() : String(callbackUrl), options);
278
+ const exactUrl = callbackUrl === undefined ? defaultCallbackUrl() : String(callbackUrl);
279
+ const joinsStartupCallback = this.startOperation !== null
280
+ && this.options.selection.authorizationCallback() === exactUrl;
281
+ return this.withLifecycleBase(options, async (base, requestOptions, generation) => {
282
+ if (joinsStartupCallback) {
283
+ const current = base.getSnapshot();
284
+ if (current.status === "ready")
285
+ return connectSuccess(current.connection);
286
+ return connectFailure(connectProblem("collection_not_ready", "Authorization did not produce a ready collection."));
287
+ }
288
+ const outcome = await base.handleAuthorizationCallback(exactUrl, requestOptions);
289
+ if (outcome.ok && this.lifecycleCurrent(generation))
290
+ await this.refresh(true, requestOptions);
291
+ return outcome;
292
+ });
186
293
  }
187
294
  ensureCapabilities(capabilities, options) {
188
- const manifestRequirements = this.requireManifest().requirements?.capabilities;
189
- const declared = new Set([
190
- ...(manifestRequirements?.required ?? []),
191
- ...(manifestRequirements?.optional ?? [])
192
- ]);
193
- if (capabilities.some((capability) => !declared.has(capability))) {
194
- return Promise.resolve(connectFailure(connectProblem("invalid_application_manifest", "Applications may only request capabilities declared in their manifest.", {
195
- details: {
196
- issues: [{
197
- path: "/requirements/capabilities",
198
- keyword: "undeclaredCapability",
199
- message: "must declare every capability requested by the application",
200
- params: {
201
- requested: capabilities,
202
- declared: [...declared]
203
- }
204
- }]
295
+ return this.withLifecycleBase(options, (base, requestOptions, generation) => {
296
+ const manifestRequirements = this.manifest.requirements?.capabilities;
297
+ const declared = new Set([
298
+ ...(manifestRequirements?.required ?? []),
299
+ ...(manifestRequirements?.optional ?? [])
300
+ ]);
301
+ if (capabilities.some((capability) => !declared.has(capability))) {
302
+ return Promise.resolve(connectFailure(connectProblem("invalid_application_manifest", "Applications may only request capabilities declared in their manifest.", {
303
+ details: {
304
+ issues: [{
305
+ path: "/requirements/capabilities",
306
+ keyword: "undeclaredCapability",
307
+ message: "must declare every capability requested by the application",
308
+ params: {
309
+ requested: capabilities,
310
+ declared: [...declared]
311
+ }
312
+ }]
313
+ }
314
+ })));
315
+ }
316
+ return base.ensureOperations(operationsForIds(capabilities), requestOptions).then(async (outcome) => {
317
+ if (outcome.ok && outcome.value.kind === "connected" && this.lifecycleCurrent(generation)) {
318
+ await this.refresh(true, requestOptions);
205
319
  }
206
- })));
207
- }
208
- return this.requireBase().ensureOperations(operationsForIds(capabilities), options);
320
+ return outcome;
321
+ });
322
+ });
323
+ }
324
+ applyCollectionSetup(options) {
325
+ return this.withLifecycleBase(options, (_base, requestOptions, generation) => this.applyCollectionSetupStarted(requestOptions, generation));
209
326
  }
210
- async applyCollectionSetup(options) {
327
+ async applyCollectionSetupStarted(options, generation) {
211
328
  if (this.snapshot.status !== "setup_review_required" || !this.setupAssessment) {
212
329
  return connectFailure(connectProblem("collection_not_ready", "There is no reviewed collection setup to apply."));
213
330
  }
@@ -228,20 +345,25 @@ export class MdbaseApplicationSession {
228
345
  .filter((pack) => pack.status === "downgrade")
229
346
  .map((pack) => pack.desired.id)
230
347
  };
231
- const budget = createRequestBudget(options, this.timeouts.requestMs);
232
- const requestOptions = { signal: budget.signal, timeoutMs: null };
233
- try {
234
- const applied = await connection.applyCollectionSetup(input, requestOptions);
235
- if (!applied.ok)
236
- return applied;
237
- this.setupAssessment = null;
238
- this.setupTypePackAdoptions = null;
239
- await this.verifySetup(this.context(connection), ++this.verificationGeneration, requestOptions);
240
- return connectSuccess(this.snapshot);
348
+ const applied = await connection.applyCollectionSetup(input, options);
349
+ if (!this.lifecycleCurrent(generation)) {
350
+ if (!applied.ok && applied.problem.code === "operation_outcome_unknown") {
351
+ return connectFailure(applied.problem);
352
+ }
353
+ return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
241
354
  }
242
- finally {
243
- budget.dispose();
355
+ if (!applied.ok) {
356
+ if (applied.problem.code === "application_declaration_mismatch") {
357
+ this.setupAssessment = null;
358
+ this.setupTypePackAdoptions = null;
359
+ this.publish({ status: "authorization_required", ...this.context(connection) });
360
+ }
361
+ return applied;
244
362
  }
363
+ this.setupAssessment = null;
364
+ this.setupTypePackAdoptions = null;
365
+ await this.verifySetup(this.context(connection), ++this.verificationGeneration, options);
366
+ return connectSuccess(this.snapshot);
245
367
  }
246
368
  async refresh(awaitVerification = false, options) {
247
369
  if (!this.base || !this.manifest)
@@ -259,7 +381,13 @@ export class MdbaseApplicationSession {
259
381
  this.publish(current);
260
382
  return;
261
383
  }
384
+ if (current.status !== "ready")
385
+ return;
262
386
  const context = this.context(current.connection);
387
+ if (this.connect.connectionApplicationId(current.collectionId) !== this.application?.id) {
388
+ this.publish({ status: "authorization_required", ...context });
389
+ return;
390
+ }
263
391
  if (!context.capabilities.requiredAvailable
264
392
  || !accessRequirementSatisfied(this.manifest, context.info)) {
265
393
  this.publish({ status: "authorization_required", ...context });
@@ -294,7 +422,9 @@ export class MdbaseApplicationSession {
294
422
  if (generation !== this.verificationGeneration)
295
423
  return;
296
424
  if (!outcome.ok) {
297
- this.publish({ status: "blocked", problem: outcome.problem, ...context });
425
+ this.publish(outcome.problem.code === "application_declaration_mismatch"
426
+ ? { status: "authorization_required", ...context }
427
+ : { status: "blocked", problem: outcome.problem, ...context });
298
428
  return;
299
429
  }
300
430
  if (!outcome.value.applicable) {
@@ -306,7 +436,9 @@ export class MdbaseApplicationSession {
306
436
  if (generation !== this.verificationGeneration)
307
437
  return;
308
438
  if (!outcome.ok) {
309
- this.publish({ status: "blocked", problem: outcome.problem, ...context });
439
+ this.publish(outcome.problem.code === "application_declaration_mismatch"
440
+ ? { status: "authorization_required", ...context }
441
+ : { status: "blocked", problem: outcome.problem, ...context });
310
442
  return;
311
443
  }
312
444
  this.setupTypePackAdoptions = adoptions;
@@ -353,16 +485,91 @@ export class MdbaseApplicationSession {
353
485
  };
354
486
  }
355
487
  publish(snapshot) {
488
+ if (this.snapshot.status === "destroyed" && snapshot.status !== "destroyed")
489
+ return;
356
490
  if (JSON.stringify(this.snapshot) === JSON.stringify(snapshot))
357
491
  return;
358
492
  this.snapshot = snapshot;
359
493
  for (const listener of this.listeners)
360
494
  listener();
361
495
  }
362
- requireBase() {
363
- if (!this.base)
364
- throw new Error("Start the application session before using it.");
365
- return this.base;
496
+ lifecycleBase() {
497
+ const status = this.snapshot.status;
498
+ if (this.startOperation || status === "starting") {
499
+ return connectFailure(connectProblem("session_starting", "The application session is still starting."));
500
+ }
501
+ if (this.snapshot.status === "start_failed")
502
+ return connectFailure(this.snapshot.problem);
503
+ if (this.base)
504
+ return connectSuccess(this.base);
505
+ const code = status === "destroyed"
506
+ ? "session_destroyed"
507
+ : "session_not_started";
508
+ return connectFailure(connectProblem(code, lifecycleProblemMessage(code)));
509
+ }
510
+ async withLifecycleBase(options, operation) {
511
+ const lifecycle = this.beginLifecycleOperation(options);
512
+ const budget = createRequestBudget(lifecycle.options, this.timeouts.requestMs);
513
+ const requestOptions = { signal: budget.signal, timeoutMs: null };
514
+ try {
515
+ const starting = this.startOperation;
516
+ if (starting) {
517
+ const started = await this.waitForStart(starting, requestOptions, null);
518
+ if (!started.ok)
519
+ return started;
520
+ }
521
+ if (!this.lifecycleCurrent(lifecycle.generation)) {
522
+ return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
523
+ }
524
+ const base = this.lifecycleBase();
525
+ if (!base.ok)
526
+ return base;
527
+ const outcome = await operation(base.value, requestOptions, lifecycle.generation);
528
+ if (!this.lifecycleCurrent(lifecycle.generation))
529
+ return this.destroyedOutcome(outcome);
530
+ return outcome;
531
+ }
532
+ catch (error) {
533
+ if (error instanceof MdbaseConnectError) {
534
+ if (!this.lifecycleCurrent(lifecycle.generation)) {
535
+ if (error.problem.code === "operation_outcome_unknown") {
536
+ return connectFailure(error.problem);
537
+ }
538
+ return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
539
+ }
540
+ return connectFailure(error.problem);
541
+ }
542
+ throw error;
543
+ }
544
+ finally {
545
+ budget.dispose();
546
+ lifecycle.dispose();
547
+ }
548
+ }
549
+ beginLifecycleOperation(options = {}) {
550
+ const controller = new AbortController();
551
+ const abort = () => controller.abort(options.signal?.reason);
552
+ if (options.signal?.aborted)
553
+ abort();
554
+ else
555
+ options.signal?.addEventListener("abort", abort, { once: true });
556
+ this.operationControllers.add(controller);
557
+ return {
558
+ generation: this.lifecycleGeneration,
559
+ options: { ...options, signal: controller.signal },
560
+ dispose: () => {
561
+ options.signal?.removeEventListener("abort", abort);
562
+ this.operationControllers.delete(controller);
563
+ }
564
+ };
565
+ }
566
+ lifecycleCurrent(generation) {
567
+ return this.snapshot.status !== "destroyed" && generation === this.lifecycleGeneration;
568
+ }
569
+ destroyedOutcome(outcome) {
570
+ if (!outcome.ok && outcome.problem.code === "operation_outcome_unknown")
571
+ return outcome;
572
+ return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
366
573
  }
367
574
  requireManifest() {
368
575
  if (!this.manifest)
@@ -461,6 +668,13 @@ function declarationIdFromFamilyIdentity(familyIdentity) {
461
668
  }
462
669
  return familyIdentity.slice(prefix.length);
463
670
  }
671
+ function lifecycleProblemMessage(code) {
672
+ if (code === "session_destroyed")
673
+ return "This application session has been destroyed.";
674
+ if (code === "session_starting")
675
+ return "The application session is still starting.";
676
+ return "Start the application session before using it.";
677
+ }
464
678
  function defaultVerificationStore() {
465
679
  if (typeof localStorage !== "undefined") {
466
680
  return {