@mdbase-dev/connect 0.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +405 -0
- package/dist/async.d.ts +1 -0
- package/dist/async.js +13 -0
- package/dist/authorization-types.d.ts +7 -0
- package/dist/authorization-types.js +1 -0
- package/dist/authorization-url.d.ts +3 -0
- package/dist/authorization-url.js +27 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.js +21 -0
- package/dist/browser/integrity.json +4 -0
- package/dist/browser/mdbase-connect.min.js +3 -0
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
- package/dist/collection-client.d.ts +57 -0
- package/dist/collection-client.js +214 -0
- package/dist/connect.d.ts +39 -0
- package/dist/connect.js +620 -0
- package/dist/connection-notifications.d.ts +25 -0
- package/dist/connection-notifications.js +179 -0
- package/dist/connection-transport.d.ts +80 -0
- package/dist/connection-transport.js +670 -0
- package/dist/connection-types.d.ts +29 -0
- package/dist/connection-types.js +1 -0
- package/dist/connection.d.ts +154 -0
- package/dist/connection.js +371 -0
- package/dist/crypto.d.ts +64 -0
- package/dist/crypto.js +391 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +83 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/internal-types.d.ts +70 -0
- package/dist/internal-types.js +1 -0
- package/dist/notifications.d.ts +47 -0
- package/dist/notifications.js +52 -0
- package/dist/operation-helpers.d.ts +24 -0
- package/dist/operation-helpers.js +129 -0
- package/dist/operation-types.d.ts +254 -0
- package/dist/operation-types.js +1 -0
- package/dist/runtime-utils.d.ts +33 -0
- package/dist/runtime-utils.js +224 -0
- package/dist/selection.d.ts +48 -0
- package/dist/selection.js +112 -0
- package/dist/session.d.ts +76 -0
- package/dist/session.js +271 -0
- package/package.json +41 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { MdbaseConnectError } from "./errors.js";
|
|
2
|
+
import { apiError, parseStored } from "./runtime-utils.js";
|
|
3
|
+
import { base64UrlBytes, randomBase64Url } from "./base64.js";
|
|
4
|
+
export class ConnectionNotifications {
|
|
5
|
+
context;
|
|
6
|
+
constructor(context) {
|
|
7
|
+
this.context = context;
|
|
8
|
+
}
|
|
9
|
+
async registerNotifications(options) {
|
|
10
|
+
const token = await this.context.authorizedToken();
|
|
11
|
+
if (!token) {
|
|
12
|
+
throw new MdbaseConnectError("not_authorized", "Connect this application before enabling notifications.");
|
|
13
|
+
}
|
|
14
|
+
const application = await this.context.register();
|
|
15
|
+
const declared = application.notifications?.criteria.map((criterion) => criterion.id) ?? [];
|
|
16
|
+
const criteria = [...new Set(options.criteria ?? declared)];
|
|
17
|
+
const undeclared = criteria.find((criterion) => !declared.includes(criterion));
|
|
18
|
+
if (undeclared) {
|
|
19
|
+
throw new MdbaseConnectError("notification_criterion_not_declared", `The application manifest does not declare notification criterion ${undeclared}.`);
|
|
20
|
+
}
|
|
21
|
+
if (criteria.length === 0) {
|
|
22
|
+
throw new MdbaseConnectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
|
|
23
|
+
}
|
|
24
|
+
const keyResponse = await fetch(`${this.context.serverUrl}/v1/notifications/vapid-public-key`);
|
|
25
|
+
const keyBody = await keyResponse.json();
|
|
26
|
+
if (!keyResponse.ok) {
|
|
27
|
+
throw apiError(keyBody, "notifications_unavailable", "Push notifications are unavailable.", keyResponse.status);
|
|
28
|
+
}
|
|
29
|
+
let pushSubscription = await options.serviceWorker.pushManager.getSubscription();
|
|
30
|
+
if (!pushSubscription) {
|
|
31
|
+
pushSubscription = await options.serviceWorker.pushManager.subscribe({
|
|
32
|
+
userVisibleOnly: true,
|
|
33
|
+
applicationServerKey: base64UrlBytes(keyBody.public_key)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const serialized = pushSubscription.toJSON();
|
|
37
|
+
if (!serialized.endpoint || !serialized.keys?.p256dh || !serialized.keys.auth) {
|
|
38
|
+
throw new MdbaseConnectError("invalid_push_subscription", "The browser returned an incomplete push subscription.");
|
|
39
|
+
}
|
|
40
|
+
const previous = parseStored(this.context.storage.getItem(this.context.notificationKey()));
|
|
41
|
+
const installationId = options.installationId
|
|
42
|
+
?? previous?.installationId
|
|
43
|
+
?? randomBase64Url(24);
|
|
44
|
+
const channelResponse = await fetch(`${this.context.serverUrl}/v1/notifications/channels`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
authorization: `Bearer ${token.accessToken}`,
|
|
48
|
+
"content-type": "application/json"
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
installation_id: installationId,
|
|
52
|
+
criteria,
|
|
53
|
+
subscription: {
|
|
54
|
+
endpoint: serialized.endpoint,
|
|
55
|
+
expirationTime: serialized.expirationTime ?? null,
|
|
56
|
+
keys: serialized.keys
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
const channelBody = await channelResponse.json();
|
|
61
|
+
if (!channelResponse.ok) {
|
|
62
|
+
throw apiError(channelBody, "notification_registration_failed", "Could not register push notifications.", channelResponse.status);
|
|
63
|
+
}
|
|
64
|
+
if (previous?.channelId && previous.channelId !== channelBody.channel_id) {
|
|
65
|
+
void fetch(`${this.context.serverUrl}/v1/notifications/channels/${previous.channelId}`, {
|
|
66
|
+
method: "DELETE",
|
|
67
|
+
headers: { authorization: `Bearer ${token.accessToken}` }
|
|
68
|
+
}).catch(() => undefined);
|
|
69
|
+
}
|
|
70
|
+
const registration = {
|
|
71
|
+
channelId: channelBody.channel_id,
|
|
72
|
+
installationId,
|
|
73
|
+
criteria
|
|
74
|
+
};
|
|
75
|
+
this.context.storage.setItem(this.context.notificationKey(), JSON.stringify(registration));
|
|
76
|
+
return registration;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Register an iOS or Android installation for Connect-managed FCM.
|
|
80
|
+
*
|
|
81
|
+
* The application manifest selects the Firebase project. Re-register the
|
|
82
|
+
* same installation whenever Firebase refreshes its token.
|
|
83
|
+
*/
|
|
84
|
+
async registerNativeNotifications(options) {
|
|
85
|
+
const token = await this.context.authorizedToken();
|
|
86
|
+
if (!token) {
|
|
87
|
+
throw new MdbaseConnectError("not_authorized", "Connect this application before enabling notifications.");
|
|
88
|
+
}
|
|
89
|
+
const application = await this.context.register();
|
|
90
|
+
if (application.notifications?.native_delivery?.mode !== "managed_fcm") {
|
|
91
|
+
throw new MdbaseConnectError("managed_fcm_not_declared", "This application does not declare Connect-managed native notifications.");
|
|
92
|
+
}
|
|
93
|
+
const declared = application.notifications.criteria.map((criterion) => criterion.id);
|
|
94
|
+
const criteria = [...new Set(options.criteria ?? declared)];
|
|
95
|
+
const undeclared = criteria.find((criterion) => !declared.includes(criterion));
|
|
96
|
+
if (undeclared) {
|
|
97
|
+
throw new MdbaseConnectError("notification_criterion_not_declared", `The application manifest does not declare notification criterion ${undeclared}.`);
|
|
98
|
+
}
|
|
99
|
+
if (criteria.length === 0) {
|
|
100
|
+
throw new MdbaseConnectError("notifications_not_declared", "This application manifest does not declare any notification criteria.");
|
|
101
|
+
}
|
|
102
|
+
const storageKey = this.context.notificationKey("fcm");
|
|
103
|
+
const previous = parseStored(this.context.storage.getItem(storageKey));
|
|
104
|
+
const installationId = options.installationId
|
|
105
|
+
?? previous?.installationId
|
|
106
|
+
?? randomBase64Url(24);
|
|
107
|
+
const response = await fetch(`${this.context.serverUrl}/v1/notifications/channels`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
authorization: `Bearer ${token.accessToken}`,
|
|
111
|
+
"content-type": "application/json"
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
installation_id: installationId,
|
|
115
|
+
criteria,
|
|
116
|
+
transport: "fcm",
|
|
117
|
+
token: options.token
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
const body = await response.json();
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw apiError(body, "notification_registration_failed", "Could not register native notifications.", response.status);
|
|
123
|
+
}
|
|
124
|
+
if (previous?.channelId && previous.channelId !== body.channel_id) {
|
|
125
|
+
void this.deleteNotificationChannel(previous.channelId, token.accessToken)
|
|
126
|
+
.catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
const registration = {
|
|
129
|
+
channelId: body.channel_id,
|
|
130
|
+
installationId,
|
|
131
|
+
transport: "fcm",
|
|
132
|
+
criteria
|
|
133
|
+
};
|
|
134
|
+
this.context.storage.setItem(storageKey, JSON.stringify(registration));
|
|
135
|
+
return registration;
|
|
136
|
+
}
|
|
137
|
+
async unregisterNativeNotifications() {
|
|
138
|
+
const storageKey = this.context.notificationKey("fcm");
|
|
139
|
+
const registration = parseStored(this.context.storage.getItem(storageKey));
|
|
140
|
+
const token = await this.context.authorizedToken();
|
|
141
|
+
if (registration?.channelId && !token) {
|
|
142
|
+
throw new MdbaseConnectError("not_authorized", "Reconnect this application before disabling native notifications.");
|
|
143
|
+
}
|
|
144
|
+
if (registration?.channelId && token) {
|
|
145
|
+
await this.deleteNotificationChannel(registration.channelId, token.accessToken);
|
|
146
|
+
}
|
|
147
|
+
this.context.storage.removeItem(storageKey);
|
|
148
|
+
}
|
|
149
|
+
async unregisterNotifications(serviceWorker) {
|
|
150
|
+
const registration = parseStored(this.context.storage.getItem(this.context.notificationKey()));
|
|
151
|
+
const token = await this.context.authorizedToken();
|
|
152
|
+
if (registration?.channelId && !token) {
|
|
153
|
+
throw new MdbaseConnectError("not_authorized", "Reconnect this application before disabling push notifications.");
|
|
154
|
+
}
|
|
155
|
+
if (registration?.channelId && token) {
|
|
156
|
+
const response = await fetch(`${this.context.serverUrl}/v1/notifications/channels/${registration.channelId}`, {
|
|
157
|
+
method: "DELETE",
|
|
158
|
+
headers: { authorization: `Bearer ${token.accessToken}` }
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok && response.status !== 404) {
|
|
161
|
+
const body = await response.json();
|
|
162
|
+
throw apiError(body, "notification_unregistration_failed", "Could not unregister push notifications.", response.status);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.context.storage.removeItem(this.context.notificationKey());
|
|
166
|
+
const subscription = await serviceWorker?.pushManager.getSubscription();
|
|
167
|
+
await subscription?.unsubscribe();
|
|
168
|
+
}
|
|
169
|
+
async deleteNotificationChannel(channelId, accessToken) {
|
|
170
|
+
const response = await fetch(`${this.context.serverUrl}/v1/notifications/channels/${channelId}`, {
|
|
171
|
+
method: "DELETE",
|
|
172
|
+
headers: { authorization: `Bearer ${accessToken}` }
|
|
173
|
+
});
|
|
174
|
+
if (!response.ok && response.status !== 404) {
|
|
175
|
+
const body = await response.json();
|
|
176
|
+
throw apiError(body, "notification_unregistration_failed", "Could not unregister push notifications.", response.status);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { CollectionOperation } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { type GrantKeyStore } from "./crypto.js";
|
|
3
|
+
import type { StoredToken } from "./internal-types.js";
|
|
4
|
+
import type { DirectAccessStatus, MdbaseConnectionRoute } from "./connection-types.js";
|
|
5
|
+
import type { OperationRequestOptions, PendingMutationSummary } from "./operation-types.js";
|
|
6
|
+
export interface ConnectionTransportInternals {
|
|
7
|
+
readonly relayEncryption: "required" | "disabled";
|
|
8
|
+
removeToken(collectionId: string, keyHandle?: string, reason?: "not_authorized" | "authorization_lost" | "invalid_stored_grant"): void;
|
|
9
|
+
storeTokenResponse(body: any, clientId: string, keyHandle?: string): StoredToken;
|
|
10
|
+
tokenKey(collectionId: string): string;
|
|
11
|
+
notificationKey(collectionId: string, transport?: "web_push" | "fcm"): string;
|
|
12
|
+
pendingMutationKey(collectionId: string): string;
|
|
13
|
+
directPreferenceKey(): string;
|
|
14
|
+
}
|
|
15
|
+
export interface ConnectionTransportOptions {
|
|
16
|
+
serverUrl: string;
|
|
17
|
+
storage: Storage;
|
|
18
|
+
keyStore: GrantKeyStore;
|
|
19
|
+
directAccessMode: "auto" | "disabled";
|
|
20
|
+
loopbackUrl: string;
|
|
21
|
+
collectionId: string;
|
|
22
|
+
internals: ConnectionTransportInternals;
|
|
23
|
+
onChange(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare class ConnectionTransport {
|
|
26
|
+
private readonly serverUrl;
|
|
27
|
+
private readonly storage;
|
|
28
|
+
private readonly keyStore;
|
|
29
|
+
private readonly directAccessMode;
|
|
30
|
+
private readonly loopbackUrl;
|
|
31
|
+
private readonly collectionId;
|
|
32
|
+
private readonly internals;
|
|
33
|
+
private readonly onChange;
|
|
34
|
+
private refreshPromise;
|
|
35
|
+
private directStatus;
|
|
36
|
+
private currentRoute;
|
|
37
|
+
private directFailures;
|
|
38
|
+
private directRetryAt;
|
|
39
|
+
constructor(options: ConnectionTransportOptions);
|
|
40
|
+
get directAccess(): DirectAccessStatus;
|
|
41
|
+
get route(): MdbaseConnectionRoute;
|
|
42
|
+
notifyStorageChanged(): void;
|
|
43
|
+
checkDirectAccess(): Promise<DirectAccessStatus>;
|
|
44
|
+
/** Call from a user gesture to request browser permission for direct local access. */
|
|
45
|
+
requestDirectAccess(): Promise<DirectAccessStatus>;
|
|
46
|
+
disableDirectAccess(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Return one offline-replication transport regardless of whether the
|
|
49
|
+
* authority is remote, directly connected, or relay connected.
|
|
50
|
+
* Authority credentials and routing stay private inside the SDK.
|
|
51
|
+
*/
|
|
52
|
+
pendingMutation(): PendingMutationSummary | null;
|
|
53
|
+
resumePendingMutation<Result>(input: unknown, options?: OperationRequestOptions): Promise<Result>;
|
|
54
|
+
performOperation<Result>(operation: CollectionOperation, input: unknown, options?: OperationRequestOptions): Promise<Result>;
|
|
55
|
+
performAuthoritySyncRequest<Result>(collectionId: string, replicaId: string, method: "GET" | "POST", path: string, input?: unknown): Promise<Result>;
|
|
56
|
+
private sendAuthoritySyncRequest;
|
|
57
|
+
private sendOperation;
|
|
58
|
+
private directCapable;
|
|
59
|
+
hasResumableMutationTransport(): boolean;
|
|
60
|
+
private directEligible;
|
|
61
|
+
private shouldAttemptDirect;
|
|
62
|
+
private probeDirectAccess;
|
|
63
|
+
private markDirectAvailable;
|
|
64
|
+
private markDirectUnavailable;
|
|
65
|
+
private setDirectStatus;
|
|
66
|
+
private setRoute;
|
|
67
|
+
private emitConnection;
|
|
68
|
+
private invalidateRejectedAuthorization;
|
|
69
|
+
currentToken(): StoredToken | null;
|
|
70
|
+
authorizedToken(): Promise<StoredToken | null>;
|
|
71
|
+
private refreshAuthorization;
|
|
72
|
+
private performRefresh;
|
|
73
|
+
private authorityProofHeaders;
|
|
74
|
+
private storeTokenResponse;
|
|
75
|
+
private tokenKey;
|
|
76
|
+
private notificationKey;
|
|
77
|
+
private pendingMutationKey;
|
|
78
|
+
private clearPendingMutation;
|
|
79
|
+
private directPreferenceKey;
|
|
80
|
+
}
|