@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,368 @@
|
|
|
1
|
+
import { validateGrantEncryption } from "./crypto.js";
|
|
2
|
+
import { connectError, serverConnectError } from "./errors.js";
|
|
3
|
+
import { canonicalJson } from "./operation-helpers.js";
|
|
4
|
+
import { bytesToBase64Url, randomBase64Url } from "./base64.js";
|
|
5
|
+
/** Resolve bundled declarations and their stable storage identity without loading URLs. */
|
|
6
|
+
export function resolveManifestSource(input, redirect) {
|
|
7
|
+
const manifest = input instanceof URL ? input.href : input ?? defaultManifestSource();
|
|
8
|
+
const manifestSource = typeof manifest === "string"
|
|
9
|
+
? manifest
|
|
10
|
+
: manifest.distribution === "portable"
|
|
11
|
+
? `bundle:${manifest.id}:${manifestStorageFingerprint(manifest)}`
|
|
12
|
+
: `bundle:${manifest.id}`;
|
|
13
|
+
const redirectUri = redirect === undefined ? (typeof manifest !== "string" && manifest.distribution === "portable"
|
|
14
|
+
? ""
|
|
15
|
+
: defaultRedirectUri()) : String(redirect);
|
|
16
|
+
return { manifest, manifestSource, redirectUri };
|
|
17
|
+
}
|
|
18
|
+
/** Normalize fetch rejection at the exact I/O boundary. */
|
|
19
|
+
export async function connectFetch(input, init, code, message) {
|
|
20
|
+
try {
|
|
21
|
+
return await fetch(input, init);
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
throw connectError(code, message, { cause });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Decode JSON without allowing native syntax/body errors across the SDK boundary. */
|
|
28
|
+
export async function decodeJsonResponse(response, code, message) {
|
|
29
|
+
let text;
|
|
30
|
+
try {
|
|
31
|
+
text = await response.text();
|
|
32
|
+
}
|
|
33
|
+
catch (cause) {
|
|
34
|
+
throw serverConnectError(code, message, { status: response.status, cause });
|
|
35
|
+
}
|
|
36
|
+
if (text.trim() === "") {
|
|
37
|
+
throw serverConnectError(code, message, { status: response.status });
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(text);
|
|
41
|
+
}
|
|
42
|
+
catch (cause) {
|
|
43
|
+
throw serverConnectError(code, message, { status: response.status, cause });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function canonicalLoopbackUrl(value) {
|
|
47
|
+
const url = new URL(value);
|
|
48
|
+
if (url.protocol !== "http:"
|
|
49
|
+
|| !["127.0.0.1", "[::1]"].includes(url.hostname)
|
|
50
|
+
|| url.username
|
|
51
|
+
|| url.password
|
|
52
|
+
|| url.pathname !== "/"
|
|
53
|
+
|| url.search
|
|
54
|
+
|| url.hash) {
|
|
55
|
+
throw connectError("invalid_loopback_url", "loopbackUrl must be an HTTP origin on 127.0.0.1 or ::1.");
|
|
56
|
+
}
|
|
57
|
+
return url.origin;
|
|
58
|
+
}
|
|
59
|
+
export async function createPkce() {
|
|
60
|
+
const verifier = randomBase64Url(32);
|
|
61
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
62
|
+
return { verifier, challenge: bytesToBase64Url(new Uint8Array(digest)) };
|
|
63
|
+
}
|
|
64
|
+
export function apiError(body, fallbackCode, fallbackMessage, status) {
|
|
65
|
+
return serverConnectError(oauthErrorCode(body) ?? body?.error?.code ?? fallbackCode, body?.error_description ?? body?.error?.message ?? fallbackMessage, {
|
|
66
|
+
status,
|
|
67
|
+
details: body?.error?.details,
|
|
68
|
+
operationOutcome: body?.error?.operation_outcome,
|
|
69
|
+
traceId: body?.error?.trace_id
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function oauthErrorCode(body) {
|
|
73
|
+
return typeof body?.error === "string" ? body.error : undefined;
|
|
74
|
+
}
|
|
75
|
+
export function validAuthorityTokenResponse(value, collectionId) {
|
|
76
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
77
|
+
return false;
|
|
78
|
+
if (typeof collectionId !== "string")
|
|
79
|
+
return false;
|
|
80
|
+
const authority = value;
|
|
81
|
+
if (typeof authority.operations_url !== "string"
|
|
82
|
+
|| typeof authority.sync_url !== "string"
|
|
83
|
+
|| typeof authority.files_url !== "string"
|
|
84
|
+
|| typeof authority.replica_id !== "string"
|
|
85
|
+
|| authority.replica_id.length === 0
|
|
86
|
+
|| typeof authority.access_token !== "string"
|
|
87
|
+
|| authority.access_token.length === 0
|
|
88
|
+
|| (authority.proof_public_key !== undefined
|
|
89
|
+
&& (typeof authority.proof_public_key !== "string"
|
|
90
|
+
|| authority.proof_public_key.length === 0)))
|
|
91
|
+
return false;
|
|
92
|
+
try {
|
|
93
|
+
const operations = new URL(authority.operations_url);
|
|
94
|
+
const sync = new URL(authority.sync_url);
|
|
95
|
+
const files = new URL(authority.files_url);
|
|
96
|
+
return [operations, sync, files].every((url) => (url.protocol === "https:"
|
|
97
|
+
|| (url.protocol === "http:"
|
|
98
|
+
&& ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname)))
|
|
99
|
+
&& !url.username
|
|
100
|
+
&& !url.password
|
|
101
|
+
&& !url.search
|
|
102
|
+
&& !url.hash)
|
|
103
|
+
&& /^\/v1\/authorities\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/operations$/i.test(operations.pathname)
|
|
104
|
+
&& /^\/v1\/authorities\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/sync$/i.test(sync.pathname)
|
|
105
|
+
&& /^\/v1\/authorities\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/files$/i.test(files.pathname)
|
|
106
|
+
&& operations.origin === sync.origin
|
|
107
|
+
&& operations.origin === files.origin
|
|
108
|
+
&& operations.pathname.split("/")[3] === collectionId
|
|
109
|
+
&& sync.pathname.split("/")[3] === collectionId
|
|
110
|
+
&& files.pathname.split("/")[3] === collectionId;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function validStoredAuthority(authority, collectionId) {
|
|
117
|
+
return validAuthorityTokenResponse({
|
|
118
|
+
operations_url: authority?.operationsUrl,
|
|
119
|
+
sync_url: authority?.syncUrl,
|
|
120
|
+
files_url: authority?.filesUrl,
|
|
121
|
+
replica_id: authority?.replicaId,
|
|
122
|
+
access_token: authority?.accessToken,
|
|
123
|
+
proof_public_key: authority?.proofPublicKey
|
|
124
|
+
}, collectionId);
|
|
125
|
+
}
|
|
126
|
+
export function validFileCapability(value) {
|
|
127
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
128
|
+
return false;
|
|
129
|
+
const capability = value;
|
|
130
|
+
const actions = capability.actions;
|
|
131
|
+
if (capability.kind !== "files"
|
|
132
|
+
|| capability.protocol_version !== 1
|
|
133
|
+
|| !Array.isArray(actions)
|
|
134
|
+
|| actions.length === 0
|
|
135
|
+
|| actions.length > 6
|
|
136
|
+
|| new Set(actions).size !== actions.length
|
|
137
|
+
|| actions.some((action) => ![
|
|
138
|
+
"list",
|
|
139
|
+
"read",
|
|
140
|
+
"add",
|
|
141
|
+
"replace",
|
|
142
|
+
"move",
|
|
143
|
+
"delete"
|
|
144
|
+
].includes(action)))
|
|
145
|
+
return false;
|
|
146
|
+
const scope = capability.scope;
|
|
147
|
+
if (!scope || typeof scope !== "object" || Array.isArray(scope))
|
|
148
|
+
return false;
|
|
149
|
+
if (scope.kind === "collection") {
|
|
150
|
+
return Object.keys(scope).length === 1;
|
|
151
|
+
}
|
|
152
|
+
return scope.kind === "selected_folders"
|
|
153
|
+
&& Object.keys(scope).length === 2
|
|
154
|
+
&& Array.isArray(scope.folders)
|
|
155
|
+
&& scope.folders.length > 0
|
|
156
|
+
&& scope.folders.length <= 100
|
|
157
|
+
&& new Set(scope.folders).size === scope.folders.length
|
|
158
|
+
&& scope.folders.every((folder) => typeof folder === "string"
|
|
159
|
+
&& folder.length > 0
|
|
160
|
+
&& folder.length <= 1_024
|
|
161
|
+
&& validFileFolder(folder));
|
|
162
|
+
}
|
|
163
|
+
function validFileFolder(folder) {
|
|
164
|
+
const reserved = new Set([
|
|
165
|
+
".mdbase",
|
|
166
|
+
".git",
|
|
167
|
+
"node_modules",
|
|
168
|
+
"_contracts",
|
|
169
|
+
"_schemas",
|
|
170
|
+
"_types",
|
|
171
|
+
"_views"
|
|
172
|
+
]);
|
|
173
|
+
return !folder.startsWith("/")
|
|
174
|
+
&& !folder.endsWith("/")
|
|
175
|
+
&& !folder.includes("\\")
|
|
176
|
+
&& folder.split("/").every((component) => {
|
|
177
|
+
const stem = component.split(".")[0]?.toUpperCase() ?? "";
|
|
178
|
+
return component !== ""
|
|
179
|
+
&& component !== "."
|
|
180
|
+
&& component !== ".."
|
|
181
|
+
&& !component.startsWith(".")
|
|
182
|
+
&& !component.endsWith(" ")
|
|
183
|
+
&& !component.endsWith(".")
|
|
184
|
+
&& !/[<>:"|?*]/.test(component)
|
|
185
|
+
&& !reserved.has(component.toLowerCase())
|
|
186
|
+
&& !["CON", "PRN", "AUX", "NUL"].includes(stem)
|
|
187
|
+
&& !/^(COM|LPT)[1-9]$/.test(stem);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
export function validStoredEncryption(encryption, collectionId) {
|
|
191
|
+
if (!encryption || typeof encryption !== "object" || Array.isArray(encryption)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (encryption.collection_id !== collectionId)
|
|
195
|
+
return false;
|
|
196
|
+
try {
|
|
197
|
+
validateGrantEncryption(encryption);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export function parseDeviceAuthorization(body) {
|
|
205
|
+
if (typeof body?.device_code !== "string"
|
|
206
|
+
|| typeof body?.user_code !== "string"
|
|
207
|
+
|| typeof body?.verification_uri !== "string"
|
|
208
|
+
|| typeof body?.verification_uri_complete !== "string"
|
|
209
|
+
|| !Number.isFinite(body?.expires_in)
|
|
210
|
+
|| !Number.isFinite(body?.interval)) {
|
|
211
|
+
throw connectError("invalid_device_authorization_response", "Connect returned an invalid downloaded application authorization response.");
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
userCode: body.user_code,
|
|
215
|
+
verificationUri: body.verification_uri,
|
|
216
|
+
verificationUriComplete: body.verification_uri_complete,
|
|
217
|
+
expiresAt: Date.now() + Math.max(1, body.expires_in) * 1_000,
|
|
218
|
+
intervalSeconds: Math.max(1, body.interval)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function parseStored(value) {
|
|
222
|
+
if (!value)
|
|
223
|
+
return null;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(value);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Parse both canonical and N-1 grant shapes so callers can diagnose legacy grants. */
|
|
232
|
+
export function parseGrantScope(value) {
|
|
233
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
234
|
+
return null;
|
|
235
|
+
const scope = value;
|
|
236
|
+
if (scope.access !== "contract" && scope.access !== "full_collection")
|
|
237
|
+
return null;
|
|
238
|
+
if (!Array.isArray(scope.contracts))
|
|
239
|
+
return null;
|
|
240
|
+
if (scope.contracts.some((contract) => !contract
|
|
241
|
+
|| typeof contract !== "object"
|
|
242
|
+
|| contract.contract_type !== "record"
|
|
243
|
+
|| typeof contract.id !== "string"
|
|
244
|
+
|| typeof contract.version !== "string"
|
|
245
|
+
|| !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(contract.version)
|
|
246
|
+
|| !/^sha256:[0-9a-f]{64}$/.test(contract.digest)
|
|
247
|
+
|| !contract.schema
|
|
248
|
+
|| typeof contract.schema !== "object"
|
|
249
|
+
|| Array.isArray(contract.schema)
|
|
250
|
+
|| !Array.isArray(contract.implementations)
|
|
251
|
+
|| contract.implementations.some((implementation) => !implementation
|
|
252
|
+
|| typeof implementation !== "object"
|
|
253
|
+
|| typeof implementation.type_name !== "string"
|
|
254
|
+
|| !Number.isInteger(implementation.type_version)
|
|
255
|
+
|| !/^sha256:[0-9a-f]{64}$/.test(implementation.digest)
|
|
256
|
+
|| !implementation.fields
|
|
257
|
+
|| typeof implementation.fields !== "object"
|
|
258
|
+
|| Array.isArray(implementation.fields)
|
|
259
|
+
|| Object.values(implementation.fields).some((field) => typeof field !== "string"))))
|
|
260
|
+
return null;
|
|
261
|
+
return scope;
|
|
262
|
+
}
|
|
263
|
+
/** Only this scope authorizes application data access. */
|
|
264
|
+
export function isCanonicalGrantScope(scope) {
|
|
265
|
+
return scope.access === "full_collection" && scope.contracts.length === 0;
|
|
266
|
+
}
|
|
267
|
+
export function stripTrailingSlash(value) {
|
|
268
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
269
|
+
}
|
|
270
|
+
export function defaultManifestSource() {
|
|
271
|
+
if (typeof location === "undefined") {
|
|
272
|
+
throw connectError("manifest_required", "manifest is required outside a browser environment.");
|
|
273
|
+
}
|
|
274
|
+
if (location.origin === "null") {
|
|
275
|
+
throw connectError("manifest_required", "Downloaded applications must provide their v1 portable manifest inline.");
|
|
276
|
+
}
|
|
277
|
+
return new URL("/.well-known/mdbase-app.json", location.origin).href;
|
|
278
|
+
}
|
|
279
|
+
export function defaultRedirectUri() {
|
|
280
|
+
if (typeof location === "undefined") {
|
|
281
|
+
throw connectError("redirect_uri_required", "redirectUri is required outside a browser environment.");
|
|
282
|
+
}
|
|
283
|
+
return location.href.split(/[?#]/)[0];
|
|
284
|
+
}
|
|
285
|
+
export function applicationStorageOrigin(manifest, manifestSource, redirectUri, registeredPortable) {
|
|
286
|
+
if ((typeof manifest !== "string" && manifest.distribution === "portable")
|
|
287
|
+
|| registeredPortable)
|
|
288
|
+
return "null";
|
|
289
|
+
const redirect = new URL(redirectUri);
|
|
290
|
+
if (["http:", "https:"].includes(redirect.protocol))
|
|
291
|
+
return redirect.origin;
|
|
292
|
+
if (typeof location !== "undefined")
|
|
293
|
+
return location.origin;
|
|
294
|
+
if (typeof manifest !== "string")
|
|
295
|
+
return new URL(manifest.homepage).origin;
|
|
296
|
+
try {
|
|
297
|
+
return new URL(manifestSource).origin;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export function collectionIdFromTokenKey(key) {
|
|
304
|
+
const marker = ":token:";
|
|
305
|
+
const index = key.lastIndexOf(marker);
|
|
306
|
+
return index < 0 ? "" : key.slice(index + marker.length);
|
|
307
|
+
}
|
|
308
|
+
export function defaultCallbackUrl() {
|
|
309
|
+
if (typeof location === "undefined") {
|
|
310
|
+
throw connectError("callback_url_required", "callbackUrl is required outside a browser environment.");
|
|
311
|
+
}
|
|
312
|
+
return location.href;
|
|
313
|
+
}
|
|
314
|
+
export function defaultStorage(memoryOnly) {
|
|
315
|
+
if (memoryOnly)
|
|
316
|
+
return new MemoryStorage();
|
|
317
|
+
if (typeof localStorage === "undefined") {
|
|
318
|
+
throw connectError("storage_required", "storage is required outside a browser environment.");
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const probe = `mdbase-connect:probe:${randomBase64Url(8)}`;
|
|
322
|
+
localStorage.setItem(probe, "1");
|
|
323
|
+
localStorage.removeItem(probe);
|
|
324
|
+
return localStorage;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return new MemoryStorage();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export class MemoryStorage {
|
|
331
|
+
values = new Map();
|
|
332
|
+
get length() {
|
|
333
|
+
return this.values.size;
|
|
334
|
+
}
|
|
335
|
+
clear() {
|
|
336
|
+
this.values.clear();
|
|
337
|
+
}
|
|
338
|
+
getItem(key) {
|
|
339
|
+
return this.values.get(key) ?? null;
|
|
340
|
+
}
|
|
341
|
+
key(index) {
|
|
342
|
+
return [...this.values.keys()][index] ?? null;
|
|
343
|
+
}
|
|
344
|
+
removeItem(key) {
|
|
345
|
+
this.values.delete(key);
|
|
346
|
+
}
|
|
347
|
+
setItem(key, value) {
|
|
348
|
+
this.values.set(String(key), String(value));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
export function isOpaquePortableManifest(manifest) {
|
|
352
|
+
if (typeof manifest === "string" || manifest.distribution !== "portable")
|
|
353
|
+
return false;
|
|
354
|
+
return typeof location === "undefined"
|
|
355
|
+
|| location.origin === "null"
|
|
356
|
+
|| !["http:", "https:"].includes(location.protocol);
|
|
357
|
+
}
|
|
358
|
+
export function manifestStorageFingerprint(manifest) {
|
|
359
|
+
const canonical = canonicalJson(manifest);
|
|
360
|
+
let first = 0x811c9dc5;
|
|
361
|
+
let second = 0x9e3779b9;
|
|
362
|
+
for (let index = 0; index < canonical.length; index += 1) {
|
|
363
|
+
const code = canonical.charCodeAt(index);
|
|
364
|
+
first = Math.imul(first ^ code, 0x01000193);
|
|
365
|
+
second = Math.imul(second ^ code, 0x85ebca6b);
|
|
366
|
+
}
|
|
367
|
+
return `${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0).toString(16).padStart(8, "0")}`;
|
|
368
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { JsonObject } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { type CollectionReadProblemCode, type ConnectOutcome } from "./outcomes.js";
|
|
3
|
+
import type { ConnectRequestOptions, ExecuteViewInput, SavedViewExecution, SavedViewPage, SavedViewPagesOptions } from "./operation-types.js";
|
|
4
|
+
type SavedViewOperation<Frontmatter extends JsonObject> = (input: ExecuteViewInput, options?: ConnectRequestOptions) => Promise<ConnectOutcome<SavedViewExecution<Frontmatter>, CollectionReadProblemCode>>;
|
|
5
|
+
export declare function coordinatedSavedViewPages<Frontmatter extends JsonObject>(executeView: SavedViewOperation<Frontmatter>, releaseQueryCursor: (cursor: string) => Promise<void>, input: ExecuteViewInput, options?: SavedViewPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<SavedViewPage<Frontmatter>, CollectionReadProblemCode>>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { connectProblem } from "./errors.js";
|
|
2
|
+
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
3
|
+
export async function* coordinatedSavedViewPages(executeView, releaseQueryCursor, input, options = {}) {
|
|
4
|
+
const { offset: requestedOffset, limit: requestedLimit, cursor: requestedCursor, ...criteria } = input;
|
|
5
|
+
let offset = nonNegativeInteger(requestedOffset, 0);
|
|
6
|
+
const firstPageSize = positiveInteger(options.firstPageSize ?? requestedLimit, 200);
|
|
7
|
+
const pageSize = positiveInteger(options.pageSize ?? requestedLimit, 1_000);
|
|
8
|
+
let cursor = requestedCursor;
|
|
9
|
+
let cursorMode = requestedCursor !== undefined;
|
|
10
|
+
let cursorToRelease = requestedCursor;
|
|
11
|
+
let loaded = 0;
|
|
12
|
+
let pageNumber = 0;
|
|
13
|
+
try {
|
|
14
|
+
while (!options.signal?.aborted) {
|
|
15
|
+
const pageCursor = cursor;
|
|
16
|
+
const outcome = await executeView({
|
|
17
|
+
...criteria,
|
|
18
|
+
limit: pageNumber === 0 ? firstPageSize : pageSize,
|
|
19
|
+
...(cursorMode && pageCursor ? { cursor: pageCursor } : { offset })
|
|
20
|
+
}, {
|
|
21
|
+
signal: options.signal,
|
|
22
|
+
timeoutMs: options.pageTimeoutMs,
|
|
23
|
+
coordination: { ...options.coordination, coalesce: false }
|
|
24
|
+
});
|
|
25
|
+
if (!outcome.ok) {
|
|
26
|
+
yield outcome;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const result = outcome.value;
|
|
30
|
+
const returnedCursor = result.meta.cursor;
|
|
31
|
+
if (returnedCursor) {
|
|
32
|
+
cursorMode = true;
|
|
33
|
+
cursor = returnedCursor;
|
|
34
|
+
cursorToRelease = returnedCursor;
|
|
35
|
+
}
|
|
36
|
+
else if (cursorMode) {
|
|
37
|
+
cursor = undefined;
|
|
38
|
+
cursorToRelease = pageCursor;
|
|
39
|
+
}
|
|
40
|
+
if (cursorMode && result.meta.hasMore && !returnedCursor) {
|
|
41
|
+
yield connectFailure(connectProblem("invalid_operation_response", "The collection authority omitted the cursor required for the next saved-view page."));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
loaded += result.results.length;
|
|
45
|
+
const complete = !result.meta.hasMore || result.results.length === 0;
|
|
46
|
+
const page = {
|
|
47
|
+
...result,
|
|
48
|
+
page: pageNumber,
|
|
49
|
+
offset,
|
|
50
|
+
loaded,
|
|
51
|
+
complete,
|
|
52
|
+
...(returnedCursor ? { cursor: returnedCursor } : {})
|
|
53
|
+
};
|
|
54
|
+
options.onProgress?.(page);
|
|
55
|
+
yield connectSuccess(page, outcome.diagnostics);
|
|
56
|
+
if (complete)
|
|
57
|
+
return;
|
|
58
|
+
offset += result.results.length;
|
|
59
|
+
pageNumber += 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (cursorMode && cursorToRelease)
|
|
64
|
+
await releaseQueryCursor(cursorToRelease);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function nonNegativeInteger(value, fallback) {
|
|
68
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
69
|
+
? Math.max(0, Math.floor(value))
|
|
70
|
+
: fallback;
|
|
71
|
+
}
|
|
72
|
+
function positiveInteger(value, fallback) {
|
|
73
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
74
|
+
? Math.floor(value)
|
|
75
|
+
: fallback;
|
|
76
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type MdbaseSelectionHistory = "push" | "replace";
|
|
2
|
+
export interface MdbaseApplicationSelection {
|
|
3
|
+
selectedCollectionId(): string | null;
|
|
4
|
+
select(collectionId: string | null, options?: {
|
|
5
|
+
history?: MdbaseSelectionHistory;
|
|
6
|
+
}): void;
|
|
7
|
+
authorizationReturnTo(): string | undefined;
|
|
8
|
+
authorizationCallback(): string | null;
|
|
9
|
+
finishAuthorization(returnTo: string | undefined, collectionId: string): void;
|
|
10
|
+
clearAuthorizationCallback(returnTo?: string): void;
|
|
11
|
+
subscribe(listener: () => void): () => void;
|
|
12
|
+
}
|
|
13
|
+
export interface MdbaseBrowserSelectionOptions {
|
|
14
|
+
collectionParameter?: string;
|
|
15
|
+
fallbackPath?: string;
|
|
16
|
+
}
|
|
17
|
+
/** URL-backed collection selection for browser and browser-shell applications. */
|
|
18
|
+
export declare class MdbaseBrowserSelection implements MdbaseApplicationSelection {
|
|
19
|
+
private readonly collectionParameter;
|
|
20
|
+
private readonly fallbackPath;
|
|
21
|
+
private readonly listeners;
|
|
22
|
+
private readonly handlePopState;
|
|
23
|
+
constructor(options?: MdbaseBrowserSelectionOptions);
|
|
24
|
+
selectedCollectionId(): string | null;
|
|
25
|
+
select(collectionId: string | null, options?: {
|
|
26
|
+
history?: MdbaseSelectionHistory;
|
|
27
|
+
}): void;
|
|
28
|
+
authorizationReturnTo(): string;
|
|
29
|
+
authorizationCallback(): string | null;
|
|
30
|
+
finishAuthorization(returnTo: string | undefined, collectionId: string): void;
|
|
31
|
+
clearAuthorizationCallback(returnTo?: string): void;
|
|
32
|
+
subscribe(listener: () => void): () => void;
|
|
33
|
+
private emit;
|
|
34
|
+
private safeAppUrl;
|
|
35
|
+
private currentUrl;
|
|
36
|
+
private browserHistory;
|
|
37
|
+
}
|
|
38
|
+
export declare class MdbaseMemorySelection implements MdbaseApplicationSelection {
|
|
39
|
+
private collectionId;
|
|
40
|
+
private readonly listeners;
|
|
41
|
+
selectedCollectionId(): string | null;
|
|
42
|
+
select(collectionId: string | null): void;
|
|
43
|
+
authorizationReturnTo(): undefined;
|
|
44
|
+
authorizationCallback(): null;
|
|
45
|
+
finishAuthorization(_returnTo: string | undefined, collectionId: string): void;
|
|
46
|
+
clearAuthorizationCallback(): void;
|
|
47
|
+
subscribe(listener: () => void): () => void;
|
|
48
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { cleanAuthorizationParameters, isAuthorizationCallbackUrl } from "./authorization-url.js";
|
|
2
|
+
import { connectError } from "./errors.js";
|
|
3
|
+
/** URL-backed collection selection for browser and browser-shell applications. */
|
|
4
|
+
export class MdbaseBrowserSelection {
|
|
5
|
+
collectionParameter;
|
|
6
|
+
fallbackPath;
|
|
7
|
+
listeners = new Set();
|
|
8
|
+
handlePopState = () => this.emit();
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.collectionParameter = options.collectionParameter ?? "collection";
|
|
11
|
+
this.fallbackPath = options.fallbackPath ?? "/";
|
|
12
|
+
}
|
|
13
|
+
selectedCollectionId() {
|
|
14
|
+
return this.currentUrl().searchParams.get(this.collectionParameter);
|
|
15
|
+
}
|
|
16
|
+
select(collectionId, options = {}) {
|
|
17
|
+
const url = cleanAuthorizationParameters(this.currentUrl());
|
|
18
|
+
if (collectionId)
|
|
19
|
+
url.searchParams.set(this.collectionParameter, collectionId);
|
|
20
|
+
else
|
|
21
|
+
url.searchParams.delete(this.collectionParameter);
|
|
22
|
+
const browserHistory = this.browserHistory();
|
|
23
|
+
browserHistory[options.history === "push" ? "pushState" : "replaceState"](browserHistory.state, "", url);
|
|
24
|
+
this.emit();
|
|
25
|
+
}
|
|
26
|
+
authorizationReturnTo() {
|
|
27
|
+
const url = cleanAuthorizationParameters(this.currentUrl());
|
|
28
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
29
|
+
}
|
|
30
|
+
authorizationCallback() {
|
|
31
|
+
const value = this.currentUrl().href;
|
|
32
|
+
return isAuthorizationCallbackUrl(value) ? value : null;
|
|
33
|
+
}
|
|
34
|
+
finishAuthorization(returnTo, collectionId) {
|
|
35
|
+
const url = this.safeAppUrl(returnTo ?? this.fallbackPath);
|
|
36
|
+
cleanAuthorizationParameters(url);
|
|
37
|
+
url.searchParams.set(this.collectionParameter, collectionId);
|
|
38
|
+
const browserHistory = this.browserHistory();
|
|
39
|
+
browserHistory.replaceState(null, "", url);
|
|
40
|
+
this.emit();
|
|
41
|
+
}
|
|
42
|
+
clearAuthorizationCallback(returnTo) {
|
|
43
|
+
const url = returnTo ? this.safeAppUrl(returnTo) : this.currentUrl();
|
|
44
|
+
const browserHistory = this.browserHistory();
|
|
45
|
+
browserHistory.replaceState(browserHistory.state, "", cleanAuthorizationParameters(url));
|
|
46
|
+
this.emit();
|
|
47
|
+
}
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
const first = this.listeners.size === 0;
|
|
50
|
+
this.listeners.add(listener);
|
|
51
|
+
if (first && typeof window !== "undefined") {
|
|
52
|
+
window.addEventListener("popstate", this.handlePopState);
|
|
53
|
+
}
|
|
54
|
+
return () => {
|
|
55
|
+
this.listeners.delete(listener);
|
|
56
|
+
if (this.listeners.size === 0 && typeof window !== "undefined") {
|
|
57
|
+
window.removeEventListener("popstate", this.handlePopState);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
emit() {
|
|
62
|
+
for (const listener of this.listeners)
|
|
63
|
+
listener();
|
|
64
|
+
}
|
|
65
|
+
safeAppUrl(value) {
|
|
66
|
+
const current = this.currentUrl();
|
|
67
|
+
const candidate = new URL(value, current.origin);
|
|
68
|
+
return candidate.origin === current.origin
|
|
69
|
+
? candidate
|
|
70
|
+
: new URL(this.fallbackPath, current.origin);
|
|
71
|
+
}
|
|
72
|
+
currentUrl() {
|
|
73
|
+
if (typeof location === "undefined") {
|
|
74
|
+
throw connectError("browser_required", "Browser selection requires a browser environment.");
|
|
75
|
+
}
|
|
76
|
+
return new URL(location.href);
|
|
77
|
+
}
|
|
78
|
+
browserHistory() {
|
|
79
|
+
if (typeof history === "undefined") {
|
|
80
|
+
throw connectError("browser_required", "Browser selection requires a browser environment.");
|
|
81
|
+
}
|
|
82
|
+
return history;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class MdbaseMemorySelection {
|
|
86
|
+
collectionId = null;
|
|
87
|
+
listeners = new Set();
|
|
88
|
+
selectedCollectionId() {
|
|
89
|
+
return this.collectionId;
|
|
90
|
+
}
|
|
91
|
+
select(collectionId) {
|
|
92
|
+
if (collectionId === this.collectionId)
|
|
93
|
+
return;
|
|
94
|
+
this.collectionId = collectionId;
|
|
95
|
+
for (const listener of this.listeners)
|
|
96
|
+
listener();
|
|
97
|
+
}
|
|
98
|
+
authorizationReturnTo() {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
authorizationCallback() {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
finishAuthorization(_returnTo, collectionId) {
|
|
105
|
+
this.select(collectionId);
|
|
106
|
+
}
|
|
107
|
+
clearAuthorizationCallback() { }
|
|
108
|
+
subscribe(listener) {
|
|
109
|
+
this.listeners.add(listener);
|
|
110
|
+
return () => this.listeners.delete(listener);
|
|
111
|
+
}
|
|
112
|
+
}
|