@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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +696 -0
  3. package/dist/advanced.d.ts +9 -0
  4. package/dist/advanced.js +6 -0
  5. package/dist/application-contract.d.ts +22 -0
  6. package/dist/application-contract.js +75 -0
  7. package/dist/application-identity.d.ts +34 -0
  8. package/dist/application-identity.js +148 -0
  9. package/dist/application-session.d.ts +167 -0
  10. package/dist/application-session.js +722 -0
  11. package/dist/async.d.ts +1 -0
  12. package/dist/async.js +19 -0
  13. package/dist/authority-proof.d.ts +3 -0
  14. package/dist/authority-proof.js +24 -0
  15. package/dist/authority-sync-request.d.ts +3 -0
  16. package/dist/authority-sync-request.js +20 -0
  17. package/dist/authorization-refresh.d.ts +14 -0
  18. package/dist/authorization-refresh.js +41 -0
  19. package/dist/authorization-types.d.ts +7 -0
  20. package/dist/authorization-types.js +1 -0
  21. package/dist/authorization-url.d.ts +5 -0
  22. package/dist/authorization-url.js +34 -0
  23. package/dist/base64.d.ts +3 -0
  24. package/dist/base64.js +21 -0
  25. package/dist/browser/integrity.json +4 -0
  26. package/dist/browser/mdbase-connect.min.js +3 -0
  27. package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
  28. package/dist/browser.d.ts +3 -0
  29. package/dist/browser.js +5 -0
  30. package/dist/capabilities.d.ts +24 -0
  31. package/dist/capabilities.js +91 -0
  32. package/dist/collection-client.d.ts +66 -0
  33. package/dist/collection-client.js +686 -0
  34. package/dist/connect-authorization-helpers.d.ts +4 -0
  35. package/dist/connect-authorization-helpers.js +25 -0
  36. package/dist/connect-options.d.ts +35 -0
  37. package/dist/connect-options.js +1 -0
  38. package/dist/connect.d.ts +66 -0
  39. package/dist/connect.js +711 -0
  40. package/dist/connection-file-transport.d.ts +34 -0
  41. package/dist/connection-file-transport.js +85 -0
  42. package/dist/connection-index.d.ts +3 -0
  43. package/dist/connection-index.js +20 -0
  44. package/dist/connection-notifications.d.ts +31 -0
  45. package/dist/connection-notifications.js +197 -0
  46. package/dist/connection-transport-internals.d.ts +24 -0
  47. package/dist/connection-transport-internals.js +1 -0
  48. package/dist/connection-transport.d.ts +64 -0
  49. package/dist/connection-transport.js +725 -0
  50. package/dist/connection-types.d.ts +38 -0
  51. package/dist/connection-types.js +1 -0
  52. package/dist/connection.d.ts +175 -0
  53. package/dist/connection.js +606 -0
  54. package/dist/crypto-entry.d.ts +7 -0
  55. package/dist/crypto-entry.js +4 -0
  56. package/dist/crypto.d.ts +63 -0
  57. package/dist/crypto.js +409 -0
  58. package/dist/direct-access.d.ts +3 -0
  59. package/dist/direct-access.js +29 -0
  60. package/dist/errors.d.ts +45 -0
  61. package/dist/errors.js +142 -0
  62. package/dist/external-store.d.ts +10 -0
  63. package/dist/external-store.js +10 -0
  64. package/dist/file-crypto.d.ts +25 -0
  65. package/dist/file-crypto.js +132 -0
  66. package/dist/file-sha256.d.ts +12 -0
  67. package/dist/file-sha256.js +143 -0
  68. package/dist/file-stream-source.d.ts +17 -0
  69. package/dist/file-stream-source.js +157 -0
  70. package/dist/file-transfer-internals.d.ts +23 -0
  71. package/dist/file-transfer-internals.js +228 -0
  72. package/dist/files.d.ts +115 -0
  73. package/dist/files.js +563 -0
  74. package/dist/fresh-encrypted-request.d.ts +4 -0
  75. package/dist/fresh-encrypted-request.js +28 -0
  76. package/dist/grant-key-leases.d.ts +24 -0
  77. package/dist/grant-key-leases.js +144 -0
  78. package/dist/hosted-file-request.d.ts +6 -0
  79. package/dist/hosted-file-request.js +63 -0
  80. package/dist/hosted-operation-response.d.ts +5 -0
  81. package/dist/hosted-operation-response.js +26 -0
  82. package/dist/index.d.ts +27 -0
  83. package/dist/index.js +10 -0
  84. package/dist/internal-types.d.ts +100 -0
  85. package/dist/internal-types.js +1 -0
  86. package/dist/local-file-transport.d.ts +37 -0
  87. package/dist/local-file-transport.js +457 -0
  88. package/dist/mdbase-connect.d.ts +28 -0
  89. package/dist/mdbase-connect.js +53 -0
  90. package/dist/notifications.d.ts +48 -0
  91. package/dist/notifications.js +52 -0
  92. package/dist/operation-helpers.d.ts +27 -0
  93. package/dist/operation-helpers.js +144 -0
  94. package/dist/operation-types.d.ts +707 -0
  95. package/dist/operation-types.js +1 -0
  96. package/dist/outcomes.d.ts +51 -0
  97. package/dist/outcomes.js +194 -0
  98. package/dist/pending-mutation-store.d.ts +13 -0
  99. package/dist/pending-mutation-store.js +64 -0
  100. package/dist/pending-recovery-transports.d.ts +5 -0
  101. package/dist/pending-recovery-transports.js +52 -0
  102. package/dist/popup-authorization.d.ts +31 -0
  103. package/dist/popup-authorization.js +100 -0
  104. package/dist/query-pagination.d.ts +6 -0
  105. package/dist/query-pagination.js +119 -0
  106. package/dist/request-budget.d.ts +34 -0
  107. package/dist/request-budget.js +104 -0
  108. package/dist/request-coordinator.d.ts +29 -0
  109. package/dist/request-coordinator.js +213 -0
  110. package/dist/retired-grant-keys.d.ts +9 -0
  111. package/dist/retired-grant-keys.js +56 -0
  112. package/dist/runtime-utils.d.ts +52 -0
  113. package/dist/runtime-utils.js +368 -0
  114. package/dist/saved-view-pagination.d.ts +6 -0
  115. package/dist/saved-view-pagination.js +76 -0
  116. package/dist/selection.d.ts +48 -0
  117. package/dist/selection.js +112 -0
  118. package/dist/session.d.ts +114 -0
  119. package/dist/session.js +569 -0
  120. package/dist/shared-operation.d.ts +7 -0
  121. package/dist/shared-operation.js +41 -0
  122. package/dist/stored-token.d.ts +10 -0
  123. package/dist/stored-token.js +64 -0
  124. package/dist/structured-readiness.d.ts +77 -0
  125. package/dist/structured-readiness.js +126 -0
  126. package/dist/token-response.d.ts +12 -0
  127. package/dist/token-response.js +81 -0
  128. package/dist/transient-retry.d.ts +22 -0
  129. package/dist/transient-retry.js +59 -0
  130. package/dist/watch-policy.d.ts +8 -0
  131. package/dist/watch-policy.js +12 -0
  132. 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.