@mdbase-dev/connect 0.1.0-beta.31 → 0.1.0-beta.33

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 (68) hide show
  1. package/README.md +112 -34
  2. package/dist/advanced.d.ts +9 -0
  3. package/dist/advanced.js +6 -0
  4. package/dist/application-session.d.ts +39 -14
  5. package/dist/application-session.js +201 -70
  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 +36 -33
  16. package/dist/collection-client.js +549 -104
  17. package/dist/connect-authorization-helpers.d.ts +3 -0
  18. package/dist/connect-authorization-helpers.js +13 -0
  19. package/dist/connect-options.d.ts +12 -4
  20. package/dist/connect.d.ts +12 -4
  21. package/dist/connect.js +102 -103
  22. package/dist/connection-file-transport.d.ts +2 -2
  23. package/dist/connection-file-transport.js +4 -4
  24. package/dist/connection-notifications.d.ts +10 -4
  25. package/dist/connection-notifications.js +41 -23
  26. package/dist/connection-transport.d.ts +19 -15
  27. package/dist/connection-transport.js +202 -202
  28. package/dist/connection-types.d.ts +5 -4
  29. package/dist/connection.d.ts +47 -46
  30. package/dist/connection.js +320 -162
  31. package/dist/crypto-entry.d.ts +7 -0
  32. package/dist/crypto-entry.js +4 -0
  33. package/dist/crypto.js +4 -4
  34. package/dist/external-store.d.ts +10 -0
  35. package/dist/external-store.js +10 -0
  36. package/dist/file-transfer-internals.d.ts +23 -0
  37. package/dist/file-transfer-internals.js +224 -0
  38. package/dist/files.d.ts +36 -14
  39. package/dist/files.js +92 -222
  40. package/dist/hosted-file-request.js +3 -3
  41. package/dist/index.d.ts +24 -19
  42. package/dist/index.js +10 -18
  43. package/dist/internal-types.d.ts +9 -1
  44. package/dist/local-file-transport.d.ts +2 -2
  45. package/dist/local-file-transport.js +13 -13
  46. package/dist/mdbase-connect.d.ts +6 -4
  47. package/dist/mdbase-connect.js +9 -8
  48. package/dist/notifications.d.ts +3 -2
  49. package/dist/operation-helpers.d.ts +3 -3
  50. package/dist/operation-helpers.js +17 -34
  51. package/dist/operation-types.d.ts +435 -45
  52. package/dist/outcomes.d.ts +15 -15
  53. package/dist/outcomes.js +13 -1
  54. package/dist/pending-mutation-store.d.ts +13 -0
  55. package/dist/pending-mutation-store.js +64 -0
  56. package/dist/request-budget.d.ts +27 -0
  57. package/dist/request-budget.js +95 -0
  58. package/dist/runtime-utils.d.ts +2 -0
  59. package/dist/runtime-utils.js +19 -0
  60. package/dist/session.d.ts +5 -4
  61. package/dist/session.js +6 -5
  62. package/dist/stored-token.d.ts +10 -0
  63. package/dist/stored-token.js +60 -0
  64. package/dist/token-response.d.ts +11 -0
  65. package/dist/token-response.js +77 -0
  66. package/dist/watch-policy.d.ts +8 -0
  67. package/dist/watch-policy.js +12 -0
  68. package/package.json +12 -2
@@ -1,17 +1,21 @@
1
1
  import { connectError } from "./errors.js";
2
- import { apiError, connectFetch, parseStored } from "./runtime-utils.js";
2
+ import { apiError, connectFetch, decodeJsonResponse, parseStored } from "./runtime-utils.js";
3
3
  import { base64UrlBytes, randomBase64Url } from "./base64.js";
4
+ import { withRequestBudget } from "./request-budget.js";
4
5
  export class ConnectionNotifications {
5
6
  context;
6
7
  constructor(context) {
7
8
  this.context = context;
8
9
  }
9
10
  async registerNotifications(options) {
10
- const token = await this.context.authorizedToken();
11
+ return withRequestBudget(options, this.context.requestTimeoutMs, (budget) => this.registerNotificationsWithinBudget(options, budget.signal));
12
+ }
13
+ async registerNotificationsWithinBudget(options, signal) {
14
+ const token = await this.context.authorizedToken(signal);
11
15
  if (!token) {
12
16
  throw connectError("not_authorized", "Connect this application before enabling notifications.");
13
17
  }
14
- const application = await this.context.register();
18
+ const application = await this.context.register(signal);
15
19
  const declared = application.notifications?.criteria.map((criterion) => criterion.id) ?? [];
16
20
  const criteria = [...new Set(options.criteria ?? declared)];
17
21
  const undeclared = criteria.find((criterion) => !declared.includes(criterion));
@@ -21,8 +25,8 @@ export class ConnectionNotifications {
21
25
  if (criteria.length === 0) {
22
26
  throw connectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
23
27
  }
24
- const keyResponse = await connectFetch(`${this.context.serverUrl}/v1/notifications/vapid-public-key`, undefined, "notifications_unavailable", "Push notifications are unavailable.");
25
- const keyBody = await keyResponse.json();
28
+ const keyResponse = await connectFetch(`${this.context.serverUrl}/v1/notifications/vapid-public-key`, { signal }, "notifications_unavailable", "Push notifications are unavailable.");
29
+ const keyBody = await decodeJsonResponse(keyResponse, "invalid_operation_response", "Connect returned an invalid notification key response.");
26
30
  if (!keyResponse.ok) {
27
31
  throw apiError(keyBody, "notifications_unavailable", "Push notifications are unavailable.", keyResponse.status);
28
32
  }
@@ -55,16 +59,18 @@ export class ConnectionNotifications {
55
59
  expirationTime: serialized.expirationTime ?? null,
56
60
  keys: serialized.keys
57
61
  }
58
- })
62
+ }),
63
+ signal
59
64
  }, "notification_registration_failed", "Could not register push notifications.");
60
- const channelBody = await channelResponse.json();
65
+ const channelBody = await decodeJsonResponse(channelResponse, "invalid_operation_response", "Connect returned an invalid notification registration response.");
61
66
  if (!channelResponse.ok) {
62
67
  throw apiError(channelBody, "notification_registration_failed", "Could not register push notifications.", channelResponse.status);
63
68
  }
64
69
  if (previous?.channelId && previous.channelId !== channelBody.channel_id) {
65
70
  void fetch(`${this.context.serverUrl}/v1/notifications/channels/${previous.channelId}`, {
66
71
  method: "DELETE",
67
- headers: { authorization: `Bearer ${token.accessToken}` }
72
+ headers: { authorization: `Bearer ${token.accessToken}` },
73
+ signal
68
74
  }).catch(() => undefined);
69
75
  }
70
76
  const registration = {
@@ -82,11 +88,14 @@ export class ConnectionNotifications {
82
88
  * same installation whenever Firebase refreshes its token.
83
89
  */
84
90
  async registerNativeNotifications(options) {
85
- const token = await this.context.authorizedToken();
91
+ return withRequestBudget(options, this.context.requestTimeoutMs, (budget) => this.registerNativeNotificationsWithinBudget(options, budget.signal));
92
+ }
93
+ async registerNativeNotificationsWithinBudget(options, signal) {
94
+ const token = await this.context.authorizedToken(signal);
86
95
  if (!token) {
87
96
  throw connectError("not_authorized", "Connect this application before enabling notifications.");
88
97
  }
89
- const application = await this.context.register();
98
+ const application = await this.context.register(signal);
90
99
  if (application.notifications?.native_delivery?.mode !== "managed_fcm") {
91
100
  throw connectError("managed_fcm_not_declared", "This application does not declare Connect-managed native notifications.");
92
101
  }
@@ -115,14 +124,15 @@ export class ConnectionNotifications {
115
124
  criteria,
116
125
  transport: "fcm",
117
126
  token: options.token
118
- })
127
+ }),
128
+ signal
119
129
  }, "notification_registration_failed", "Could not register native notifications.");
120
- const body = await response.json();
130
+ const body = await decodeJsonResponse(response, "invalid_operation_response", "Connect returned an invalid native notification response.");
121
131
  if (!response.ok) {
122
132
  throw apiError(body, "notification_registration_failed", "Could not register native notifications.", response.status);
123
133
  }
124
134
  if (previous?.channelId && previous.channelId !== body.channel_id) {
125
- void this.deleteNotificationChannel(previous.channelId, token.accessToken)
135
+ void this.deleteNotificationChannel(previous.channelId, token.accessToken, signal)
126
136
  .catch(() => undefined);
127
137
  }
128
138
  const registration = {
@@ -134,31 +144,38 @@ export class ConnectionNotifications {
134
144
  this.context.storage.setItem(storageKey, JSON.stringify(registration));
135
145
  return registration;
136
146
  }
137
- async unregisterNativeNotifications() {
147
+ async unregisterNativeNotifications(options = {}) {
148
+ return withRequestBudget(options, this.context.requestTimeoutMs, (budget) => this.unregisterNativeNotificationsWithinBudget(budget.signal));
149
+ }
150
+ async unregisterNativeNotificationsWithinBudget(signal) {
138
151
  const storageKey = this.context.notificationKey("fcm");
139
152
  const registration = parseStored(this.context.storage.getItem(storageKey));
140
- const token = await this.context.authorizedToken();
153
+ const token = await this.context.authorizedToken(signal);
141
154
  if (registration?.channelId && !token) {
142
155
  throw connectError("not_authorized", "Reconnect this application before disabling native notifications.");
143
156
  }
144
157
  if (registration?.channelId && token) {
145
- await this.deleteNotificationChannel(registration.channelId, token.accessToken);
158
+ await this.deleteNotificationChannel(registration.channelId, token.accessToken, signal);
146
159
  }
147
160
  this.context.storage.removeItem(storageKey);
148
161
  }
149
- async unregisterNotifications(serviceWorker) {
162
+ async unregisterNotifications(serviceWorker, options = {}) {
163
+ return withRequestBudget(options, this.context.requestTimeoutMs, (budget) => this.unregisterNotificationsWithinBudget(serviceWorker, budget.signal));
164
+ }
165
+ async unregisterNotificationsWithinBudget(serviceWorker, signal) {
150
166
  const registration = parseStored(this.context.storage.getItem(this.context.notificationKey()));
151
- const token = await this.context.authorizedToken();
167
+ const token = await this.context.authorizedToken(signal);
152
168
  if (registration?.channelId && !token) {
153
169
  throw connectError("not_authorized", "Reconnect this application before disabling push notifications.");
154
170
  }
155
171
  if (registration?.channelId && token) {
156
172
  const response = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels/${registration.channelId}`, {
157
173
  method: "DELETE",
158
- headers: { authorization: `Bearer ${token.accessToken}` }
174
+ headers: { authorization: `Bearer ${token.accessToken}` },
175
+ signal
159
176
  }, "notification_unregistration_failed", "Could not unregister push notifications.");
160
177
  if (!response.ok && response.status !== 404) {
161
- const body = await response.json();
178
+ const body = await decodeJsonResponse(response, "invalid_operation_response", "Connect returned an invalid notification response.");
162
179
  throw apiError(body, "notification_unregistration_failed", "Could not unregister push notifications.", response.status);
163
180
  }
164
181
  }
@@ -166,13 +183,14 @@ export class ConnectionNotifications {
166
183
  const subscription = await serviceWorker?.pushManager.getSubscription();
167
184
  await subscription?.unsubscribe();
168
185
  }
169
- async deleteNotificationChannel(channelId, accessToken) {
186
+ async deleteNotificationChannel(channelId, accessToken, signal) {
170
187
  const response = await connectFetch(`${this.context.serverUrl}/v1/notifications/channels/${channelId}`, {
171
188
  method: "DELETE",
172
- headers: { authorization: `Bearer ${accessToken}` }
189
+ headers: { authorization: `Bearer ${accessToken}` },
190
+ signal
173
191
  }, "notification_unregistration_failed", "Could not unregister push notifications.");
174
192
  if (!response.ok && response.status !== 404) {
175
- const body = await response.json();
193
+ const body = await decodeJsonResponse(response, "invalid_operation_response", "Connect returned an invalid notification response.");
176
194
  throw apiError(body, "notification_unregistration_failed", "Could not unregister push notifications.", response.status);
177
195
  }
178
196
  }
@@ -2,14 +2,14 @@ import type { CollectionOperation } from "@mdbase-dev/connect-protocol";
2
2
  import { type GrantKeyStore } from "./crypto.js";
3
3
  import type { StoredToken } from "./internal-types.js";
4
4
  import type { DirectAccessStatus, MdbaseConnectionRoute } from "./connection-types.js";
5
- import type { OperationRequestOptions, PendingMutationSummary } from "./operation-types.js";
5
+ import type { ConnectRequestOptions, PendingMutationSummary } from "./operation-types.js";
6
6
  import { ConnectionFileTransport } from "./connection-file-transport.js";
7
+ import { type ResolvedConnectTimeouts } from "./request-budget.js";
7
8
  export interface ConnectionTransportInternals {
8
9
  readonly relayEncryption: "required" | "disabled";
9
- removeToken(collectionId: string, keyHandle?: string, reason?: "not_authorized" | "authorization_lost" | "invalid_stored_grant"): void;
10
+ removeToken(collectionId: string, keyHandle?: string, reason?: "not_authorized" | "authorization_lost" | "invalid_stored_grant", discardPending?: boolean): void;
10
11
  storeTokenResponse(body: any, clientId: string, keyHandle?: string): StoredToken;
11
12
  tokenKey(collectionId: string): string;
12
- notificationKey(collectionId: string, transport?: "web_push" | "fcm"): string;
13
13
  pendingMutationKey(collectionId: string): string;
14
14
  directPreferenceKey(): string;
15
15
  }
@@ -22,6 +22,7 @@ export interface ConnectionTransportOptions {
22
22
  collectionId: string;
23
23
  internals: ConnectionTransportInternals;
24
24
  onChange(): void;
25
+ timeouts: ResolvedConnectTimeouts;
25
26
  }
26
27
  export declare class ConnectionTransport {
27
28
  private readonly serverUrl;
@@ -32,6 +33,8 @@ export declare class ConnectionTransport {
32
33
  private readonly collectionId;
33
34
  private readonly internals;
34
35
  private readonly onChange;
36
+ private readonly timeouts;
37
+ private readonly pendingMutationStore;
35
38
  readonly files: ConnectionFileTransport;
36
39
  private refreshPromise;
37
40
  private directStatus;
@@ -42,19 +45,23 @@ export declare class ConnectionTransport {
42
45
  get directAccess(): DirectAccessStatus;
43
46
  get route(): MdbaseConnectionRoute;
44
47
  notifyStorageChanged(): void;
45
- checkDirectAccess(): Promise<DirectAccessStatus>;
48
+ checkDirectAccess(options?: ConnectRequestOptions): Promise<DirectAccessStatus>;
49
+ private checkDirectAccessWithinBudget;
46
50
  /** Call from a user gesture to request browser permission for direct local access. */
47
- requestDirectAccess(): Promise<DirectAccessStatus>;
51
+ requestDirectAccess(options?: ConnectRequestOptions): Promise<DirectAccessStatus>;
52
+ private requestDirectAccessWithinBudget;
48
53
  disableDirectAccess(): void;
49
54
  /**
50
55
  * Return one offline-replication transport regardless of whether the
51
56
  * authority is remote, directly connected, or relay connected.
52
57
  * Authority credentials and routing stay private inside the SDK.
53
58
  */
54
- pendingMutation(): PendingMutationSummary | null;
55
- resumePendingMutation<Result>(input: unknown, options?: OperationRequestOptions): Promise<Result>;
56
- performOperation<Result>(operation: CollectionOperation, input: unknown, options?: OperationRequestOptions): Promise<Result>;
57
- performAuthoritySyncRequest<Result>(collectionId: string, replicaId: string, method: "GET" | "POST", path: string, input?: unknown): Promise<Result>;
59
+ pendingMutations(): readonly PendingMutationSummary[];
60
+ pendingMutation(requestId: string): PendingMutationSummary | null;
61
+ recoverPendingMutation<Result>(requestId: string, options?: ConnectRequestOptions): Promise<Result>;
62
+ performOperation<Result>(operation: CollectionOperation, input: unknown, options?: ConnectRequestOptions): Promise<Result>;
63
+ private performOperationWithinBudget;
64
+ performAuthoritySyncRequest<Result>(collectionId: string, replicaId: string, method: "GET" | "POST", path: string, input?: unknown, options?: ConnectRequestOptions): Promise<Result>;
58
65
  private sendAuthoritySyncRequest;
59
66
  private sendOperation;
60
67
  private directCapable;
@@ -69,14 +76,11 @@ export declare class ConnectionTransport {
69
76
  private emitConnection;
70
77
  private invalidateRejectedAuthorization;
71
78
  currentToken(): StoredToken | null;
72
- authorizedToken(): Promise<StoredToken | null>;
79
+ authorizedToken(options?: ConnectRequestOptions): Promise<StoredToken | null>;
73
80
  private refreshAuthorization;
74
81
  private performRefresh;
75
- private authorityProofHeaders;
76
82
  private storeTokenResponse;
77
- private tokenKey;
78
- private notificationKey;
79
- private pendingMutationKey;
83
+ private storedPendingMutations;
84
+ private storedPendingMutation;
80
85
  private clearPendingMutation;
81
- private directPreferenceKey;
82
86
  }