@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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +696 -0
  3. package/dist/advanced.d.ts +9 -0
  4. package/dist/advanced.js +6 -0
  5. package/dist/application-contract.d.ts +22 -0
  6. package/dist/application-contract.js +75 -0
  7. package/dist/application-identity.d.ts +34 -0
  8. package/dist/application-identity.js +148 -0
  9. package/dist/application-session.d.ts +167 -0
  10. package/dist/application-session.js +722 -0
  11. package/dist/async.d.ts +1 -0
  12. package/dist/async.js +19 -0
  13. package/dist/authority-proof.d.ts +3 -0
  14. package/dist/authority-proof.js +24 -0
  15. package/dist/authority-sync-request.d.ts +3 -0
  16. package/dist/authority-sync-request.js +20 -0
  17. package/dist/authorization-refresh.d.ts +14 -0
  18. package/dist/authorization-refresh.js +41 -0
  19. package/dist/authorization-types.d.ts +7 -0
  20. package/dist/authorization-types.js +1 -0
  21. package/dist/authorization-url.d.ts +5 -0
  22. package/dist/authorization-url.js +34 -0
  23. package/dist/base64.d.ts +3 -0
  24. package/dist/base64.js +21 -0
  25. package/dist/browser/integrity.json +4 -0
  26. package/dist/browser/mdbase-connect.min.js +3 -0
  27. package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
  28. package/dist/browser.d.ts +3 -0
  29. package/dist/browser.js +5 -0
  30. package/dist/capabilities.d.ts +24 -0
  31. package/dist/capabilities.js +91 -0
  32. package/dist/collection-client.d.ts +66 -0
  33. package/dist/collection-client.js +686 -0
  34. package/dist/connect-authorization-helpers.d.ts +4 -0
  35. package/dist/connect-authorization-helpers.js +25 -0
  36. package/dist/connect-options.d.ts +35 -0
  37. package/dist/connect-options.js +1 -0
  38. package/dist/connect.d.ts +66 -0
  39. package/dist/connect.js +711 -0
  40. package/dist/connection-file-transport.d.ts +34 -0
  41. package/dist/connection-file-transport.js +85 -0
  42. package/dist/connection-index.d.ts +3 -0
  43. package/dist/connection-index.js +20 -0
  44. package/dist/connection-notifications.d.ts +31 -0
  45. package/dist/connection-notifications.js +197 -0
  46. package/dist/connection-transport-internals.d.ts +24 -0
  47. package/dist/connection-transport-internals.js +1 -0
  48. package/dist/connection-transport.d.ts +64 -0
  49. package/dist/connection-transport.js +725 -0
  50. package/dist/connection-types.d.ts +38 -0
  51. package/dist/connection-types.js +1 -0
  52. package/dist/connection.d.ts +175 -0
  53. package/dist/connection.js +606 -0
  54. package/dist/crypto-entry.d.ts +7 -0
  55. package/dist/crypto-entry.js +4 -0
  56. package/dist/crypto.d.ts +63 -0
  57. package/dist/crypto.js +409 -0
  58. package/dist/direct-access.d.ts +3 -0
  59. package/dist/direct-access.js +29 -0
  60. package/dist/errors.d.ts +45 -0
  61. package/dist/errors.js +142 -0
  62. package/dist/external-store.d.ts +10 -0
  63. package/dist/external-store.js +10 -0
  64. package/dist/file-crypto.d.ts +25 -0
  65. package/dist/file-crypto.js +132 -0
  66. package/dist/file-sha256.d.ts +12 -0
  67. package/dist/file-sha256.js +143 -0
  68. package/dist/file-stream-source.d.ts +17 -0
  69. package/dist/file-stream-source.js +157 -0
  70. package/dist/file-transfer-internals.d.ts +23 -0
  71. package/dist/file-transfer-internals.js +228 -0
  72. package/dist/files.d.ts +115 -0
  73. package/dist/files.js +563 -0
  74. package/dist/fresh-encrypted-request.d.ts +4 -0
  75. package/dist/fresh-encrypted-request.js +28 -0
  76. package/dist/grant-key-leases.d.ts +24 -0
  77. package/dist/grant-key-leases.js +144 -0
  78. package/dist/hosted-file-request.d.ts +6 -0
  79. package/dist/hosted-file-request.js +63 -0
  80. package/dist/hosted-operation-response.d.ts +5 -0
  81. package/dist/hosted-operation-response.js +26 -0
  82. package/dist/index.d.ts +27 -0
  83. package/dist/index.js +10 -0
  84. package/dist/internal-types.d.ts +100 -0
  85. package/dist/internal-types.js +1 -0
  86. package/dist/local-file-transport.d.ts +37 -0
  87. package/dist/local-file-transport.js +457 -0
  88. package/dist/mdbase-connect.d.ts +28 -0
  89. package/dist/mdbase-connect.js +53 -0
  90. package/dist/notifications.d.ts +48 -0
  91. package/dist/notifications.js +52 -0
  92. package/dist/operation-helpers.d.ts +27 -0
  93. package/dist/operation-helpers.js +144 -0
  94. package/dist/operation-types.d.ts +707 -0
  95. package/dist/operation-types.js +1 -0
  96. package/dist/outcomes.d.ts +51 -0
  97. package/dist/outcomes.js +194 -0
  98. package/dist/pending-mutation-store.d.ts +13 -0
  99. package/dist/pending-mutation-store.js +64 -0
  100. package/dist/pending-recovery-transports.d.ts +5 -0
  101. package/dist/pending-recovery-transports.js +52 -0
  102. package/dist/popup-authorization.d.ts +31 -0
  103. package/dist/popup-authorization.js +100 -0
  104. package/dist/query-pagination.d.ts +6 -0
  105. package/dist/query-pagination.js +119 -0
  106. package/dist/request-budget.d.ts +34 -0
  107. package/dist/request-budget.js +104 -0
  108. package/dist/request-coordinator.d.ts +29 -0
  109. package/dist/request-coordinator.js +213 -0
  110. package/dist/retired-grant-keys.d.ts +9 -0
  111. package/dist/retired-grant-keys.js +56 -0
  112. package/dist/runtime-utils.d.ts +52 -0
  113. package/dist/runtime-utils.js +368 -0
  114. package/dist/saved-view-pagination.d.ts +6 -0
  115. package/dist/saved-view-pagination.js +76 -0
  116. package/dist/selection.d.ts +48 -0
  117. package/dist/selection.js +112 -0
  118. package/dist/session.d.ts +114 -0
  119. package/dist/session.js +569 -0
  120. package/dist/shared-operation.d.ts +7 -0
  121. package/dist/shared-operation.js +41 -0
  122. package/dist/stored-token.d.ts +10 -0
  123. package/dist/stored-token.js +64 -0
  124. package/dist/structured-readiness.d.ts +77 -0
  125. package/dist/structured-readiness.js +126 -0
  126. package/dist/token-response.d.ts +12 -0
  127. package/dist/token-response.js +81 -0
  128. package/dist/transient-retry.d.ts +22 -0
  129. package/dist/transient-retry.js +59 -0
  130. package/dist/watch-policy.d.ts +8 -0
  131. package/dist/watch-policy.js +12 -0
  132. 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
+ }
@@ -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
+ }