@mdbase-dev/connect 0.1.0-beta.100
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/LICENSE +21 -0
- package/README.md +696 -0
- package/dist/advanced.d.ts +9 -0
- package/dist/advanced.js +6 -0
- package/dist/application-contract.d.ts +22 -0
- package/dist/application-contract.js +75 -0
- package/dist/application-identity.d.ts +34 -0
- package/dist/application-identity.js +148 -0
- package/dist/application-session.d.ts +167 -0
- package/dist/application-session.js +722 -0
- package/dist/async.d.ts +1 -0
- package/dist/async.js +19 -0
- package/dist/authority-proof.d.ts +3 -0
- package/dist/authority-proof.js +24 -0
- package/dist/authority-sync-request.d.ts +3 -0
- package/dist/authority-sync-request.js +20 -0
- package/dist/authorization-refresh.d.ts +14 -0
- package/dist/authorization-refresh.js +41 -0
- package/dist/authorization-types.d.ts +7 -0
- package/dist/authorization-types.js +1 -0
- package/dist/authorization-url.d.ts +5 -0
- package/dist/authorization-url.js +34 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.js +21 -0
- package/dist/browser/integrity.json +4 -0
- package/dist/browser/mdbase-connect.min.js +3 -0
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +5 -0
- package/dist/capabilities.d.ts +24 -0
- package/dist/capabilities.js +91 -0
- package/dist/collection-client.d.ts +66 -0
- package/dist/collection-client.js +686 -0
- package/dist/connect-authorization-helpers.d.ts +4 -0
- package/dist/connect-authorization-helpers.js +25 -0
- package/dist/connect-options.d.ts +35 -0
- package/dist/connect-options.js +1 -0
- package/dist/connect.d.ts +66 -0
- package/dist/connect.js +711 -0
- package/dist/connection-file-transport.d.ts +34 -0
- package/dist/connection-file-transport.js +85 -0
- package/dist/connection-index.d.ts +3 -0
- package/dist/connection-index.js +20 -0
- package/dist/connection-notifications.d.ts +31 -0
- package/dist/connection-notifications.js +197 -0
- package/dist/connection-transport-internals.d.ts +24 -0
- package/dist/connection-transport-internals.js +1 -0
- package/dist/connection-transport.d.ts +64 -0
- package/dist/connection-transport.js +725 -0
- package/dist/connection-types.d.ts +38 -0
- package/dist/connection-types.js +1 -0
- package/dist/connection.d.ts +175 -0
- package/dist/connection.js +606 -0
- package/dist/crypto-entry.d.ts +7 -0
- package/dist/crypto-entry.js +4 -0
- package/dist/crypto.d.ts +63 -0
- package/dist/crypto.js +409 -0
- package/dist/direct-access.d.ts +3 -0
- package/dist/direct-access.js +29 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +142 -0
- package/dist/external-store.d.ts +10 -0
- package/dist/external-store.js +10 -0
- package/dist/file-crypto.d.ts +25 -0
- package/dist/file-crypto.js +132 -0
- package/dist/file-sha256.d.ts +12 -0
- package/dist/file-sha256.js +143 -0
- package/dist/file-stream-source.d.ts +17 -0
- package/dist/file-stream-source.js +157 -0
- package/dist/file-transfer-internals.d.ts +23 -0
- package/dist/file-transfer-internals.js +228 -0
- package/dist/files.d.ts +115 -0
- package/dist/files.js +563 -0
- package/dist/fresh-encrypted-request.d.ts +4 -0
- package/dist/fresh-encrypted-request.js +28 -0
- package/dist/grant-key-leases.d.ts +24 -0
- package/dist/grant-key-leases.js +144 -0
- package/dist/hosted-file-request.d.ts +6 -0
- package/dist/hosted-file-request.js +63 -0
- package/dist/hosted-operation-response.d.ts +5 -0
- package/dist/hosted-operation-response.js +26 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +10 -0
- package/dist/internal-types.d.ts +100 -0
- package/dist/internal-types.js +1 -0
- package/dist/local-file-transport.d.ts +37 -0
- package/dist/local-file-transport.js +457 -0
- package/dist/mdbase-connect.d.ts +28 -0
- package/dist/mdbase-connect.js +53 -0
- package/dist/notifications.d.ts +48 -0
- package/dist/notifications.js +52 -0
- package/dist/operation-helpers.d.ts +27 -0
- package/dist/operation-helpers.js +144 -0
- package/dist/operation-types.d.ts +707 -0
- package/dist/operation-types.js +1 -0
- package/dist/outcomes.d.ts +51 -0
- package/dist/outcomes.js +194 -0
- package/dist/pending-mutation-store.d.ts +13 -0
- package/dist/pending-mutation-store.js +64 -0
- package/dist/pending-recovery-transports.d.ts +5 -0
- package/dist/pending-recovery-transports.js +52 -0
- package/dist/popup-authorization.d.ts +31 -0
- package/dist/popup-authorization.js +100 -0
- package/dist/query-pagination.d.ts +6 -0
- package/dist/query-pagination.js +119 -0
- package/dist/request-budget.d.ts +34 -0
- package/dist/request-budget.js +104 -0
- package/dist/request-coordinator.d.ts +29 -0
- package/dist/request-coordinator.js +213 -0
- package/dist/retired-grant-keys.d.ts +9 -0
- package/dist/retired-grant-keys.js +56 -0
- package/dist/runtime-utils.d.ts +52 -0
- package/dist/runtime-utils.js +368 -0
- package/dist/saved-view-pagination.d.ts +6 -0
- package/dist/saved-view-pagination.js +76 -0
- package/dist/selection.d.ts +48 -0
- package/dist/selection.js +112 -0
- package/dist/session.d.ts +114 -0
- package/dist/session.js +569 -0
- package/dist/shared-operation.d.ts +7 -0
- package/dist/shared-operation.js +41 -0
- package/dist/stored-token.d.ts +10 -0
- package/dist/stored-token.js +64 -0
- package/dist/structured-readiness.d.ts +77 -0
- package/dist/structured-readiness.js +126 -0
- package/dist/token-response.d.ts +12 -0
- package/dist/token-response.js +81 -0
- package/dist/transient-retry.d.ts +22 -0
- package/dist/transient-retry.js +59 -0
- package/dist/watch-policy.d.ts +8 -0
- package/dist/watch-policy.js +12 -0
- package/package.json +51 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import { APPLICATION_SETUP_OPERATIONS } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { declaredOperations, validateAuthorizationSelection } from "./application-contract.js";
|
|
3
|
+
import { applicationReadinessContext, collectionSetupInput, publishedStructuredReadiness, verifyStructuredReadiness, verificationKey, verificationValue } from "./structured-readiness.js";
|
|
4
|
+
import { MdbaseConnectError, connectProblem } from "./errors.js";
|
|
5
|
+
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
6
|
+
import { createRequestBudget, resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
|
|
7
|
+
import { defaultCallbackUrl } from "./runtime-utils.js";
|
|
8
|
+
import { declarationIdFromFamilyIdentity } from "./connect-authorization-helpers.js";
|
|
9
|
+
import { MdbaseSession } from "./session.js";
|
|
10
|
+
export class MdbaseMemoryVerificationStore {
|
|
11
|
+
values = new Map();
|
|
12
|
+
read(key) {
|
|
13
|
+
return this.values.get(key) ?? null;
|
|
14
|
+
}
|
|
15
|
+
write(key, value) {
|
|
16
|
+
this.values.set(key, value);
|
|
17
|
+
}
|
|
18
|
+
remove(key) {
|
|
19
|
+
this.values.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The application-level lifecycle boundary. It derives authorization from the
|
|
24
|
+
* bundled manifest, exposes semantic capabilities, and never mutates collection
|
|
25
|
+
* setup without an explicit `applyCollectionSetup` call.
|
|
26
|
+
*/
|
|
27
|
+
export class MdbaseApplicationSession {
|
|
28
|
+
connect;
|
|
29
|
+
options;
|
|
30
|
+
timeouts;
|
|
31
|
+
listeners = new Set();
|
|
32
|
+
verificationStore;
|
|
33
|
+
snapshot = { status: "not_started", connections: [] };
|
|
34
|
+
manifest = null;
|
|
35
|
+
application = null;
|
|
36
|
+
base = null;
|
|
37
|
+
stopBase;
|
|
38
|
+
setupAssessment = null;
|
|
39
|
+
setupTypePackAdoptions = null;
|
|
40
|
+
verificationGeneration = 0;
|
|
41
|
+
readinessController;
|
|
42
|
+
lifecycleGeneration = 0;
|
|
43
|
+
operationControllers = new Set();
|
|
44
|
+
startOperation = null;
|
|
45
|
+
constructor(connect, options, timeouts = resolveConnectTimeouts()) {
|
|
46
|
+
this.connect = connect;
|
|
47
|
+
this.options = options;
|
|
48
|
+
this.timeouts = timeouts;
|
|
49
|
+
this.verificationStore = options.verificationStore ?? defaultVerificationStore();
|
|
50
|
+
}
|
|
51
|
+
start(options) {
|
|
52
|
+
if (this.snapshot.status === "destroyed") {
|
|
53
|
+
return Promise.resolve(connectFailure(connectProblem("session_destroyed", "This application session has been destroyed.")));
|
|
54
|
+
}
|
|
55
|
+
if (this.base && !this.startOperation)
|
|
56
|
+
return Promise.resolve(connectSuccess(this.snapshot));
|
|
57
|
+
const operation = this.startOperation ?? this.beginStart();
|
|
58
|
+
return this.waitForStart(operation, options, this.timeouts.watchStartMs);
|
|
59
|
+
}
|
|
60
|
+
async waitForStart(operation, options, timeoutMs) {
|
|
61
|
+
operation.waiters += 1;
|
|
62
|
+
try {
|
|
63
|
+
return await withRequestBudget(options, timeoutMs, () => operation.promise);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof MdbaseConnectError) {
|
|
67
|
+
return connectFailure(error.problem);
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
operation.waiters -= 1;
|
|
73
|
+
if (operation.waiters === 0 && this.startOperation === operation) {
|
|
74
|
+
this.abandonStart(operation);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
abandonStart(operation) {
|
|
79
|
+
operation.controller.abort();
|
|
80
|
+
this.readinessController?.abort();
|
|
81
|
+
this.startOperation = null;
|
|
82
|
+
this.lifecycleGeneration += 1;
|
|
83
|
+
this.verificationGeneration += 1;
|
|
84
|
+
if (this.base)
|
|
85
|
+
this.cleanupBase(this.base);
|
|
86
|
+
this.application = null;
|
|
87
|
+
this.manifest = null;
|
|
88
|
+
this.setupAssessment = null;
|
|
89
|
+
this.setupTypePackAdoptions = null;
|
|
90
|
+
this.publish({ status: "not_started", connections: this.connect.connections() });
|
|
91
|
+
}
|
|
92
|
+
beginStart() {
|
|
93
|
+
this.publish({ status: "starting", connections: this.connect.connections() });
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const generation = ++this.lifecycleGeneration;
|
|
96
|
+
const operation = {
|
|
97
|
+
promise: this.startWithinBudget({ signal: controller.signal, timeoutMs: null }, generation),
|
|
98
|
+
controller,
|
|
99
|
+
waiters: 0,
|
|
100
|
+
generation
|
|
101
|
+
};
|
|
102
|
+
this.startOperation = operation;
|
|
103
|
+
const settled = () => {
|
|
104
|
+
if (this.startOperation === operation)
|
|
105
|
+
this.startOperation = null;
|
|
106
|
+
};
|
|
107
|
+
operation.promise.then(settled, settled);
|
|
108
|
+
return operation;
|
|
109
|
+
}
|
|
110
|
+
async startWithinBudget(options, generation) {
|
|
111
|
+
try {
|
|
112
|
+
return await this.performStartWithinBudget(options, generation);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof MdbaseConnectError) {
|
|
116
|
+
return this.startFailure(error.problem, generation);
|
|
117
|
+
}
|
|
118
|
+
if (generation === this.lifecycleGeneration) {
|
|
119
|
+
this.verificationGeneration += 1;
|
|
120
|
+
if (this.base)
|
|
121
|
+
this.cleanupBase(this.base);
|
|
122
|
+
this.application = null;
|
|
123
|
+
this.manifest = null;
|
|
124
|
+
this.setupAssessment = null;
|
|
125
|
+
this.setupTypePackAdoptions = null;
|
|
126
|
+
this.publish({ status: "not_started", connections: this.connect.connections() });
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async performStartWithinBudget(options, generation) {
|
|
132
|
+
const registration = await this.connect.register(options);
|
|
133
|
+
if (!registration.ok)
|
|
134
|
+
return this.startFailure(registration.problem, generation);
|
|
135
|
+
const manifest = await this.connect.manifest(options);
|
|
136
|
+
if (!manifest.ok)
|
|
137
|
+
return this.startFailure(manifest.problem, generation);
|
|
138
|
+
if (manifest.value.requirements?.access !== "full_collection") {
|
|
139
|
+
return this.startFailure(connectProblem("invalid_application_manifest", "Application sessions require explicit full_collection access.", {
|
|
140
|
+
details: {
|
|
141
|
+
issues: [{
|
|
142
|
+
path: "/requirements/access",
|
|
143
|
+
keyword: "const",
|
|
144
|
+
message: "must be full_collection",
|
|
145
|
+
params: { allowedValue: "full_collection" }
|
|
146
|
+
}]
|
|
147
|
+
}
|
|
148
|
+
}), generation);
|
|
149
|
+
}
|
|
150
|
+
const capabilities = manifest.value.requirements.capabilities;
|
|
151
|
+
if (!capabilities) {
|
|
152
|
+
return this.startFailure(connectProblem("invalid_application_manifest", "Application sessions require a versioned semantic capability contract.", {
|
|
153
|
+
details: {
|
|
154
|
+
issues: [{
|
|
155
|
+
path: "/requirements/capabilities",
|
|
156
|
+
keyword: "required",
|
|
157
|
+
message: "is required for application sessions",
|
|
158
|
+
params: {}
|
|
159
|
+
}]
|
|
160
|
+
}
|
|
161
|
+
}), generation);
|
|
162
|
+
}
|
|
163
|
+
if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
|
|
164
|
+
return connectFailure(connectProblem("operation_cancelled", "Application session startup was cancelled.", { operationOutcome: "not_sent" }));
|
|
165
|
+
}
|
|
166
|
+
this.application = registration.value;
|
|
167
|
+
this.manifest = manifest.value;
|
|
168
|
+
const base = new MdbaseSession(this.connect, {
|
|
169
|
+
selection: this.options.selection,
|
|
170
|
+
autoSelect: this.options.autoSelect,
|
|
171
|
+
operations: operationsForSession(capabilities, manifest.value)
|
|
172
|
+
});
|
|
173
|
+
this.base = base;
|
|
174
|
+
try {
|
|
175
|
+
const started = await base.start(options);
|
|
176
|
+
if (!started.ok) {
|
|
177
|
+
this.cleanupBase(base);
|
|
178
|
+
return this.startFailure(started.problem, generation);
|
|
179
|
+
}
|
|
180
|
+
if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
|
|
181
|
+
this.cleanupBase(base);
|
|
182
|
+
return connectFailure(connectProblem("operation_cancelled", "Application session startup was cancelled.", { operationOutcome: "not_sent" }));
|
|
183
|
+
}
|
|
184
|
+
this.stopBase = base.subscribe(() => {
|
|
185
|
+
if (this.base === base)
|
|
186
|
+
void this.refresh();
|
|
187
|
+
});
|
|
188
|
+
await this.refresh(true, options);
|
|
189
|
+
if (generation !== this.lifecycleGeneration || options.signal?.aborted) {
|
|
190
|
+
this.cleanupBase(base);
|
|
191
|
+
return connectFailure(connectProblem("operation_cancelled", "Application session startup was cancelled.", { operationOutcome: "not_sent" }));
|
|
192
|
+
}
|
|
193
|
+
return connectSuccess(this.snapshot);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
this.cleanupBase(base);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
startFailure(problem, generation) {
|
|
201
|
+
if (generation === this.lifecycleGeneration) {
|
|
202
|
+
this.publish({
|
|
203
|
+
status: "start_failed",
|
|
204
|
+
problem,
|
|
205
|
+
connections: this.connect.connections()
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return connectFailure(problem);
|
|
209
|
+
}
|
|
210
|
+
destroy() {
|
|
211
|
+
if (this.snapshot.status === "destroyed")
|
|
212
|
+
return;
|
|
213
|
+
this.lifecycleGeneration += 1;
|
|
214
|
+
this.verificationGeneration += 1;
|
|
215
|
+
this.readinessController?.abort();
|
|
216
|
+
this.startOperation?.controller.abort();
|
|
217
|
+
this.startOperation = null;
|
|
218
|
+
for (const controller of this.operationControllers)
|
|
219
|
+
controller.abort();
|
|
220
|
+
this.operationControllers.clear();
|
|
221
|
+
if (this.base)
|
|
222
|
+
this.cleanupBase(this.base);
|
|
223
|
+
this.application = null;
|
|
224
|
+
this.manifest = null;
|
|
225
|
+
this.setupAssessment = null;
|
|
226
|
+
this.setupTypePackAdoptions = null;
|
|
227
|
+
this.publish({ status: "destroyed", connections: [] });
|
|
228
|
+
this.listeners.clear();
|
|
229
|
+
}
|
|
230
|
+
cleanupBase(base) {
|
|
231
|
+
if (this.base !== base)
|
|
232
|
+
return;
|
|
233
|
+
this.stopBase?.();
|
|
234
|
+
this.stopBase = undefined;
|
|
235
|
+
base.destroy();
|
|
236
|
+
this.base = null;
|
|
237
|
+
}
|
|
238
|
+
getSnapshot() {
|
|
239
|
+
return this.snapshot;
|
|
240
|
+
}
|
|
241
|
+
subscribe(listener) {
|
|
242
|
+
this.listeners.add(listener);
|
|
243
|
+
return () => this.listeners.delete(listener);
|
|
244
|
+
}
|
|
245
|
+
connection() {
|
|
246
|
+
const current = this.base?.getSnapshot();
|
|
247
|
+
return current?.status === "ready" ? current.connection : null;
|
|
248
|
+
}
|
|
249
|
+
select(collectionId, options = {}) {
|
|
250
|
+
const base = this.lifecycleBase();
|
|
251
|
+
return base.ok ? base.value.select(collectionId, options) : base;
|
|
252
|
+
}
|
|
253
|
+
clearSelection(options = {}) {
|
|
254
|
+
const base = this.lifecycleBase();
|
|
255
|
+
if (!base.ok)
|
|
256
|
+
return base;
|
|
257
|
+
base.value.clearSelection(options);
|
|
258
|
+
return connectSuccess(undefined);
|
|
259
|
+
}
|
|
260
|
+
forget(collectionId) {
|
|
261
|
+
const base = this.lifecycleBase();
|
|
262
|
+
if (!base.ok)
|
|
263
|
+
return base;
|
|
264
|
+
base.value.forget(collectionId);
|
|
265
|
+
return connectSuccess(undefined);
|
|
266
|
+
}
|
|
267
|
+
authorize(target, options = {}) {
|
|
268
|
+
return this.withLifecycleBase(options, async (base, requestOptions, generation) => {
|
|
269
|
+
const outcome = await base.authorize(target, { ...options, ...requestOptions });
|
|
270
|
+
if (outcome.ok && outcome.value.kind === "connected" && this.lifecycleCurrent(generation)) {
|
|
271
|
+
await this.refresh(true, requestOptions);
|
|
272
|
+
}
|
|
273
|
+
return outcome;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
handleAuthorizationCallback(callbackUrl, options) {
|
|
277
|
+
const exactUrl = String(callbackUrl);
|
|
278
|
+
const joinsStartupCallback = this.startOperation !== null
|
|
279
|
+
&& this.options.selection.authorizationCallback() === exactUrl;
|
|
280
|
+
return this.withLifecycleBase(options, async (base, requestOptions, generation) => {
|
|
281
|
+
if (joinsStartupCallback) {
|
|
282
|
+
const current = base.getSnapshot();
|
|
283
|
+
if (current.status === "ready")
|
|
284
|
+
return connectSuccess(current.connection);
|
|
285
|
+
return connectFailure(connectProblem("collection_not_ready", "Authorization did not produce a ready collection."));
|
|
286
|
+
}
|
|
287
|
+
const outcome = await base.handleAuthorizationCallback(exactUrl, requestOptions);
|
|
288
|
+
if (outcome.ok && this.lifecycleCurrent(generation))
|
|
289
|
+
await this.refresh(true, requestOptions);
|
|
290
|
+
return outcome;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
completeAuthorization(callbackUrl, options) {
|
|
294
|
+
const exactUrl = callbackUrl === undefined ? defaultCallbackUrl() : String(callbackUrl);
|
|
295
|
+
const joinsStartupCallback = this.startOperation !== null
|
|
296
|
+
&& this.options.selection.authorizationCallback() === exactUrl;
|
|
297
|
+
return this.withLifecycleBase(options, async (base, requestOptions, generation) => {
|
|
298
|
+
if (joinsStartupCallback) {
|
|
299
|
+
const current = base.getSnapshot();
|
|
300
|
+
if (current.status === "ready")
|
|
301
|
+
return connectSuccess(current.connection);
|
|
302
|
+
return connectFailure(connectProblem("collection_not_ready", "Authorization did not produce a ready collection."));
|
|
303
|
+
}
|
|
304
|
+
const outcome = await base.handleAuthorizationCallback(exactUrl, requestOptions);
|
|
305
|
+
if (outcome.ok && this.lifecycleCurrent(generation))
|
|
306
|
+
await this.refresh(true, requestOptions);
|
|
307
|
+
return outcome;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
ensureCapabilities(capabilities, options) {
|
|
311
|
+
return this.withLifecycleBase(options, (base, requestOptions, generation) => {
|
|
312
|
+
validateAuthorizationSelection(this.manifest.requirements, { ...options, capabilities });
|
|
313
|
+
const manifestRequirements = this.manifest.requirements?.capabilities;
|
|
314
|
+
const declared = new Set([
|
|
315
|
+
...(manifestRequirements?.required ?? []),
|
|
316
|
+
...(manifestRequirements?.optional ?? [])
|
|
317
|
+
]);
|
|
318
|
+
if (capabilities.some((capability) => !declared.has(capability))) {
|
|
319
|
+
return Promise.resolve(connectFailure(connectProblem("invalid_application_manifest", "Applications may only request capabilities declared in their manifest.", {
|
|
320
|
+
details: {
|
|
321
|
+
issues: [{
|
|
322
|
+
path: "/requirements/capabilities",
|
|
323
|
+
keyword: "undeclaredCapability",
|
|
324
|
+
message: "must declare every capability requested by the application",
|
|
325
|
+
params: {
|
|
326
|
+
requested: capabilities,
|
|
327
|
+
declared: [...declared]
|
|
328
|
+
}
|
|
329
|
+
}]
|
|
330
|
+
}
|
|
331
|
+
})));
|
|
332
|
+
}
|
|
333
|
+
return base.ensureCapabilities(capabilities, requestOptions).then(async (outcome) => {
|
|
334
|
+
if (outcome.ok && outcome.value.kind === "connected" && this.lifecycleCurrent(generation)) {
|
|
335
|
+
await this.refresh(true, requestOptions);
|
|
336
|
+
}
|
|
337
|
+
return outcome;
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
ensureOperations(operations, options) {
|
|
342
|
+
return this.withLifecycleBase(options, async (base, requestOptions, generation) => {
|
|
343
|
+
validateAuthorizationSelection(this.manifest.requirements, { ...options, operations });
|
|
344
|
+
const outcome = await base.ensureOperations(operations, requestOptions);
|
|
345
|
+
if (outcome.ok && outcome.value.kind === "connected" && this.lifecycleCurrent(generation))
|
|
346
|
+
await this.refresh(true, requestOptions);
|
|
347
|
+
return outcome;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
applyCollectionSetup(options) {
|
|
351
|
+
return this.withLifecycleBase(options, (_base, requestOptions, generation) => this.applyCollectionSetupStarted(requestOptions, generation));
|
|
352
|
+
}
|
|
353
|
+
async applyCollectionSetupStarted(options, generation) {
|
|
354
|
+
if (this.snapshot.status !== "setup_review_required" || !this.setupAssessment) {
|
|
355
|
+
return connectFailure(connectProblem("collection_not_ready", "There is no reviewed collection setup to apply."));
|
|
356
|
+
}
|
|
357
|
+
if (!this.setupAssessment.applicable) {
|
|
358
|
+
return connectFailure(connectProblem("collection_not_ready", "Resolve the collection setup conflicts before applying this update."));
|
|
359
|
+
}
|
|
360
|
+
const connection = this.connection();
|
|
361
|
+
if (!connection) {
|
|
362
|
+
return connectFailure(connectProblem("collection_not_ready", "The collection is not ready."));
|
|
363
|
+
}
|
|
364
|
+
const verificationGeneration = this.verificationGeneration;
|
|
365
|
+
const assessment = this.setupAssessment;
|
|
366
|
+
const input = {
|
|
367
|
+
...this.collectionSetupInput(this.setupTypePackAdoptions ?? undefined),
|
|
368
|
+
expectedAssessmentDigest: assessment.assessmentDigest,
|
|
369
|
+
expectedCollectionRevision: assessment.collectionRevision,
|
|
370
|
+
expectedProvisionDigest: assessment.provisionDigest,
|
|
371
|
+
allowTypePackDowngrades: assessment.typePacks
|
|
372
|
+
.filter((pack) => pack.status === "downgrade")
|
|
373
|
+
.map((pack) => pack.desired.id)
|
|
374
|
+
};
|
|
375
|
+
const applied = await connection.applyCollectionSetup(input, options);
|
|
376
|
+
if (!this.lifecycleCurrent(generation)) {
|
|
377
|
+
if (!applied.ok && applied.problem.code === "operation_outcome_unknown") {
|
|
378
|
+
return connectFailure(applied.problem);
|
|
379
|
+
}
|
|
380
|
+
return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
|
|
381
|
+
}
|
|
382
|
+
if (verificationGeneration !== this.verificationGeneration || this.connection() !== connection) {
|
|
383
|
+
return connectFailure(connectProblem("collection_not_ready", "The selected collection changed during setup."));
|
|
384
|
+
}
|
|
385
|
+
if (!applied.ok) {
|
|
386
|
+
if (applied.problem.code === "application_declaration_mismatch") {
|
|
387
|
+
this.setupAssessment = null;
|
|
388
|
+
this.setupTypePackAdoptions = null;
|
|
389
|
+
this.publish({ status: "authorization_required", ...this.context(connection) });
|
|
390
|
+
}
|
|
391
|
+
return applied;
|
|
392
|
+
}
|
|
393
|
+
this.setupAssessment = null;
|
|
394
|
+
this.setupTypePackAdoptions = null;
|
|
395
|
+
await this.verifySetup(this.context(connection), ++this.verificationGeneration, options);
|
|
396
|
+
return connectSuccess(this.snapshot);
|
|
397
|
+
}
|
|
398
|
+
async refresh(awaitVerification = false, options) {
|
|
399
|
+
if (!this.base || !this.manifest)
|
|
400
|
+
return;
|
|
401
|
+
const current = this.base.getSnapshot();
|
|
402
|
+
this.readinessController?.abort();
|
|
403
|
+
this.verificationGeneration += 1;
|
|
404
|
+
const generation = this.verificationGeneration;
|
|
405
|
+
this.setupAssessment = null;
|
|
406
|
+
this.setupTypePackAdoptions = null;
|
|
407
|
+
if (current.status === "unselected") {
|
|
408
|
+
this.publish({ status: "unselected", connections: current.connections });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (current.status === "unavailable") {
|
|
412
|
+
this.publish(current);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (current.status !== "ready")
|
|
416
|
+
return;
|
|
417
|
+
const context = this.context(current.connection);
|
|
418
|
+
if (this.connect.connectionApplicationId(current.collectionId) !== this.application?.id) {
|
|
419
|
+
this.publish({ status: "authorization_required", ...context });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!context.capabilities.requiredAvailable
|
|
423
|
+
|| context.readiness?.files.state === "requires_authorization"
|
|
424
|
+
|| !accessRequirementSatisfied(this.manifest, context.info)) {
|
|
425
|
+
this.publish({ status: "authorization_required", ...context });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const managesSetup = (this.manifest.provisions?.type_packs.length ?? 0) > 0
|
|
429
|
+
|| (this.manifest.provisions?.configuration?.length ?? 0) > 0;
|
|
430
|
+
if (!managesSetup) {
|
|
431
|
+
const verification = this.publishReady(context, generation, options);
|
|
432
|
+
if (awaitVerification)
|
|
433
|
+
await verification;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const key = verificationKey(this.manifest, current.collectionId);
|
|
437
|
+
if (context.readiness?.contracts.state !== "checking"
|
|
438
|
+
&& this.verificationStore.read(key) === verificationValue(this.manifest)) {
|
|
439
|
+
this.publish({ status: "ready", verification: "cached", ...context });
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
this.publish({ status: "checking_setup", ...context });
|
|
443
|
+
}
|
|
444
|
+
const verification = this.verifySetup(context, generation, options);
|
|
445
|
+
if (awaitVerification)
|
|
446
|
+
await verification;
|
|
447
|
+
}
|
|
448
|
+
async verifySetup(context, generation, options) {
|
|
449
|
+
const connection = this.connection();
|
|
450
|
+
const manifest = this.requireManifest();
|
|
451
|
+
if (!connection || connection.collectionId !== context.collectionId)
|
|
452
|
+
return;
|
|
453
|
+
const initialInput = this.collectionSetupInput();
|
|
454
|
+
let outcome = options
|
|
455
|
+
? await connection.assessCollectionSetup(initialInput, options)
|
|
456
|
+
: await connection.assessCollectionSetup(initialInput);
|
|
457
|
+
if (generation !== this.verificationGeneration)
|
|
458
|
+
return;
|
|
459
|
+
if (!outcome.ok) {
|
|
460
|
+
this.publish(outcome.problem.code === "application_declaration_mismatch"
|
|
461
|
+
? { status: "authorization_required", ...context }
|
|
462
|
+
: { status: "blocked", problem: outcome.problem, ...context });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (!outcome.value.applicable) {
|
|
466
|
+
const adoptions = reviewableTypePackAdoptions(outcome.value);
|
|
467
|
+
if (Object.keys(adoptions).length > 0) {
|
|
468
|
+
outcome = options
|
|
469
|
+
? await connection.assessCollectionSetup(this.collectionSetupInput(adoptions), options)
|
|
470
|
+
: await connection.assessCollectionSetup(this.collectionSetupInput(adoptions));
|
|
471
|
+
if (generation !== this.verificationGeneration)
|
|
472
|
+
return;
|
|
473
|
+
if (!outcome.ok) {
|
|
474
|
+
this.publish(outcome.problem.code === "application_declaration_mismatch"
|
|
475
|
+
? { status: "authorization_required", ...context }
|
|
476
|
+
: { status: "blocked", problem: outcome.problem, ...context });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
this.setupTypePackAdoptions = adoptions;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (generation !== this.verificationGeneration)
|
|
483
|
+
return;
|
|
484
|
+
if (outcome.value.status !== "current") {
|
|
485
|
+
this.setupAssessment = outcome.value;
|
|
486
|
+
this.verificationStore.remove(verificationKey(manifest, context.collectionId));
|
|
487
|
+
this.publish({
|
|
488
|
+
status: "setup_review_required",
|
|
489
|
+
update: collectionSetupUpdate(outcome.value, manifest.provisions?.type_packs ?? []),
|
|
490
|
+
...context
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
this.verificationStore.write(verificationKey(manifest, context.collectionId), verificationValue(manifest));
|
|
495
|
+
if (context.readiness)
|
|
496
|
+
context.readiness.setup = { state: "current", evidence: [{ source: "authority", fact: "Collection setup assessment is current." }] };
|
|
497
|
+
await this.publishReady(context, generation, options);
|
|
498
|
+
}
|
|
499
|
+
async publishReady(context, generation, options) {
|
|
500
|
+
const contracts = this.requireManifest().requirements?.contracts ?? [];
|
|
501
|
+
if (context.readiness && contracts.length) {
|
|
502
|
+
const connection = this.connection();
|
|
503
|
+
if (!connection || connection.collectionId !== context.collectionId)
|
|
504
|
+
return;
|
|
505
|
+
this.publish({ status: "checking_setup", ...context });
|
|
506
|
+
const controller = new AbortController();
|
|
507
|
+
this.readinessController?.abort();
|
|
508
|
+
this.readinessController = controller;
|
|
509
|
+
try {
|
|
510
|
+
if (!await verifyStructuredReadiness({
|
|
511
|
+
...context, readiness: context.readiness, contracts, connection, controller, options,
|
|
512
|
+
requestMs: this.timeouts.requestMs,
|
|
513
|
+
isCurrent: () => generation === this.verificationGeneration && this.connection() === connection,
|
|
514
|
+
publish: result => this.publish({ ...result, ...context })
|
|
515
|
+
}))
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
if (this.readinessController === controller)
|
|
520
|
+
this.readinessController = undefined;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (generation === this.verificationGeneration)
|
|
524
|
+
this.publish({ status: "ready", verification: "verified", ...context });
|
|
525
|
+
}
|
|
526
|
+
collectionSetupInput(typePackAdoptions) {
|
|
527
|
+
const manifest = this.requireManifest();
|
|
528
|
+
const application = this.requireApplication();
|
|
529
|
+
return collectionSetupInput(manifest, declarationIdFromFamilyIdentity(application.family_identity), `sha256:${application.manifest_digest}`, typePackAdoptions);
|
|
530
|
+
}
|
|
531
|
+
context(connection) {
|
|
532
|
+
return applicationReadinessContext(this.requireManifest(), connection.collectionId, connection.info(), this.connect.connections());
|
|
533
|
+
}
|
|
534
|
+
publish(snapshot) {
|
|
535
|
+
if (this.snapshot.status === "destroyed" && snapshot.status !== "destroyed")
|
|
536
|
+
return;
|
|
537
|
+
if ("readiness" in snapshot && snapshot.readiness) {
|
|
538
|
+
snapshot = { ...snapshot, readiness: publishedStructuredReadiness(snapshot.readiness, snapshot.status, snapshot.status === "ready" ? snapshot.verification : undefined) };
|
|
539
|
+
}
|
|
540
|
+
if (JSON.stringify(this.snapshot) === JSON.stringify(snapshot))
|
|
541
|
+
return;
|
|
542
|
+
this.snapshot = snapshot;
|
|
543
|
+
for (const listener of this.listeners)
|
|
544
|
+
listener();
|
|
545
|
+
}
|
|
546
|
+
lifecycleBase() {
|
|
547
|
+
const status = this.snapshot.status;
|
|
548
|
+
if (this.startOperation || status === "starting") {
|
|
549
|
+
return connectFailure(connectProblem("session_starting", "The application session is still starting."));
|
|
550
|
+
}
|
|
551
|
+
if (this.snapshot.status === "start_failed")
|
|
552
|
+
return connectFailure(this.snapshot.problem);
|
|
553
|
+
if (this.base)
|
|
554
|
+
return connectSuccess(this.base);
|
|
555
|
+
const code = status === "destroyed"
|
|
556
|
+
? "session_destroyed"
|
|
557
|
+
: "session_not_started";
|
|
558
|
+
return connectFailure(connectProblem(code, lifecycleProblemMessage(code)));
|
|
559
|
+
}
|
|
560
|
+
async withLifecycleBase(options, operation) {
|
|
561
|
+
const lifecycle = this.beginLifecycleOperation(options);
|
|
562
|
+
const budget = createRequestBudget(lifecycle.options, this.timeouts.requestMs);
|
|
563
|
+
const requestOptions = { signal: budget.signal, timeoutMs: null };
|
|
564
|
+
try {
|
|
565
|
+
const starting = this.startOperation;
|
|
566
|
+
if (starting) {
|
|
567
|
+
const started = await this.waitForStart(starting, requestOptions, null);
|
|
568
|
+
if (!started.ok)
|
|
569
|
+
return started;
|
|
570
|
+
}
|
|
571
|
+
if (!this.lifecycleCurrent(lifecycle.generation)) {
|
|
572
|
+
return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
|
|
573
|
+
}
|
|
574
|
+
const base = this.lifecycleBase();
|
|
575
|
+
if (!base.ok)
|
|
576
|
+
return base;
|
|
577
|
+
const outcome = await operation(base.value, requestOptions, lifecycle.generation);
|
|
578
|
+
if (!this.lifecycleCurrent(lifecycle.generation))
|
|
579
|
+
return this.destroyedOutcome(outcome);
|
|
580
|
+
return outcome;
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
if (error instanceof MdbaseConnectError) {
|
|
584
|
+
if (!this.lifecycleCurrent(lifecycle.generation)) {
|
|
585
|
+
if (error.problem.code === "operation_outcome_unknown") {
|
|
586
|
+
return connectFailure(error.problem);
|
|
587
|
+
}
|
|
588
|
+
return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
|
|
589
|
+
}
|
|
590
|
+
return connectFailure(error.problem);
|
|
591
|
+
}
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
budget.dispose();
|
|
596
|
+
lifecycle.dispose();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
beginLifecycleOperation(options = {}) {
|
|
600
|
+
const controller = new AbortController();
|
|
601
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
602
|
+
if (options.signal?.aborted)
|
|
603
|
+
abort();
|
|
604
|
+
else
|
|
605
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
606
|
+
this.operationControllers.add(controller);
|
|
607
|
+
return {
|
|
608
|
+
generation: this.lifecycleGeneration,
|
|
609
|
+
options: { ...options, signal: controller.signal },
|
|
610
|
+
dispose: () => {
|
|
611
|
+
options.signal?.removeEventListener("abort", abort);
|
|
612
|
+
this.operationControllers.delete(controller);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
lifecycleCurrent(generation) {
|
|
617
|
+
return this.snapshot.status !== "destroyed" && generation === this.lifecycleGeneration;
|
|
618
|
+
}
|
|
619
|
+
destroyedOutcome(outcome) {
|
|
620
|
+
if (!outcome.ok && outcome.problem.code === "operation_outcome_unknown")
|
|
621
|
+
return outcome;
|
|
622
|
+
return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
|
|
623
|
+
}
|
|
624
|
+
requireManifest() {
|
|
625
|
+
if (!this.manifest)
|
|
626
|
+
throw new Error("Start the application session before using it.");
|
|
627
|
+
return this.manifest;
|
|
628
|
+
}
|
|
629
|
+
requireApplication() {
|
|
630
|
+
if (!this.application)
|
|
631
|
+
throw new Error("Start the application session before using it.");
|
|
632
|
+
return this.application;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function operationsForSession(requirements, manifest) {
|
|
636
|
+
const hasSetup = (manifest.provisions?.type_packs.length ?? 0) > 0
|
|
637
|
+
|| (manifest.provisions?.configuration?.length ?? 0) > 0;
|
|
638
|
+
return [
|
|
639
|
+
...declaredOperations(manifest.requirements),
|
|
640
|
+
...(requirements.contract_version === 2 && hasSetup ? APPLICATION_SETUP_OPERATIONS : [])
|
|
641
|
+
];
|
|
642
|
+
}
|
|
643
|
+
function definitionUpdate(provision, assessment) {
|
|
644
|
+
const statusReason = assessment.status === "conflict"
|
|
645
|
+
? "Existing user-owned resources conflict with this application definition pack."
|
|
646
|
+
: assessment.status === "install"
|
|
647
|
+
? "This collection needs the application definitions installed."
|
|
648
|
+
: `This collection has a ${assessment.status} definition change to review.`;
|
|
649
|
+
return {
|
|
650
|
+
id: assessment.desired.id,
|
|
651
|
+
name: provision.manifest.name ?? assessment.desired.id,
|
|
652
|
+
status: assessment.status,
|
|
653
|
+
applicable: assessment.applicable,
|
|
654
|
+
assessmentDigest: assessment.assessmentDigest,
|
|
655
|
+
...(assessment.current ? { currentVersion: assessment.current.version } : {}),
|
|
656
|
+
desiredVersion: assessment.desired.version,
|
|
657
|
+
resources: assessment.resources,
|
|
658
|
+
contractSetups: assessment.contractSetups,
|
|
659
|
+
canApply: assessment.applicable,
|
|
660
|
+
reason: statusReason
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function collectionSetupUpdate(assessment, provisions) {
|
|
664
|
+
const typePacks = assessment.typePacks.map((pack) => {
|
|
665
|
+
const provision = provisions.find((candidate) => candidate.manifest.id === pack.desired.id);
|
|
666
|
+
if (!provision) {
|
|
667
|
+
throw new Error(`Collection setup returned undeclared type pack '${pack.desired.id}'.`);
|
|
668
|
+
}
|
|
669
|
+
return definitionUpdate(provision, pack);
|
|
670
|
+
});
|
|
671
|
+
const configurationChanges = assessment.configuration.filter((entry) => entry.action !== "current").length;
|
|
672
|
+
const reason = assessment.status === "conflict"
|
|
673
|
+
? "Existing collection policy conflicts with this application's required setup."
|
|
674
|
+
: `This application requires ${configurationChanges} configuration change${configurationChanges === 1 ? "" : "s"} and ${typePacks.length} definition update${typePacks.length === 1 ? "" : "s"}.`;
|
|
675
|
+
return {
|
|
676
|
+
status: assessment.status,
|
|
677
|
+
applicable: assessment.applicable,
|
|
678
|
+
assessmentDigest: assessment.assessmentDigest,
|
|
679
|
+
collectionRevision: assessment.collectionRevision,
|
|
680
|
+
provisionDigest: assessment.provisionDigest,
|
|
681
|
+
configuration: assessment.configuration,
|
|
682
|
+
typePacks,
|
|
683
|
+
canApply: assessment.applicable,
|
|
684
|
+
reason
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function reviewableTypePackAdoptions(assessment) {
|
|
688
|
+
const adoptions = {};
|
|
689
|
+
for (const pack of assessment.typePacks) {
|
|
690
|
+
const resources = Object.fromEntries(pack.resources
|
|
691
|
+
.filter((resource) => resource.action === "conflict"
|
|
692
|
+
&& resource.mode === "managed"
|
|
693
|
+
&& resource.currentDigest !== undefined
|
|
694
|
+
&& resource.installedDigest === undefined)
|
|
695
|
+
.map((resource) => [resource.target, resource.currentDigest]));
|
|
696
|
+
if (Object.keys(resources).length > 0)
|
|
697
|
+
adoptions[pack.desired.id] = resources;
|
|
698
|
+
}
|
|
699
|
+
return adoptions;
|
|
700
|
+
}
|
|
701
|
+
function accessRequirementSatisfied(manifest, connection) {
|
|
702
|
+
return manifest.requirements?.access === "full_collection"
|
|
703
|
+
&& connection.scope.access === "full_collection"
|
|
704
|
+
&& connection.scope.contracts.length === 0;
|
|
705
|
+
}
|
|
706
|
+
function lifecycleProblemMessage(code) {
|
|
707
|
+
if (code === "session_destroyed")
|
|
708
|
+
return "This application session has been destroyed.";
|
|
709
|
+
if (code === "session_starting")
|
|
710
|
+
return "The application session is still starting.";
|
|
711
|
+
return "Start the application session before using it.";
|
|
712
|
+
}
|
|
713
|
+
function defaultVerificationStore() {
|
|
714
|
+
if (typeof localStorage !== "undefined") {
|
|
715
|
+
return {
|
|
716
|
+
read: (key) => localStorage.getItem(key),
|
|
717
|
+
write: (key, value) => localStorage.setItem(key, value),
|
|
718
|
+
remove: (key) => localStorage.removeItem(key)
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
return new MdbaseMemoryVerificationStore();
|
|
722
|
+
}
|