@mdbase-dev/connect 0.1.0-beta.27 → 0.1.0-beta.29

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.
@@ -0,0 +1,302 @@
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
+ details: {
48
+ issues: [{
49
+ path: "/requirements/capabilities",
50
+ keyword: "required",
51
+ message: "is required for application sessions",
52
+ params: {}
53
+ }]
54
+ }
55
+ }));
56
+ }
57
+ this.manifest = manifest.value;
58
+ this.base = new MdbaseSession(this.connect, {
59
+ selection: this.options.selection,
60
+ autoSelect: this.options.autoSelect,
61
+ operations: operationsForSession(capabilities)
62
+ });
63
+ this.stopBase = this.base.subscribe(() => this.refresh());
64
+ const started = await this.base.start();
65
+ if (!started.ok)
66
+ return started;
67
+ await this.refresh(true);
68
+ return connectSuccess(this.snapshot);
69
+ }
70
+ destroy() {
71
+ this.verificationGeneration += 1;
72
+ this.stopBase?.();
73
+ this.stopBase = undefined;
74
+ this.base?.destroy();
75
+ this.base = null;
76
+ }
77
+ getSnapshot() {
78
+ return this.snapshot;
79
+ }
80
+ subscribe(listener) {
81
+ this.listeners.add(listener);
82
+ return () => this.listeners.delete(listener);
83
+ }
84
+ connection() {
85
+ const current = this.base?.getSnapshot();
86
+ return current?.status === "ready" ? current.connection : null;
87
+ }
88
+ select(collectionId, options = {}) {
89
+ return this.requireBase().select(collectionId, options);
90
+ }
91
+ clearSelection(options = {}) {
92
+ this.requireBase().clearSelection(options);
93
+ }
94
+ forget(collectionId) {
95
+ this.requireBase().forget(collectionId);
96
+ }
97
+ authorize(target, options = {}) {
98
+ return this.requireBase().authorize(target, options);
99
+ }
100
+ handleAuthorizationCallback(callbackUrl) {
101
+ return this.requireBase().handleAuthorizationCallback(callbackUrl);
102
+ }
103
+ ensureCapabilities(capabilities) {
104
+ const manifestRequirements = this.requireManifest().requirements?.capabilities;
105
+ const declared = new Set([
106
+ ...(manifestRequirements?.required ?? []),
107
+ ...(manifestRequirements?.optional ?? [])
108
+ ]);
109
+ if (capabilities.some((capability) => !declared.has(capability))) {
110
+ return Promise.resolve(connectFailure(connectProblem("invalid_application_manifest", "Applications may only request capabilities declared in their manifest.", {
111
+ details: {
112
+ issues: [{
113
+ path: "/requirements/capabilities",
114
+ keyword: "undeclaredCapability",
115
+ message: "must declare every capability requested by the application",
116
+ params: {
117
+ requested: capabilities,
118
+ declared: [...declared]
119
+ }
120
+ }]
121
+ }
122
+ })));
123
+ }
124
+ return this.requireBase().ensureOperations(operationsForIds(capabilities));
125
+ }
126
+ async applyDefinitionUpdates() {
127
+ if (this.snapshot.status !== "definition_review_required" || this.reviewEntries.length === 0) {
128
+ return connectFailure(connectProblem("collection_not_ready", "There are no reviewed definition updates to apply."));
129
+ }
130
+ if (this.reviewEntries.some(({ assessment }) => !assessment.applicable)) {
131
+ return connectFailure(connectProblem("collection_not_ready", "Resolve the definition conflicts before applying this update."));
132
+ }
133
+ const connection = this.connection();
134
+ if (!connection) {
135
+ return connectFailure(connectProblem("collection_not_ready", "The collection is not ready."));
136
+ }
137
+ for (const { provision, assessment } of this.reviewEntries) {
138
+ const applied = await connection.applyTypePack({
139
+ provision,
140
+ installed_by: this.requireManifest().id,
141
+ expected_assessment_digest: assessment.assessment_digest,
142
+ ...(assessment.status === "downgrade" ? { allow_downgrade: true } : {})
143
+ });
144
+ if (!applied.ok)
145
+ return applied;
146
+ }
147
+ this.reviewEntries = [];
148
+ await this.verifyDefinitions(this.context(connection), ++this.verificationGeneration);
149
+ return connectSuccess(this.snapshot);
150
+ }
151
+ async refresh(awaitVerification = false) {
152
+ if (!this.base || !this.manifest)
153
+ return;
154
+ const current = this.base.getSnapshot();
155
+ this.verificationGeneration += 1;
156
+ const generation = this.verificationGeneration;
157
+ this.reviewEntries = [];
158
+ if (current.status === "unselected") {
159
+ this.publish({ status: "unselected", connections: current.connections });
160
+ return;
161
+ }
162
+ if (current.status === "unavailable") {
163
+ this.publish(current);
164
+ return;
165
+ }
166
+ const context = this.context(current.connection);
167
+ if (!context.capabilities.requiredAvailable) {
168
+ this.publish({ status: "authorization_required", ...context });
169
+ return;
170
+ }
171
+ const managesDefinitions = this.manifest.requirements?.capabilities?.required
172
+ .includes("definitions.type-pack.apply") ?? false;
173
+ const provisions = managesDefinitions
174
+ ? this.manifest.provisions?.type_packs ?? []
175
+ : [];
176
+ if (provisions.length === 0) {
177
+ this.publish({ status: "ready", verification: "verified", ...context });
178
+ return;
179
+ }
180
+ const key = verificationKey(this.manifest, current.collectionId);
181
+ if (this.verificationStore.read(key) === verificationValue(this.manifest)) {
182
+ this.publish({ status: "ready", verification: "cached", ...context });
183
+ }
184
+ else {
185
+ this.publish({ status: "checking_definitions", ...context });
186
+ }
187
+ const verification = this.verifyDefinitions(context, generation);
188
+ if (awaitVerification)
189
+ await verification;
190
+ }
191
+ async verifyDefinitions(context, generation) {
192
+ const connection = this.connection();
193
+ const manifest = this.requireManifest();
194
+ if (!connection || connection.collectionId !== context.collectionId)
195
+ return;
196
+ const entries = [];
197
+ const managesDefinitions = manifest.requirements?.capabilities?.required
198
+ .includes("definitions.type-pack.apply") ?? false;
199
+ for (const provision of managesDefinitions ? manifest.provisions?.type_packs ?? [] : []) {
200
+ const outcome = await connection.assessTypePack({
201
+ provision,
202
+ installed_by: manifest.id
203
+ });
204
+ if (generation !== this.verificationGeneration)
205
+ return;
206
+ if (!outcome.ok) {
207
+ this.publish({ status: "blocked", problem: outcome.problem, ...context });
208
+ return;
209
+ }
210
+ if (outcome.value.status !== "current") {
211
+ entries.push({ provision, assessment: outcome.value });
212
+ }
213
+ }
214
+ if (generation !== this.verificationGeneration)
215
+ return;
216
+ if (entries.length > 0) {
217
+ this.reviewEntries = entries;
218
+ this.verificationStore.remove(verificationKey(manifest, context.collectionId));
219
+ this.publish({
220
+ status: "definition_review_required",
221
+ updates: entries.map(definitionUpdate),
222
+ ...context
223
+ });
224
+ return;
225
+ }
226
+ this.verificationStore.write(verificationKey(manifest, context.collectionId), verificationValue(manifest));
227
+ this.publish({ status: "ready", verification: "verified", ...context });
228
+ }
229
+ context(connection) {
230
+ const info = connection.info();
231
+ return {
232
+ collectionId: connection.collectionId,
233
+ info,
234
+ capabilities: effectiveCapabilities(this.requireManifest().requirements.capabilities, this.requireManifest(), info),
235
+ connections: this.connect.connections()
236
+ };
237
+ }
238
+ publish(snapshot) {
239
+ if (JSON.stringify(this.snapshot) === JSON.stringify(snapshot))
240
+ return;
241
+ this.snapshot = snapshot;
242
+ for (const listener of this.listeners)
243
+ listener();
244
+ }
245
+ requireBase() {
246
+ if (!this.base)
247
+ throw new Error("Start the application session before using it.");
248
+ return this.base;
249
+ }
250
+ requireManifest() {
251
+ if (!this.manifest)
252
+ throw new Error("Start the application session before using it.");
253
+ return this.manifest;
254
+ }
255
+ }
256
+ function operationsForSession(requirements) {
257
+ return operationsForApplicationCapabilities(requirements);
258
+ }
259
+ function operationsForIds(capabilities) {
260
+ const definitions = capabilities.flatMap((id) => capabilityOperations(id));
261
+ return [...new Set(definitions)];
262
+ }
263
+ function definitionUpdate(entry) {
264
+ const { assessment } = entry;
265
+ const statusReason = assessment.status === "conflict"
266
+ ? "Existing user-owned resources conflict with this application definition pack."
267
+ : assessment.status === "install"
268
+ ? "This collection needs the application definitions installed."
269
+ : `This collection has a ${assessment.status} definition change to review.`;
270
+ return {
271
+ id: assessment.desired.id,
272
+ name: entry.provision.manifest.name ?? assessment.desired.id,
273
+ status: assessment.status,
274
+ applicable: assessment.applicable,
275
+ assessmentDigest: assessment.assessment_digest,
276
+ ...(assessment.current ? { currentVersion: assessment.current.version } : {}),
277
+ desiredVersion: assessment.desired.version,
278
+ resources: assessment.resources,
279
+ contractSetups: assessment.contract_setups,
280
+ canApply: assessment.applicable,
281
+ reason: statusReason
282
+ };
283
+ }
284
+ function verificationKey(manifest, collectionId) {
285
+ return `mdbase-application-session:v1:${manifest.id}:${collectionId}`;
286
+ }
287
+ function verificationValue(manifest) {
288
+ return JSON.stringify({
289
+ capabilities: manifest.requirements?.capabilities,
290
+ provisions: manifest.provisions?.type_packs ?? []
291
+ });
292
+ }
293
+ function defaultVerificationStore() {
294
+ if (typeof localStorage !== "undefined") {
295
+ return {
296
+ read: (key) => localStorage.getItem(key),
297
+ write: (key, value) => localStorage.setItem(key, value),
298
+ remove: (key) => localStorage.removeItem(key)
299
+ };
300
+ }
301
+ return new MdbaseMemoryVerificationStore();
302
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "file": "mdbase-connect.min.js",
3
- "integrity": "sha384-DjJkmk65xgvpJONlTzVuGLRLNCDOhbOdcG4xoq+le2zjOf+cYDEiN4Ctdl+HyrCL"
3
+ "integrity": "sha384-Cw/OpEwBlbmIBjDZ/JDyh1HiQoKqitRwVSPaYpNCg33rNDYZr/EdSxv5oRa4jP+I"
4
4
  }