@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.
- package/LICENSE +21 -0
- package/README.md +405 -0
- package/dist/async.d.ts +1 -0
- package/dist/async.js +13 -0
- package/dist/authorization-types.d.ts +7 -0
- package/dist/authorization-types.js +1 -0
- package/dist/authorization-url.d.ts +3 -0
- package/dist/authorization-url.js +27 -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/collection-client.d.ts +57 -0
- package/dist/collection-client.js +214 -0
- package/dist/connect.d.ts +39 -0
- package/dist/connect.js +620 -0
- package/dist/connection-notifications.d.ts +25 -0
- package/dist/connection-notifications.js +179 -0
- package/dist/connection-transport.d.ts +80 -0
- package/dist/connection-transport.js +670 -0
- package/dist/connection-types.d.ts +29 -0
- package/dist/connection-types.js +1 -0
- package/dist/connection.d.ts +154 -0
- package/dist/connection.js +371 -0
- package/dist/crypto.d.ts +64 -0
- package/dist/crypto.js +391 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +83 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/internal-types.d.ts +70 -0
- package/dist/internal-types.js +1 -0
- package/dist/notifications.d.ts +47 -0
- package/dist/notifications.js +52 -0
- package/dist/operation-helpers.d.ts +24 -0
- package/dist/operation-helpers.js +129 -0
- package/dist/operation-types.d.ts +254 -0
- package/dist/operation-types.js +1 -0
- package/dist/runtime-utils.d.ts +33 -0
- package/dist/runtime-utils.js +224 -0
- package/dist/selection.d.ts +48 -0
- package/dist/selection.js +112 -0
- package/dist/session.d.ts +76 -0
- package/dist/session.js +271 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mdbase connect contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# `@mdbase-dev/connect`
|
|
2
|
+
|
|
3
|
+
Browser SDK for dynamically discovered mdbase connect applications.
|
|
4
|
+
|
|
5
|
+
It also ships a dependency-free browser bundle for downloaded HTML
|
|
6
|
+
applications. See the [portable application guide](../../docs/portable-apps.md)
|
|
7
|
+
for the v1 manifest, device-code flow, version-pinned CDN URL, SRI metadata,
|
|
8
|
+
and `file://` storage boundary.
|
|
9
|
+
|
|
10
|
+
The complete developer guide covers
|
|
11
|
+
[setup](https://mdbase.dev/sdk/quickstart/),
|
|
12
|
+
[manifests and contracts](https://mdbase.dev/sdk/manifest/),
|
|
13
|
+
[authorization](https://mdbase.dev/sdk/authorization/),
|
|
14
|
+
[operations](https://mdbase.dev/sdk/operations/),
|
|
15
|
+
[routing](https://mdbase.dev/sdk/routing/),
|
|
16
|
+
[offline sync](https://mdbase.dev/sdk/offline-sync/), and
|
|
17
|
+
[testing](https://mdbase.dev/sdk/testing/).
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const mdbase = new MdbaseConnect({
|
|
21
|
+
serverUrl: "https://connect.mdbase.dev",
|
|
22
|
+
manifest: new URL(".well-known/mdbase-app.json", location.href).href,
|
|
23
|
+
redirectUri: "https://workouts.example/auth/mdbase/callback"
|
|
24
|
+
});
|
|
25
|
+
const session = mdbase.createSession({
|
|
26
|
+
operations: ["describe", "changes", "read", "query", "update"],
|
|
27
|
+
selection: new MdbaseBrowserSelection()
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await session.start();
|
|
31
|
+
if (session.getSnapshot().status === "unselected") {
|
|
32
|
+
await session.authorize("choose");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const snapshot = session.getSnapshot();
|
|
36
|
+
if (snapshot.status !== "ready") throw new Error("Choose an authorized collection.");
|
|
37
|
+
const connection = snapshot.connection;
|
|
38
|
+
const description = await connection.describe();
|
|
39
|
+
const workouts = await connection.query({ types: ["workout"] });
|
|
40
|
+
await connection.update({
|
|
41
|
+
path: "workouts/monday.md",
|
|
42
|
+
patch: { completed: true },
|
|
43
|
+
if_revision: workouts.result.results[0].revision
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const preview = await connection.preflightRename({
|
|
47
|
+
from: "workouts/monday.md",
|
|
48
|
+
to: "archive/monday.md",
|
|
49
|
+
update_refs: true,
|
|
50
|
+
if_revision: workouts.result.results[0].revision
|
|
51
|
+
});
|
|
52
|
+
console.log(preview.result.references_affected);
|
|
53
|
+
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
await connection.renameWithProgress({
|
|
56
|
+
from: "workouts/monday.md",
|
|
57
|
+
to: "archive/monday.md",
|
|
58
|
+
update_refs: true,
|
|
59
|
+
if_revision: workouts.result.results[0].revision
|
|
60
|
+
}, {
|
|
61
|
+
preflight: preview.result,
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
onProgress: ({ state, estimate, cancellable }) => {
|
|
64
|
+
console.log(state, estimate?.affectedRecords, cancellable);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
for await (const change of connection.watch()) {
|
|
69
|
+
console.log(change.type, change.payload.path);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`MdbaseConnect` is the application-level authorization registry.
|
|
74
|
+
`MdbaseSession` is the normal application boundary: it owns active collection
|
|
75
|
+
selection, authorization callback completion, access capabilities, and one
|
|
76
|
+
reactive snapshot. Use `getSnapshot()` and `subscribe()` directly or through
|
|
77
|
+
your framework's external-store integration. A snapshot is `unselected`,
|
|
78
|
+
`ready`, or `unavailable`; an unavailable bookmark includes the explicit
|
|
79
|
+
collection ID and reason instead of collapsing every recovery state to `null`.
|
|
80
|
+
|
|
81
|
+
`MdbaseBrowserSelection` keeps the stable collection ID in
|
|
82
|
+
`?collection=<id>`, preserves unrelated path/query/hash and router state,
|
|
83
|
+
restores authorization callbacks safely, and reports browser back/forward
|
|
84
|
+
changes to the session. The session auto-selects only when exactly one saved
|
|
85
|
+
connection exists. Switching is state-driven and must not reload the page:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
session.select(collectionId, { history: "replace" });
|
|
89
|
+
session.clearSelection();
|
|
90
|
+
session.forget(collectionId);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Selection validates the saved authorization before changing the URL. Explicit
|
|
94
|
+
unavailable IDs remain authoritative so applications can offer exact
|
|
95
|
+
reauthorization or another collection:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
await session.authorize("selected"); // exact bookmarked collection
|
|
99
|
+
await session.authorize("choose"); // any compatible collection
|
|
100
|
+
await session.authorize({ collectionId }); // exact adoption/migration target
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Collection IDs are opaque non-secret locators that may appear in browser
|
|
104
|
+
history and logs. Grants remain the authorization boundary, and names are
|
|
105
|
+
display text that may change.
|
|
106
|
+
|
|
107
|
+
Bundled v1 application manifests can declare runtime notification criteria.
|
|
108
|
+
Register a service worker from a user gesture to receive standards-based Web
|
|
109
|
+
Push while the app is closed:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const worker = await navigator.serviceWorker.register("/service-worker.js");
|
|
113
|
+
await connection.registerNotifications({
|
|
114
|
+
serviceWorker: worker,
|
|
115
|
+
criteria: ["workout.reminder"]
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
In the service worker:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { showMdbasePushNotification } from "@mdbase-dev/connect";
|
|
123
|
+
|
|
124
|
+
self.addEventListener("push", (event) => {
|
|
125
|
+
event.waitUntil(
|
|
126
|
+
showMdbasePushNotification(self.registration, event.data?.json())
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Pushes contain an opaque signal and cursor plus static declaration presentation,
|
|
132
|
+
never record paths or content. Treat them as a wake-up hint and read current
|
|
133
|
+
authorized state after opening. Registration atomically replaces the selected
|
|
134
|
+
criteria for that installation. See the
|
|
135
|
+
[runtime notifications guide](../../docs/notifications.md) for declaration and
|
|
136
|
+
deployment examples.
|
|
137
|
+
|
|
138
|
+
Native shells can use one FCM token for Android and iOS when the declaration
|
|
139
|
+
declares `notifications.native_delivery.mode` as `managed_fcm`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
await connection.registerNativeNotifications({
|
|
143
|
+
token: await nativeMessaging.getToken(),
|
|
144
|
+
criteria: ["workout.reminder"]
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Re-register when Firebase rotates the token. Parse the string-valued
|
|
149
|
+
notification data with `parseMdbaseNativeNotificationData()`, refresh current
|
|
150
|
+
collection state, and call `unregisterNativeNotifications()` before deleting
|
|
151
|
+
the token on opt-out. The public Firebase project ID is read from the
|
|
152
|
+
application declaration; service credentials are never embedded in the app.
|
|
153
|
+
|
|
154
|
+
Connect-managed FCM makes Connect a trusted sender for the application's
|
|
155
|
+
Firebase project. It suits single-owner deployments; applications with a
|
|
156
|
+
broader audience should normally declare signed webhook delivery and keep
|
|
157
|
+
their Apple/Firebase credentials in their own backend. The notifications guide
|
|
158
|
+
documents both threat models. New or changed declaration criteria never silently
|
|
159
|
+
broaden an existing grant: handle `notification_reauthorization_required` by
|
|
160
|
+
running authorization again before retrying registration.
|
|
161
|
+
|
|
162
|
+
Before opening a feature, inspect its exact authorization gap instead of
|
|
163
|
+
waiting for an operation to fail:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const required = ["read", "query", "update"] as const;
|
|
167
|
+
const capabilities = connection.authorizationCapabilities([...required]);
|
|
168
|
+
if (!capabilities.sufficient) {
|
|
169
|
+
console.log("Needs", capabilities.missingOperations);
|
|
170
|
+
await connection.requestOperations([...required]);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`requestOperations()` is a no-op when the current grant is sufficient. When a
|
|
175
|
+
replacement grant is needed, it requests the least-privilege union of the
|
|
176
|
+
already granted operations and the missing requirements. An
|
|
177
|
+
`insufficient_access` error carries the same `grantedOperations`,
|
|
178
|
+
`missingOperations`, and `requiredOperations` metadata with a `reauthorize`
|
|
179
|
+
recovery action.
|
|
180
|
+
|
|
181
|
+
Applications that declare an `mdbase.runtime.timer.fired` notification criterion can keep
|
|
182
|
+
one-shot reminders at the collection authority:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
await connection.reconcileTimers({
|
|
186
|
+
namespace: "workout-reminders",
|
|
187
|
+
criterion_id: "workout.reminder",
|
|
188
|
+
timers: [{
|
|
189
|
+
id: "workout-42",
|
|
190
|
+
fire_at: new Date("2026-07-25T10:00:00Z").toISOString()
|
|
191
|
+
}]
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The SDK also exposes `listTimers()`, `putTimer()`, and `cancelTimer()`.
|
|
196
|
+
Reconciliation is atomic and is normally the safest way to project a complete
|
|
197
|
+
desired timer set. IDs and optional timer data stay at the local connector or
|
|
198
|
+
hosted provider and are not included in notification signals.
|
|
199
|
+
|
|
200
|
+
When the local connector definitively rejects an encrypted grant, the SDK does
|
|
201
|
+
not bypass that decision through the relay. It classifies the error as requiring
|
|
202
|
+
authorization, removes only the matching stale credential, and emits a `null`
|
|
203
|
+
connection through `onConnectionChange()`. A subsequent
|
|
204
|
+
`requestOperations()` call therefore starts authorization instead of trusting
|
|
205
|
+
the stale cached capability list.
|
|
206
|
+
|
|
207
|
+
Applications with full collection access can also register and maintain type
|
|
208
|
+
definitions. Type source is returned with a revision token so updates cannot
|
|
209
|
+
silently overwrite a definition changed by another application:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const created = await connection.createType({
|
|
213
|
+
document: `---
|
|
214
|
+
kind: mdbase.type
|
|
215
|
+
name: workout
|
|
216
|
+
version: 1
|
|
217
|
+
schema:
|
|
218
|
+
dialect: json-schema-2020-12
|
|
219
|
+
value:
|
|
220
|
+
type: object
|
|
221
|
+
---
|
|
222
|
+
`
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const current = await connection.readType({ name: "workout" });
|
|
226
|
+
await connection.updateType({
|
|
227
|
+
path: current.result.path,
|
|
228
|
+
document: current.result.document.replace("version: 1", "version: 2"),
|
|
229
|
+
if_revision: current.result.revision
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Request `read_type`, `create_type`, and `update_type` during authorization.
|
|
234
|
+
Contract-scoped applications cannot manage collection-wide type definitions.
|
|
235
|
+
|
|
236
|
+
To install a complete catalog pack without exposing its individual collection
|
|
237
|
+
paths, request `install_type_pack`, fetch and verify the published provision,
|
|
238
|
+
then pass it to the atomic collection operation:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const response = await fetch(pack.provisionUrl);
|
|
242
|
+
const provision = await response.json();
|
|
243
|
+
await connection.installTypePack(provision);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The caller remains responsible for verifying the catalog digest before sending
|
|
247
|
+
the provision. Existing targets with different content are rejected.
|
|
248
|
+
|
|
249
|
+
For a local collection, ask for same-computer access from a user gesture:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const status = await connection.checkDirectAccess();
|
|
253
|
+
if (status === "permission_required") {
|
|
254
|
+
directButton.onclick = () => connection.requestDirectAccess();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
connection.onConnectionChange((info) => {
|
|
258
|
+
console.log(info?.route); // "direct", "relay", or "remote"
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
After permission is granted, all grantable operations prefer the fixed
|
|
263
|
+
loopback connector automatically. If it is absent or unavailable, the SDK
|
|
264
|
+
retries the exact encrypted envelope through the relay. Durable connector
|
|
265
|
+
receipts prevent an ambiguous direct write from running twice. The SDK retains
|
|
266
|
+
one unresolved encrypted mutation locally and requires that exact write to be
|
|
267
|
+
retried before a later mutation can overtake it. Set
|
|
268
|
+
`directAccess: "disabled"` only when an embedding environment cannot use
|
|
269
|
+
loopback requests.
|
|
270
|
+
|
|
271
|
+
The SDK loads the v1 manifest from the application's own bundle and
|
|
272
|
+
posts it inline to Connect. Connect identifies the exact canonical declaration
|
|
273
|
+
by its digest. The declared reverse-domain ID and name are presentation
|
|
274
|
+
metadata, not proof of a publisher; grants remain bound to the authorization
|
|
275
|
+
completed by that installation. No developer account, public manifest host, or
|
|
276
|
+
manually issued client secret is required.
|
|
277
|
+
|
|
278
|
+
Applications declare exact data-contract versions in their bundled manifest.
|
|
279
|
+
Connect offers collections that already provide those contracts or can install
|
|
280
|
+
the declared type pack safely. The developer helper computes every resource
|
|
281
|
+
digest:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { defineTypePack } from "@mdbase-dev/connect-dev";
|
|
285
|
+
|
|
286
|
+
const contract = `---
|
|
287
|
+
kind: mdbase.contract
|
|
288
|
+
contract_type: record
|
|
289
|
+
id: example.work-item
|
|
290
|
+
version: 1.0.0
|
|
291
|
+
record_schema:
|
|
292
|
+
dialect: json-schema-2020-12
|
|
293
|
+
value:
|
|
294
|
+
type: object
|
|
295
|
+
required: [title]
|
|
296
|
+
properties:
|
|
297
|
+
title: { type: string }
|
|
298
|
+
---
|
|
299
|
+
`;
|
|
300
|
+
const type = `---
|
|
301
|
+
kind: mdbase.type
|
|
302
|
+
name: work-item
|
|
303
|
+
version: 1
|
|
304
|
+
schema:
|
|
305
|
+
dialect: json-schema-2020-12
|
|
306
|
+
value:
|
|
307
|
+
type: object
|
|
308
|
+
implements:
|
|
309
|
+
- contract: example.work-item
|
|
310
|
+
version: 1.0.0
|
|
311
|
+
fields: { title: title }
|
|
312
|
+
---
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
export const manifest = {
|
|
316
|
+
manifest_version: 1,
|
|
317
|
+
id: "dev.mdbase.worklog",
|
|
318
|
+
name: "Worklog",
|
|
319
|
+
homepage: "https://worklog.example",
|
|
320
|
+
redirect_uris: ["https://worklog.example/auth/mdbase/callback"],
|
|
321
|
+
requirements: {
|
|
322
|
+
contracts: [{ id: "example.work-item", version: "1.0.0" }]
|
|
323
|
+
},
|
|
324
|
+
provisions: {
|
|
325
|
+
type_packs: [defineTypePack({
|
|
326
|
+
id: "example.work-items",
|
|
327
|
+
version: "1.0.0",
|
|
328
|
+
provides: [{ id: "example.work-item", version: "1.0.0" }],
|
|
329
|
+
resources: [
|
|
330
|
+
{ kind: "contract", source: "_contracts/example.work-item.md", document: contract },
|
|
331
|
+
{ kind: "type", source: "_types/work-item.md", document: type }
|
|
332
|
+
]
|
|
333
|
+
})]
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Set `collection_kind` to `hosted` when the application needs a durable
|
|
339
|
+
provider-backed collection and the offline sync transport returned by
|
|
340
|
+
`sync()`. Connect then offers and accepts collections with replication capability.
|
|
341
|
+
|
|
342
|
+
Access defaults to the record types supplied by `requirements.contracts`.
|
|
343
|
+
Set `access` to `full_collection` when the application needs collection-level
|
|
344
|
+
features such as saved views. Required contracts still determine compatibility
|
|
345
|
+
and are provisioned during approval.
|
|
346
|
+
|
|
347
|
+
`listViews()` returns each named view's selected result properties in display
|
|
348
|
+
order. Property descriptors retain source labels for projected and computed
|
|
349
|
+
values. `executeView()` returns their values on each result row.
|
|
350
|
+
|
|
351
|
+
Provisioning is part of the approval flow. The connector validates and
|
|
352
|
+
installs each missing type pack transactionally, verifies its exact contracts
|
|
353
|
+
and implementations, and creates the scoped grant afterward. The application
|
|
354
|
+
is not granted collection-wide type-management access.
|
|
355
|
+
|
|
356
|
+
The SDK returns the mdbase operation envelope, carries revision tokens in typed
|
|
357
|
+
record results, and accepts `if_revision` on mutations. `describe()` exposes
|
|
358
|
+
JSON Schemas, portable type definitions, canonical collection settings, and
|
|
359
|
+
first-class data contracts. `watch()` resumes from a local collection cursor;
|
|
360
|
+
the Connect server does not store the change feed.
|
|
361
|
+
`preflightRename()` and `preflightDelete()` run the canonical collection
|
|
362
|
+
operation without changing records or advancing the change cursor, so an app
|
|
363
|
+
can show authoritative reference impact before asking for confirmation.
|
|
364
|
+
`renameWithProgress()` and `deleteWithProgress()` expose preflight, ready,
|
|
365
|
+
applying, completed, and cancelled phases with an impact estimate. Cancellation
|
|
366
|
+
remains available during an encrypted local/relay mutation because the SDK
|
|
367
|
+
persists its exact encrypted request before dispatch. If waiting is cancelled
|
|
368
|
+
after dispatch, `pendingMutation()` reports the interruption and
|
|
369
|
+
`resumePendingMutation()` safely recovers the connector's durable receipt using
|
|
370
|
+
the exact same input. Other providers are cancellable until apply begins and
|
|
371
|
+
then run to a definitive response.
|
|
372
|
+
Authorization is retained in `localStorage` by default. Access tokens are
|
|
373
|
+
renewed with rotating refresh tokens; passing a custom `Storage` implementation
|
|
374
|
+
allows a host to choose another persistence boundary.
|
|
375
|
+
|
|
376
|
+
Native shells can pass `navigate` to open the authorization URL in the system
|
|
377
|
+
browser and list a reverse-domain callback such as
|
|
378
|
+
`dev.mdbase.worklog://auth/mdbase/callback` in the bundled declaration. Its
|
|
379
|
+
scheme must match the declaration ID. PKCE remains mandatory. Call
|
|
380
|
+
`completeAuthorization(callbackUrl)` when the application receives the deep
|
|
381
|
+
link.
|
|
382
|
+
|
|
383
|
+
New authorizations require encrypted relay protocol 1 by default. The SDK keeps
|
|
384
|
+
independent non-extractable P-256 ECDH agreement and ECDSA signing keys plus an
|
|
385
|
+
atomic message counter in IndexedDB. It encrypts operation inputs for the
|
|
386
|
+
connector, decrypts connector results locally, and signs hosted authority
|
|
387
|
+
requests with the independent signing key. Set `relayEncryption: "disabled"`
|
|
388
|
+
only for development where end-to-end relay encryption is intentionally
|
|
389
|
+
unavailable; an encrypted grant never falls back to plaintext.
|
|
390
|
+
|
|
391
|
+
For a hosted collection, the same authorization exchange returns a short-lived,
|
|
392
|
+
grant-bound provider capability. The SDK routes operations directly to the
|
|
393
|
+
hosted Rust data plane, binds browser requests to the approved callback origin, and does
|
|
394
|
+
not send record payloads through the Connect control plane. Refresh rotation,
|
|
395
|
+
permission narrowing, and revocation keep the same public SDK behavior across
|
|
396
|
+
local and hosted authorities.
|
|
397
|
+
|
|
398
|
+
`sync()` exposes a provider-neutral sync transport without exposing the
|
|
399
|
+
provider credential. It refreshes the grant-bound capability as needed and can
|
|
400
|
+
be passed directly to `@mdbase-dev/connect-sync` for an offline application cache.
|
|
401
|
+
|
|
402
|
+
Record-facing code can depend on `MdbaseCollectionClient` instead of the OAuth
|
|
403
|
+
client. It accepts a small `MdbaseCollectionTransport`, which is the stable seam
|
|
404
|
+
used by Connect, the developer sandbox, and future hosted providers. This keeps
|
|
405
|
+
application logic independent of authorization and deployment topology.
|
package/dist/async.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function abortableDelay(milliseconds: number, signal?: AbortSignal): Promise<void>;
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function abortableDelay(milliseconds, signal) {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
if (signal?.aborted) {
|
|
4
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const timeout = setTimeout(resolve, milliseconds);
|
|
8
|
+
signal?.addEventListener("abort", () => {
|
|
9
|
+
clearTimeout(timeout);
|
|
10
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
11
|
+
}, { once: true });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MdbaseConnectError } from "./errors.js";
|
|
2
|
+
export function cleanAuthorizationParameters(url) {
|
|
3
|
+
for (const parameter of ["code", "state", "error", "error_description"]) {
|
|
4
|
+
url.searchParams.delete(parameter);
|
|
5
|
+
}
|
|
6
|
+
return url;
|
|
7
|
+
}
|
|
8
|
+
export function isAuthorizationCallbackUrl(value) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return url.searchParams.has("state")
|
|
17
|
+
&& (url.searchParams.has("code") || url.searchParams.has("error"));
|
|
18
|
+
}
|
|
19
|
+
export function authorizationReturnToFromError(error) {
|
|
20
|
+
if (!(error instanceof MdbaseConnectError)
|
|
21
|
+
|| !error.details
|
|
22
|
+
|| typeof error.details !== "object"
|
|
23
|
+
|| Array.isArray(error.details))
|
|
24
|
+
return undefined;
|
|
25
|
+
const returnTo = error.details.returnTo;
|
|
26
|
+
return typeof returnTo === "string" ? returnTo : undefined;
|
|
27
|
+
}
|
package/dist/base64.d.ts
ADDED
package/dist/base64.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function randomBase64Url(size) {
|
|
2
|
+
const bytes = new Uint8Array(size);
|
|
3
|
+
crypto.getRandomValues(bytes);
|
|
4
|
+
return bytesToBase64Url(bytes);
|
|
5
|
+
}
|
|
6
|
+
export function bytesToBase64Url(bytes) {
|
|
7
|
+
let binary = "";
|
|
8
|
+
for (const byte of bytes)
|
|
9
|
+
binary += String.fromCharCode(byte);
|
|
10
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
11
|
+
}
|
|
12
|
+
export function base64UrlBytes(value) {
|
|
13
|
+
const padded = value.replaceAll("-", "+").replaceAll("_", "/")
|
|
14
|
+
.padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
15
|
+
const binary = atob(padded);
|
|
16
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
17
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
18
|
+
bytes[index] = binary.charCodeAt(index);
|
|
19
|
+
}
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|