@mdbase-dev/connect 0.1.0-beta.21

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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +405 -0
  3. package/dist/async.d.ts +1 -0
  4. package/dist/async.js +13 -0
  5. package/dist/authorization-types.d.ts +7 -0
  6. package/dist/authorization-types.js +1 -0
  7. package/dist/authorization-url.d.ts +3 -0
  8. package/dist/authorization-url.js +27 -0
  9. package/dist/base64.d.ts +3 -0
  10. package/dist/base64.js +21 -0
  11. package/dist/browser/integrity.json +4 -0
  12. package/dist/browser/mdbase-connect.min.js +3 -0
  13. package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
  14. package/dist/collection-client.d.ts +57 -0
  15. package/dist/collection-client.js +214 -0
  16. package/dist/connect.d.ts +39 -0
  17. package/dist/connect.js +620 -0
  18. package/dist/connection-notifications.d.ts +25 -0
  19. package/dist/connection-notifications.js +179 -0
  20. package/dist/connection-transport.d.ts +80 -0
  21. package/dist/connection-transport.js +670 -0
  22. package/dist/connection-types.d.ts +29 -0
  23. package/dist/connection-types.js +1 -0
  24. package/dist/connection.d.ts +154 -0
  25. package/dist/connection.js +371 -0
  26. package/dist/crypto.d.ts +64 -0
  27. package/dist/crypto.js +391 -0
  28. package/dist/errors.d.ts +31 -0
  29. package/dist/errors.js +83 -0
  30. package/dist/index.d.ts +13 -0
  31. package/dist/index.js +12 -0
  32. package/dist/internal-types.d.ts +70 -0
  33. package/dist/internal-types.js +1 -0
  34. package/dist/notifications.d.ts +47 -0
  35. package/dist/notifications.js +52 -0
  36. package/dist/operation-helpers.d.ts +24 -0
  37. package/dist/operation-helpers.js +129 -0
  38. package/dist/operation-types.d.ts +254 -0
  39. package/dist/operation-types.js +1 -0
  40. package/dist/runtime-utils.d.ts +33 -0
  41. package/dist/runtime-utils.js +224 -0
  42. package/dist/selection.d.ts +48 -0
  43. package/dist/selection.js +112 -0
  44. package/dist/session.d.ts +76 -0
  45. package/dist/session.js +271 -0
  46. package/package.json +41 -0
@@ -0,0 +1,271 @@
1
+ import { authorizationReturnToFromError, isAuthorizationCallbackUrl } from "./authorization-url.js";
2
+ import { MdbaseConnectError } from "./errors.js";
3
+ import { DEFAULT_OPERATIONS } from "./internal-types.js";
4
+ import { uniqueOperations } from "./operation-helpers.js";
5
+ export class MdbaseSession {
6
+ connect;
7
+ operations;
8
+ selection;
9
+ autoSelect;
10
+ listeners = new Set();
11
+ snapshot;
12
+ snapshotKey = "";
13
+ stopConnections;
14
+ stopSelection;
15
+ stopActiveConnection;
16
+ activeCollectionId = null;
17
+ unavailableReason = "not_authorized";
18
+ started = false;
19
+ refreshing = false;
20
+ refreshQueued = false;
21
+ transactionDepth = 0;
22
+ callbackPromise = null;
23
+ constructor(connect, options) {
24
+ this.connect = connect;
25
+ this.operations = uniqueOperations(options.operations ?? DEFAULT_OPERATIONS);
26
+ this.selection = options.selection;
27
+ this.autoSelect = options.autoSelect ?? "only";
28
+ this.snapshot = { status: "unselected", connections: [] };
29
+ this.refresh();
30
+ }
31
+ async start() {
32
+ if (!this.started) {
33
+ this.started = true;
34
+ this.stopConnections = this.connect.onConnectionsChange(() => this.refresh());
35
+ this.stopSelection = this.selection.subscribe(() => {
36
+ this.unavailableReason = "not_authorized";
37
+ this.refresh();
38
+ });
39
+ }
40
+ const callback = this.selection.authorizationCallback();
41
+ if (callback)
42
+ await this.handleAuthorizationCallback(callback);
43
+ else
44
+ this.autoSelectOnlyConnection();
45
+ this.refresh();
46
+ return this.snapshot;
47
+ }
48
+ destroy() {
49
+ this.stopConnections?.();
50
+ this.stopSelection?.();
51
+ this.stopActiveConnection?.();
52
+ this.stopConnections = undefined;
53
+ this.stopSelection = undefined;
54
+ this.stopActiveConnection = undefined;
55
+ this.activeCollectionId = null;
56
+ this.started = false;
57
+ }
58
+ getSnapshot() {
59
+ return this.snapshot;
60
+ }
61
+ subscribe(listener) {
62
+ this.listeners.add(listener);
63
+ return () => this.listeners.delete(listener);
64
+ }
65
+ select(collectionId, options = {}) {
66
+ const connection = this.connect.connection(collectionId);
67
+ if (!connection) {
68
+ throw new MdbaseConnectError("unknown_collection", "This collection is not authorized for this application.");
69
+ }
70
+ this.unavailableReason = "not_authorized";
71
+ this.selection.select(collectionId, options);
72
+ this.refresh();
73
+ return connection;
74
+ }
75
+ clearSelection(options = {}) {
76
+ this.unavailableReason = "not_authorized";
77
+ this.selection.select(null, options);
78
+ this.refresh();
79
+ }
80
+ forget(collectionId) {
81
+ this.transactionDepth += 1;
82
+ try {
83
+ const selected = this.selection.selectedCollectionId() === collectionId;
84
+ this.connect.connection(collectionId)?.forget();
85
+ if (selected)
86
+ this.selection.select(null, { history: "replace" });
87
+ }
88
+ finally {
89
+ this.transactionDepth -= 1;
90
+ this.refresh();
91
+ }
92
+ }
93
+ async authorize(target, options = {}) {
94
+ const selectedCollectionId = this.selection.selectedCollectionId();
95
+ if (target === "selected" && !selectedCollectionId) {
96
+ throw new MdbaseConnectError("collection_not_selected", "Choose a collection before updating its access.");
97
+ }
98
+ const targetCollectionId = typeof target === "object"
99
+ ? target.collectionId
100
+ : target === "selected"
101
+ ? selectedCollectionId
102
+ : null;
103
+ this.transactionDepth += 1;
104
+ try {
105
+ const outcome = await this.connect.authorize({
106
+ ...options,
107
+ operations: this.operations,
108
+ target: targetCollectionId === null
109
+ ? { kind: "choose" }
110
+ : { kind: "collection", collectionId: targetCollectionId },
111
+ returnTo: this.selection.authorizationReturnTo()
112
+ });
113
+ if (outcome.kind === "connected") {
114
+ this.selection.finishAuthorization(outcome.returnTo, outcome.connection.collectionId);
115
+ }
116
+ return outcome;
117
+ }
118
+ finally {
119
+ this.transactionDepth -= 1;
120
+ this.refresh();
121
+ }
122
+ }
123
+ async ensureOperations(requiredOperations) {
124
+ const current = this.snapshot;
125
+ if (current.status !== "ready") {
126
+ throw new MdbaseConnectError("collection_not_ready", "Choose an authorized collection first.");
127
+ }
128
+ const required = uniqueOperations(requiredOperations);
129
+ const capabilities = current.connection.authorizationCapabilities(required);
130
+ if (capabilities.sufficient)
131
+ return { kind: "unchanged", connection: current.connection };
132
+ this.transactionDepth += 1;
133
+ try {
134
+ const outcome = await this.connect.authorize({
135
+ operations: uniqueOperations([...capabilities.grantedOperations, ...capabilities.missingOperations]),
136
+ target: { kind: "collection", collectionId: current.collectionId },
137
+ returnTo: this.selection.authorizationReturnTo()
138
+ });
139
+ if (outcome.kind === "connected") {
140
+ this.selection.finishAuthorization(outcome.returnTo, outcome.connection.collectionId);
141
+ }
142
+ return outcome;
143
+ }
144
+ finally {
145
+ this.transactionDepth -= 1;
146
+ this.refresh();
147
+ }
148
+ }
149
+ handleAuthorizationCallback(callbackUrl) {
150
+ if (!isAuthorizationCallbackUrl(callbackUrl)) {
151
+ return Promise.reject(new MdbaseConnectError("invalid_callback", "This URL is not an authorization callback."));
152
+ }
153
+ if (this.callbackPromise)
154
+ return this.callbackPromise;
155
+ this.transactionDepth += 1;
156
+ const completion = this.connect.completeAuthorization(callbackUrl)
157
+ .then((result) => {
158
+ this.unavailableReason = "not_authorized";
159
+ this.selection.finishAuthorization(result.returnTo, result.connection.collectionId);
160
+ this.refresh();
161
+ return result.connection;
162
+ })
163
+ .catch((error) => {
164
+ const returnTo = authorizationReturnToFromError(error);
165
+ this.selection.clearAuthorizationCallback(returnTo);
166
+ this.refresh();
167
+ throw error;
168
+ })
169
+ .finally(() => {
170
+ this.transactionDepth -= 1;
171
+ this.refresh();
172
+ if (this.callbackPromise === completion)
173
+ this.callbackPromise = null;
174
+ });
175
+ this.callbackPromise = completion;
176
+ return completion;
177
+ }
178
+ autoSelectOnlyConnection() {
179
+ if (this.autoSelect !== "only" || this.selection.selectedCollectionId())
180
+ return;
181
+ const connections = this.connect.connections();
182
+ if (connections.length === 1) {
183
+ this.selection.select(connections[0].collectionId, { history: "replace" });
184
+ }
185
+ }
186
+ refresh() {
187
+ if (this.transactionDepth > 0) {
188
+ this.refreshQueued = true;
189
+ return;
190
+ }
191
+ if (this.refreshing) {
192
+ this.refreshQueued = true;
193
+ return;
194
+ }
195
+ this.refreshing = true;
196
+ try {
197
+ do {
198
+ this.refreshQueued = false;
199
+ this.refreshOnce();
200
+ } while (this.refreshQueued);
201
+ }
202
+ finally {
203
+ this.refreshing = false;
204
+ }
205
+ }
206
+ refreshOnce() {
207
+ const collectionId = this.selection.selectedCollectionId();
208
+ const connections = this.connect.connections();
209
+ const connection = collectionId ? this.connect.connection(collectionId) : null;
210
+ if (connection?.collectionId !== this.activeCollectionId) {
211
+ this.stopActiveConnection?.();
212
+ this.activeCollectionId = connection?.collectionId ?? null;
213
+ this.stopActiveConnection = connection?.onConnectionChange((info) => {
214
+ if (!info && this.selection.selectedCollectionId() === connection.collectionId) {
215
+ this.unavailableReason = "authorization_lost";
216
+ }
217
+ this.refresh();
218
+ });
219
+ }
220
+ let next;
221
+ if (!collectionId) {
222
+ next = { status: "unselected", connections };
223
+ }
224
+ else if (!connection || !connection.info()) {
225
+ next = {
226
+ status: "unavailable",
227
+ collectionId,
228
+ reason: this.connect.unavailableReason(collectionId) ?? this.unavailableReason,
229
+ connections
230
+ };
231
+ }
232
+ else {
233
+ const info = connection.info();
234
+ next = {
235
+ status: "ready",
236
+ collectionId,
237
+ connection,
238
+ info,
239
+ access: connection.authorizationCapabilities(this.operations),
240
+ connections
241
+ };
242
+ }
243
+ const key = sessionSnapshotKey(next);
244
+ if (key === this.snapshotKey)
245
+ return;
246
+ this.snapshot = next;
247
+ this.snapshotKey = key;
248
+ for (const listener of this.listeners)
249
+ listener();
250
+ }
251
+ }
252
+ function sessionSnapshotKey(snapshot) {
253
+ if (snapshot.status === "unselected") {
254
+ return JSON.stringify({ status: snapshot.status, connections: snapshot.connections });
255
+ }
256
+ if (snapshot.status === "unavailable") {
257
+ return JSON.stringify({
258
+ status: snapshot.status,
259
+ collectionId: snapshot.collectionId,
260
+ reason: snapshot.reason,
261
+ connections: snapshot.connections
262
+ });
263
+ }
264
+ return JSON.stringify({
265
+ status: snapshot.status,
266
+ collectionId: snapshot.collectionId,
267
+ info: snapshot.info,
268
+ access: snapshot.access,
269
+ connections: snapshot.connections
270
+ });
271
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@mdbase-dev/connect",
3
+ "version": "0.1.0-beta.21",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mdbase-dev/mdbase-connect.git",
10
+ "directory": "packages/client"
11
+ },
12
+ "homepage": "https://mdbase.dev/sdk/",
13
+ "sideEffects": false,
14
+ "browser": "./dist/browser/mdbase-connect.min.js",
15
+ "unpkg": "./dist/browser/mdbase-connect.min.js",
16
+ "jsdelivr": "./dist/browser/mdbase-connect.min.js",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.21"
29
+ },
30
+ "devDependencies": {
31
+ "esbuild": "^0.28.1",
32
+ "typescript": "^7.0.2",
33
+ "vitest": "^4.1.10"
34
+ },
35
+ "scripts": {
36
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node scripts/build-browser.mjs",
37
+ "test": "vitest run",
38
+ "test:browser-storage": "pnpm build && node scripts/browser-storage.test.mjs",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit"
40
+ }
41
+ }