@mdbase-dev/connect 0.1.0-beta.93 → 0.1.0-beta.95
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 +59 -21
- package/dist/application-contract.d.ts +22 -0
- package/dist/application-contract.js +75 -0
- package/dist/application-session.d.ts +10 -10
- package/dist/application-session.js +90 -55
- 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 +4 -3
- package/dist/capabilities.js +25 -47
- package/dist/connect-options.d.ts +2 -1
- package/dist/connect.d.ts +2 -1
- package/dist/connect.js +41 -34
- package/dist/connection-transport-internals.d.ts +1 -1
- package/dist/connection-transport.js +1 -1
- package/dist/connection.d.ts +11 -0
- package/dist/connection.js +38 -11
- package/dist/index.d.ts +3 -1
- package/dist/internal-types.d.ts +4 -2
- package/dist/internal-types.js +1 -1
- package/dist/mdbase-connect.d.ts +1 -1
- package/dist/operation-types.d.ts +4 -3
- package/dist/outcomes.d.ts +3 -3
- package/dist/outcomes.js +1 -0
- package/dist/request-coordinator.js +1 -0
- package/dist/runtime-utils.d.ts +11 -1
- package/dist/runtime-utils.js +18 -0
- package/dist/session.d.ts +12 -5
- package/dist/session.js +41 -8
- package/dist/stored-token.d.ts +1 -1
- package/dist/stored-token.js +8 -4
- package/dist/structured-readiness.d.ts +77 -0
- package/dist/structured-readiness.js +126 -0
- package/dist/token-response.js +4 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -151,6 +151,46 @@ 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
|
+
### Legacy v1 and capability-group v2 declarations
|
|
155
|
+
|
|
156
|
+
The same SDK accepts both declaration versions without translating legacy exact
|
|
157
|
+
operations into broader groups. `requirements.capabilities.contract_version`
|
|
158
|
+
selects the semantics; an absent capability declaration means legacy v1.
|
|
159
|
+
`manifest_version` remains the document-format version, not this selector.
|
|
160
|
+
|
|
161
|
+
- **V1:** `authorize({ operations })`, `connection.requestOperations(operations)`,
|
|
162
|
+
and `session.ensureOperations(operations)` retain independent exact operations.
|
|
163
|
+
Incremental requests retain the existing exact grant. Legacy `files.actions`
|
|
164
|
+
and capability aliases (for example `records.update` and `files.read`) remain
|
|
165
|
+
supported. Bare registry/connection `authorize()` retains the predecessor's
|
|
166
|
+
`describe, changes, read, query` default; application sessions derive their
|
|
167
|
+
exact operation selection from the legacy capability declaration.
|
|
168
|
+
- **V2:** use `capabilities`, `requestCapabilities()`, and `ensureCapabilities()`.
|
|
169
|
+
Required groups are always included; omitted optional selection requests all
|
|
170
|
+
declared optional groups. Incremental requests retain already granted declared
|
|
171
|
+
groups. Files use `required`/`optional` actions.
|
|
172
|
+
|
|
173
|
+
Mixing `operations` and `capabilities`, or using an operations authorization API
|
|
174
|
+
with a v2 declaration, returns `invalid_application_manifest`, including for
|
|
175
|
+
JavaScript callers and already-sufficient grants. A rejected v2 request is never
|
|
176
|
+
retried as v1. Saved credentials and pending mutations are not translated or
|
|
177
|
+
replayed when selecting a semantic version; signed authorization retains its
|
|
178
|
+
registered declaration digest and exact recovery-transport requirements.
|
|
179
|
+
|
|
180
|
+
The SDK's `MdbaseAppManifest` and capability input types are explicit v1/v2
|
|
181
|
+
unions. `LegacyMdbaseAppManifest`, `LegacyApplicationRequirements`, and
|
|
182
|
+
`MdbaseApplicationRequirements` are available for explicit annotations. The
|
|
183
|
+
protocol package's canonical `ApplicationRequirements` remains v2-only.
|
|
184
|
+
Application sessions still require an explicit full-collection, versioned
|
|
185
|
+
capability declaration; no-capability legacy declarations can use the registry
|
|
186
|
+
or advanced session APIs.
|
|
187
|
+
|
|
188
|
+
This bridge is not a v2 readiness or rollout gate: structured v2 file,
|
|
189
|
+
notification, offline-runtime, and authority readiness evidence remains
|
|
190
|
+
incomplete. Legacy readiness aliases are evaluated only for v1. Positive server
|
|
191
|
+
and selected-authority v2 support gating remains separate work; this SDK change
|
|
192
|
+
does not advertise new support or authorize automatic consumer conversion.
|
|
193
|
+
|
|
154
194
|
Before and during startup the session distinguishes `not_started`, `starting`,
|
|
155
195
|
and `start_failed`; the last state carries the original typed problem and a
|
|
156
196
|
later `start()` retries. `destroyed` is terminal. Once started, the session
|
|
@@ -304,22 +344,17 @@ documents both threat models. New or changed declaration criteria never silently
|
|
|
304
344
|
broaden an existing grant: handle `notification_reauthorization_required` by
|
|
305
345
|
running authorization again before retrying registration.
|
|
306
346
|
|
|
307
|
-
Before opening a feature,
|
|
308
|
-
|
|
347
|
+
Before opening a feature, request its declared capability group instead of
|
|
348
|
+
selecting individual transport operations:
|
|
309
349
|
|
|
310
350
|
```ts
|
|
311
|
-
|
|
312
|
-
const capabilities = connection.authorizationCapabilities([...required]);
|
|
313
|
-
if (!capabilities.sufficient) {
|
|
314
|
-
console.log("Needs", capabilities.missingOperations);
|
|
315
|
-
await connection.requestOperations([...required]);
|
|
316
|
-
}
|
|
351
|
+
await connection.requestCapabilities(["records.edit"]);
|
|
317
352
|
```
|
|
318
353
|
|
|
319
|
-
`
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
354
|
+
`requestCapabilities()` is a no-op when the current grant already includes the
|
|
355
|
+
complete group. When authorization is needed, required groups remain fixed and
|
|
356
|
+
the requested optional groups are expanded atomically. An
|
|
357
|
+
`insufficient_access` problem carries the internal `granted_operations`,
|
|
323
358
|
`missing_operations`, and `required_operations` metadata with a `reauthorize`
|
|
324
359
|
recovery action.
|
|
325
360
|
|
|
@@ -346,11 +381,11 @@ When the local connector definitively rejects an encrypted grant, the SDK does
|
|
|
346
381
|
not bypass that decision through the relay. It classifies the error as requiring
|
|
347
382
|
authorization, removes only the matching stale credential, and emits a `null`
|
|
348
383
|
connection through `onConnectionChange()`. A subsequent
|
|
349
|
-
`
|
|
384
|
+
`requestCapabilities()` call therefore starts authorization instead of trusting
|
|
350
385
|
the stale cached capability list.
|
|
351
386
|
|
|
352
|
-
Applications
|
|
353
|
-
|
|
387
|
+
Applications can register and maintain type definitions when their
|
|
388
|
+
collection-wide grant includes the corresponding operations. Type source is returned with a revision token so updates cannot
|
|
354
389
|
silently overwrite a definition changed by another application:
|
|
355
390
|
|
|
356
391
|
```ts
|
|
@@ -379,7 +414,8 @@ if (!updatedType.ok) renderProblem(updatedType.problem);
|
|
|
379
414
|
```
|
|
380
415
|
|
|
381
416
|
Request `read_type`, `create_type`, and `update_type` during authorization.
|
|
382
|
-
|
|
417
|
+
A legacy contract-scoped grant is inactive and must be reauthorized; contracts
|
|
418
|
+
themselves never confer type-management permission.
|
|
383
419
|
|
|
384
420
|
To install a complete catalog pack without exposing its individual collection
|
|
385
421
|
paths, request `assess_type_pack` and `apply_type_pack`, fetch and verify the
|
|
@@ -501,6 +537,7 @@ export const manifest = {
|
|
|
501
537
|
homepage: "https://worklog.example",
|
|
502
538
|
redirect_uris: ["https://worklog.example/auth/mdbase/callback"],
|
|
503
539
|
requirements: {
|
|
540
|
+
access: "full_collection",
|
|
504
541
|
contracts: workItemPack.provides
|
|
505
542
|
},
|
|
506
543
|
provisions: {
|
|
@@ -513,10 +550,11 @@ Set `collection_kind` to `hosted` when the application needs a durable
|
|
|
513
550
|
provider-backed collection and the offline sync transport returned by
|
|
514
551
|
`sync()`. Connect then offers and accepts collections with replication capability.
|
|
515
552
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
and
|
|
553
|
+
Every SDK manifest must explicitly set `requirements.access` to the N-1 wire
|
|
554
|
+
spelling `full_collection`. An active grant covers the entire selected
|
|
555
|
+
collection; omission and legacy `contract` scope never satisfy application
|
|
556
|
+
access and require reauthorization. Required contracts remain separate
|
|
557
|
+
compatibility, setup, validation, and semantic-adapter evidence.
|
|
520
558
|
|
|
521
559
|
`listViews()` returns each named view's selected result properties in display
|
|
522
560
|
order. Property descriptors retain source labels for projected and computed
|
|
@@ -524,7 +562,7 @@ values. `executeView()` returns their values on each result row.
|
|
|
524
562
|
|
|
525
563
|
Provisioning is part of the approval flow. The connector validates and
|
|
526
564
|
installs each missing type pack transactionally, verifies its exact contracts
|
|
527
|
-
and implementations, and creates the
|
|
565
|
+
and implementations, and creates the collection grant afterward. The application
|
|
528
566
|
is not granted collection-wide type-management access.
|
|
529
567
|
|
|
530
568
|
The SDK returns typed outcomes, carries successful mdbase diagnostics alongside
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ApplicationCapabilityId, type ApplicationCapabilityRequirements, type LegacyApplicationCapabilityRequirements, type LegacyApplicationCapabilityId, type ApplicationRequirements, type LegacyApplicationRequirements, type MdbaseAppManifest, type LegacyMdbaseAppManifest, type CollectionOperation } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
export declare const DEFAULT_OPERATIONS: CollectionOperation[];
|
|
3
|
+
/** SDK input compatibility boundary; protocol ApplicationRequirements remains v2-only. */
|
|
4
|
+
export type MdbaseApplicationManifest = MdbaseAppManifest | LegacyMdbaseAppManifest;
|
|
5
|
+
export type MdbaseApplicationRequirements = ApplicationRequirements | LegacyApplicationRequirements;
|
|
6
|
+
export type MdbaseApplicationCapabilityRequirements = ApplicationCapabilityRequirements | LegacyApplicationCapabilityRequirements;
|
|
7
|
+
export type MdbaseApplicationCapabilityId = ApplicationCapabilityId | LegacyApplicationCapabilityId;
|
|
8
|
+
interface MdbaseAuthorizationSelection {
|
|
9
|
+
operations?: CollectionOperation[];
|
|
10
|
+
capabilities?: MdbaseApplicationCapabilityId[];
|
|
11
|
+
}
|
|
12
|
+
export declare function semanticContractVersion(requirements: MdbaseApplicationRequirements): 1 | 2;
|
|
13
|
+
export declare function invalidAuthorizationSelection(message: string): never;
|
|
14
|
+
export declare function validateAuthorizationSelection(requirements: MdbaseApplicationRequirements, options: MdbaseAuthorizationSelection): void;
|
|
15
|
+
export declare function declaredOperations(requirements: MdbaseApplicationRequirements): CollectionOperation[];
|
|
16
|
+
export declare function authorizationOperations(application: {
|
|
17
|
+
requirements: MdbaseApplicationRequirements;
|
|
18
|
+
provisions?: MdbaseApplicationManifest["provisions"];
|
|
19
|
+
}, options: MdbaseAuthorizationSelection): CollectionOperation[];
|
|
20
|
+
export declare function retainedCapabilities(requirements: MdbaseApplicationRequirements, operations: CollectionOperation[]): MdbaseApplicationCapabilityId[];
|
|
21
|
+
export declare function authorizationFiles(requirements: MdbaseApplicationRequirements): import("@mdbase-dev/connect-protocol").ApplicationFileRequest | undefined;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { APPLICATION_SETUP_OPERATIONS, applicationFileRequest, capabilityOperationsForContractVersion } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { connectError } from "./errors.js";
|
|
3
|
+
export const DEFAULT_OPERATIONS = ["describe", "changes", "read", "query"];
|
|
4
|
+
export function semanticContractVersion(requirements) {
|
|
5
|
+
const version = requirements.capabilities === undefined ? 1 : requirements.capabilities?.contract_version;
|
|
6
|
+
if (version !== 1 && version !== 2)
|
|
7
|
+
invalidAuthorizationSelection("Unsupported declared capability contract version.");
|
|
8
|
+
return version;
|
|
9
|
+
}
|
|
10
|
+
export function invalidAuthorizationSelection(message) {
|
|
11
|
+
throw connectError("invalid_application_manifest", message, {
|
|
12
|
+
details: { issues: [{ path: "/requirements/capabilities", keyword: "authorizationSelection", message, params: {} }] }
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function validateAuthorizationSelection(requirements, options) {
|
|
16
|
+
if (options.operations !== undefined && options.capabilities !== undefined) {
|
|
17
|
+
invalidAuthorizationSelection("Specify operations or capabilities, not both.");
|
|
18
|
+
}
|
|
19
|
+
if (options.operations !== undefined && semanticContractVersion(requirements) !== 1) {
|
|
20
|
+
invalidAuthorizationSelection("Exact operations authorization is only supported for legacy v1 declarations.");
|
|
21
|
+
}
|
|
22
|
+
if (options.capabilities !== undefined) {
|
|
23
|
+
const declared = new Set([
|
|
24
|
+
...(requirements.capabilities?.required ?? []),
|
|
25
|
+
...(requirements.capabilities?.optional ?? [])
|
|
26
|
+
]);
|
|
27
|
+
if (options.capabilities.some((id) => !declared.has(id)
|
|
28
|
+
|| capabilityOperationsForContractVersion(semanticContractVersion(requirements), id) === undefined)) {
|
|
29
|
+
invalidAuthorizationSelection("Authorization can only include capabilities declared by the application.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function declaredOperations(requirements) {
|
|
34
|
+
const capabilities = requirements.capabilities;
|
|
35
|
+
if (!capabilities)
|
|
36
|
+
return [...DEFAULT_OPERATIONS];
|
|
37
|
+
return [...new Set([...capabilities.required, ...(capabilities.optional ?? [])].flatMap((id) => capabilityOperationsForContractVersion(capabilities.contract_version, id) ?? []))];
|
|
38
|
+
}
|
|
39
|
+
export function authorizationOperations(application, options) {
|
|
40
|
+
const { requirements } = application;
|
|
41
|
+
validateAuthorizationSelection(requirements, options);
|
|
42
|
+
// Legacy callers selected independent exact operations, including subsets of declarations.
|
|
43
|
+
if (semanticContractVersion(requirements) === 1) {
|
|
44
|
+
if (options.operations !== undefined)
|
|
45
|
+
return [...new Set(options.operations)];
|
|
46
|
+
if (options.capabilities === undefined)
|
|
47
|
+
return [...DEFAULT_OPERATIONS];
|
|
48
|
+
}
|
|
49
|
+
const declared = requirements.capabilities;
|
|
50
|
+
const selected = [...(declared?.required ?? []), ...(options.capabilities ?? declared?.optional ?? [])];
|
|
51
|
+
const operations = selected.flatMap((id) => capabilityOperationsForContractVersion(semanticContractVersion(requirements), id) ?? []);
|
|
52
|
+
const hasSetup = (application.provisions?.type_packs.length ?? 0) > 0
|
|
53
|
+
|| (application.provisions?.configuration?.length ?? 0) > 0;
|
|
54
|
+
return [...new Set([
|
|
55
|
+
...operations,
|
|
56
|
+
...(semanticContractVersion(requirements) === 2 && hasSetup ? APPLICATION_SETUP_OPERATIONS : [])
|
|
57
|
+
])];
|
|
58
|
+
}
|
|
59
|
+
export function retainedCapabilities(requirements, operations) {
|
|
60
|
+
return [...(requirements.capabilities?.required ?? []), ...(requirements.capabilities?.optional ?? [])].filter((id) => (capabilityOperationsForContractVersion(semanticContractVersion(requirements), id) ?? []).every((operation) => operations.includes(operation)));
|
|
61
|
+
}
|
|
62
|
+
export function authorizationFiles(requirements) {
|
|
63
|
+
const files = requirements.files;
|
|
64
|
+
if (!files)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (semanticContractVersion(requirements) === 1) {
|
|
67
|
+
if (!("actions" in files) || "required" in files || "optional" in files) {
|
|
68
|
+
invalidAuthorizationSelection("Legacy file declarations must use exact files.actions.");
|
|
69
|
+
}
|
|
70
|
+
return { actions: [...files.actions], scope: structuredClone(files.scope) };
|
|
71
|
+
}
|
|
72
|
+
if ("actions" in files)
|
|
73
|
+
invalidAuthorizationSelection("V2 file declarations must use required/optional file actions.");
|
|
74
|
+
return applicationFileRequest(files);
|
|
75
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type
|
|
1
|
+
import { type ConnectProblem, type JsonObject, type CollectionOperation } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { type MdbaseApplicationManifest as MdbaseAppManifest, type MdbaseApplicationCapabilityId as ApplicationCapabilityId } from "./application-contract.js";
|
|
3
|
+
import { type ApplicationSessionContext } from "./structured-readiness.js";
|
|
3
4
|
import type { MdbaseAuthorizationOutcome, MdbaseAuthorizeOptions, MdbaseConnection } from "./connection.js";
|
|
4
5
|
import type { MdbaseConnectionInfo } from "./connection-types.js";
|
|
5
6
|
import { type ConnectOutcome, type CollectionTypeProblemCode, type RegistrationProblemCode, type SessionProblemCode } from "./outcomes.js";
|
|
@@ -53,12 +54,6 @@ export interface MdbaseCollectionSetupUpdate {
|
|
|
53
54
|
canApply: boolean;
|
|
54
55
|
reason: string;
|
|
55
56
|
}
|
|
56
|
-
interface ApplicationSessionContext {
|
|
57
|
-
collectionId: string;
|
|
58
|
-
info: MdbaseConnectionInfo;
|
|
59
|
-
capabilities: MdbaseEffectiveCapabilities;
|
|
60
|
-
connections: MdbaseConnectionInfo[];
|
|
61
|
-
}
|
|
62
57
|
export type MdbaseApplicationSessionSnapshot = {
|
|
63
58
|
status: "not_started";
|
|
64
59
|
connections: MdbaseConnectionInfo[];
|
|
@@ -117,6 +112,7 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
|
|
|
117
112
|
private setupAssessment;
|
|
118
113
|
private setupTypePackAdoptions;
|
|
119
114
|
private verificationGeneration;
|
|
115
|
+
private readinessController?;
|
|
120
116
|
private lifecycleGeneration;
|
|
121
117
|
private readonly operationControllers;
|
|
122
118
|
private startOperation;
|
|
@@ -142,17 +138,22 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
|
|
|
142
138
|
forget(collectionId: string): ConnectOutcome<void, SessionProblemCode>;
|
|
143
139
|
authorize(target: "choose" | "selected" | {
|
|
144
140
|
collectionId: string;
|
|
145
|
-
}, options?: Omit<MdbaseAuthorizeOptions, "
|
|
141
|
+
}, options?: Omit<MdbaseAuthorizeOptions, "returnTo" | "target">): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
|
|
146
142
|
handleAuthorizationCallback(callbackUrl: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, SessionProblemCode>>;
|
|
147
143
|
completeAuthorization(callbackUrl?: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, SessionProblemCode>>;
|
|
148
144
|
ensureCapabilities(capabilities: ApplicationCapabilityId[], options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter> | {
|
|
149
145
|
kind: "unchanged";
|
|
150
146
|
connection: MdbaseConnection<Frontmatter>;
|
|
151
147
|
}, SessionProblemCode>>;
|
|
148
|
+
ensureOperations(operations: CollectionOperation[], options?: ConnectRequestOptions): Promise<ConnectOutcome<{
|
|
149
|
+
kind: "unchanged";
|
|
150
|
+
connection: MdbaseConnection<Frontmatter>;
|
|
151
|
+
} | MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
|
|
152
152
|
applyCollectionSetup(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, CollectionTypeProblemCode | SessionProblemCode>>;
|
|
153
153
|
private applyCollectionSetupStarted;
|
|
154
154
|
private refresh;
|
|
155
155
|
private verifySetup;
|
|
156
|
+
private publishReady;
|
|
156
157
|
private collectionSetupInput;
|
|
157
158
|
private context;
|
|
158
159
|
private publish;
|
|
@@ -164,4 +165,3 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
|
|
|
164
165
|
private requireManifest;
|
|
165
166
|
private requireApplication;
|
|
166
167
|
}
|
|
167
|
-
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
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
4
|
import { MdbaseConnectError, connectProblem } from "./errors.js";
|
|
5
5
|
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
6
6
|
import { createRequestBudget, resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
|
|
7
7
|
import { defaultCallbackUrl } from "./runtime-utils.js";
|
|
8
|
+
import { declarationIdFromFamilyIdentity } from "./connect-authorization-helpers.js";
|
|
8
9
|
import { MdbaseSession } from "./session.js";
|
|
9
10
|
export class MdbaseMemoryVerificationStore {
|
|
10
11
|
values = new Map();
|
|
@@ -37,6 +38,7 @@ export class MdbaseApplicationSession {
|
|
|
37
38
|
setupAssessment = null;
|
|
38
39
|
setupTypePackAdoptions = null;
|
|
39
40
|
verificationGeneration = 0;
|
|
41
|
+
readinessController;
|
|
40
42
|
lifecycleGeneration = 0;
|
|
41
43
|
operationControllers = new Set();
|
|
42
44
|
startOperation = null;
|
|
@@ -75,6 +77,7 @@ export class MdbaseApplicationSession {
|
|
|
75
77
|
}
|
|
76
78
|
abandonStart(operation) {
|
|
77
79
|
operation.controller.abort();
|
|
80
|
+
this.readinessController?.abort();
|
|
78
81
|
this.startOperation = null;
|
|
79
82
|
this.lifecycleGeneration += 1;
|
|
80
83
|
this.verificationGeneration += 1;
|
|
@@ -132,7 +135,19 @@ export class MdbaseApplicationSession {
|
|
|
132
135
|
const manifest = await this.connect.manifest(options);
|
|
133
136
|
if (!manifest.ok)
|
|
134
137
|
return this.startFailure(manifest.problem, generation);
|
|
135
|
-
|
|
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;
|
|
136
151
|
if (!capabilities) {
|
|
137
152
|
return this.startFailure(connectProblem("invalid_application_manifest", "Application sessions require a versioned semantic capability contract.", {
|
|
138
153
|
details: {
|
|
@@ -153,7 +168,7 @@ export class MdbaseApplicationSession {
|
|
|
153
168
|
const base = new MdbaseSession(this.connect, {
|
|
154
169
|
selection: this.options.selection,
|
|
155
170
|
autoSelect: this.options.autoSelect,
|
|
156
|
-
operations: operationsForSession(capabilities)
|
|
171
|
+
operations: operationsForSession(capabilities, manifest.value)
|
|
157
172
|
});
|
|
158
173
|
this.base = base;
|
|
159
174
|
try {
|
|
@@ -197,6 +212,7 @@ export class MdbaseApplicationSession {
|
|
|
197
212
|
return;
|
|
198
213
|
this.lifecycleGeneration += 1;
|
|
199
214
|
this.verificationGeneration += 1;
|
|
215
|
+
this.readinessController?.abort();
|
|
200
216
|
this.startOperation?.controller.abort();
|
|
201
217
|
this.startOperation = null;
|
|
202
218
|
for (const controller of this.operationControllers)
|
|
@@ -293,6 +309,7 @@ export class MdbaseApplicationSession {
|
|
|
293
309
|
}
|
|
294
310
|
ensureCapabilities(capabilities, options) {
|
|
295
311
|
return this.withLifecycleBase(options, (base, requestOptions, generation) => {
|
|
312
|
+
validateAuthorizationSelection(this.manifest.requirements, { ...options, capabilities });
|
|
296
313
|
const manifestRequirements = this.manifest.requirements?.capabilities;
|
|
297
314
|
const declared = new Set([
|
|
298
315
|
...(manifestRequirements?.required ?? []),
|
|
@@ -313,7 +330,7 @@ export class MdbaseApplicationSession {
|
|
|
313
330
|
}
|
|
314
331
|
})));
|
|
315
332
|
}
|
|
316
|
-
return base.
|
|
333
|
+
return base.ensureCapabilities(capabilities, requestOptions).then(async (outcome) => {
|
|
317
334
|
if (outcome.ok && outcome.value.kind === "connected" && this.lifecycleCurrent(generation)) {
|
|
318
335
|
await this.refresh(true, requestOptions);
|
|
319
336
|
}
|
|
@@ -321,6 +338,15 @@ export class MdbaseApplicationSession {
|
|
|
321
338
|
});
|
|
322
339
|
});
|
|
323
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
|
+
}
|
|
324
350
|
applyCollectionSetup(options) {
|
|
325
351
|
return this.withLifecycleBase(options, (_base, requestOptions, generation) => this.applyCollectionSetupStarted(requestOptions, generation));
|
|
326
352
|
}
|
|
@@ -335,6 +361,7 @@ export class MdbaseApplicationSession {
|
|
|
335
361
|
if (!connection) {
|
|
336
362
|
return connectFailure(connectProblem("collection_not_ready", "The collection is not ready."));
|
|
337
363
|
}
|
|
364
|
+
const verificationGeneration = this.verificationGeneration;
|
|
338
365
|
const assessment = this.setupAssessment;
|
|
339
366
|
const input = {
|
|
340
367
|
...this.collectionSetupInput(this.setupTypePackAdoptions ?? undefined),
|
|
@@ -352,6 +379,9 @@ export class MdbaseApplicationSession {
|
|
|
352
379
|
}
|
|
353
380
|
return connectFailure(connectProblem("session_destroyed", lifecycleProblemMessage("session_destroyed")));
|
|
354
381
|
}
|
|
382
|
+
if (verificationGeneration !== this.verificationGeneration || this.connection() !== connection) {
|
|
383
|
+
return connectFailure(connectProblem("collection_not_ready", "The selected collection changed during setup."));
|
|
384
|
+
}
|
|
355
385
|
if (!applied.ok) {
|
|
356
386
|
if (applied.problem.code === "application_declaration_mismatch") {
|
|
357
387
|
this.setupAssessment = null;
|
|
@@ -369,6 +399,7 @@ export class MdbaseApplicationSession {
|
|
|
369
399
|
if (!this.base || !this.manifest)
|
|
370
400
|
return;
|
|
371
401
|
const current = this.base.getSnapshot();
|
|
402
|
+
this.readinessController?.abort();
|
|
372
403
|
this.verificationGeneration += 1;
|
|
373
404
|
const generation = this.verificationGeneration;
|
|
374
405
|
this.setupAssessment = null;
|
|
@@ -389,18 +420,22 @@ export class MdbaseApplicationSession {
|
|
|
389
420
|
return;
|
|
390
421
|
}
|
|
391
422
|
if (!context.capabilities.requiredAvailable
|
|
423
|
+
|| context.readiness?.files.state === "requires_authorization"
|
|
392
424
|
|| !accessRequirementSatisfied(this.manifest, context.info)) {
|
|
393
425
|
this.publish({ status: "authorization_required", ...context });
|
|
394
426
|
return;
|
|
395
427
|
}
|
|
396
|
-
const managesSetup = this.manifest.
|
|
397
|
-
|
|
428
|
+
const managesSetup = (this.manifest.provisions?.type_packs.length ?? 0) > 0
|
|
429
|
+
|| (this.manifest.provisions?.configuration?.length ?? 0) > 0;
|
|
398
430
|
if (!managesSetup) {
|
|
399
|
-
this.
|
|
431
|
+
const verification = this.publishReady(context, generation, options);
|
|
432
|
+
if (awaitVerification)
|
|
433
|
+
await verification;
|
|
400
434
|
return;
|
|
401
435
|
}
|
|
402
436
|
const key = verificationKey(this.manifest, current.collectionId);
|
|
403
|
-
if (
|
|
437
|
+
if (context.readiness?.contracts.state !== "checking"
|
|
438
|
+
&& this.verificationStore.read(key) === verificationValue(this.manifest)) {
|
|
404
439
|
this.publish({ status: "ready", verification: "cached", ...context });
|
|
405
440
|
}
|
|
406
441
|
else {
|
|
@@ -457,36 +492,51 @@ export class MdbaseApplicationSession {
|
|
|
457
492
|
return;
|
|
458
493
|
}
|
|
459
494
|
this.verificationStore.write(verificationKey(manifest, context.collectionId), verificationValue(manifest));
|
|
460
|
-
|
|
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 });
|
|
461
525
|
}
|
|
462
526
|
collectionSetupInput(typePackAdoptions) {
|
|
463
527
|
const manifest = this.requireManifest();
|
|
464
528
|
const application = this.requireApplication();
|
|
465
|
-
return {
|
|
466
|
-
applicationId: declarationIdFromFamilyIdentity(application.family_identity),
|
|
467
|
-
declarationDigest: `sha256:${application.manifest_digest}`,
|
|
468
|
-
requirements: {
|
|
469
|
-
configuration: manifest.requirements?.configuration ?? []
|
|
470
|
-
},
|
|
471
|
-
provisions: {
|
|
472
|
-
configuration: manifest.provisions?.configuration ?? [],
|
|
473
|
-
typePacks: manifest.provisions?.type_packs ?? []
|
|
474
|
-
},
|
|
475
|
-
...(typePackAdoptions ? { typePackAdoptions } : {})
|
|
476
|
-
};
|
|
529
|
+
return collectionSetupInput(manifest, declarationIdFromFamilyIdentity(application.family_identity), `sha256:${application.manifest_digest}`, typePackAdoptions);
|
|
477
530
|
}
|
|
478
531
|
context(connection) {
|
|
479
|
-
|
|
480
|
-
return {
|
|
481
|
-
collectionId: connection.collectionId,
|
|
482
|
-
info,
|
|
483
|
-
capabilities: effectiveCapabilities(this.requireManifest().requirements.capabilities, this.requireManifest(), info),
|
|
484
|
-
connections: this.connect.connections()
|
|
485
|
-
};
|
|
532
|
+
return applicationReadinessContext(this.requireManifest(), connection.collectionId, connection.info(), this.connect.connections());
|
|
486
533
|
}
|
|
487
534
|
publish(snapshot) {
|
|
488
535
|
if (this.snapshot.status === "destroyed" && snapshot.status !== "destroyed")
|
|
489
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
|
+
}
|
|
490
540
|
if (JSON.stringify(this.snapshot) === JSON.stringify(snapshot))
|
|
491
541
|
return;
|
|
492
542
|
this.snapshot = snapshot;
|
|
@@ -582,12 +632,13 @@ export class MdbaseApplicationSession {
|
|
|
582
632
|
return this.application;
|
|
583
633
|
}
|
|
584
634
|
}
|
|
585
|
-
function operationsForSession(requirements) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
+
];
|
|
591
642
|
}
|
|
592
643
|
function definitionUpdate(provision, assessment) {
|
|
593
644
|
const statusReason = assessment.status === "conflict"
|
|
@@ -647,26 +698,10 @@ function reviewableTypePackAdoptions(assessment) {
|
|
|
647
698
|
}
|
|
648
699
|
return adoptions;
|
|
649
700
|
}
|
|
650
|
-
function verificationKey(manifest, collectionId) {
|
|
651
|
-
return `mdbase-application-session:v1:${manifest.id}:${collectionId}`;
|
|
652
|
-
}
|
|
653
|
-
function verificationValue(manifest) {
|
|
654
|
-
return JSON.stringify({
|
|
655
|
-
capabilities: manifest.requirements?.capabilities,
|
|
656
|
-
requirements: manifest.requirements?.configuration ?? [],
|
|
657
|
-
provisions: manifest.provisions ?? {}
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
701
|
function accessRequirementSatisfied(manifest, connection) {
|
|
661
|
-
return manifest.requirements?.access
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
function declarationIdFromFamilyIdentity(familyIdentity) {
|
|
665
|
-
const prefix = "bundle:";
|
|
666
|
-
if (!familyIdentity.startsWith(prefix) || familyIdentity.length === prefix.length) {
|
|
667
|
-
throw new Error("The registered application has no valid declaration identity.");
|
|
668
|
-
}
|
|
669
|
-
return familyIdentity.slice(prefix.length);
|
|
702
|
+
return manifest.requirements?.access === "full_collection"
|
|
703
|
+
&& connection.scope.access === "full_collection"
|
|
704
|
+
&& connection.scope.contracts.length === 0;
|
|
670
705
|
}
|
|
671
706
|
function lifecycleProblemMessage(code) {
|
|
672
707
|
if (code === "session_destroyed")
|