@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.
- package/README.md +75 -19
- package/dist/advanced.d.ts +7 -0
- package/dist/advanced.js +4 -0
- package/dist/application-session.d.ts +10 -5
- package/dist/application-session.js +32 -16
- package/dist/async.js +11 -5
- package/dist/authority-proof.d.ts +3 -0
- package/dist/authority-proof.js +24 -0
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +3 -3
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +5 -0
- package/dist/capabilities.js +2 -1
- package/dist/collection-client.d.ts +28 -28
- package/dist/collection-client.js +42 -42
- package/dist/connect-options.d.ts +12 -4
- package/dist/connect.d.ts +12 -4
- package/dist/connect.js +105 -104
- package/dist/connection-file-transport.d.ts +2 -2
- package/dist/connection-file-transport.js +4 -4
- package/dist/connection-notifications.d.ts +10 -4
- package/dist/connection-notifications.js +41 -23
- package/dist/connection-transport.d.ts +19 -15
- package/dist/connection-transport.js +202 -202
- package/dist/connection-types.d.ts +5 -4
- package/dist/connection.d.ts +43 -43
- package/dist/connection.js +221 -77
- package/dist/crypto-entry.d.ts +4 -0
- package/dist/crypto-entry.js +4 -0
- package/dist/crypto.js +4 -4
- package/dist/external-store.d.ts +10 -0
- package/dist/external-store.js +10 -0
- package/dist/file-transfer-internals.d.ts +22 -0
- package/dist/file-transfer-internals.js +186 -0
- package/dist/files.d.ts +16 -12
- package/dist/files.js +52 -206
- package/dist/hosted-file-request.js +3 -3
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -5
- package/dist/internal-types.d.ts +8 -1
- package/dist/local-file-transport.d.ts +2 -2
- package/dist/local-file-transport.js +13 -13
- package/dist/mdbase-connect.d.ts +6 -4
- package/dist/mdbase-connect.js +9 -8
- package/dist/notifications.d.ts +3 -2
- package/dist/operation-helpers.d.ts +3 -3
- package/dist/operation-helpers.js +12 -29
- package/dist/operation-types.d.ts +33 -8
- package/dist/outcomes.d.ts +15 -15
- package/dist/outcomes.js +13 -1
- package/dist/pending-mutation-store.d.ts +13 -0
- package/dist/pending-mutation-store.js +64 -0
- package/dist/request-budget.d.ts +27 -0
- package/dist/request-budget.js +95 -0
- package/dist/runtime-utils.d.ts +2 -0
- package/dist/runtime-utils.js +19 -0
- package/dist/session.d.ts +4 -3
- package/dist/session.js +4 -4
- package/dist/stored-token.d.ts +10 -0
- package/dist/stored-token.js +60 -0
- package/dist/token-response.d.ts +11 -0
- package/dist/token-response.js +77 -0
- 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
|
-
|
|
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`,
|
|
25
|
-
const keyBody = await keyResponse.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
78
|
-
private
|
|
79
|
-
private pendingMutationKey;
|
|
83
|
+
private storedPendingMutations;
|
|
84
|
+
private storedPendingMutation;
|
|
80
85
|
private clearPendingMutation;
|
|
81
|
-
private directPreferenceKey;
|
|
82
86
|
}
|