@mdbase-dev/connect 0.1.0-beta.26 → 0.1.0-beta.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -44
- package/dist/application-identity.d.ts +32 -3
- package/dist/application-identity.js +104 -12
- package/dist/application-session.d.ts +120 -0
- package/dist/application-session.js +281 -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/capabilities.d.ts +23 -0
- package/dist/capabilities.js +112 -0
- package/dist/collection-client.d.ts +3 -2
- package/dist/collection-client.js +5 -2
- package/dist/connect-options.d.ts +3 -0
- package/dist/connect.d.ts +8 -0
- package/dist/connect.js +73 -102
- package/dist/connection-types.d.ts +7 -0
- package/dist/connection.d.ts +3 -14
- package/dist/connection.js +8 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/internal-types.d.ts +0 -1
- package/dist/mdbase-connect.d.ts +6 -2
- package/dist/mdbase-connect.js +10 -3
- package/dist/operation-helpers.js +1 -1
- package/dist/outcomes.d.ts +3 -3
- package/dist/outcomes.js +4 -2
- package/dist/runtime-utils.d.ts +2 -0
- package/dist/runtime-utils.js +23 -0
- package/dist/selection.d.ts +3 -3
- package/dist/session.d.ts +2 -2
- package/package.json +2 -2
- package/dist/first-contact-presentation.d.ts +0 -5
- package/dist/first-contact-presentation.js +0 -51
- package/dist/first-contact.d.ts +0 -14
- package/dist/first-contact.js +0 -145
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { capabilityOperations, operationsForApplicationCapabilities } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
/* The versioned protocol package is the sole capability-to-operation compiler. */
|
|
3
|
+
import { effectiveCapabilities } from "./capabilities.js";
|
|
4
|
+
import { connectProblem } from "./errors.js";
|
|
5
|
+
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
6
|
+
import { MdbaseSession } from "./session.js";
|
|
7
|
+
export class MdbaseMemoryVerificationStore {
|
|
8
|
+
values = new Map();
|
|
9
|
+
read(key) {
|
|
10
|
+
return this.values.get(key) ?? null;
|
|
11
|
+
}
|
|
12
|
+
write(key, value) {
|
|
13
|
+
this.values.set(key, value);
|
|
14
|
+
}
|
|
15
|
+
remove(key) {
|
|
16
|
+
this.values.delete(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The application-level lifecycle boundary. It derives authorization from the
|
|
21
|
+
* bundled manifest, exposes semantic capabilities, and never mutates collection
|
|
22
|
+
* definitions without an explicit `applyDefinitionUpdates` call.
|
|
23
|
+
*/
|
|
24
|
+
export class MdbaseApplicationSession {
|
|
25
|
+
connect;
|
|
26
|
+
options;
|
|
27
|
+
listeners = new Set();
|
|
28
|
+
verificationStore;
|
|
29
|
+
snapshot = { status: "opening", connections: [] };
|
|
30
|
+
manifest = null;
|
|
31
|
+
base = null;
|
|
32
|
+
stopBase;
|
|
33
|
+
reviewEntries = [];
|
|
34
|
+
verificationGeneration = 0;
|
|
35
|
+
constructor(connect, options) {
|
|
36
|
+
this.connect = connect;
|
|
37
|
+
this.options = options;
|
|
38
|
+
this.verificationStore = options.verificationStore ?? defaultVerificationStore();
|
|
39
|
+
}
|
|
40
|
+
async start() {
|
|
41
|
+
const manifest = await this.connect.manifest();
|
|
42
|
+
if (!manifest.ok)
|
|
43
|
+
return manifest;
|
|
44
|
+
const capabilities = manifest.value.requirements?.capabilities;
|
|
45
|
+
if (!capabilities) {
|
|
46
|
+
return connectFailure(connectProblem("invalid_application_manifest", "Application sessions require a versioned semantic capability contract."));
|
|
47
|
+
}
|
|
48
|
+
this.manifest = manifest.value;
|
|
49
|
+
this.base = new MdbaseSession(this.connect, {
|
|
50
|
+
selection: this.options.selection,
|
|
51
|
+
autoSelect: this.options.autoSelect,
|
|
52
|
+
operations: operationsForSession(capabilities)
|
|
53
|
+
});
|
|
54
|
+
this.stopBase = this.base.subscribe(() => this.refresh());
|
|
55
|
+
const started = await this.base.start();
|
|
56
|
+
if (!started.ok)
|
|
57
|
+
return started;
|
|
58
|
+
await this.refresh(true);
|
|
59
|
+
return connectSuccess(this.snapshot);
|
|
60
|
+
}
|
|
61
|
+
destroy() {
|
|
62
|
+
this.verificationGeneration += 1;
|
|
63
|
+
this.stopBase?.();
|
|
64
|
+
this.stopBase = undefined;
|
|
65
|
+
this.base?.destroy();
|
|
66
|
+
this.base = null;
|
|
67
|
+
}
|
|
68
|
+
getSnapshot() {
|
|
69
|
+
return this.snapshot;
|
|
70
|
+
}
|
|
71
|
+
subscribe(listener) {
|
|
72
|
+
this.listeners.add(listener);
|
|
73
|
+
return () => this.listeners.delete(listener);
|
|
74
|
+
}
|
|
75
|
+
connection() {
|
|
76
|
+
const current = this.base?.getSnapshot();
|
|
77
|
+
return current?.status === "ready" ? current.connection : null;
|
|
78
|
+
}
|
|
79
|
+
select(collectionId, options = {}) {
|
|
80
|
+
return this.requireBase().select(collectionId, options);
|
|
81
|
+
}
|
|
82
|
+
clearSelection(options = {}) {
|
|
83
|
+
this.requireBase().clearSelection(options);
|
|
84
|
+
}
|
|
85
|
+
forget(collectionId) {
|
|
86
|
+
this.requireBase().forget(collectionId);
|
|
87
|
+
}
|
|
88
|
+
authorize(target, options = {}) {
|
|
89
|
+
return this.requireBase().authorize(target, options);
|
|
90
|
+
}
|
|
91
|
+
handleAuthorizationCallback(callbackUrl) {
|
|
92
|
+
return this.requireBase().handleAuthorizationCallback(callbackUrl);
|
|
93
|
+
}
|
|
94
|
+
ensureCapabilities(capabilities) {
|
|
95
|
+
const manifestRequirements = this.requireManifest().requirements?.capabilities;
|
|
96
|
+
const declared = new Set([
|
|
97
|
+
...(manifestRequirements?.required ?? []),
|
|
98
|
+
...(manifestRequirements?.optional ?? [])
|
|
99
|
+
]);
|
|
100
|
+
if (capabilities.some((capability) => !declared.has(capability))) {
|
|
101
|
+
return Promise.resolve(connectFailure(connectProblem("invalid_application_manifest", "Applications may only request capabilities declared in their manifest.")));
|
|
102
|
+
}
|
|
103
|
+
return this.requireBase().ensureOperations(operationsForIds(capabilities));
|
|
104
|
+
}
|
|
105
|
+
async applyDefinitionUpdates() {
|
|
106
|
+
if (this.snapshot.status !== "definition_review_required" || this.reviewEntries.length === 0) {
|
|
107
|
+
return connectFailure(connectProblem("collection_not_ready", "There are no reviewed definition updates to apply."));
|
|
108
|
+
}
|
|
109
|
+
if (this.reviewEntries.some(({ assessment }) => !assessment.applicable)) {
|
|
110
|
+
return connectFailure(connectProblem("collection_not_ready", "Resolve the definition conflicts before applying this update."));
|
|
111
|
+
}
|
|
112
|
+
const connection = this.connection();
|
|
113
|
+
if (!connection) {
|
|
114
|
+
return connectFailure(connectProblem("collection_not_ready", "The collection is not ready."));
|
|
115
|
+
}
|
|
116
|
+
for (const { provision, assessment } of this.reviewEntries) {
|
|
117
|
+
const applied = await connection.applyTypePack({
|
|
118
|
+
provision,
|
|
119
|
+
installed_by: this.requireManifest().id,
|
|
120
|
+
expected_assessment_digest: assessment.assessment_digest,
|
|
121
|
+
...(assessment.status === "downgrade" ? { allow_downgrade: true } : {})
|
|
122
|
+
});
|
|
123
|
+
if (!applied.ok)
|
|
124
|
+
return applied;
|
|
125
|
+
}
|
|
126
|
+
this.reviewEntries = [];
|
|
127
|
+
await this.verifyDefinitions(this.context(connection), ++this.verificationGeneration);
|
|
128
|
+
return connectSuccess(this.snapshot);
|
|
129
|
+
}
|
|
130
|
+
async refresh(awaitVerification = false) {
|
|
131
|
+
if (!this.base || !this.manifest)
|
|
132
|
+
return;
|
|
133
|
+
const current = this.base.getSnapshot();
|
|
134
|
+
this.verificationGeneration += 1;
|
|
135
|
+
const generation = this.verificationGeneration;
|
|
136
|
+
this.reviewEntries = [];
|
|
137
|
+
if (current.status === "unselected") {
|
|
138
|
+
this.publish({ status: "unselected", connections: current.connections });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (current.status === "unavailable") {
|
|
142
|
+
this.publish(current);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const context = this.context(current.connection);
|
|
146
|
+
if (!context.capabilities.requiredAvailable) {
|
|
147
|
+
this.publish({ status: "authorization_required", ...context });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const managesDefinitions = this.manifest.requirements?.capabilities?.required
|
|
151
|
+
.includes("definitions.type-pack.apply") ?? false;
|
|
152
|
+
const provisions = managesDefinitions
|
|
153
|
+
? this.manifest.provisions?.type_packs ?? []
|
|
154
|
+
: [];
|
|
155
|
+
if (provisions.length === 0) {
|
|
156
|
+
this.publish({ status: "ready", verification: "verified", ...context });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const key = verificationKey(this.manifest, current.collectionId);
|
|
160
|
+
if (this.verificationStore.read(key) === verificationValue(this.manifest)) {
|
|
161
|
+
this.publish({ status: "ready", verification: "cached", ...context });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
this.publish({ status: "checking_definitions", ...context });
|
|
165
|
+
}
|
|
166
|
+
const verification = this.verifyDefinitions(context, generation);
|
|
167
|
+
if (awaitVerification)
|
|
168
|
+
await verification;
|
|
169
|
+
}
|
|
170
|
+
async verifyDefinitions(context, generation) {
|
|
171
|
+
const connection = this.connection();
|
|
172
|
+
const manifest = this.requireManifest();
|
|
173
|
+
if (!connection || connection.collectionId !== context.collectionId)
|
|
174
|
+
return;
|
|
175
|
+
const entries = [];
|
|
176
|
+
const managesDefinitions = manifest.requirements?.capabilities?.required
|
|
177
|
+
.includes("definitions.type-pack.apply") ?? false;
|
|
178
|
+
for (const provision of managesDefinitions ? manifest.provisions?.type_packs ?? [] : []) {
|
|
179
|
+
const outcome = await connection.assessTypePack({
|
|
180
|
+
provision,
|
|
181
|
+
installed_by: manifest.id
|
|
182
|
+
});
|
|
183
|
+
if (generation !== this.verificationGeneration)
|
|
184
|
+
return;
|
|
185
|
+
if (!outcome.ok) {
|
|
186
|
+
this.publish({ status: "blocked", problem: outcome.problem, ...context });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (outcome.value.status !== "current") {
|
|
190
|
+
entries.push({ provision, assessment: outcome.value });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (generation !== this.verificationGeneration)
|
|
194
|
+
return;
|
|
195
|
+
if (entries.length > 0) {
|
|
196
|
+
this.reviewEntries = entries;
|
|
197
|
+
this.verificationStore.remove(verificationKey(manifest, context.collectionId));
|
|
198
|
+
this.publish({
|
|
199
|
+
status: "definition_review_required",
|
|
200
|
+
updates: entries.map(definitionUpdate),
|
|
201
|
+
...context
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.verificationStore.write(verificationKey(manifest, context.collectionId), verificationValue(manifest));
|
|
206
|
+
this.publish({ status: "ready", verification: "verified", ...context });
|
|
207
|
+
}
|
|
208
|
+
context(connection) {
|
|
209
|
+
const info = connection.info();
|
|
210
|
+
return {
|
|
211
|
+
collectionId: connection.collectionId,
|
|
212
|
+
info,
|
|
213
|
+
capabilities: effectiveCapabilities(this.requireManifest().requirements.capabilities, this.requireManifest(), info),
|
|
214
|
+
connections: this.connect.connections()
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
publish(snapshot) {
|
|
218
|
+
if (JSON.stringify(this.snapshot) === JSON.stringify(snapshot))
|
|
219
|
+
return;
|
|
220
|
+
this.snapshot = snapshot;
|
|
221
|
+
for (const listener of this.listeners)
|
|
222
|
+
listener();
|
|
223
|
+
}
|
|
224
|
+
requireBase() {
|
|
225
|
+
if (!this.base)
|
|
226
|
+
throw new Error("Start the application session before using it.");
|
|
227
|
+
return this.base;
|
|
228
|
+
}
|
|
229
|
+
requireManifest() {
|
|
230
|
+
if (!this.manifest)
|
|
231
|
+
throw new Error("Start the application session before using it.");
|
|
232
|
+
return this.manifest;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function operationsForSession(requirements) {
|
|
236
|
+
return operationsForApplicationCapabilities(requirements);
|
|
237
|
+
}
|
|
238
|
+
function operationsForIds(capabilities) {
|
|
239
|
+
const definitions = capabilities.flatMap((id) => capabilityOperations(id));
|
|
240
|
+
return [...new Set(definitions)];
|
|
241
|
+
}
|
|
242
|
+
function definitionUpdate(entry) {
|
|
243
|
+
const { assessment } = entry;
|
|
244
|
+
const statusReason = assessment.status === "conflict"
|
|
245
|
+
? "Existing user-owned resources conflict with this application definition pack."
|
|
246
|
+
: assessment.status === "install"
|
|
247
|
+
? "This collection needs the application definitions installed."
|
|
248
|
+
: `This collection has a ${assessment.status} definition change to review.`;
|
|
249
|
+
return {
|
|
250
|
+
id: assessment.desired.id,
|
|
251
|
+
name: entry.provision.manifest.name ?? assessment.desired.id,
|
|
252
|
+
status: assessment.status,
|
|
253
|
+
applicable: assessment.applicable,
|
|
254
|
+
assessmentDigest: assessment.assessment_digest,
|
|
255
|
+
...(assessment.current ? { currentVersion: assessment.current.version } : {}),
|
|
256
|
+
desiredVersion: assessment.desired.version,
|
|
257
|
+
resources: assessment.resources,
|
|
258
|
+
contractSetups: assessment.contract_setups,
|
|
259
|
+
canApply: assessment.applicable,
|
|
260
|
+
reason: statusReason
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function verificationKey(manifest, collectionId) {
|
|
264
|
+
return `mdbase-application-session:v1:${manifest.id}:${collectionId}`;
|
|
265
|
+
}
|
|
266
|
+
function verificationValue(manifest) {
|
|
267
|
+
return JSON.stringify({
|
|
268
|
+
capabilities: manifest.requirements?.capabilities,
|
|
269
|
+
provisions: manifest.provisions?.type_packs ?? []
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function defaultVerificationStore() {
|
|
273
|
+
if (typeof localStorage !== "undefined") {
|
|
274
|
+
return {
|
|
275
|
+
read: (key) => localStorage.getItem(key),
|
|
276
|
+
write: (key, value) => localStorage.setItem(key, value),
|
|
277
|
+
remove: (key) => localStorage.removeItem(key)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return new MdbaseMemoryVerificationStore();
|
|
281
|
+
}
|