@mdbase-dev/connect 0.1.0-beta.30 → 0.1.0-beta.32

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 (64) hide show
  1. package/README.md +75 -19
  2. package/dist/advanced.d.ts +7 -0
  3. package/dist/advanced.js +4 -0
  4. package/dist/application-session.d.ts +10 -5
  5. package/dist/application-session.js +32 -16
  6. package/dist/async.js +11 -5
  7. package/dist/authority-proof.d.ts +3 -0
  8. package/dist/authority-proof.js +24 -0
  9. package/dist/browser/integrity.json +1 -1
  10. package/dist/browser/mdbase-connect.min.js +3 -3
  11. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  12. package/dist/browser.d.ts +3 -0
  13. package/dist/browser.js +5 -0
  14. package/dist/capabilities.js +2 -1
  15. package/dist/collection-client.d.ts +28 -28
  16. package/dist/collection-client.js +42 -42
  17. package/dist/connect-options.d.ts +12 -4
  18. package/dist/connect.d.ts +12 -4
  19. package/dist/connect.js +105 -104
  20. package/dist/connection-file-transport.d.ts +2 -2
  21. package/dist/connection-file-transport.js +4 -4
  22. package/dist/connection-notifications.d.ts +10 -4
  23. package/dist/connection-notifications.js +41 -23
  24. package/dist/connection-transport.d.ts +19 -15
  25. package/dist/connection-transport.js +202 -202
  26. package/dist/connection-types.d.ts +5 -4
  27. package/dist/connection.d.ts +43 -43
  28. package/dist/connection.js +221 -77
  29. package/dist/crypto-entry.d.ts +4 -0
  30. package/dist/crypto-entry.js +4 -0
  31. package/dist/crypto.js +4 -4
  32. package/dist/external-store.d.ts +10 -0
  33. package/dist/external-store.js +10 -0
  34. package/dist/file-transfer-internals.d.ts +22 -0
  35. package/dist/file-transfer-internals.js +186 -0
  36. package/dist/files.d.ts +16 -12
  37. package/dist/files.js +52 -206
  38. package/dist/hosted-file-request.js +3 -3
  39. package/dist/index.d.ts +2 -5
  40. package/dist/index.js +1 -5
  41. package/dist/internal-types.d.ts +8 -1
  42. package/dist/local-file-transport.d.ts +2 -2
  43. package/dist/local-file-transport.js +13 -13
  44. package/dist/mdbase-connect.d.ts +6 -4
  45. package/dist/mdbase-connect.js +9 -8
  46. package/dist/notifications.d.ts +3 -2
  47. package/dist/operation-helpers.d.ts +3 -3
  48. package/dist/operation-helpers.js +12 -29
  49. package/dist/operation-types.d.ts +33 -8
  50. package/dist/outcomes.d.ts +15 -15
  51. package/dist/outcomes.js +13 -1
  52. package/dist/pending-mutation-store.d.ts +13 -0
  53. package/dist/pending-mutation-store.js +64 -0
  54. package/dist/request-budget.d.ts +27 -0
  55. package/dist/request-budget.js +95 -0
  56. package/dist/runtime-utils.d.ts +2 -0
  57. package/dist/runtime-utils.js +19 -0
  58. package/dist/session.d.ts +4 -3
  59. package/dist/session.js +4 -4
  60. package/dist/stored-token.d.ts +10 -0
  61. package/dist/stored-token.js +60 -0
  62. package/dist/token-response.d.ts +11 -0
  63. package/dist/token-response.js +77 -0
  64. package/package.json +12 -2
package/README.md CHANGED
@@ -20,11 +20,18 @@ The complete developer guide covers
20
20
  const mdbase = new MdbaseConnect({
21
21
  serverUrl: "https://connect.mdbase.dev",
22
22
  manifest: new URL(".well-known/mdbase-app.json", location.href).href,
23
- redirectUri: "https://workouts.example/auth/mdbase/callback"
23
+ redirectUri: "https://workouts.example/auth/mdbase/callback",
24
+ timeouts: {
25
+ requestMs: 15_000,
26
+ watchStartMs: 20_000,
27
+ uploadMs: 120_000,
28
+ syncMs: 60_000
29
+ }
24
30
  });
25
- const session = mdbase.createApplicationSession({
31
+ const session = mdbase.application({
26
32
  selection: new MdbaseBrowserSelection()
27
33
  });
34
+ const unsubscribeSession = session.subscribe(() => render(session.getSnapshot()));
28
35
 
29
36
  const started = await session.start();
30
37
  if (!started.ok) {
@@ -56,12 +63,20 @@ const updated = await connection.update({
56
63
  });
57
64
  if (!updated.ok) renderProblem(updated.problem);
58
65
 
59
- for await (const change of connection.watch()) {
60
- if (!change.ok) {
61
- renderProblem(change.problem);
62
- break;
63
- }
64
- console.log(change.value.type, change.value.payload.path);
66
+ const pageLifetime = new AbortController();
67
+ const watch = await connection.watch(
68
+ { lifetimeSignal: pageLifetime.signal },
69
+ { timeoutMs: 20_000 }
70
+ );
71
+ if (!watch.ok) {
72
+ renderProblem(watch.problem);
73
+ } else {
74
+ const unsubscribeWatch = watch.value.subscribe(
75
+ (change) => console.log(change.type, change.payload.path),
76
+ renderWatchStatus,
77
+ renderProblem
78
+ );
79
+ // On navigation: unsubscribeWatch(); watch.value.close();
65
80
  }
66
81
  ```
67
82
 
@@ -70,6 +85,45 @@ reserved for programming errors and broken SDK invariants. See
70
85
  [typed outcomes and recovery](../../docs/sdk-outcomes.md) for the complete
71
86
  problem model, setup failures, mutation uncertainty, and UI guidance.
72
87
 
88
+ React applications can use the same session without introducing another state
89
+ owner:
90
+
91
+ ```ts
92
+ const store = externalStore(session);
93
+
94
+ function useMdbaseSession() {
95
+ return useSyncExternalStore(
96
+ store.subscribe,
97
+ store.getSnapshot,
98
+ store.getSnapshot
99
+ );
100
+ }
101
+ ```
102
+
103
+ Browser and native callbacks share the same typed completion boundary:
104
+
105
+ ```ts
106
+ const browserResult = await session.completeAuthorization(location.href, {
107
+ timeoutMs: 15_000
108
+ });
109
+ const nativeResult = await session.completeAuthorization(deepLinkUrl, {
110
+ signal: appForeground.signal,
111
+ timeoutMs: 15_000
112
+ });
113
+ ```
114
+
115
+ Unknown writes are recovered through their durable handles, never by calling
116
+ the original mutation again with reconstructed input:
117
+
118
+ ```ts
119
+ for (const pending of connection.pendingMutations()) {
120
+ showRecoveryAction(pending.requestId, async () => {
121
+ const recovered = await pending.recover({ timeoutMs: 30_000 });
122
+ if (!recovered.ok) renderProblem(recovered.problem);
123
+ });
124
+ }
125
+ ```
126
+
73
127
  `MdbaseConnect` is the application-level authorization registry.
74
128
  `MdbaseApplicationSession` is the normal application boundary: it owns active
75
129
  collection selection, authorization completion, semantic capabilities,
@@ -424,10 +478,11 @@ can show authoritative reference impact before asking for confirmation.
424
478
  applying, completed, and cancelled phases with an impact estimate. Cancellation
425
479
  remains available during an encrypted local/relay mutation because the SDK
426
480
  persists its exact encrypted request before dispatch. If waiting is cancelled
427
- after dispatch, `pendingMutation()` reports the interruption and
428
- `resumePendingMutation()` safely recovers the connector's durable receipt using
429
- the exact same input. Other providers are cancellable until apply begins and
430
- then run to a definitive response.
481
+ after dispatch, `pendingMutations()` exposes durable handles. A handle's
482
+ `recover()` method resends the exact stored request identity and bytes; callers
483
+ never reconstruct or resupply mutation input. Multiple interrupted writes are
484
+ independently recoverable. Other providers are cancellable until apply begins
485
+ and then run to a definitive response.
431
486
  Authorization is retained in `localStorage` by default. Access tokens are
432
487
  renewed with rotating refresh tokens; passing a custom `Storage` implementation
433
488
  allows a host to choose another persistence boundary.
@@ -441,13 +496,14 @@ and the shell passes that URL to `completeAuthorization()`. PKCE prevents the
441
496
  callback code from being exchanged without the pending application state and
442
497
  verifier.
443
498
 
444
- New authorizations require encrypted relay protocol 1 by default. The SDK keeps
445
- independent non-extractable P-256 ECDH agreement and ECDSA signing keys plus an
446
- atomic message counter in IndexedDB. It encrypts operation inputs for the
447
- connector, decrypts connector results locally, and signs hosted authority
448
- requests with the independent signing key. Set `relayEncryption: "disabled"`
449
- only for development where end-to-end relay encryption is intentionally
450
- unavailable; an encrypted grant never falls back to plaintext.
499
+ New authorizations require operation transport v2 and grant encryption profile
500
+ v1 by default. The SDK keeps independent non-extractable P-256 ECDH agreement
501
+ and ECDSA signing keys plus an atomic message counter in IndexedDB. It encrypts
502
+ operation inputs for the connector, decrypts connector results locally, and
503
+ signs hosted authority requests with the independent signing key. Set
504
+ `relayEncryption: "disabled"` only for development where end-to-end relay
505
+ encryption is intentionally unavailable; an encrypted grant never falls back
506
+ to plaintext.
451
507
 
452
508
  For a hosted collection, the same authorization exchange returns a short-lived,
453
509
  grant-bound provider capability. The SDK routes operations directly to the
@@ -0,0 +1,7 @@
1
+ /** Low-level construction and test seams. Ordinary applications use the root package. */
2
+ export { MdbaseCollectionClient } from "./collection-client.js";
3
+ export type { MdbaseCollectionTransport } from "./operation-types.js";
4
+ export { createPkce } from "./runtime-utils.js";
5
+ export type { MdbaseApplicationVerificationStore, MdbaseApplicationSessionConnect } from "./application-session.js";
6
+ export { MdbaseSession } from "./session.js";
7
+ export type { MdbaseSessionConnect, MdbaseSessionOptions } from "./session.js";
@@ -0,0 +1,4 @@
1
+ /** Low-level construction and test seams. Ordinary applications use the root package. */
2
+ export { MdbaseCollectionClient } from "./collection-client.js";
3
+ export { createPkce } from "./runtime-utils.js";
4
+ export { MdbaseSession } from "./session.js";
@@ -4,9 +4,11 @@ import type { MdbaseAuthorizationOutcome, MdbaseAuthorizeOptions, MdbaseConnecti
4
4
  import type { MdbaseConnectionInfo } from "./connection-types.js";
5
5
  import { type ConnectOutcome, type CollectionTypeProblemCode, type AuthorizationProblemCode, type RegistrationProblemCode, type SessionProblemCode } from "./outcomes.js";
6
6
  import type { MdbaseApplicationSelection, MdbaseSelectionHistory } from "./selection.js";
7
+ import type { ConnectRequestOptions } from "./operation-types.js";
8
+ import { type ResolvedConnectTimeouts } from "./request-budget.js";
7
9
  import { type MdbaseSessionConnect, type MdbaseUnavailableReason } from "./session.js";
8
10
  export interface MdbaseApplicationSessionConnect<Frontmatter extends JsonObject = JsonObject> extends MdbaseSessionConnect<Frontmatter> {
9
- manifest(): Promise<ConnectOutcome<MdbaseAppManifest, RegistrationProblemCode>>;
11
+ manifest(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAppManifest, RegistrationProblemCode>>;
10
12
  }
11
13
  export interface MdbaseApplicationSessionOptions {
12
14
  selection: MdbaseApplicationSelection;
@@ -80,6 +82,7 @@ export type MdbaseApplicationSessionSnapshot = {
80
82
  export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = JsonObject> {
81
83
  private readonly connect;
82
84
  private readonly options;
85
+ private readonly timeouts;
83
86
  private readonly listeners;
84
87
  private readonly verificationStore;
85
88
  private snapshot;
@@ -88,8 +91,9 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
88
91
  private stopBase?;
89
92
  private reviewEntries;
90
93
  private verificationGeneration;
91
- constructor(connect: MdbaseApplicationSessionConnect<Frontmatter>, options: MdbaseApplicationSessionOptions);
92
- start(): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, SessionProblemCode>>;
94
+ constructor(connect: MdbaseApplicationSessionConnect<Frontmatter>, options: MdbaseApplicationSessionOptions, timeouts?: ResolvedConnectTimeouts);
95
+ start(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, SessionProblemCode>>;
96
+ private startWithinBudget;
93
97
  destroy(): void;
94
98
  getSnapshot(): MdbaseApplicationSessionSnapshot;
95
99
  subscribe(listener: () => void): () => void;
@@ -104,12 +108,13 @@ export declare class MdbaseApplicationSession<Frontmatter extends JsonObject = J
104
108
  authorize(target: "choose" | "selected" | {
105
109
  collectionId: string;
106
110
  }, options?: Omit<MdbaseAuthorizeOptions, "operations" | "returnTo" | "target">): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
107
- handleAuthorizationCallback(callbackUrl: string): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
111
+ handleAuthorizationCallback(callbackUrl: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
112
+ completeAuthorization(callbackUrl?: string | URL, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
108
113
  ensureCapabilities(capabilities: ApplicationCapabilityId[]): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter> | {
109
114
  kind: "unchanged";
110
115
  connection: MdbaseConnection<Frontmatter>;
111
116
  }, SessionProblemCode>>;
112
- applyDefinitionUpdates(): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, CollectionTypeProblemCode | "collection_not_ready">>;
117
+ applyDefinitionUpdates(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationSessionSnapshot, CollectionTypeProblemCode | "collection_not_ready">>;
113
118
  private refresh;
114
119
  private verifyDefinitions;
115
120
  private context;
@@ -3,6 +3,8 @@ import { capabilityOperations, operationsForApplicationCapabilities } from "@mdb
3
3
  import { effectiveCapabilities } from "./capabilities.js";
4
4
  import { connectProblem } from "./errors.js";
5
5
  import { connectFailure, connectSuccess } from "./outcomes.js";
6
+ import { resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
7
+ import { defaultCallbackUrl } from "./runtime-utils.js";
6
8
  import { MdbaseSession } from "./session.js";
7
9
  export class MdbaseMemoryVerificationStore {
8
10
  values = new Map();
@@ -24,6 +26,7 @@ export class MdbaseMemoryVerificationStore {
24
26
  export class MdbaseApplicationSession {
25
27
  connect;
26
28
  options;
29
+ timeouts;
27
30
  listeners = new Set();
28
31
  verificationStore;
29
32
  snapshot = { status: "opening", connections: [] };
@@ -32,13 +35,17 @@ export class MdbaseApplicationSession {
32
35
  stopBase;
33
36
  reviewEntries = [];
34
37
  verificationGeneration = 0;
35
- constructor(connect, options) {
38
+ constructor(connect, options, timeouts = resolveConnectTimeouts()) {
36
39
  this.connect = connect;
37
40
  this.options = options;
41
+ this.timeouts = timeouts;
38
42
  this.verificationStore = options.verificationStore ?? defaultVerificationStore();
39
43
  }
40
- async start() {
41
- const manifest = await this.connect.manifest();
44
+ start(options) {
45
+ return withRequestBudget(options, this.timeouts.watchStartMs, (budget) => this.startWithinBudget({ signal: budget.signal, timeoutMs: null }));
46
+ }
47
+ async startWithinBudget(options) {
48
+ const manifest = await this.connect.manifest(options);
42
49
  if (!manifest.ok)
43
50
  return manifest;
44
51
  const capabilities = manifest.value.requirements?.capabilities;
@@ -61,10 +68,10 @@ export class MdbaseApplicationSession {
61
68
  operations: operationsForSession(capabilities)
62
69
  });
63
70
  this.stopBase = this.base.subscribe(() => this.refresh());
64
- const started = await this.base.start();
71
+ const started = await this.base.start(options);
65
72
  if (!started.ok)
66
73
  return started;
67
- await this.refresh(true);
74
+ await this.refresh(true, options);
68
75
  return connectSuccess(this.snapshot);
69
76
  }
70
77
  destroy() {
@@ -97,8 +104,11 @@ export class MdbaseApplicationSession {
97
104
  authorize(target, options = {}) {
98
105
  return this.requireBase().authorize(target, options);
99
106
  }
100
- handleAuthorizationCallback(callbackUrl) {
101
- return this.requireBase().handleAuthorizationCallback(callbackUrl);
107
+ handleAuthorizationCallback(callbackUrl, options) {
108
+ return this.requireBase().handleAuthorizationCallback(String(callbackUrl), options);
109
+ }
110
+ completeAuthorization(callbackUrl, options) {
111
+ return this.requireBase().handleAuthorizationCallback(callbackUrl === undefined ? defaultCallbackUrl() : String(callbackUrl), options);
102
112
  }
103
113
  ensureCapabilities(capabilities) {
104
114
  const manifestRequirements = this.requireManifest().requirements?.capabilities;
@@ -123,7 +133,7 @@ export class MdbaseApplicationSession {
123
133
  }
124
134
  return this.requireBase().ensureOperations(operationsForIds(capabilities));
125
135
  }
126
- async applyDefinitionUpdates() {
136
+ async applyDefinitionUpdates(options) {
127
137
  if (this.snapshot.status !== "definition_review_required" || this.reviewEntries.length === 0) {
128
138
  return connectFailure(connectProblem("collection_not_ready", "There are no reviewed definition updates to apply."));
129
139
  }
@@ -135,20 +145,23 @@ export class MdbaseApplicationSession {
135
145
  return connectFailure(connectProblem("collection_not_ready", "The collection is not ready."));
136
146
  }
137
147
  for (const { provision, assessment } of this.reviewEntries) {
138
- const applied = await connection.applyTypePack({
148
+ const input = {
139
149
  provision,
140
150
  installed_by: this.requireManifest().id,
141
151
  expected_assessment_digest: assessment.assessment_digest,
142
152
  ...(assessment.status === "downgrade" ? { allow_downgrade: true } : {})
143
- });
153
+ };
154
+ const applied = options
155
+ ? await connection.applyTypePack(input, options)
156
+ : await connection.applyTypePack(input);
144
157
  if (!applied.ok)
145
158
  return applied;
146
159
  }
147
160
  this.reviewEntries = [];
148
- await this.verifyDefinitions(this.context(connection), ++this.verificationGeneration);
161
+ await this.verifyDefinitions(this.context(connection), ++this.verificationGeneration, options);
149
162
  return connectSuccess(this.snapshot);
150
163
  }
151
- async refresh(awaitVerification = false) {
164
+ async refresh(awaitVerification = false, options) {
152
165
  if (!this.base || !this.manifest)
153
166
  return;
154
167
  const current = this.base.getSnapshot();
@@ -184,11 +197,11 @@ export class MdbaseApplicationSession {
184
197
  else {
185
198
  this.publish({ status: "checking_definitions", ...context });
186
199
  }
187
- const verification = this.verifyDefinitions(context, generation);
200
+ const verification = this.verifyDefinitions(context, generation, options);
188
201
  if (awaitVerification)
189
202
  await verification;
190
203
  }
191
- async verifyDefinitions(context, generation) {
204
+ async verifyDefinitions(context, generation, options) {
192
205
  const connection = this.connection();
193
206
  const manifest = this.requireManifest();
194
207
  if (!connection || connection.collectionId !== context.collectionId)
@@ -197,10 +210,13 @@ export class MdbaseApplicationSession {
197
210
  const managesDefinitions = manifest.requirements?.capabilities?.required
198
211
  .includes("definitions.type-pack.apply") ?? false;
199
212
  for (const provision of managesDefinitions ? manifest.provisions?.type_packs ?? [] : []) {
200
- const outcome = await connection.assessTypePack({
213
+ const input = {
201
214
  provision,
202
215
  installed_by: manifest.id
203
- });
216
+ };
217
+ const outcome = options
218
+ ? await connection.assessTypePack(input, options)
219
+ : await connection.assessTypePack(input);
204
220
  if (generation !== this.verificationGeneration)
205
221
  return;
206
222
  if (!outcome.ok) {
package/dist/async.js CHANGED
@@ -1,13 +1,19 @@
1
1
  export function abortableDelay(milliseconds, signal) {
2
2
  return new Promise((resolve, reject) => {
3
3
  if (signal?.aborted) {
4
- reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
4
+ reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
5
5
  return;
6
6
  }
7
- const timeout = setTimeout(resolve, milliseconds);
8
- signal?.addEventListener("abort", () => {
7
+ const cleanup = () => signal?.removeEventListener("abort", abort);
8
+ const timeout = setTimeout(() => {
9
+ cleanup();
10
+ resolve();
11
+ }, milliseconds);
12
+ const abort = () => {
9
13
  clearTimeout(timeout);
10
- reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
11
- }, { once: true });
14
+ cleanup();
15
+ reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
16
+ };
17
+ signal?.addEventListener("abort", abort, { once: true });
12
18
  });
13
19
  }
@@ -0,0 +1,3 @@
1
+ import { type GrantKeyStore } from "./crypto.js";
2
+ import type { StoredToken } from "./internal-types.js";
3
+ export declare function authorityProofHeaders(keyStore: GrantKeyStore, token: StoredToken, method: string, url: string, body: string | undefined, credential: string): Promise<Record<string, string>>;
@@ -0,0 +1,24 @@
1
+ import { RelayCryptoError, signAuthorityRequest } from "./crypto.js";
2
+ import { connectError, serverConnectError } from "./errors.js";
3
+ export async function authorityProofHeaders(keyStore, token, method, url, body, credential) {
4
+ if (!token.authority?.proofPublicKey)
5
+ return {};
6
+ if (!token.keyHandle) {
7
+ throw connectError("missing_grant_key", "Reconnect this application to restore remote authority request signing.");
8
+ }
9
+ try {
10
+ const target = new URL(url);
11
+ return await signAuthorityRequest(keyStore, token.keyHandle, token.authority.proofPublicKey, {
12
+ method,
13
+ target: `${target.pathname}${target.search}`,
14
+ body,
15
+ credential
16
+ });
17
+ }
18
+ catch (error) {
19
+ if (error instanceof RelayCryptoError) {
20
+ throw serverConnectError(error.code, error.message);
21
+ }
22
+ throw error;
23
+ }
24
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "file": "mdbase-connect.min.js",
3
- "integrity": "sha384-Cw/OpEwBlbmIBjDZ/JDyh1HiQoKqitRwVSPaYpNCg33rNDYZr/EdSxv5oRa4jP+I"
3
+ "integrity": "sha384-HYIXh8E0r6OVNVpvmRcFYSjLi278951j0TbDZeZGQQ/jcLhe/Ih1TvTZVdqJGqY7"
4
4
  }