@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,606 @@
|
|
|
1
|
+
import { capabilityOperationsForContractVersion } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { retainedCapabilities, semanticContractVersion, validateAuthorizationSelection } from "./application-contract.js";
|
|
3
|
+
import { MdbaseCollectionClient } from "./collection-client.js";
|
|
4
|
+
import { ConnectionNotifications } from "./connection-notifications.js";
|
|
5
|
+
import { ConnectionTransport } from "./connection-transport.js";
|
|
6
|
+
import { MdbaseConnectError, connectProblem, operationProblem } from "./errors.js";
|
|
7
|
+
import { MdbaseFileClient } from "./files.js";
|
|
8
|
+
import { DEFAULT_OPERATIONS } from "./internal-types.js";
|
|
9
|
+
import { assertDeletePreview, assertRenamePreview, deleteEstimate, isCancellation, renameEstimate, throwIfCancelled, uniqueOperations } from "./operation-helpers.js";
|
|
10
|
+
import { AUTHORIZATION_PROBLEM_CODES, COLLECTION_CHANGES_PROBLEM_CODES, COLLECTION_MUTATION_PROBLEM_CODES, DIRECT_ACCESS_PROBLEM_CODES, NOTIFICATION_PROBLEM_CODES, REGISTRATION_PROBLEM_CODES, captureConnectOutcome, connectFailure, connectSuccess } from "./outcomes.js";
|
|
11
|
+
import { withCooperativeRequestBudget, withRequestBudget } from "./request-budget.js";
|
|
12
|
+
import { CollectionRequestCoordinator } from "./request-coordinator.js";
|
|
13
|
+
export class MdbaseConnection {
|
|
14
|
+
internals;
|
|
15
|
+
collectionId;
|
|
16
|
+
collectionClient;
|
|
17
|
+
notifications;
|
|
18
|
+
transport;
|
|
19
|
+
files;
|
|
20
|
+
connectionListeners = new Set();
|
|
21
|
+
constructor(internals, collectionId) {
|
|
22
|
+
this.internals = internals;
|
|
23
|
+
this.collectionId = collectionId;
|
|
24
|
+
this.transport = new ConnectionTransport({
|
|
25
|
+
serverUrl: internals.serverUrl,
|
|
26
|
+
storage: internals.storage,
|
|
27
|
+
keyStore: internals.keyStore,
|
|
28
|
+
directAccessMode: internals.directAccessMode,
|
|
29
|
+
loopbackUrl: internals.loopbackUrl,
|
|
30
|
+
collectionId,
|
|
31
|
+
internals,
|
|
32
|
+
timeouts: internals.timeouts,
|
|
33
|
+
onChange: () => this.emitConnection()
|
|
34
|
+
});
|
|
35
|
+
this.files = new MdbaseFileClient(() => this.fileCapability, (method, path, input, signal) => this.transport.files.control(method, path, input, signal), {
|
|
36
|
+
uploadChunk: (session, chunkIndex, bytes, signal) => this.transport.files.uploadChunk(session, chunkIndex, bytes, signal),
|
|
37
|
+
downloadChunk: (session, chunkIndex, signal) => this.transport.files.downloadChunk(session, chunkIndex, signal)
|
|
38
|
+
}, {
|
|
39
|
+
downloadPart: (session, partIndex, expectedLength, signal) => this.transport.files.downloadHostedPart(session, partIndex, expectedLength, signal)
|
|
40
|
+
}, internals.timeouts);
|
|
41
|
+
this.collectionClient = new MdbaseCollectionClient(new CollectionRequestCoordinator({
|
|
42
|
+
operation: (operation, input, requestOptions) => this.transport.performOperation(operation, input, requestOptions)
|
|
43
|
+
}, internals.timeouts.requestMs), internals.timeouts.requestMs);
|
|
44
|
+
this.notifications = new ConnectionNotifications({
|
|
45
|
+
serverUrl: internals.serverUrl,
|
|
46
|
+
storage: internals.storage,
|
|
47
|
+
authorizedToken: (signal) => this.transport.authorizedToken({ signal, timeoutMs: null }),
|
|
48
|
+
register: (signal) => this.internals.register({ signal, timeoutMs: null }),
|
|
49
|
+
notificationKey: (transport) => internals.notificationKey(collectionId, transport),
|
|
50
|
+
requestTimeoutMs: internals.timeouts.requestMs
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
get displayName() {
|
|
54
|
+
return this.transport.currentToken()?.collectionName
|
|
55
|
+
?? `Collection ${this.collectionId.slice(0, 8)}`;
|
|
56
|
+
}
|
|
57
|
+
get operations() {
|
|
58
|
+
return [...(this.transport.currentToken()?.operations ?? [])];
|
|
59
|
+
}
|
|
60
|
+
get scope() {
|
|
61
|
+
return this.transport.currentToken()?.scope ?? null;
|
|
62
|
+
}
|
|
63
|
+
get fileCapability() {
|
|
64
|
+
return this.transport.currentToken()?.fileCapability ?? null;
|
|
65
|
+
}
|
|
66
|
+
get directAccess() {
|
|
67
|
+
return this.transport.directAccess;
|
|
68
|
+
}
|
|
69
|
+
get route() {
|
|
70
|
+
return this.transport.route;
|
|
71
|
+
}
|
|
72
|
+
register(options) {
|
|
73
|
+
return captureConnectOutcome(() => this.internals.register(options), REGISTRATION_PROBLEM_CODES);
|
|
74
|
+
}
|
|
75
|
+
info() {
|
|
76
|
+
const token = this.transport.currentToken();
|
|
77
|
+
return token ? {
|
|
78
|
+
collectionId: token.collectionId,
|
|
79
|
+
displayName: token.collectionName,
|
|
80
|
+
operations: [...token.operations],
|
|
81
|
+
scope: token.scope,
|
|
82
|
+
...(token.fileCapability ? { fileCapability: token.fileCapability } : {}),
|
|
83
|
+
authority: token.authority
|
|
84
|
+
? { kind: "hosted", durability: "provider" }
|
|
85
|
+
: { kind: "connector", durability: "computer" },
|
|
86
|
+
route: this.transport.route,
|
|
87
|
+
directAccess: this.transport.directAccess
|
|
88
|
+
} : null;
|
|
89
|
+
}
|
|
90
|
+
authorizationCapabilities(requiredOperations = DEFAULT_OPERATIONS) {
|
|
91
|
+
const grantedOperations = this.operations;
|
|
92
|
+
const missingOperations = uniqueOperations(requiredOperations)
|
|
93
|
+
.filter((operation) => !grantedOperations.includes(operation));
|
|
94
|
+
return {
|
|
95
|
+
authorized: this.info() !== null,
|
|
96
|
+
sufficient: this.info() !== null && missingOperations.length === 0,
|
|
97
|
+
collectionId: this.collectionId,
|
|
98
|
+
grantedOperations,
|
|
99
|
+
missingOperations
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
hasOperations(requiredOperations) {
|
|
103
|
+
return this.authorizationCapabilities(requiredOperations).sufficient;
|
|
104
|
+
}
|
|
105
|
+
authorize(options = {}) {
|
|
106
|
+
return captureConnectOutcome(() => this.internals.authorize({
|
|
107
|
+
...options,
|
|
108
|
+
target: { kind: "collection", collectionId: this.collectionId }
|
|
109
|
+
}), AUTHORIZATION_PROBLEM_CODES);
|
|
110
|
+
}
|
|
111
|
+
async requestCapabilities(capabilities, options = {}) {
|
|
112
|
+
return captureConnectOutcome(async () => {
|
|
113
|
+
const application = await this.internals.register(options);
|
|
114
|
+
validateAuthorizationSelection(application.requirements, { ...options, capabilities });
|
|
115
|
+
const sufficient = this.info() !== null && capabilities.every((capability) => (capabilityOperationsForContractVersion(semanticContractVersion(application.requirements), capability) ?? [])
|
|
116
|
+
.every((operation) => this.operations.includes(operation)));
|
|
117
|
+
if (sufficient)
|
|
118
|
+
return { kind: "unchanged", connection: this };
|
|
119
|
+
if (semanticContractVersion(application.requirements) === 1) {
|
|
120
|
+
const { capabilities: _selection, ...legacyOptions } = options;
|
|
121
|
+
return this.internals.authorize({
|
|
122
|
+
...legacyOptions,
|
|
123
|
+
target: { kind: "collection", collectionId: this.collectionId },
|
|
124
|
+
operations: [...new Set([...this.operations, ...capabilities.flatMap((id) => capabilityOperationsForContractVersion(1, id) ?? [])])]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const retained = retainedCapabilities(application.requirements, this.operations);
|
|
128
|
+
return this.internals.authorize({
|
|
129
|
+
...options,
|
|
130
|
+
target: { kind: "collection", collectionId: this.collectionId },
|
|
131
|
+
capabilities: [...new Set([...retained, ...capabilities])]
|
|
132
|
+
});
|
|
133
|
+
}, AUTHORIZATION_PROBLEM_CODES);
|
|
134
|
+
}
|
|
135
|
+
async requestOperations(requiredOperations, options = {}) {
|
|
136
|
+
return captureConnectOutcome(async () => {
|
|
137
|
+
const application = await this.internals.register(options);
|
|
138
|
+
validateAuthorizationSelection(application.requirements, { ...options, operations: requiredOperations });
|
|
139
|
+
const authorization = this.authorizationCapabilities(requiredOperations);
|
|
140
|
+
if (authorization.sufficient)
|
|
141
|
+
return { kind: "unchanged", connection: this };
|
|
142
|
+
return this.internals.authorize({
|
|
143
|
+
...options,
|
|
144
|
+
target: { kind: "collection", collectionId: this.collectionId },
|
|
145
|
+
operations: [...new Set([...authorization.grantedOperations, ...requiredOperations])]
|
|
146
|
+
});
|
|
147
|
+
}, AUTHORIZATION_PROBLEM_CODES);
|
|
148
|
+
}
|
|
149
|
+
onConnectionChange(listener) {
|
|
150
|
+
this.connectionListeners.add(listener);
|
|
151
|
+
listener(this.info());
|
|
152
|
+
return () => this.connectionListeners.delete(listener);
|
|
153
|
+
}
|
|
154
|
+
notifyStorageChanged() {
|
|
155
|
+
this.transport.notifyStorageChanged();
|
|
156
|
+
}
|
|
157
|
+
checkDirectAccess(options) {
|
|
158
|
+
return captureConnectOutcome(() => this.transport.checkDirectAccess(options), DIRECT_ACCESS_PROBLEM_CODES);
|
|
159
|
+
}
|
|
160
|
+
/** Call from a user gesture to request browser permission for direct local access. */
|
|
161
|
+
requestDirectAccess(options) {
|
|
162
|
+
return captureConnectOutcome(() => this.transport.requestDirectAccess(options), DIRECT_ACCESS_PROBLEM_CODES);
|
|
163
|
+
}
|
|
164
|
+
disableDirectAccess() {
|
|
165
|
+
this.transport.disableDirectAccess();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Return one offline-replication transport regardless of whether the
|
|
169
|
+
* authority is remote, directly connected, or relay connected.
|
|
170
|
+
* Authority credentials and routing stay private inside the SDK.
|
|
171
|
+
*/
|
|
172
|
+
sync() {
|
|
173
|
+
const token = this.transport.currentToken();
|
|
174
|
+
if (!token)
|
|
175
|
+
return null;
|
|
176
|
+
const collectionId = token.collectionId;
|
|
177
|
+
if (!token.authority) {
|
|
178
|
+
if (!token.grantId || !token.operations.includes("sync"))
|
|
179
|
+
return null;
|
|
180
|
+
const replicaId = token.grantId;
|
|
181
|
+
return {
|
|
182
|
+
collectionId,
|
|
183
|
+
replicaId,
|
|
184
|
+
transport: {
|
|
185
|
+
openSession: (options) => this.transport.performOperation("sync", { action: "open_session" }, syncRequestOptions(options, this.internals.timeouts.syncMs)),
|
|
186
|
+
snapshot: (snapshotId, page, options) => this.transport.performOperation("sync", {
|
|
187
|
+
action: "snapshot",
|
|
188
|
+
snapshot_id: snapshotId,
|
|
189
|
+
...(page ? { page } : {})
|
|
190
|
+
}, syncRequestOptions(options, this.internals.timeouts.syncMs)),
|
|
191
|
+
changes: (after, limit = 200, options) => this.transport.performOperation("sync", {
|
|
192
|
+
action: "changes",
|
|
193
|
+
after,
|
|
194
|
+
limit
|
|
195
|
+
}, syncRequestOptions(options, this.internals.timeouts.syncMs)),
|
|
196
|
+
mutate: (mutation, options) => this.transport.performOperation("sync", {
|
|
197
|
+
action: "mutate",
|
|
198
|
+
mutation
|
|
199
|
+
}, syncRequestOptions(options, this.internals.timeouts.syncMs))
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const replicaId = token.authority.replicaId;
|
|
204
|
+
return {
|
|
205
|
+
collectionId,
|
|
206
|
+
replicaId,
|
|
207
|
+
transport: {
|
|
208
|
+
openSession: (options) => this.transport.performAuthoritySyncRequest(collectionId, replicaId, "POST", "sessions", undefined, options),
|
|
209
|
+
snapshot: (snapshotId, page, options) => {
|
|
210
|
+
const query = new URLSearchParams({ snapshot_id: snapshotId });
|
|
211
|
+
if (page)
|
|
212
|
+
query.set("page", page);
|
|
213
|
+
return this.transport.performAuthoritySyncRequest(collectionId, replicaId, "GET", `snapshot?${query}`, undefined, options);
|
|
214
|
+
},
|
|
215
|
+
changes: (after, limit = 200, options) => this.transport.performAuthoritySyncRequest(collectionId, replicaId, "GET", `changes?${new URLSearchParams({ after: String(after), limit: String(limit) })}`, undefined, options),
|
|
216
|
+
mutate: (mutation, options) => this.transport.performAuthoritySyncRequest(collectionId, replicaId, "POST", "mutations", mutation, options)
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Register this browser installation for manifest-declared Web Push.
|
|
222
|
+
*
|
|
223
|
+
* Criteria are evaluated by the collection authority. Push payloads contain
|
|
224
|
+
* only an opaque cursor and static presentation copy.
|
|
225
|
+
*/
|
|
226
|
+
registerNotifications(options) {
|
|
227
|
+
return captureConnectOutcome(() => this.notifications.registerNotifications(options), NOTIFICATION_PROBLEM_CODES);
|
|
228
|
+
}
|
|
229
|
+
registerNativeNotifications(options) {
|
|
230
|
+
return captureConnectOutcome(() => this.notifications.registerNativeNotifications(options), NOTIFICATION_PROBLEM_CODES);
|
|
231
|
+
}
|
|
232
|
+
unregisterNativeNotifications(options) {
|
|
233
|
+
return captureConnectOutcome(() => this.notifications.unregisterNativeNotifications(options), NOTIFICATION_PROBLEM_CODES);
|
|
234
|
+
}
|
|
235
|
+
unregisterNotifications(serviceWorker, options) {
|
|
236
|
+
return captureConnectOutcome(() => this.notifications.unregisterNotifications(serviceWorker, options), NOTIFICATION_PROBLEM_CODES);
|
|
237
|
+
}
|
|
238
|
+
forget() {
|
|
239
|
+
const token = this.transport.currentToken();
|
|
240
|
+
this.internals.removeToken(this.collectionId, token?.keyHandle, "not_authorized", true);
|
|
241
|
+
this.transport.notifyStorageChanged();
|
|
242
|
+
}
|
|
243
|
+
describe(options) {
|
|
244
|
+
return this.collectionClient.describe(options);
|
|
245
|
+
}
|
|
246
|
+
changes(input = {}, options) {
|
|
247
|
+
return this.collectionClient.changes(input, options);
|
|
248
|
+
}
|
|
249
|
+
read(input, options) {
|
|
250
|
+
return this.collectionClient.read(input, options);
|
|
251
|
+
}
|
|
252
|
+
query(input = {}, options) {
|
|
253
|
+
return this.collectionClient.query(input, options);
|
|
254
|
+
}
|
|
255
|
+
queryPages(input = {}, options = {}) {
|
|
256
|
+
return this.collectionClient.queryPages(input, options);
|
|
257
|
+
}
|
|
258
|
+
queryAll(input = {}, options = {}) {
|
|
259
|
+
return this.collectionClient.queryAll(input, options);
|
|
260
|
+
}
|
|
261
|
+
listViews(options) {
|
|
262
|
+
return this.collectionClient.listViews(options);
|
|
263
|
+
}
|
|
264
|
+
executeView(input, options) {
|
|
265
|
+
return this.collectionClient.executeView(input, options);
|
|
266
|
+
}
|
|
267
|
+
executeViewPages(input, options = {}) {
|
|
268
|
+
return this.collectionClient.executeViewPages(input, options);
|
|
269
|
+
}
|
|
270
|
+
readViewSource(input, options) {
|
|
271
|
+
return this.collectionClient.readViewSource(input, options);
|
|
272
|
+
}
|
|
273
|
+
createViewSource(input, options) {
|
|
274
|
+
return this.collectionClient.createViewSource(input, options);
|
|
275
|
+
}
|
|
276
|
+
updateViewSource(input, options) {
|
|
277
|
+
return this.collectionClient.updateViewSource(input, options);
|
|
278
|
+
}
|
|
279
|
+
deleteViewSource(input, options) {
|
|
280
|
+
return this.collectionClient.deleteViewSource(input, options);
|
|
281
|
+
}
|
|
282
|
+
create(input, options) {
|
|
283
|
+
return this.collectionClient.create(input, options);
|
|
284
|
+
}
|
|
285
|
+
update(input, options) {
|
|
286
|
+
return this.collectionClient.update(input, options);
|
|
287
|
+
}
|
|
288
|
+
delete(input, options) {
|
|
289
|
+
return this.collectionClient.delete(input, options);
|
|
290
|
+
}
|
|
291
|
+
preflightDelete(input, options) {
|
|
292
|
+
return this.collectionClient.preflightDelete(input, options);
|
|
293
|
+
}
|
|
294
|
+
rename(input, options) {
|
|
295
|
+
return this.collectionClient.rename(input, options);
|
|
296
|
+
}
|
|
297
|
+
preflightRename(input, options) {
|
|
298
|
+
return this.collectionClient.preflightRename(input, options);
|
|
299
|
+
}
|
|
300
|
+
async renameWithProgress(input, options = {}) {
|
|
301
|
+
return withCooperativeRequestBudget(options, this.internals.timeouts.requestMs, async (budget) => {
|
|
302
|
+
const started = Date.now();
|
|
303
|
+
const resumed = false;
|
|
304
|
+
let estimate;
|
|
305
|
+
const emit = (state, cancellable, completedUnits = 0) => {
|
|
306
|
+
options.onProgress?.({
|
|
307
|
+
operation: "rename",
|
|
308
|
+
state,
|
|
309
|
+
elapsedMs: Date.now() - started,
|
|
310
|
+
cancellable,
|
|
311
|
+
resumed,
|
|
312
|
+
completedUnits,
|
|
313
|
+
...(estimate ? { estimate } : {})
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
try {
|
|
317
|
+
throwIfCancelled(budget.signal);
|
|
318
|
+
emit("preflighting", true);
|
|
319
|
+
const previewOutcome = options.preflight
|
|
320
|
+
? connectSuccess(options.preflight)
|
|
321
|
+
: await this.preflightRename(input, { signal: budget.signal, timeoutMs: null });
|
|
322
|
+
if (!previewOutcome.ok)
|
|
323
|
+
return previewOutcome;
|
|
324
|
+
const preview = previewOutcome.value;
|
|
325
|
+
try {
|
|
326
|
+
assertRenamePreview(input, preview);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error instanceof MdbaseConnectError) {
|
|
330
|
+
return connectFailure(error.problem);
|
|
331
|
+
}
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
estimate = renameEstimate(input, preview);
|
|
335
|
+
emit("ready", true);
|
|
336
|
+
throwIfCancelled(budget.signal);
|
|
337
|
+
const cancellable = this.transport.hasResumableMutationTransport();
|
|
338
|
+
emit("applying", cancellable);
|
|
339
|
+
const result = await this.rename(input, cancellable
|
|
340
|
+
? { signal: budget.signal, timeoutMs: null }
|
|
341
|
+
: { timeoutMs: remainingTimeout(budget.remainingMs()) });
|
|
342
|
+
if (result.ok)
|
|
343
|
+
emit("completed", false, estimate.totalUnits);
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (isCancellation(error, budget.signal))
|
|
348
|
+
emit("cancelled", false);
|
|
349
|
+
if (error instanceof MdbaseConnectError) {
|
|
350
|
+
return connectFailure(error.problem);
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async deleteWithProgress(input, options = {}) {
|
|
357
|
+
return withCooperativeRequestBudget(options, this.internals.timeouts.requestMs, async (budget) => {
|
|
358
|
+
const started = Date.now();
|
|
359
|
+
const resumed = false;
|
|
360
|
+
let estimate;
|
|
361
|
+
const emit = (state, cancellable, completedUnits = 0) => {
|
|
362
|
+
options.onProgress?.({
|
|
363
|
+
operation: "delete",
|
|
364
|
+
state,
|
|
365
|
+
elapsedMs: Date.now() - started,
|
|
366
|
+
cancellable,
|
|
367
|
+
resumed,
|
|
368
|
+
completedUnits,
|
|
369
|
+
...(estimate ? { estimate } : {})
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
try {
|
|
373
|
+
throwIfCancelled(budget.signal);
|
|
374
|
+
emit("preflighting", true);
|
|
375
|
+
const previewOutcome = options.preflight
|
|
376
|
+
? connectSuccess(options.preflight)
|
|
377
|
+
: await this.preflightDelete(input, { signal: budget.signal, timeoutMs: null });
|
|
378
|
+
if (!previewOutcome.ok)
|
|
379
|
+
return previewOutcome;
|
|
380
|
+
const preview = previewOutcome.value;
|
|
381
|
+
try {
|
|
382
|
+
assertDeletePreview(input, preview);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
if (error instanceof MdbaseConnectError) {
|
|
386
|
+
return connectFailure(error.problem);
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
estimate = deleteEstimate(preview);
|
|
391
|
+
emit("ready", true);
|
|
392
|
+
throwIfCancelled(budget.signal);
|
|
393
|
+
const cancellable = this.transport.hasResumableMutationTransport();
|
|
394
|
+
emit("applying", cancellable);
|
|
395
|
+
const result = await this.delete(input, cancellable
|
|
396
|
+
? { signal: budget.signal, timeoutMs: null }
|
|
397
|
+
: { timeoutMs: remainingTimeout(budget.remainingMs()) });
|
|
398
|
+
if (result.ok)
|
|
399
|
+
emit("completed", false, estimate.totalUnits);
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
if (isCancellation(error, budget.signal))
|
|
404
|
+
emit("cancelled", false);
|
|
405
|
+
if (error instanceof MdbaseConnectError) {
|
|
406
|
+
return connectFailure(error.problem);
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
pendingMutations() {
|
|
413
|
+
return this.transport.pendingMutations().map((summary) => ({
|
|
414
|
+
...summary,
|
|
415
|
+
recover: (options) => captureConnectOutcome(() => this.recoverPendingMutation(summary.requestId, options), COLLECTION_MUTATION_PROBLEM_CODES)
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
pendingMutation(requestId) {
|
|
419
|
+
const summary = this.transport.pendingMutation(requestId);
|
|
420
|
+
if (!summary)
|
|
421
|
+
return null;
|
|
422
|
+
return {
|
|
423
|
+
...summary,
|
|
424
|
+
recover: (options) => captureConnectOutcome(() => this.recoverPendingMutation(requestId, options), COLLECTION_MUTATION_PROBLEM_CODES)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async recoverPendingMutation(requestId, options) {
|
|
428
|
+
const result = await this.transport.recoverPendingMutation(requestId, options);
|
|
429
|
+
if (result && typeof result === "object" && "valid" in result) {
|
|
430
|
+
const envelope = result;
|
|
431
|
+
if (!envelope.valid)
|
|
432
|
+
throw new MdbaseConnectError(operationProblem(envelope));
|
|
433
|
+
return envelope.result;
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
validate(input = {}, options) {
|
|
438
|
+
return this.collectionClient.validate(input, options);
|
|
439
|
+
}
|
|
440
|
+
readType(input, options) {
|
|
441
|
+
return this.collectionClient.readType(input, options);
|
|
442
|
+
}
|
|
443
|
+
createType(input, options) {
|
|
444
|
+
return this.collectionClient.createType(input, options);
|
|
445
|
+
}
|
|
446
|
+
updateType(input, options) {
|
|
447
|
+
return this.collectionClient.updateType(input, options);
|
|
448
|
+
}
|
|
449
|
+
assessTypePack(input, options) {
|
|
450
|
+
return this.collectionClient.assessTypePack(input, options);
|
|
451
|
+
}
|
|
452
|
+
applyTypePack(input, options) {
|
|
453
|
+
return this.collectionClient.applyTypePack(input, options);
|
|
454
|
+
}
|
|
455
|
+
assessCollectionSetup(input, options) {
|
|
456
|
+
return this.collectionClient.assessCollectionSetup(input, options);
|
|
457
|
+
}
|
|
458
|
+
applyCollectionSetup(input, options) {
|
|
459
|
+
return this.collectionClient.applyCollectionSetup(input, options);
|
|
460
|
+
}
|
|
461
|
+
listTimers(namespace, options) {
|
|
462
|
+
return this.collectionClient.listTimers(namespace, options);
|
|
463
|
+
}
|
|
464
|
+
putTimer(input, options) {
|
|
465
|
+
return this.collectionClient.putTimer(input, options);
|
|
466
|
+
}
|
|
467
|
+
cancelTimer(input, options) {
|
|
468
|
+
return this.collectionClient.cancelTimer(input, options);
|
|
469
|
+
}
|
|
470
|
+
reconcileTimers(input, options) {
|
|
471
|
+
return this.collectionClient.reconcileTimers(input, options);
|
|
472
|
+
}
|
|
473
|
+
watch(input = {}, options = {}) {
|
|
474
|
+
return captureConnectOutcome(() => withRequestBudget(options, this.internals.timeouts.watchStartMs, async (budget) => {
|
|
475
|
+
const initial = await this.collectionClient.changes(input.cursor === undefined ? {} : { after: input.cursor, limit: 200 }, { signal: budget.signal, timeoutMs: null });
|
|
476
|
+
if (!initial.ok)
|
|
477
|
+
throw new MdbaseConnectError(initial.problem);
|
|
478
|
+
if (initial.value.reset) {
|
|
479
|
+
throw new MdbaseConnectError(connectProblem("change_cursor_reset", "The collection change cursor expired. Refresh collection state before subscribing again."));
|
|
480
|
+
}
|
|
481
|
+
return new CollectionWatchSubscription(this.collectionClient, initial.value.cursor, input, input.cursor === undefined ? [] : initial.value.events, this.internals.timeouts.watchStartMs);
|
|
482
|
+
}), COLLECTION_CHANGES_PROBLEM_CODES);
|
|
483
|
+
}
|
|
484
|
+
emitConnection() {
|
|
485
|
+
const connection = this.info();
|
|
486
|
+
for (const listener of this.connectionListeners)
|
|
487
|
+
listener(connection);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
class CollectionWatchSubscription {
|
|
491
|
+
client;
|
|
492
|
+
input;
|
|
493
|
+
watchStartTimeoutMs;
|
|
494
|
+
changes = new Set();
|
|
495
|
+
statuses = new Set();
|
|
496
|
+
problems = new Set();
|
|
497
|
+
controller = new AbortController();
|
|
498
|
+
removeLifetimeAbort;
|
|
499
|
+
currentStatus;
|
|
500
|
+
currentProblem = null;
|
|
501
|
+
pendingChanges;
|
|
502
|
+
constructor(client, cursor, input, pendingChanges, watchStartTimeoutMs) {
|
|
503
|
+
this.client = client;
|
|
504
|
+
this.input = input;
|
|
505
|
+
this.watchStartTimeoutMs = watchStartTimeoutMs;
|
|
506
|
+
this.pendingChanges = [...pendingChanges];
|
|
507
|
+
this.currentStatus = { state: "connected", cursor, recovered: false };
|
|
508
|
+
const lifetimeSignal = input.lifetimeSignal;
|
|
509
|
+
if (lifetimeSignal?.aborted)
|
|
510
|
+
this.close();
|
|
511
|
+
else if (lifetimeSignal) {
|
|
512
|
+
const close = () => this.close();
|
|
513
|
+
lifetimeSignal.addEventListener("abort", close, { once: true });
|
|
514
|
+
this.removeLifetimeAbort = () => lifetimeSignal.removeEventListener("abort", close);
|
|
515
|
+
}
|
|
516
|
+
if (!this.controller.signal.aborted)
|
|
517
|
+
void this.run(cursor);
|
|
518
|
+
}
|
|
519
|
+
get status() {
|
|
520
|
+
return this.currentStatus;
|
|
521
|
+
}
|
|
522
|
+
get problem() {
|
|
523
|
+
return this.currentProblem;
|
|
524
|
+
}
|
|
525
|
+
subscribe(listener, onStatus, onProblem) {
|
|
526
|
+
this.changes.add(listener);
|
|
527
|
+
if (onStatus) {
|
|
528
|
+
this.statuses.add(onStatus);
|
|
529
|
+
onStatus(this.currentStatus);
|
|
530
|
+
}
|
|
531
|
+
if (onProblem) {
|
|
532
|
+
this.problems.add(onProblem);
|
|
533
|
+
if (this.currentProblem)
|
|
534
|
+
onProblem(this.currentProblem);
|
|
535
|
+
}
|
|
536
|
+
for (const change of this.pendingChanges)
|
|
537
|
+
listener(change);
|
|
538
|
+
this.pendingChanges = [];
|
|
539
|
+
return () => {
|
|
540
|
+
this.changes.delete(listener);
|
|
541
|
+
if (onStatus)
|
|
542
|
+
this.statuses.delete(onStatus);
|
|
543
|
+
if (onProblem)
|
|
544
|
+
this.problems.delete(onProblem);
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
close() {
|
|
548
|
+
if (this.controller.signal.aborted)
|
|
549
|
+
return;
|
|
550
|
+
this.controller.abort();
|
|
551
|
+
this.removeLifetimeAbort?.();
|
|
552
|
+
this.removeLifetimeAbort = undefined;
|
|
553
|
+
const cursor = "cursor" in this.currentStatus ? this.currentStatus.cursor : undefined;
|
|
554
|
+
this.publishStatus({ state: "closed", ...(cursor === undefined ? {} : { cursor }) });
|
|
555
|
+
}
|
|
556
|
+
async run(cursor) {
|
|
557
|
+
let firstStatus = true;
|
|
558
|
+
const iterator = this.client.watch({
|
|
559
|
+
cursor,
|
|
560
|
+
pollIntervalMs: this.input.pollIntervalMs,
|
|
561
|
+
retry: this.input.retry,
|
|
562
|
+
signal: this.controller.signal,
|
|
563
|
+
timeoutMs: this.watchStartTimeoutMs,
|
|
564
|
+
onStatus: (status) => {
|
|
565
|
+
if (firstStatus && status.state === "connecting") {
|
|
566
|
+
firstStatus = false;
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
firstStatus = false;
|
|
570
|
+
this.publishStatus(status);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
try {
|
|
574
|
+
for await (const outcome of iterator) {
|
|
575
|
+
if (this.controller.signal.aborted)
|
|
576
|
+
return;
|
|
577
|
+
if (!outcome.ok) {
|
|
578
|
+
this.currentProblem = outcome.problem;
|
|
579
|
+
for (const listener of this.problems)
|
|
580
|
+
listener(outcome.problem);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
for (const listener of this.changes)
|
|
584
|
+
listener(outcome.value);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
if (!this.controller.signal.aborted)
|
|
589
|
+
this.close();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
publishStatus(status) {
|
|
593
|
+
this.currentStatus = status;
|
|
594
|
+
for (const listener of this.statuses)
|
|
595
|
+
listener(status);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function syncRequestOptions(options, defaultTimeoutMs) {
|
|
599
|
+
return {
|
|
600
|
+
...options,
|
|
601
|
+
timeoutMs: options?.timeoutMs === undefined ? defaultTimeoutMs : options.timeoutMs
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function remainingTimeout(remainingMs) {
|
|
605
|
+
return remainingMs === null ? null : Math.max(1, Math.ceil(remainingMs));
|
|
606
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Opt-in cryptographic storage and transport primitives. */
|
|
2
|
+
export { IndexedDbGrantKeyStore, MemoryGrantKeyStore, RelayCryptoError, signAuthorityRequest, authorityProofMessage, encryptRelayRequest, decryptRelayResponse, deriveP256SharedSecret, validateGrantEncryption } from "./crypto.js";
|
|
3
|
+
export type { GrantKeyRecord, GrantKeyStore, RelayBinding, AuthorityProofInput } from "./crypto.js";
|
|
4
|
+
export { FileTransferCryptoError, GrantFileTransferCipher, deriveFileTransferKey } from "./file-crypto.js";
|
|
5
|
+
export type { FileTransferBinding } from "./file-crypto.js";
|
|
6
|
+
export { IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, ApplicationIdentityStoreError, applicationInstallationId, signApplicationAuthorization, applicationIdentity } from "./application-identity.js";
|
|
7
|
+
export type { ApplicationIdentity, ApplicationIdentityStore } from "./application-identity.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Opt-in cryptographic storage and transport primitives. */
|
|
2
|
+
export { IndexedDbGrantKeyStore, MemoryGrantKeyStore, RelayCryptoError, signAuthorityRequest, authorityProofMessage, encryptRelayRequest, decryptRelayResponse, deriveP256SharedSecret, validateGrantEncryption } from "./crypto.js";
|
|
3
|
+
export { FileTransferCryptoError, GrantFileTransferCipher, deriveFileTransferKey } from "./file-crypto.js";
|
|
4
|
+
export { IndexedDbApplicationIdentityStore, MemoryApplicationIdentityStore, ApplicationIdentityStoreError, applicationInstallationId, signApplicationAuthorization, applicationIdentity } from "./application-identity.js";
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ConnectProblem, type EncryptedRelayOperation, type EncryptedRelayOperationRequest, type EncryptedRelayOperationResponse, type GrantEncryption } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
export interface GrantKeyRecord {
|
|
3
|
+
handle: string;
|
|
4
|
+
agreementPrivateKey: CryptoKey;
|
|
5
|
+
agreementPublicKey: string;
|
|
6
|
+
signingPrivateKey: CryptoKey;
|
|
7
|
+
signingPublicKey: string;
|
|
8
|
+
}
|
|
9
|
+
export interface GrantKeyStore {
|
|
10
|
+
create(handle: string): Promise<GrantKeyRecord>;
|
|
11
|
+
get(handle: string): Promise<GrantKeyRecord | null>;
|
|
12
|
+
nextCounter(handle: string): Promise<string>;
|
|
13
|
+
delete(handle: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** Origin-scoped storage for non-extractable grant keys and atomic counters. */
|
|
16
|
+
export declare class IndexedDbGrantKeyStore implements GrantKeyStore {
|
|
17
|
+
private database;
|
|
18
|
+
create(handle: string): Promise<GrantKeyRecord>;
|
|
19
|
+
get(handle: string): Promise<GrantKeyRecord | null>;
|
|
20
|
+
nextCounter(handle: string): Promise<string>;
|
|
21
|
+
delete(handle: string): Promise<void>;
|
|
22
|
+
private open;
|
|
23
|
+
}
|
|
24
|
+
/** Deterministic process-local storage for tests, CLIs, and non-browser adapters. */
|
|
25
|
+
export declare class MemoryGrantKeyStore implements GrantKeyStore {
|
|
26
|
+
private readonly records;
|
|
27
|
+
private sequence;
|
|
28
|
+
create(handle: string): Promise<GrantKeyRecord>;
|
|
29
|
+
get(handle: string): Promise<GrantKeyRecord | null>;
|
|
30
|
+
nextCounter(handle: string): Promise<string>;
|
|
31
|
+
delete(handle: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export interface RelayBinding {
|
|
34
|
+
grantId: string;
|
|
35
|
+
applicationId: string;
|
|
36
|
+
encryption: GrantEncryption;
|
|
37
|
+
}
|
|
38
|
+
export interface AuthorityProofInput {
|
|
39
|
+
method: string;
|
|
40
|
+
target: string;
|
|
41
|
+
body?: string;
|
|
42
|
+
credential: string;
|
|
43
|
+
timestamp?: number;
|
|
44
|
+
nonce?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Sign one remote-authority or token-refresh request with the approved grant key. */
|
|
47
|
+
export declare function signAuthorityRequest(store: GrantKeyStore, handle: string, expectedPublicKey: string, input: AuthorityProofInput): Promise<Record<string, string>>;
|
|
48
|
+
export declare function authorityProofMessage(input: Required<Pick<AuthorityProofInput, "method" | "target" | "credential" | "timestamp" | "nonce">> & Pick<AuthorityProofInput, "body">): Promise<string>;
|
|
49
|
+
export declare function encryptRelayRequest(store: GrantKeyStore, handle: string, binding: RelayBinding, operation: EncryptedRelayOperation, input: unknown, requestId?: string, deadlineUnixMs?: number): Promise<EncryptedRelayOperationRequest>;
|
|
50
|
+
export declare function decryptRelayResponse<Result>(store: GrantKeyStore, handle: string, binding: RelayBinding, request: EncryptedRelayOperationRequest, response: EncryptedRelayOperationResponse): Promise<{
|
|
51
|
+
ok: true;
|
|
52
|
+
result: Result;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
problem: ConnectProblem;
|
|
56
|
+
}>;
|
|
57
|
+
export declare class RelayCryptoError extends Error {
|
|
58
|
+
readonly code: string;
|
|
59
|
+
constructor(code: string, message: string);
|
|
60
|
+
}
|
|
61
|
+
/** @internal Shared ECDH primitive used by the independently keyed file data plane. */
|
|
62
|
+
export declare function deriveP256SharedSecret(privateKey: CryptoKey, peerPublicKey: string): Promise<ArrayBuffer>;
|
|
63
|
+
export declare function validateGrantEncryption(encryption: GrantEncryption): void;
|