@mdbase-dev/connect 0.1.0-beta.84 → 0.1.0-beta.86
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 +33 -6
- package/dist/advanced.d.ts +1 -1
- package/dist/application-session.d.ts +31 -10
- package/dist/application-session.js +278 -64
- package/dist/authorization-refresh.d.ts +14 -0
- package/dist/authorization-refresh.js +41 -0
- package/dist/authorization-url.d.ts +1 -0
- package/dist/authorization-url.js +9 -0
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +3 -3
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/connect.d.ts +4 -1
- package/dist/connect.js +33 -26
- package/dist/connection-file-transport.d.ts +3 -0
- package/dist/connection-file-transport.js +59 -16
- package/dist/connection-transport-internals.d.ts +2 -0
- package/dist/connection-transport.d.ts +3 -1
- package/dist/connection-transport.js +83 -78
- package/dist/grant-key-leases.d.ts +24 -0
- package/dist/grant-key-leases.js +144 -0
- package/dist/hosted-file-request.d.ts +2 -2
- package/dist/hosted-file-request.js +4 -4
- package/dist/internal-types.d.ts +2 -0
- package/dist/local-file-transport.d.ts +8 -2
- package/dist/local-file-transport.js +71 -20
- package/dist/mdbase-connect.d.ts +1 -0
- package/dist/mdbase-connect.js +3 -0
- package/dist/outcomes.d.ts +12 -12
- package/dist/outcomes.js +6 -0
- package/dist/pending-recovery-transports.d.ts +1 -0
- package/dist/pending-recovery-transports.js +7 -3
- package/dist/retired-grant-keys.d.ts +9 -0
- package/dist/retired-grant-keys.js +56 -0
- package/dist/session.d.ts +36 -7
- package/dist/session.js +287 -34
- package/dist/shared-operation.d.ts +7 -0
- package/dist/shared-operation.js +41 -0
- package/dist/token-response.d.ts +2 -1
- package/dist/token-response.js +2 -1
- 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
|
-
|
|
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.
|
|
157
|
-
|
|
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:
|
package/dist/advanced.d.ts
CHANGED
|
@@ -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
|
|
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: "
|
|
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>,
|
|
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>,
|
|
131
|
-
completeAuthorization(callbackUrl?: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>,
|
|
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 |
|
|
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
|
|
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,
|
|
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: "
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
234
|
+
const base = this.lifecycleBase();
|
|
235
|
+
return base.ok ? base.value.select(collectionId, options) : base;
|
|
171
236
|
}
|
|
172
237
|
clearSelection(options = {}) {
|
|
173
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 {
|