@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,114 @@
|
|
|
1
|
+
import type { CollectionOperation, ConnectProblem, JsonObject } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { type MdbaseApplicationManifest, type MdbaseApplicationCapabilityId as ApplicationCapabilityId } from "./application-contract.js";
|
|
3
|
+
import { MdbaseConnection, type MdbaseAuthorizationOutcome, type MdbaseAuthorizationResult, type MdbaseAuthorizeOptions } from "./connection.js";
|
|
4
|
+
import type { MdbaseAuthorizationCapabilities, MdbaseConnectionInfo } from "./connection-types.js";
|
|
5
|
+
import { type AuthorizationProblemCode, type RegistrationProblemCode, type ConnectOutcome, type SessionProblemCode } from "./outcomes.js";
|
|
6
|
+
import type { MdbaseSelectionHistory, MdbaseApplicationSelection } from "./selection.js";
|
|
7
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
8
|
+
export interface MdbaseSessionConnect<Frontmatter extends JsonObject> {
|
|
9
|
+
manifest?(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationManifest, RegistrationProblemCode>>;
|
|
10
|
+
authorize(options?: MdbaseAuthorizeOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, AuthorizationProblemCode>>;
|
|
11
|
+
completeAuthorization(callbackUrl: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationResult<Frontmatter>, AuthorizationProblemCode>>;
|
|
12
|
+
connections(): MdbaseConnectionInfo[];
|
|
13
|
+
connection(collectionId: string): MdbaseConnection<Frontmatter> | null;
|
|
14
|
+
unavailableReason(collectionId: string): MdbaseUnavailableReason | null;
|
|
15
|
+
onConnectionsChange(listener: (connections: MdbaseConnectionInfo[]) => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
export interface MdbaseSessionOptions {
|
|
18
|
+
selection: MdbaseApplicationSelection;
|
|
19
|
+
operations?: CollectionOperation[];
|
|
20
|
+
autoSelect?: "only" | "never";
|
|
21
|
+
}
|
|
22
|
+
export type MdbaseUnavailableReason = "not_authorized" | "authorization_lost" | "invalid_stored_grant" | "legacy_scope_reauthorization_required";
|
|
23
|
+
export type MdbaseSessionSnapshot<Frontmatter extends JsonObject = JsonObject> = {
|
|
24
|
+
status: "not_started";
|
|
25
|
+
connections: MdbaseConnectionInfo[];
|
|
26
|
+
} | {
|
|
27
|
+
status: "starting";
|
|
28
|
+
connections: MdbaseConnectionInfo[];
|
|
29
|
+
} | {
|
|
30
|
+
status: "start_failed";
|
|
31
|
+
problem: ConnectProblem<SessionProblemCode>;
|
|
32
|
+
connections: MdbaseConnectionInfo[];
|
|
33
|
+
} | {
|
|
34
|
+
status: "destroyed";
|
|
35
|
+
connections: MdbaseConnectionInfo[];
|
|
36
|
+
} | {
|
|
37
|
+
status: "unselected";
|
|
38
|
+
connections: MdbaseConnectionInfo[];
|
|
39
|
+
} | {
|
|
40
|
+
status: "ready";
|
|
41
|
+
collectionId: string;
|
|
42
|
+
connection: MdbaseConnection<Frontmatter>;
|
|
43
|
+
info: MdbaseConnectionInfo;
|
|
44
|
+
access: MdbaseAuthorizationCapabilities;
|
|
45
|
+
connections: MdbaseConnectionInfo[];
|
|
46
|
+
} | {
|
|
47
|
+
status: "unavailable";
|
|
48
|
+
collectionId: string;
|
|
49
|
+
reason: MdbaseUnavailableReason;
|
|
50
|
+
connections: MdbaseConnectionInfo[];
|
|
51
|
+
};
|
|
52
|
+
export declare class MdbaseSession<Frontmatter extends JsonObject = JsonObject> {
|
|
53
|
+
private readonly connect;
|
|
54
|
+
private readonly operations;
|
|
55
|
+
private readonly selection;
|
|
56
|
+
private readonly autoSelect;
|
|
57
|
+
private readonly listeners;
|
|
58
|
+
private snapshot;
|
|
59
|
+
private snapshotKey;
|
|
60
|
+
private stopConnections?;
|
|
61
|
+
private stopSelection?;
|
|
62
|
+
private stopActiveConnection?;
|
|
63
|
+
private activeCollectionId;
|
|
64
|
+
private unavailableReason;
|
|
65
|
+
private started;
|
|
66
|
+
private destroyed;
|
|
67
|
+
private refreshing;
|
|
68
|
+
private refreshQueued;
|
|
69
|
+
private transactionDepth;
|
|
70
|
+
private lifecycleGeneration;
|
|
71
|
+
private readonly operationControllers;
|
|
72
|
+
private startOperation;
|
|
73
|
+
private readonly callbackCompletions;
|
|
74
|
+
constructor(connect: MdbaseSessionConnect<Frontmatter>, options: MdbaseSessionOptions);
|
|
75
|
+
start(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseSessionSnapshot<Frontmatter>, SessionProblemCode>>;
|
|
76
|
+
private beginStart;
|
|
77
|
+
private waitForStart;
|
|
78
|
+
private performStart;
|
|
79
|
+
destroy(): void;
|
|
80
|
+
private stopLifecycleSubscriptions;
|
|
81
|
+
getSnapshot(): MdbaseSessionSnapshot<Frontmatter>;
|
|
82
|
+
subscribe(listener: () => void): () => void;
|
|
83
|
+
select(collectionId: string, options?: {
|
|
84
|
+
history?: MdbaseSelectionHistory;
|
|
85
|
+
}): ConnectOutcome<MdbaseConnection<Frontmatter>, SessionProblemCode>;
|
|
86
|
+
clearSelection(options?: {
|
|
87
|
+
history?: MdbaseSelectionHistory;
|
|
88
|
+
}): ConnectOutcome<void, SessionProblemCode>;
|
|
89
|
+
forget(collectionId: string): ConnectOutcome<void, SessionProblemCode>;
|
|
90
|
+
authorize(target: "choose" | "selected" | {
|
|
91
|
+
collectionId: string;
|
|
92
|
+
}, options?: Omit<MdbaseAuthorizeOptions, "returnTo" | "target">): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
|
|
93
|
+
ensureOperations(requiredOperations: CollectionOperation[], options?: ConnectRequestOptions): Promise<ConnectOutcome<{
|
|
94
|
+
kind: "unchanged";
|
|
95
|
+
connection: MdbaseConnection<Frontmatter>;
|
|
96
|
+
} | MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
|
|
97
|
+
ensureCapabilities(requiredCapabilities: ApplicationCapabilityId[], options?: ConnectRequestOptions): Promise<ConnectOutcome<{
|
|
98
|
+
kind: "unchanged";
|
|
99
|
+
connection: MdbaseConnection<Frontmatter>;
|
|
100
|
+
} | MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
|
|
101
|
+
private ensureAuthorization;
|
|
102
|
+
handleAuthorizationCallback(callbackUrl: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, SessionProblemCode>>;
|
|
103
|
+
private completeAuthorizationCallback;
|
|
104
|
+
private beginCallbackCompletion;
|
|
105
|
+
private waitForCallbackCompletion;
|
|
106
|
+
private lifecycleProblem;
|
|
107
|
+
private beginLifecycleOperation;
|
|
108
|
+
private lifecycleCurrent;
|
|
109
|
+
private destroyedOutcome;
|
|
110
|
+
private autoSelectOnlyConnection;
|
|
111
|
+
private refresh;
|
|
112
|
+
private publishCurrent;
|
|
113
|
+
private publish;
|
|
114
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { APPLICATION_CAPABILITY_DEFINITIONS, capabilityOperationsForContractVersion } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { retainedCapabilities, semanticContractVersion, validateAuthorizationSelection } from "./application-contract.js";
|
|
3
|
+
import { authorizationCallbackState, authorizationReturnToFromProblem, isAuthorizationCallbackUrl } from "./authorization-url.js";
|
|
4
|
+
import { MdbaseConnectError, connectProblem } from "./errors.js";
|
|
5
|
+
import { DEFAULT_OPERATIONS } from "./internal-types.js";
|
|
6
|
+
import { uniqueOperations } from "./operation-helpers.js";
|
|
7
|
+
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
8
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STARTUP_TIMEOUT_MS, requestAbortReason, withRequestBudget } from "./request-budget.js";
|
|
9
|
+
export class MdbaseSession {
|
|
10
|
+
connect;
|
|
11
|
+
operations;
|
|
12
|
+
selection;
|
|
13
|
+
autoSelect;
|
|
14
|
+
listeners = new Set();
|
|
15
|
+
snapshot;
|
|
16
|
+
snapshotKey = "";
|
|
17
|
+
stopConnections;
|
|
18
|
+
stopSelection;
|
|
19
|
+
stopActiveConnection;
|
|
20
|
+
activeCollectionId = null;
|
|
21
|
+
unavailableReason = "not_authorized";
|
|
22
|
+
started = false;
|
|
23
|
+
destroyed = false;
|
|
24
|
+
refreshing = false;
|
|
25
|
+
refreshQueued = false;
|
|
26
|
+
transactionDepth = 0;
|
|
27
|
+
lifecycleGeneration = 0;
|
|
28
|
+
operationControllers = new Set();
|
|
29
|
+
startOperation = null;
|
|
30
|
+
callbackCompletions = new Map();
|
|
31
|
+
constructor(connect, options) {
|
|
32
|
+
this.connect = connect;
|
|
33
|
+
this.operations = uniqueOperations(options.operations ?? DEFAULT_OPERATIONS);
|
|
34
|
+
this.selection = options.selection;
|
|
35
|
+
this.autoSelect = options.autoSelect ?? "only";
|
|
36
|
+
this.snapshot = { status: "not_started", connections: [] };
|
|
37
|
+
}
|
|
38
|
+
start(options) {
|
|
39
|
+
if (this.destroyed) {
|
|
40
|
+
return Promise.resolve(connectFailure(connectProblem("session_destroyed", "This session has been destroyed.")));
|
|
41
|
+
}
|
|
42
|
+
if (this.started && !this.startOperation) {
|
|
43
|
+
return Promise.resolve(connectSuccess(this.snapshot));
|
|
44
|
+
}
|
|
45
|
+
const operation = this.startOperation ?? this.beginStart();
|
|
46
|
+
return this.waitForStart(operation, options);
|
|
47
|
+
}
|
|
48
|
+
beginStart() {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const generation = ++this.lifecycleGeneration;
|
|
51
|
+
this.publish({ status: "starting", connections: this.connect.connections() });
|
|
52
|
+
const operation = {
|
|
53
|
+
promise: this.performStart({ signal: controller.signal, timeoutMs: null }, generation),
|
|
54
|
+
controller,
|
|
55
|
+
waiters: 0,
|
|
56
|
+
generation
|
|
57
|
+
};
|
|
58
|
+
this.startOperation = operation;
|
|
59
|
+
const settled = () => {
|
|
60
|
+
if (this.startOperation === operation)
|
|
61
|
+
this.startOperation = null;
|
|
62
|
+
};
|
|
63
|
+
operation.promise.then(settled, settled);
|
|
64
|
+
return operation;
|
|
65
|
+
}
|
|
66
|
+
async waitForStart(operation, options) {
|
|
67
|
+
operation.waiters += 1;
|
|
68
|
+
try {
|
|
69
|
+
return await withRequestBudget(options, DEFAULT_STARTUP_TIMEOUT_MS, () => operation.promise);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof MdbaseConnectError) {
|
|
73
|
+
return connectFailure(error.problem);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
operation.waiters -= 1;
|
|
79
|
+
if (operation.waiters === 0 && this.startOperation === operation) {
|
|
80
|
+
operation.controller.abort();
|
|
81
|
+
this.startOperation = null;
|
|
82
|
+
this.lifecycleGeneration += 1;
|
|
83
|
+
this.stopLifecycleSubscriptions();
|
|
84
|
+
this.started = false;
|
|
85
|
+
this.publish({ status: "not_started", connections: this.connect.connections() });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async performStart(options, generation) {
|
|
90
|
+
try {
|
|
91
|
+
this.started = true;
|
|
92
|
+
this.stopConnections = this.connect.onConnectionsChange(() => this.refresh());
|
|
93
|
+
this.stopSelection = this.selection.subscribe(() => {
|
|
94
|
+
this.unavailableReason = "not_authorized";
|
|
95
|
+
this.refresh();
|
|
96
|
+
});
|
|
97
|
+
const callback = this.selection.authorizationCallback();
|
|
98
|
+
if (callback) {
|
|
99
|
+
const completed = await this.completeAuthorizationCallback(callback, options);
|
|
100
|
+
if (!completed.ok) {
|
|
101
|
+
if (generation === this.lifecycleGeneration && !this.destroyed) {
|
|
102
|
+
this.stopLifecycleSubscriptions();
|
|
103
|
+
this.started = false;
|
|
104
|
+
this.publish({
|
|
105
|
+
status: "start_failed",
|
|
106
|
+
problem: completed.problem,
|
|
107
|
+
connections: this.connect.connections()
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return completed;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
this.autoSelectOnlyConnection();
|
|
115
|
+
if (generation !== this.lifecycleGeneration || this.destroyed) {
|
|
116
|
+
return connectFailure(connectProblem("session_destroyed", "This session has been destroyed."));
|
|
117
|
+
}
|
|
118
|
+
this.publishCurrent();
|
|
119
|
+
return connectSuccess(this.snapshot);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error instanceof MdbaseConnectError) {
|
|
123
|
+
const problem = error.problem;
|
|
124
|
+
if (generation === this.lifecycleGeneration && !this.destroyed) {
|
|
125
|
+
this.stopLifecycleSubscriptions();
|
|
126
|
+
this.started = false;
|
|
127
|
+
this.publish({ status: "start_failed", problem, connections: this.connect.connections() });
|
|
128
|
+
}
|
|
129
|
+
return connectFailure(problem);
|
|
130
|
+
}
|
|
131
|
+
if (generation === this.lifecycleGeneration && !this.destroyed) {
|
|
132
|
+
this.stopLifecycleSubscriptions();
|
|
133
|
+
this.started = false;
|
|
134
|
+
this.publish({ status: "not_started", connections: this.connect.connections() });
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
destroy() {
|
|
140
|
+
if (this.destroyed)
|
|
141
|
+
return;
|
|
142
|
+
this.lifecycleGeneration += 1;
|
|
143
|
+
this.startOperation?.controller.abort();
|
|
144
|
+
this.startOperation = null;
|
|
145
|
+
for (const controller of this.operationControllers)
|
|
146
|
+
controller.abort();
|
|
147
|
+
this.operationControllers.clear();
|
|
148
|
+
this.stopLifecycleSubscriptions();
|
|
149
|
+
this.started = false;
|
|
150
|
+
this.destroyed = true;
|
|
151
|
+
this.callbackCompletions.clear();
|
|
152
|
+
this.publish({ status: "destroyed", connections: [] });
|
|
153
|
+
this.listeners.clear();
|
|
154
|
+
}
|
|
155
|
+
stopLifecycleSubscriptions() {
|
|
156
|
+
this.stopConnections?.();
|
|
157
|
+
this.stopSelection?.();
|
|
158
|
+
this.stopActiveConnection?.();
|
|
159
|
+
this.stopConnections = undefined;
|
|
160
|
+
this.stopSelection = undefined;
|
|
161
|
+
this.stopActiveConnection = undefined;
|
|
162
|
+
this.activeCollectionId = null;
|
|
163
|
+
}
|
|
164
|
+
getSnapshot() {
|
|
165
|
+
return this.snapshot;
|
|
166
|
+
}
|
|
167
|
+
subscribe(listener) {
|
|
168
|
+
this.listeners.add(listener);
|
|
169
|
+
return () => this.listeners.delete(listener);
|
|
170
|
+
}
|
|
171
|
+
select(collectionId, options = {}) {
|
|
172
|
+
const lifecycle = this.lifecycleProblem();
|
|
173
|
+
if (lifecycle)
|
|
174
|
+
return connectFailure(lifecycle);
|
|
175
|
+
const connection = this.connect.connection(collectionId);
|
|
176
|
+
if (!connection) {
|
|
177
|
+
return connectFailure(connectProblem("unknown_collection", "This collection is not authorized for this application."));
|
|
178
|
+
}
|
|
179
|
+
this.unavailableReason = "not_authorized";
|
|
180
|
+
this.selection.select(collectionId, options);
|
|
181
|
+
this.refresh();
|
|
182
|
+
return connectSuccess(connection);
|
|
183
|
+
}
|
|
184
|
+
clearSelection(options = {}) {
|
|
185
|
+
const lifecycle = this.lifecycleProblem();
|
|
186
|
+
if (lifecycle)
|
|
187
|
+
return connectFailure(lifecycle);
|
|
188
|
+
this.unavailableReason = "not_authorized";
|
|
189
|
+
this.selection.select(null, options);
|
|
190
|
+
this.refresh();
|
|
191
|
+
return connectSuccess(undefined);
|
|
192
|
+
}
|
|
193
|
+
forget(collectionId) {
|
|
194
|
+
const lifecycle = this.lifecycleProblem();
|
|
195
|
+
if (lifecycle)
|
|
196
|
+
return connectFailure(lifecycle);
|
|
197
|
+
this.transactionDepth += 1;
|
|
198
|
+
try {
|
|
199
|
+
const selected = this.selection.selectedCollectionId() === collectionId;
|
|
200
|
+
this.connect.connection(collectionId)?.forget();
|
|
201
|
+
if (selected)
|
|
202
|
+
this.selection.select(null, { history: "replace" });
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
this.transactionDepth -= 1;
|
|
206
|
+
this.refresh();
|
|
207
|
+
}
|
|
208
|
+
return connectSuccess(undefined);
|
|
209
|
+
}
|
|
210
|
+
async authorize(target, options = {}) {
|
|
211
|
+
const lifecycle = this.lifecycleProblem();
|
|
212
|
+
if (lifecycle)
|
|
213
|
+
return connectFailure(lifecycle);
|
|
214
|
+
const selectedCollectionId = this.selection.selectedCollectionId();
|
|
215
|
+
if (target === "selected" && !selectedCollectionId) {
|
|
216
|
+
return connectFailure(connectProblem("collection_not_selected", "Choose a collection before updating its access."));
|
|
217
|
+
}
|
|
218
|
+
const targetCollectionId = typeof target === "object"
|
|
219
|
+
? target.collectionId
|
|
220
|
+
: target === "selected"
|
|
221
|
+
? selectedCollectionId
|
|
222
|
+
: null;
|
|
223
|
+
const operation = this.beginLifecycleOperation(options);
|
|
224
|
+
this.transactionDepth += 1;
|
|
225
|
+
try {
|
|
226
|
+
const declaration = await this.connect.manifest?.(operation.options);
|
|
227
|
+
if (declaration && !declaration.ok)
|
|
228
|
+
return declaration;
|
|
229
|
+
if (!this.lifecycleCurrent(operation.generation))
|
|
230
|
+
return this.destroyedOutcome(connectSuccess(undefined));
|
|
231
|
+
const version = declaration ? semanticContractVersion(declaration.value.requirements) : 1;
|
|
232
|
+
const outcome = await this.connect.authorize({
|
|
233
|
+
...operation.options,
|
|
234
|
+
...(version === 1 && options.operations === undefined && options.capabilities === undefined
|
|
235
|
+
? { operations: this.operations } : {}),
|
|
236
|
+
target: targetCollectionId === null
|
|
237
|
+
? { kind: "choose" }
|
|
238
|
+
: { kind: "collection", collectionId: targetCollectionId },
|
|
239
|
+
returnTo: this.selection.authorizationReturnTo()
|
|
240
|
+
});
|
|
241
|
+
if (!this.lifecycleCurrent(operation.generation))
|
|
242
|
+
return this.destroyedOutcome(outcome);
|
|
243
|
+
if (outcome.ok && outcome.value.kind === "connected") {
|
|
244
|
+
this.selection.finishAuthorization(outcome.value.returnTo, outcome.value.connection.collectionId);
|
|
245
|
+
}
|
|
246
|
+
return outcome;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
operation.dispose();
|
|
250
|
+
this.transactionDepth -= 1;
|
|
251
|
+
this.refresh();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
ensureOperations(requiredOperations, options) {
|
|
255
|
+
return this.ensureAuthorization({ operations: requiredOperations }, options);
|
|
256
|
+
}
|
|
257
|
+
ensureCapabilities(requiredCapabilities, options) {
|
|
258
|
+
return this.ensureAuthorization({ capabilities: requiredCapabilities }, options);
|
|
259
|
+
}
|
|
260
|
+
async ensureAuthorization(selection, options) {
|
|
261
|
+
const lifecycle = this.lifecycleProblem();
|
|
262
|
+
if (lifecycle)
|
|
263
|
+
return connectFailure(lifecycle);
|
|
264
|
+
const current = this.snapshot;
|
|
265
|
+
if (current.status !== "ready") {
|
|
266
|
+
return connectFailure(connectProblem("collection_not_ready", "Choose an authorized collection first."));
|
|
267
|
+
}
|
|
268
|
+
const operation = this.beginLifecycleOperation(options);
|
|
269
|
+
this.transactionDepth += 1;
|
|
270
|
+
try {
|
|
271
|
+
const declaration = await this.connect.manifest?.(operation.options);
|
|
272
|
+
if (declaration && !declaration.ok)
|
|
273
|
+
return declaration;
|
|
274
|
+
if (!this.lifecycleCurrent(operation.generation))
|
|
275
|
+
return this.destroyedOutcome(connectSuccess(undefined));
|
|
276
|
+
// Custom advanced session adapters predating manifest() retain exact v1
|
|
277
|
+
// operations. The concrete SDK always supplies its declared manifest.
|
|
278
|
+
const requirements = declaration?.value.requirements ?? ("operations" in selection
|
|
279
|
+
? { access: "full_collection", contracts: [] }
|
|
280
|
+
: { access: "full_collection", contracts: [], capabilities: { contract_version: 2, required: [], optional: Object.keys(APPLICATION_CAPABILITY_DEFINITIONS) } });
|
|
281
|
+
validateAuthorizationSelection(requirements, { ...options, ...selection });
|
|
282
|
+
const requiredOperations = "operations" in selection ? selection.operations : selection.capabilities.flatMap((id) => capabilityOperationsForContractVersion(semanticContractVersion(requirements), id) ?? []);
|
|
283
|
+
const authorization = current.connection.authorizationCapabilities(requiredOperations);
|
|
284
|
+
if (authorization.sufficient)
|
|
285
|
+
return connectSuccess({ kind: "unchanged", connection: current.connection });
|
|
286
|
+
const incremental = "operations" in selection || semanticContractVersion(requirements) === 1
|
|
287
|
+
? { operations: uniqueOperations([...authorization.grantedOperations, ...requiredOperations]) }
|
|
288
|
+
: { capabilities: [...new Set([...retainedCapabilities(requirements, authorization.grantedOperations), ...selection.capabilities])] };
|
|
289
|
+
const outcome = await this.connect.authorize({
|
|
290
|
+
...operation.options,
|
|
291
|
+
...incremental,
|
|
292
|
+
target: { kind: "collection", collectionId: current.collectionId },
|
|
293
|
+
returnTo: this.selection.authorizationReturnTo()
|
|
294
|
+
});
|
|
295
|
+
if (!this.lifecycleCurrent(operation.generation))
|
|
296
|
+
return this.destroyedOutcome(outcome);
|
|
297
|
+
if (outcome.ok && outcome.value.kind === "connected") {
|
|
298
|
+
this.selection.finishAuthorization(outcome.value.returnTo, outcome.value.connection.collectionId);
|
|
299
|
+
}
|
|
300
|
+
return outcome;
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (error instanceof MdbaseConnectError)
|
|
304
|
+
return connectFailure(error.problem);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
operation.dispose();
|
|
309
|
+
this.transactionDepth -= 1;
|
|
310
|
+
this.refresh();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
handleAuthorizationCallback(callbackUrl, options) {
|
|
314
|
+
const lifecycle = this.lifecycleProblem();
|
|
315
|
+
if (lifecycle)
|
|
316
|
+
return Promise.resolve(connectFailure(lifecycle));
|
|
317
|
+
if (!isAuthorizationCallbackUrl(callbackUrl)) {
|
|
318
|
+
return Promise.resolve(connectFailure(connectProblem("invalid_callback", "This URL is not an authorization callback.")));
|
|
319
|
+
}
|
|
320
|
+
return this.completeAuthorizationCallback(callbackUrl, options);
|
|
321
|
+
}
|
|
322
|
+
completeAuthorizationCallback(callbackUrl, options) {
|
|
323
|
+
if (options?.signal?.aborted) {
|
|
324
|
+
const error = requestAbortReason(options.signal);
|
|
325
|
+
return Promise.resolve(connectFailure(error.problem));
|
|
326
|
+
}
|
|
327
|
+
const state = authorizationCallbackState(callbackUrl);
|
|
328
|
+
if (!state) {
|
|
329
|
+
return Promise.resolve(connectFailure(connectProblem("invalid_callback", "Authorization callback is missing its state.")));
|
|
330
|
+
}
|
|
331
|
+
const operation = this.callbackCompletions.get(state)
|
|
332
|
+
?? this.beginCallbackCompletion(callbackUrl, state);
|
|
333
|
+
return this.waitForCallbackCompletion(operation, options);
|
|
334
|
+
}
|
|
335
|
+
beginCallbackCompletion(callbackUrl, state) {
|
|
336
|
+
const controller = new AbortController();
|
|
337
|
+
const generation = this.lifecycleGeneration;
|
|
338
|
+
this.transactionDepth += 1;
|
|
339
|
+
this.operationControllers.add(controller);
|
|
340
|
+
const operation = {
|
|
341
|
+
state,
|
|
342
|
+
controller,
|
|
343
|
+
waiters: 0,
|
|
344
|
+
settled: false,
|
|
345
|
+
abandoned: false,
|
|
346
|
+
promise: undefined
|
|
347
|
+
};
|
|
348
|
+
const completion = this.connect.completeAuthorization(callbackUrl, {
|
|
349
|
+
signal: controller.signal,
|
|
350
|
+
timeoutMs: null
|
|
351
|
+
}).catch((error) => {
|
|
352
|
+
if (error instanceof MdbaseConnectError) {
|
|
353
|
+
return connectFailure(error.problem);
|
|
354
|
+
}
|
|
355
|
+
throw error;
|
|
356
|
+
});
|
|
357
|
+
operation.promise = completion
|
|
358
|
+
.then((outcome) => {
|
|
359
|
+
if (generation !== this.lifecycleGeneration || this.destroyed) {
|
|
360
|
+
return this.destroyedOutcome(outcome);
|
|
361
|
+
}
|
|
362
|
+
if (!outcome.ok) {
|
|
363
|
+
if (!operation.abandoned && outcome.problem.recovery !== "retry") {
|
|
364
|
+
const returnTo = authorizationReturnToFromProblem(outcome.problem);
|
|
365
|
+
this.selection.clearAuthorizationCallback(returnTo);
|
|
366
|
+
}
|
|
367
|
+
this.refresh();
|
|
368
|
+
return outcome;
|
|
369
|
+
}
|
|
370
|
+
const result = outcome.value;
|
|
371
|
+
this.unavailableReason = "not_authorized";
|
|
372
|
+
this.selection.finishAuthorization(result.returnTo, result.connection.collectionId);
|
|
373
|
+
this.refresh();
|
|
374
|
+
return connectSuccess(result.connection, outcome.diagnostics);
|
|
375
|
+
})
|
|
376
|
+
.catch((error) => {
|
|
377
|
+
if (generation !== this.lifecycleGeneration || this.destroyed) {
|
|
378
|
+
return connectFailure(connectProblem("session_destroyed", "This session was destroyed before authorization completion finished."));
|
|
379
|
+
}
|
|
380
|
+
if (!operation.abandoned) {
|
|
381
|
+
this.selection.clearAuthorizationCallback();
|
|
382
|
+
this.refresh();
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
})
|
|
386
|
+
.finally(() => {
|
|
387
|
+
operation.settled = true;
|
|
388
|
+
this.operationControllers.delete(controller);
|
|
389
|
+
this.transactionDepth -= 1;
|
|
390
|
+
this.refresh();
|
|
391
|
+
if (this.callbackCompletions.get(state) === operation) {
|
|
392
|
+
this.callbackCompletions.delete(state);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
this.callbackCompletions.set(state, operation);
|
|
396
|
+
return operation;
|
|
397
|
+
}
|
|
398
|
+
async waitForCallbackCompletion(operation, options) {
|
|
399
|
+
operation.waiters += 1;
|
|
400
|
+
try {
|
|
401
|
+
return await withRequestBudget(options, DEFAULT_REQUEST_TIMEOUT_MS, () => operation.promise);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (error instanceof MdbaseConnectError) {
|
|
405
|
+
return connectFailure(error.problem);
|
|
406
|
+
}
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
operation.waiters -= 1;
|
|
411
|
+
if (operation.waiters === 0
|
|
412
|
+
&& !operation.settled
|
|
413
|
+
&& this.callbackCompletions.get(operation.state) === operation) {
|
|
414
|
+
operation.abandoned = true;
|
|
415
|
+
this.callbackCompletions.delete(operation.state);
|
|
416
|
+
operation.controller.abort();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
lifecycleProblem() {
|
|
421
|
+
if (this.destroyed) {
|
|
422
|
+
return connectProblem("session_destroyed", "This session has been destroyed.");
|
|
423
|
+
}
|
|
424
|
+
if (this.snapshot.status === "starting") {
|
|
425
|
+
return connectProblem("session_starting", "The session is still starting.");
|
|
426
|
+
}
|
|
427
|
+
if (this.snapshot.status === "start_failed")
|
|
428
|
+
return this.snapshot.problem;
|
|
429
|
+
if (!this.started) {
|
|
430
|
+
return connectProblem("session_not_started", "Start the session before using it.");
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
beginLifecycleOperation(options = {}) {
|
|
435
|
+
const controller = new AbortController();
|
|
436
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
437
|
+
if (options.signal?.aborted)
|
|
438
|
+
abort();
|
|
439
|
+
else
|
|
440
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
441
|
+
this.operationControllers.add(controller);
|
|
442
|
+
return {
|
|
443
|
+
generation: this.lifecycleGeneration,
|
|
444
|
+
options: { ...options, signal: controller.signal },
|
|
445
|
+
dispose: () => {
|
|
446
|
+
options.signal?.removeEventListener("abort", abort);
|
|
447
|
+
this.operationControllers.delete(controller);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
lifecycleCurrent(generation) {
|
|
452
|
+
return !this.destroyed && generation === this.lifecycleGeneration;
|
|
453
|
+
}
|
|
454
|
+
destroyedOutcome(outcome) {
|
|
455
|
+
if (!outcome.ok && outcome.problem.code === "operation_outcome_unknown")
|
|
456
|
+
return outcome;
|
|
457
|
+
return connectFailure(connectProblem("session_destroyed", "This session was destroyed before the operation finished."));
|
|
458
|
+
}
|
|
459
|
+
autoSelectOnlyConnection() {
|
|
460
|
+
if (this.autoSelect !== "only" || this.selection.selectedCollectionId())
|
|
461
|
+
return;
|
|
462
|
+
const connections = this.connect.connections();
|
|
463
|
+
if (connections.length === 1) {
|
|
464
|
+
this.selection.select(connections[0].collectionId, { history: "replace" });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
refresh() {
|
|
468
|
+
if (!this.started
|
|
469
|
+
|| this.destroyed
|
|
470
|
+
|| this.snapshot.status === "starting"
|
|
471
|
+
|| this.snapshot.status === "start_failed")
|
|
472
|
+
return;
|
|
473
|
+
if (this.transactionDepth > 0) {
|
|
474
|
+
this.refreshQueued = true;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (this.refreshing) {
|
|
478
|
+
this.refreshQueued = true;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
this.refreshing = true;
|
|
482
|
+
try {
|
|
483
|
+
do {
|
|
484
|
+
this.refreshQueued = false;
|
|
485
|
+
this.publishCurrent();
|
|
486
|
+
} while (this.refreshQueued);
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
this.refreshing = false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
publishCurrent() {
|
|
493
|
+
const collectionId = this.selection.selectedCollectionId();
|
|
494
|
+
const connections = this.connect.connections();
|
|
495
|
+
const connection = collectionId ? this.connect.connection(collectionId) : null;
|
|
496
|
+
if (connection?.collectionId !== this.activeCollectionId) {
|
|
497
|
+
this.stopActiveConnection?.();
|
|
498
|
+
this.activeCollectionId = connection?.collectionId ?? null;
|
|
499
|
+
this.stopActiveConnection = connection?.onConnectionChange((info) => {
|
|
500
|
+
if (!info && this.selection.selectedCollectionId() === connection.collectionId) {
|
|
501
|
+
this.unavailableReason = "authorization_lost";
|
|
502
|
+
}
|
|
503
|
+
this.refresh();
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
let next;
|
|
507
|
+
if (!collectionId) {
|
|
508
|
+
next = { status: "unselected", connections };
|
|
509
|
+
}
|
|
510
|
+
else if (!connection || !connection.info()) {
|
|
511
|
+
next = {
|
|
512
|
+
status: "unavailable",
|
|
513
|
+
collectionId,
|
|
514
|
+
reason: this.connect.unavailableReason(collectionId) ?? this.unavailableReason,
|
|
515
|
+
connections
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
const info = connection.info();
|
|
520
|
+
next = {
|
|
521
|
+
status: "ready",
|
|
522
|
+
collectionId,
|
|
523
|
+
connection,
|
|
524
|
+
info,
|
|
525
|
+
access: connection.authorizationCapabilities(this.operations),
|
|
526
|
+
connections
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
this.publish(next);
|
|
530
|
+
}
|
|
531
|
+
publish(snapshot) {
|
|
532
|
+
if (this.snapshot.status === "destroyed" && snapshot.status !== "destroyed")
|
|
533
|
+
return;
|
|
534
|
+
const key = sessionSnapshotKey(snapshot);
|
|
535
|
+
if (key === this.snapshotKey)
|
|
536
|
+
return;
|
|
537
|
+
this.snapshot = snapshot;
|
|
538
|
+
this.snapshotKey = key;
|
|
539
|
+
for (const listener of this.listeners)
|
|
540
|
+
listener();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function sessionSnapshotKey(snapshot) {
|
|
544
|
+
if (snapshot.status === "not_started"
|
|
545
|
+
|| snapshot.status === "starting"
|
|
546
|
+
|| snapshot.status === "destroyed")
|
|
547
|
+
return JSON.stringify(snapshot);
|
|
548
|
+
if (snapshot.status === "start_failed") {
|
|
549
|
+
return JSON.stringify({ status: snapshot.status, problem: snapshot.problem, connections: snapshot.connections });
|
|
550
|
+
}
|
|
551
|
+
if (snapshot.status === "unselected") {
|
|
552
|
+
return JSON.stringify({ status: snapshot.status, connections: snapshot.connections });
|
|
553
|
+
}
|
|
554
|
+
if (snapshot.status === "unavailable") {
|
|
555
|
+
return JSON.stringify({
|
|
556
|
+
status: snapshot.status,
|
|
557
|
+
collectionId: snapshot.collectionId,
|
|
558
|
+
reason: snapshot.reason,
|
|
559
|
+
connections: snapshot.connections
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return JSON.stringify({
|
|
563
|
+
status: snapshot.status,
|
|
564
|
+
collectionId: snapshot.collectionId,
|
|
565
|
+
info: snapshot.info,
|
|
566
|
+
access: snapshot.access,
|
|
567
|
+
connections: snapshot.connections
|
|
568
|
+
});
|
|
569
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
2
|
+
export declare class SharedOperationMap<Result> {
|
|
3
|
+
private readonly operations;
|
|
4
|
+
run(key: string, options: ConnectRequestOptions | undefined, defaultTimeoutMs: number | null, start: (signal: AbortSignal) => Promise<Result>): Promise<Result>;
|
|
5
|
+
private begin;
|
|
6
|
+
private wait;
|
|
7
|
+
}
|