@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
package/README.md
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
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
|
+
Ordinary applications install only `@mdbase-dev/connect`. Add
|
|
11
|
+
`@mdbase-dev/connect-dev` for declaration/type-pack authoring,
|
|
12
|
+
`@mdbase-dev/connect-testing` for supported test fixtures,
|
|
13
|
+
`@mdbase-dev/connect-sync` for an offline mirror, or the Pickle package only
|
|
14
|
+
when the application uses that feature. Wire and authority implementations use
|
|
15
|
+
`@mdbase-dev/connect-protocol`; ordinary application code does not.
|
|
16
|
+
|
|
17
|
+
The complete developer guide covers
|
|
18
|
+
[setup](https://mdbase.dev/sdk/quickstart/),
|
|
19
|
+
[manifests and contracts](https://mdbase.dev/sdk/manifest/),
|
|
20
|
+
[authorization](https://mdbase.dev/sdk/authorization/),
|
|
21
|
+
[operations](https://mdbase.dev/sdk/operations/),
|
|
22
|
+
[routing](https://mdbase.dev/sdk/routing/),
|
|
23
|
+
[offline sync](https://mdbase.dev/sdk/offline-sync/), and
|
|
24
|
+
[testing](https://mdbase.dev/sdk/testing/).
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const mdbase = new MdbaseConnect({
|
|
28
|
+
serverUrl: "https://connect.mdbase.dev",
|
|
29
|
+
manifest: new URL(".well-known/mdbase-app.json", location.href).href,
|
|
30
|
+
redirectUri: "https://workouts.example/auth/mdbase/callback",
|
|
31
|
+
timeouts: {
|
|
32
|
+
requestMs: 15_000,
|
|
33
|
+
watchStartMs: 20_000,
|
|
34
|
+
uploadMs: 120_000,
|
|
35
|
+
syncMs: 60_000
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const session = mdbase.application({
|
|
39
|
+
selection: new MdbaseBrowserSelection()
|
|
40
|
+
});
|
|
41
|
+
const unsubscribeSession = session.subscribe(() => render(session.getSnapshot()));
|
|
42
|
+
|
|
43
|
+
const startup = new AbortController();
|
|
44
|
+
const started = await session.start({ signal: startup.signal, timeoutMs: 20_000 });
|
|
45
|
+
if (!started.ok) {
|
|
46
|
+
renderProblem(started.problem);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (session.getSnapshot().status === "unselected") {
|
|
50
|
+
const authorized = await session.authorize("choose");
|
|
51
|
+
if (!authorized.ok) {
|
|
52
|
+
renderProblem(authorized.problem);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const snapshot = session.getSnapshot();
|
|
58
|
+
if (snapshot.status !== "ready") throw new Error("Choose an authorized collection.");
|
|
59
|
+
const connection = session.connection();
|
|
60
|
+
if (!connection) throw new Error("The ready session has no active connection.");
|
|
61
|
+
const queried = await connection.query({ types: ["workout"] });
|
|
62
|
+
if (!queried.ok) {
|
|
63
|
+
renderProblem(queried.problem);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const workouts = queried.value;
|
|
67
|
+
const current = await connection.read({ path: workouts.results[0].path });
|
|
68
|
+
if (!current.ok) {
|
|
69
|
+
renderProblem(current.problem);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const updated = await connection.update({
|
|
73
|
+
path: current.value.path,
|
|
74
|
+
patch: { completed: true },
|
|
75
|
+
ifRevision: current.value.revision
|
|
76
|
+
});
|
|
77
|
+
if (!updated.ok) renderProblem(updated.problem);
|
|
78
|
+
|
|
79
|
+
const pageLifetime = new AbortController();
|
|
80
|
+
const watch = await connection.watch(
|
|
81
|
+
{ lifetimeSignal: pageLifetime.signal },
|
|
82
|
+
{ timeoutMs: 20_000 }
|
|
83
|
+
);
|
|
84
|
+
if (!watch.ok) {
|
|
85
|
+
renderProblem(watch.problem);
|
|
86
|
+
} else {
|
|
87
|
+
const unsubscribeWatch = watch.value.subscribe(
|
|
88
|
+
(change) => console.log(change.type, change.payload.path),
|
|
89
|
+
renderWatchStatus,
|
|
90
|
+
renderProblem
|
|
91
|
+
);
|
|
92
|
+
// On navigation: unsubscribeWatch(); watch.value.close();
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Expected failures are returned as typed `ConnectOutcome` values. Exceptions are
|
|
97
|
+
reserved for programming errors and broken SDK invariants. See
|
|
98
|
+
[typed outcomes and recovery](../../docs/sdk-outcomes.md) for the complete
|
|
99
|
+
problem model, setup failures, mutation uncertainty, and UI guidance.
|
|
100
|
+
|
|
101
|
+
SDK call options, operation inputs, results, descriptors, and progress events
|
|
102
|
+
use camelCase. The client translates canonical snake_case protocol payloads at
|
|
103
|
+
the authority boundary. Versioned JSON documents such as `MdbaseAppManifest`
|
|
104
|
+
and `TypePackProvision`, problem detail objects, diagnostics, and user-owned
|
|
105
|
+
frontmatter retain the exact field names defined by their own schemas.
|
|
106
|
+
|
|
107
|
+
React applications can use the same session without introducing another state
|
|
108
|
+
owner:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const store = externalStore(session);
|
|
112
|
+
|
|
113
|
+
function useMdbaseSession() {
|
|
114
|
+
return useSyncExternalStore(
|
|
115
|
+
store.subscribe,
|
|
116
|
+
store.getSnapshot,
|
|
117
|
+
store.getSnapshot
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Browser and native callbacks share the same typed completion boundary:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const browserResult = await session.completeAuthorization(location.href, {
|
|
126
|
+
timeoutMs: 15_000
|
|
127
|
+
});
|
|
128
|
+
const nativeResult = await session.completeAuthorization(deepLinkUrl, {
|
|
129
|
+
signal: appForeground.signal,
|
|
130
|
+
timeoutMs: 15_000
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Unknown writes are recovered through their durable handles, never by calling
|
|
135
|
+
the original mutation again with reconstructed input:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
for (const pending of connection.pendingMutations()) {
|
|
139
|
+
showRecoveryAction(pending.requestId, async () => {
|
|
140
|
+
const recovered = await pending.recover({ timeoutMs: 30_000 });
|
|
141
|
+
if (!recovered.ok) renderProblem(recovered.problem);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`MdbaseConnect` is the application-level authorization registry.
|
|
147
|
+
`MdbaseApplicationSession` is the normal application boundary: it owns active
|
|
148
|
+
collection selection, authorization completion, semantic capabilities,
|
|
149
|
+
definition compatibility, and one reactive snapshot. Applications declare
|
|
150
|
+
versioned capabilities in their manifest; they never maintain a parallel array
|
|
151
|
+
of protocol operations. Use `getSnapshot()` and `subscribe()` directly or
|
|
152
|
+
through your framework's external-store integration.
|
|
153
|
+
|
|
154
|
+
### Legacy v1 and capability-group v2 declarations
|
|
155
|
+
|
|
156
|
+
The same SDK accepts both declaration versions without translating legacy exact
|
|
157
|
+
operations into broader groups. `requirements.capabilities.contract_version`
|
|
158
|
+
selects the semantics; an absent capability declaration means legacy v1.
|
|
159
|
+
`manifest_version` remains the document-format version, not this selector.
|
|
160
|
+
|
|
161
|
+
- **V1:** `authorize({ operations })`, `connection.requestOperations(operations)`,
|
|
162
|
+
and `session.ensureOperations(operations)` retain independent exact operations.
|
|
163
|
+
Incremental requests retain the existing exact grant. Legacy `files.actions`
|
|
164
|
+
and capability aliases (for example `records.update` and `files.read`) remain
|
|
165
|
+
supported. Bare registry/connection `authorize()` retains the predecessor's
|
|
166
|
+
`describe, changes, read, query` default; application sessions derive their
|
|
167
|
+
exact operation selection from the legacy capability declaration.
|
|
168
|
+
- **V2:** use `capabilities`, `requestCapabilities()`, and `ensureCapabilities()`.
|
|
169
|
+
Required groups are always included; omitted optional selection requests all
|
|
170
|
+
declared optional groups. Incremental requests retain already granted declared
|
|
171
|
+
groups. Files use `required`/`optional` actions.
|
|
172
|
+
|
|
173
|
+
Mixing `operations` and `capabilities`, or using an operations authorization API
|
|
174
|
+
with a v2 declaration, returns `invalid_application_manifest`, including for
|
|
175
|
+
JavaScript callers and already-sufficient grants. A rejected v2 request is never
|
|
176
|
+
retried as v1. Saved credentials and pending mutations are not translated or
|
|
177
|
+
replayed when selecting a semantic version; signed authorization retains its
|
|
178
|
+
registered declaration digest and exact recovery-transport requirements.
|
|
179
|
+
|
|
180
|
+
The SDK's `MdbaseAppManifest` and capability input types are explicit v1/v2
|
|
181
|
+
unions. `LegacyMdbaseAppManifest`, `LegacyApplicationRequirements`, and
|
|
182
|
+
`MdbaseApplicationRequirements` are available for explicit annotations. The
|
|
183
|
+
protocol package's canonical `ApplicationRequirements` remains v2-only.
|
|
184
|
+
Application sessions still require an explicit full-collection, versioned
|
|
185
|
+
capability declaration; no-capability legacy declarations can use the registry
|
|
186
|
+
or advanced session APIs.
|
|
187
|
+
|
|
188
|
+
This bridge is not a v2 readiness or rollout gate: structured v2 file,
|
|
189
|
+
notification, offline-runtime, and authority readiness evidence remains
|
|
190
|
+
incomplete. Legacy readiness aliases are evaluated only for v1. Positive server
|
|
191
|
+
and selected-authority v2 support gating remains separate work; this SDK change
|
|
192
|
+
does not advertise new support or authorize automatic consumer conversion.
|
|
193
|
+
|
|
194
|
+
Before and during startup the session distinguishes `not_started`, `starting`,
|
|
195
|
+
and `start_failed`; the last state carries the original typed problem and a
|
|
196
|
+
later `start()` retries. `destroyed` is terminal. Once started, the session
|
|
197
|
+
distinguishes `authorization_required`, `checking_setup`,
|
|
198
|
+
`setup_review_required`, `ready`, `unavailable`, and `blocked`. Setup
|
|
199
|
+
inspection is read-only.
|
|
200
|
+
While startup owns a provisional connection, synchronous methods return the
|
|
201
|
+
catalogued `session_starting` outcome and cannot mutate selection or grants.
|
|
202
|
+
After `start_failed`, every lifecycle-dependent method returns the exact
|
|
203
|
+
`snapshot.problem` object.
|
|
204
|
+
|
|
205
|
+
If an update is needed, render the supplied plan and apply the exact assessment
|
|
206
|
+
the user reviewed:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const snapshot = session.getSnapshot();
|
|
210
|
+
if (snapshot.status === "setup_review_required") {
|
|
211
|
+
renderCollectionSetup(snapshot.update);
|
|
212
|
+
const applied = await session.applyCollectionSetup({ timeoutMs: 30_000 });
|
|
213
|
+
if (!applied.ok) renderProblem(applied.problem);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
A `ready` snapshot may be `cached` for immediate startup and is then reverified
|
|
218
|
+
in the background. Route details remain diagnostics; feature UI should read
|
|
219
|
+
`snapshot.capabilities`, whose values explain whether a capability is available,
|
|
220
|
+
needs authorization/setup, is temporarily unavailable, or is unsupported by the
|
|
221
|
+
selected authority.
|
|
222
|
+
|
|
223
|
+
`MdbaseBrowserSelection` keeps the stable collection ID in
|
|
224
|
+
`?collection=<id>`, preserves unrelated path/query/hash and router state, and
|
|
225
|
+
reports browser back/forward
|
|
226
|
+
changes to the session. The session auto-selects only when exactly one saved
|
|
227
|
+
connection exists. Switching is state-driven and must not reload the page:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const selected = session.select(collectionId, { history: "replace" });
|
|
231
|
+
const cleared = session.clearSelection();
|
|
232
|
+
const forgotten = session.forget(collectionId);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
These synchronous methods return lifecycle-aware `ConnectOutcome` values.
|
|
236
|
+
Explicit `forget()` and `forgetAll()` remove the saved grant, all pending
|
|
237
|
+
mutation recovery records for that connection, and coordinate grant-key cleanup
|
|
238
|
+
with other tabs and workers through Web Locks. Browsers without Web Locks retain
|
|
239
|
+
retired and forgotten key material in the configured key store rather than risk
|
|
240
|
+
deleting a key another context is using; applications that require physical key
|
|
241
|
+
removal must clear that store only after closing all application contexts.
|
|
242
|
+
Automatic authorization invalidation keeps pending recovery state so
|
|
243
|
+
reauthorization can resume an outcome-unknown mutation safely.
|
|
244
|
+
Async lifecycle-dependent methods wait for an in-progress `start()` within the
|
|
245
|
+
same request budget and return typed lifecycle, timeout, or cancellation
|
|
246
|
+
outcomes rather than throwing for session state.
|
|
247
|
+
|
|
248
|
+
If a saved grant belongs to an earlier registered identity for the same
|
|
249
|
+
application declaration, the snapshot is `authorization_required`. Connect
|
|
250
|
+
does not discard or replay that grant's pending mutations and does not open an
|
|
251
|
+
authorization flow automatically; call `authorize("selected")` explicitly.
|
|
252
|
+
|
|
253
|
+
Selection validates the saved authorization before changing the URL. Explicit
|
|
254
|
+
unavailable IDs remain authoritative so applications can offer exact
|
|
255
|
+
reauthorization or another collection:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
await session.authorize("selected"); // exact bookmarked collection
|
|
259
|
+
await session.authorize("choose"); // any compatible collection
|
|
260
|
+
await session.authorize({ collectionId }); // exact adoption/migration target
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Web applications can keep their current page visible while Connect owns the
|
|
264
|
+
collection and permission decision:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
await session.authorize("choose", { presentation: "popup" });
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Call popup authorization directly from a click or other user gesture. The SDK
|
|
271
|
+
pre-opens one Connect window (browsers may show it as a tab), waits for the
|
|
272
|
+
registered callback to complete in that window, updates the existing session,
|
|
273
|
+
and closes it. If the browser blocks the popup, the same request safely falls
|
|
274
|
+
back to the existing full-page redirect. `presentation: "redirect"` remains
|
|
275
|
+
the default. Applications still run their ordinary callback handling in the
|
|
276
|
+
popup; Connect remains the sole owner of compatible collection choice and exact
|
|
277
|
+
permission review.
|
|
278
|
+
|
|
279
|
+
Collection IDs are opaque non-secret locators that may appear in browser
|
|
280
|
+
history and logs. Grants remain the authorization boundary, and names are
|
|
281
|
+
display text that may change.
|
|
282
|
+
|
|
283
|
+
The SDK keeps one non-extractable P-256 installation signing identity per
|
|
284
|
+
server/application and signs every authorization ceiling with it. Grant
|
|
285
|
+
agreement and signing keys remain disposable and are replaced on
|
|
286
|
+
reauthorization. For local collections, the SDK also pins the connector public
|
|
287
|
+
key after the first successful authorization and rejects silent replacement as
|
|
288
|
+
`connector_identity_changed`, even after the grant is forgotten. Only call
|
|
289
|
+
`forgetConnectorIdentity(connectorId)` after independently verifying an
|
|
290
|
+
intentional connector replacement.
|
|
291
|
+
|
|
292
|
+
Bundled v1 application manifests can declare runtime notification criteria.
|
|
293
|
+
Register a service worker from a user gesture to receive standards-based Web
|
|
294
|
+
Push while the app is closed:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const worker = await navigator.serviceWorker.register("/service-worker.js");
|
|
298
|
+
await connection.registerNotifications({
|
|
299
|
+
serviceWorker: worker,
|
|
300
|
+
criteria: ["workout.reminder"]
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
In the service worker:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { showMdbasePushNotification } from "@mdbase-dev/connect";
|
|
308
|
+
|
|
309
|
+
self.addEventListener("push", (event) => {
|
|
310
|
+
event.waitUntil(
|
|
311
|
+
showMdbasePushNotification(self.registration, event.data?.json())
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Pushes contain an opaque signal and cursor plus static declaration presentation,
|
|
317
|
+
never record paths or content. Treat them as a wake-up hint and read current
|
|
318
|
+
authorized state after opening. Registration atomically replaces the selected
|
|
319
|
+
criteria for that installation. See the
|
|
320
|
+
[runtime notifications guide](../../docs/notifications.md) for declaration and
|
|
321
|
+
deployment examples.
|
|
322
|
+
|
|
323
|
+
Native shells can use one FCM token for Android and iOS when the declaration
|
|
324
|
+
declares `notifications.native_delivery.mode` as `managed_fcm`:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
await connection.registerNativeNotifications({
|
|
328
|
+
token: await nativeMessaging.getToken(),
|
|
329
|
+
criteria: ["workout.reminder"]
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Re-register when Firebase rotates the token. Parse the string-valued
|
|
334
|
+
notification data with `parseMdbaseNativeNotificationData()`, refresh current
|
|
335
|
+
collection state, and call `unregisterNativeNotifications()` before deleting
|
|
336
|
+
the token on opt-out. The public Firebase project ID is read from the
|
|
337
|
+
application declaration; service credentials are never embedded in the app.
|
|
338
|
+
|
|
339
|
+
Connect-managed FCM makes Connect a trusted sender for the application's
|
|
340
|
+
Firebase project. It suits single-owner deployments; applications with a
|
|
341
|
+
broader audience should normally declare signed webhook delivery and keep
|
|
342
|
+
their Apple/Firebase credentials in their own backend. The notifications guide
|
|
343
|
+
documents both threat models. New or changed declaration criteria never silently
|
|
344
|
+
broaden an existing grant: handle `notification_reauthorization_required` by
|
|
345
|
+
running authorization again before retrying registration.
|
|
346
|
+
|
|
347
|
+
Before opening a feature, request its declared capability group instead of
|
|
348
|
+
selecting individual transport operations:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
await connection.requestCapabilities(["records.edit"]);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`requestCapabilities()` is a no-op when the current grant already includes the
|
|
355
|
+
complete group. When authorization is needed, required groups remain fixed and
|
|
356
|
+
the requested optional groups are expanded atomically. An
|
|
357
|
+
`insufficient_access` problem carries the internal `granted_operations`,
|
|
358
|
+
`missing_operations`, and `required_operations` metadata with a `reauthorize`
|
|
359
|
+
recovery action.
|
|
360
|
+
|
|
361
|
+
Applications that declare an `mdbase.runtime.timer.fired` notification criterion can keep
|
|
362
|
+
one-shot reminders at the collection authority:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
await connection.reconcileTimers({
|
|
366
|
+
namespace: "workout-reminders",
|
|
367
|
+
criterionId: "workout.reminder",
|
|
368
|
+
timers: [{
|
|
369
|
+
id: "workout-42",
|
|
370
|
+
fireAt: new Date("2026-07-25T10:00:00Z").toISOString()
|
|
371
|
+
}]
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The SDK also exposes `listTimers()`, `putTimer()`, and `cancelTimer()`.
|
|
376
|
+
Reconciliation is atomic and is normally the safest way to project a complete
|
|
377
|
+
desired timer set. IDs and optional timer data stay at the local connector or
|
|
378
|
+
hosted provider and are not included in notification signals.
|
|
379
|
+
|
|
380
|
+
When the local connector definitively rejects an encrypted grant, the SDK does
|
|
381
|
+
not bypass that decision through the relay. It classifies the error as requiring
|
|
382
|
+
authorization, removes only the matching stale credential, and emits a `null`
|
|
383
|
+
connection through `onConnectionChange()`. A subsequent
|
|
384
|
+
`requestCapabilities()` call therefore starts authorization instead of trusting
|
|
385
|
+
the stale cached capability list.
|
|
386
|
+
|
|
387
|
+
Applications can register and maintain type definitions when their
|
|
388
|
+
collection-wide grant includes the corresponding operations. Type source is returned with a revision token so updates cannot
|
|
389
|
+
silently overwrite a definition changed by another application:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const created = await connection.createType({
|
|
393
|
+
document: `---
|
|
394
|
+
kind: mdbase.type
|
|
395
|
+
name: workout
|
|
396
|
+
version: 1
|
|
397
|
+
schema:
|
|
398
|
+
dialect: json-schema-2020-12
|
|
399
|
+
value:
|
|
400
|
+
type: object
|
|
401
|
+
---
|
|
402
|
+
`
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!created.ok) return renderProblem(created.problem);
|
|
406
|
+
const current = await connection.readType({ name: "workout" });
|
|
407
|
+
if (!current.ok) return renderProblem(current.problem);
|
|
408
|
+
const updatedType = await connection.updateType({
|
|
409
|
+
path: current.value.path,
|
|
410
|
+
document: current.value.document.replace("version: 1", "version: 2"),
|
|
411
|
+
ifRevision: current.value.revision
|
|
412
|
+
});
|
|
413
|
+
if (!updatedType.ok) renderProblem(updatedType.problem);
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Request `read_type`, `create_type`, and `update_type` during authorization.
|
|
417
|
+
A legacy contract-scoped grant is inactive and must be reauthorized; contracts
|
|
418
|
+
themselves never confer type-management permission.
|
|
419
|
+
|
|
420
|
+
To install a complete catalog pack without exposing its individual collection
|
|
421
|
+
paths, request `assess_type_pack` and `apply_type_pack`, fetch and verify the
|
|
422
|
+
published provision, then review and apply the authority's exact plan:
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
const response = await fetch(pack.provisionUrl);
|
|
426
|
+
const provision = await response.json();
|
|
427
|
+
const installedBy = "dev.example.catalog";
|
|
428
|
+
const assessment = await connection.assessTypePack({
|
|
429
|
+
provision,
|
|
430
|
+
installedBy
|
|
431
|
+
});
|
|
432
|
+
if (!assessment.ok) return renderProblem(assessment.problem);
|
|
433
|
+
|
|
434
|
+
renderDefinitionChanges(assessment.value);
|
|
435
|
+
const applied = await connection.applyTypePack({
|
|
436
|
+
provision,
|
|
437
|
+
installedBy,
|
|
438
|
+
expectedAssessmentDigest: assessment.value.assessmentDigest
|
|
439
|
+
});
|
|
440
|
+
if (!applied.ok) return renderProblem(applied.problem);
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
The caller remains responsible for verifying the catalog digest before sending
|
|
444
|
+
the provision. Apply rechecks the reviewed assessment inside the transaction;
|
|
445
|
+
stale plans and locally modified targets fail without partial writes.
|
|
446
|
+
|
|
447
|
+
For a local collection, ask for same-computer access from a user gesture:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
const status = await connection.checkDirectAccess();
|
|
451
|
+
if (!status.ok) return renderProblem(status.problem);
|
|
452
|
+
if (status.value === "permission_required") {
|
|
453
|
+
directButton.onclick = () => connection.requestDirectAccess();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
connection.onConnectionChange((info) => {
|
|
457
|
+
console.log(info?.route); // "direct", "relay", or "remote"
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
After permission is granted, all grantable operations prefer the fixed
|
|
462
|
+
loopback connector automatically. If it is absent or unavailable, the SDK
|
|
463
|
+
retries the exact encrypted envelope through the relay. Durable connector
|
|
464
|
+
receipts prevent an ambiguous direct write from running twice. The SDK retains
|
|
465
|
+
one unresolved encrypted mutation locally and requires that exact write to be
|
|
466
|
+
retried before a later mutation can overtake it. Set
|
|
467
|
+
`directAccess: "disabled"` only when an embedding environment cannot use
|
|
468
|
+
loopback requests.
|
|
469
|
+
|
|
470
|
+
The SDK loads the v1 manifest from the application's own bundle and
|
|
471
|
+
posts it inline to Connect. Connect identifies the exact canonical declaration
|
|
472
|
+
by its digest. The declared reverse-domain ID and name are presentation
|
|
473
|
+
metadata, not proof of a publisher; grants remain bound to the authorization
|
|
474
|
+
completed by that installation. No developer account, public manifest host, or
|
|
475
|
+
manually issued client secret is required.
|
|
476
|
+
|
|
477
|
+
Applications declare exact data-contract versions in their bundled manifest.
|
|
478
|
+
Connect offers collections that already provide those contracts or can install
|
|
479
|
+
the declared type pack safely. The developer helper computes every resource
|
|
480
|
+
digest:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import { defineTypePack } from "@mdbase-dev/connect-dev";
|
|
484
|
+
|
|
485
|
+
const contract = `---
|
|
486
|
+
kind: mdbase.contract
|
|
487
|
+
contract_type: record
|
|
488
|
+
id: example.work-item
|
|
489
|
+
version: 1.0.0
|
|
490
|
+
record_schema:
|
|
491
|
+
dialect: json-schema-2020-12
|
|
492
|
+
value:
|
|
493
|
+
type: object
|
|
494
|
+
required: [title]
|
|
495
|
+
properties:
|
|
496
|
+
title: { type: string }
|
|
497
|
+
---
|
|
498
|
+
`;
|
|
499
|
+
const type = `---
|
|
500
|
+
kind: mdbase.type
|
|
501
|
+
name: work-item
|
|
502
|
+
version: 1
|
|
503
|
+
schema:
|
|
504
|
+
dialect: json-schema-2020-12
|
|
505
|
+
value:
|
|
506
|
+
type: object
|
|
507
|
+
implements:
|
|
508
|
+
- contract: example.work-item
|
|
509
|
+
version: 1.0.0
|
|
510
|
+
fields: { title: title }
|
|
511
|
+
---
|
|
512
|
+
`;
|
|
513
|
+
|
|
514
|
+
const workItemPack = defineTypePack({
|
|
515
|
+
id: "example.work-items",
|
|
516
|
+
version: "1.0.0",
|
|
517
|
+
resources: [
|
|
518
|
+
{
|
|
519
|
+
kind: "contract",
|
|
520
|
+
mode: "managed",
|
|
521
|
+
source: "_contracts/example.work-item.md",
|
|
522
|
+
document: contract
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
kind: "type",
|
|
526
|
+
mode: "seed",
|
|
527
|
+
source: "_types/work-item.md",
|
|
528
|
+
document: type
|
|
529
|
+
}
|
|
530
|
+
]
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
export const manifest = {
|
|
534
|
+
manifest_version: 1,
|
|
535
|
+
id: "dev.mdbase.worklog",
|
|
536
|
+
name: "Worklog",
|
|
537
|
+
homepage: "https://worklog.example",
|
|
538
|
+
redirect_uris: ["https://worklog.example/auth/mdbase/callback"],
|
|
539
|
+
requirements: {
|
|
540
|
+
access: "full_collection",
|
|
541
|
+
contracts: workItemPack.provides
|
|
542
|
+
},
|
|
543
|
+
provisions: {
|
|
544
|
+
type_packs: [workItemPack]
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Set `collection_kind` to `hosted` when the application needs a durable
|
|
550
|
+
provider-backed collection and the offline sync transport returned by
|
|
551
|
+
`sync()`. Connect then offers and accepts collections with replication capability.
|
|
552
|
+
|
|
553
|
+
Every SDK manifest must explicitly set `requirements.access` to the N-1 wire
|
|
554
|
+
spelling `full_collection`. An active grant covers the entire selected
|
|
555
|
+
collection; omission and legacy `contract` scope never satisfy application
|
|
556
|
+
access and require reauthorization. Required contracts remain separate
|
|
557
|
+
compatibility, setup, validation, and semantic-adapter evidence.
|
|
558
|
+
|
|
559
|
+
`listViews()` returns each named view's selected result properties in display
|
|
560
|
+
order. Property descriptors retain source labels for projected and computed
|
|
561
|
+
values. `executeView()` returns their values on each result row.
|
|
562
|
+
|
|
563
|
+
Provisioning is part of the approval flow. The connector validates and
|
|
564
|
+
installs each missing type pack transactionally, verifies its exact contracts
|
|
565
|
+
and implementations, and creates the collection grant afterward. The application
|
|
566
|
+
is not granted collection-wide type-management access.
|
|
567
|
+
|
|
568
|
+
The SDK returns typed outcomes, carries successful mdbase diagnostics alongside
|
|
569
|
+
the value, carries revision tokens in typed record results, and accepts
|
|
570
|
+
`ifRevision` on mutations. `describe()` exposes
|
|
571
|
+
JSON Schemas, portable type definitions, canonical collection settings, and
|
|
572
|
+
first-class data contracts. `watch()` resumes from a local collection cursor;
|
|
573
|
+
the Connect server does not store the change feed.
|
|
574
|
+
`queryAll()` treats `timeoutMs` as one deadline for the complete paginated
|
|
575
|
+
query. The caller-driven `queryPages()` iterator instead accepts
|
|
576
|
+
`pageTimeoutMs`, an independent budget for each page, plus a lifetime `signal`.
|
|
577
|
+
Large hosted pages may omit `meta.totalCount` and return
|
|
578
|
+
`meta.totalCountOutcome: { status: "deferred", budget: "eager_summary_rows", ... }`
|
|
579
|
+
so page latency is not coupled to a collection-wide count. `meta.hasMore` and the
|
|
580
|
+
opaque cursor remain authoritative for paging. `queryAll()` reports the exact
|
|
581
|
+
number of returned records after it has intentionally consumed the complete
|
|
582
|
+
sequence.
|
|
583
|
+
`preflightRename()` and `preflightDelete()` run the canonical collection
|
|
584
|
+
operation without changing records or advancing the change cursor, so an app
|
|
585
|
+
can show authoritative reference impact before asking for confirmation.
|
|
586
|
+
`renameWithProgress()` and `deleteWithProgress()` expose preflight, ready,
|
|
587
|
+
applying, completed, and cancelled phases with an impact estimate. Cancellation
|
|
588
|
+
remains available during an encrypted local/relay mutation because the SDK
|
|
589
|
+
persists its exact encrypted request before dispatch. If waiting is cancelled
|
|
590
|
+
after dispatch, `pendingMutations()` exposes durable handles. A handle's
|
|
591
|
+
`recover()` method resends the exact stored request identity and bytes; callers
|
|
592
|
+
never reconstruct or resupply mutation input. Multiple interrupted writes are
|
|
593
|
+
independently recoverable. Other providers are cancellable until apply begins
|
|
594
|
+
and then run to a definitive response.
|
|
595
|
+
Authorization is retained in `localStorage` by default. Access tokens are
|
|
596
|
+
renewed with rotating refresh tokens; passing a custom `Storage` implementation
|
|
597
|
+
allows a host to choose another persistence boundary.
|
|
598
|
+
|
|
599
|
+
Native shells can pass `navigate` to open the approval URL in the system
|
|
600
|
+
browser and list a reverse-domain callback such as
|
|
601
|
+
`dev.mdbase.worklog://auth/mdbase/callback` in the bundled declaration. Its
|
|
602
|
+
scheme must match the declaration ID. `authorize()` returns `redirecting`; the
|
|
603
|
+
browser redirects to the declared callback with a short-lived code and state,
|
|
604
|
+
and the shell passes that URL to `completeAuthorization()`. PKCE prevents the
|
|
605
|
+
callback code from being exchanged without the pending application state and
|
|
606
|
+
verifier.
|
|
607
|
+
|
|
608
|
+
New authorizations require operation transport v3 and grant encryption profile
|
|
609
|
+
v1 by default. The SDK keeps independent non-extractable P-256 ECDH agreement
|
|
610
|
+
and ECDSA signing keys plus an atomic message counter in IndexedDB. It encrypts
|
|
611
|
+
operation inputs for the connector, decrypts connector results locally, and
|
|
612
|
+
signs hosted authority requests with the independent signing key. Set
|
|
613
|
+
`relayEncryption: "disabled"` only for development where end-to-end relay
|
|
614
|
+
encryption is intentionally unavailable; an encrypted grant never falls back
|
|
615
|
+
to plaintext.
|
|
616
|
+
|
|
617
|
+
For a hosted collection, the same authorization exchange returns a short-lived,
|
|
618
|
+
grant-bound provider capability. The SDK routes operations directly to the
|
|
619
|
+
hosted Rust data plane, binds browser requests to the approved callback origin, and does
|
|
620
|
+
not send record payloads through the Connect control plane. Refresh rotation,
|
|
621
|
+
permission narrowing, and revocation keep the same public SDK behavior across
|
|
622
|
+
local and hosted authorities.
|
|
623
|
+
|
|
624
|
+
`sync()` exposes a provider-neutral sync transport without exposing the
|
|
625
|
+
provider credential. It refreshes the grant-bound capability as needed and can
|
|
626
|
+
be passed directly to `@mdbase-dev/connect-sync` for an offline application cache.
|
|
627
|
+
|
|
628
|
+
Applications request non-Markdown files separately from record contracts:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
requirements: {
|
|
632
|
+
contracts: [],
|
|
633
|
+
files: {
|
|
634
|
+
actions: ["list", "read", "add", "replace", "move", "delete"],
|
|
635
|
+
scope: { kind: "selected_folders", folders: ["Photos"] }
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
The granted connection exposes one storage-neutral facade. Hosted uploads go
|
|
641
|
+
directly to private R2 objects through short-lived prepared requests. Downloads
|
|
642
|
+
use authenticated bounded ranges so the authority can recheck access between
|
|
643
|
+
parts. Object keys, multipart ETags, provider credentials, retries, and
|
|
644
|
+
integrity checks stay inside the SDK.
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
const saved = await connection.files.upload("Photos/image.jpg", browserFile, {
|
|
648
|
+
onProgress: ({ phase, transferredBytes, totalBytes }) => {
|
|
649
|
+
console.log(phase, transferredBytes, totalBytes);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const large = await connection.files.uploadStream("Media/video.mp4", {
|
|
654
|
+
size: manifest.size,
|
|
655
|
+
contentDigest: manifest.sha256,
|
|
656
|
+
stream: response.body!
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
for await (const file of connection.files.list({ folder: "Photos" })) {
|
|
660
|
+
const stream = await connection.files.downloadStream(file);
|
|
661
|
+
await stream.pipeTo(destination);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const moved = await connection.files.move(saved, "Archive/image.jpg");
|
|
665
|
+
await connection.files.delete(moved);
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
`download()` and `downloadBytes()` are convenient for values up to 64 MiB.
|
|
669
|
+
Use `downloadStream()` for larger files; it verifies the pinned revision while
|
|
670
|
+
forwarding response chunks with native stream backpressure rather than
|
|
671
|
+
buffering a complete hosted range.
|
|
672
|
+
The live stream retries unopened chunks and can switch a local download between
|
|
673
|
+
direct and relay. It does not persist progress after consumption stops or
|
|
674
|
+
resume a hosted range after part of that range has reached the consumer;
|
|
675
|
+
restart `downloadStream()` to reopen the pinned revision in those cases.
|
|
676
|
+
|
|
677
|
+
`upload()` accepts `Blob`, `ArrayBuffer`, and typed-array values and hashes them
|
|
678
|
+
before opening the transfer. `uploadStream()` accepts a `ReadableStream` or
|
|
679
|
+
async iterable plus its exact size and `sha256:…` commitment. It verifies the
|
|
680
|
+
stream while uploading sequentially without accumulating multiple file parts.
|
|
681
|
+
SDK memory is bounded by one assembled negotiated part plus at most one source
|
|
682
|
+
chunk; a single-part upload may therefore hold the complete file. Yielded
|
|
683
|
+
source chunks must fit within the authority's negotiated upload part; ordinary
|
|
684
|
+
browser and Node streams already produce much smaller chunks. After an
|
|
685
|
+
ambiguous failure, call it again with a newly opened source and the same
|
|
686
|
+
`transferId` to resume safely.
|
|
687
|
+
|
|
688
|
+
Both lifecycle methods are optimistic and use the descriptor revision by
|
|
689
|
+
default. Pass a stable `mutationId` when retrying after an ambiguous network
|
|
690
|
+
failure; the authority returns the original durable receipt instead of applying
|
|
691
|
+
the change twice.
|
|
692
|
+
|
|
693
|
+
Record-facing code can depend on `MdbaseCollectionClient` instead of the OAuth
|
|
694
|
+
client. It accepts a small `MdbaseCollectionTransport`, which is the stable seam
|
|
695
|
+
used by Connect, the developer sandbox, and future hosted providers. This keeps
|
|
696
|
+
application logic independent of authorization and deployment topology.
|