Page MenuHomePhabricator

No OneTemporary

diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js
index 3544896bf..012a32d6f 100644
--- a/keyserver/src/push/crypto.js
+++ b/keyserver/src/push/crypto.js
@@ -1,387 +1,417 @@
// @flow
import apn from '@parse/node-apn';
+import crypto from 'crypto';
import invariant from 'invariant';
import _cloneDeep from 'lodash/fp/cloneDeep.js';
import type {
PlainTextWebNotification,
WebNotification,
} from 'lib/types/notif-types.js';
+import { toBase64URL } from 'lib/utils/base64.js';
import type {
AndroidNotification,
AndroidNotificationPayload,
AndroidNotificationRescind,
NotificationTargetDevice,
} from './types.js';
import { encryptAndUpdateOlmSession } from '../updaters/olm-session-updater.js';
+import { encrypt, generateKey } from '../utils/aes-crypto-utils.js';
import { getOlmUtility } from '../utils/olm-utils.js';
async function encryptIOSNotification(
cookieID: string,
notification: apn.Notification,
codeVersion?: ?number,
notificationSizeValidator?: apn.Notification => boolean,
): Promise<{
+notification: apn.Notification,
+payloadSizeExceeded: boolean,
+encryptedPayloadHash?: string,
+encryptionOrder?: number,
}> {
invariant(
!notification.collapseId,
'Collapsible notifications encryption currently not implemented',
);
const encryptedNotification = new apn.Notification();
encryptedNotification.id = notification.id;
encryptedNotification.payload.id = notification.id;
encryptedNotification.topic = notification.topic;
encryptedNotification.sound = notification.aps.sound;
encryptedNotification.pushType = 'alert';
encryptedNotification.mutableContent = true;
const { id, ...payloadSansId } = notification.payload;
const unencryptedPayload = {
...payloadSansId,
badge: notification.aps.badge.toString(),
merged: notification.body,
};
try {
const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
let dbPersistCondition;
if (notificationSizeValidator) {
dbPersistCondition = ({ serializedPayload }) => {
const notifCopy = _cloneDeep(encryptedNotification);
notifCopy.payload.encryptedPayload = serializedPayload.body;
return notificationSizeValidator(notifCopy);
};
}
const {
encryptedMessages: { serializedPayload },
dbPersistConditionViolated,
encryptionOrder,
} = await encryptAndUpdateOlmSession(
cookieID,
'notifications',
{
serializedPayload: unencryptedSerializedPayload,
},
dbPersistCondition,
);
encryptedNotification.payload.encryptedPayload = serializedPayload.body;
if (codeVersion && codeVersion >= 254 && codeVersion % 2 === 0) {
encryptedNotification.aps = {
alert: { body: 'ENCRYPTED' },
...encryptedNotification.aps,
};
}
const encryptedPayloadHash = getOlmUtility().sha256(serializedPayload.body);
return {
notification: encryptedNotification,
payloadSizeExceeded: !!dbPersistConditionViolated,
encryptedPayloadHash,
encryptionOrder,
};
} catch (e) {
console.log('Notification encryption failed: ' + e);
encryptedNotification.body = notification.body;
encryptedNotification.threadId = notification.payload.threadID;
invariant(
typeof notification.aps.badge === 'number',
'Unencrypted notification must have badge as a number',
);
encryptedNotification.badge = notification.aps.badge;
encryptedNotification.payload = {
...encryptedNotification.payload,
...notification.payload,
encryptionFailed: 1,
};
return {
notification: encryptedNotification,
payloadSizeExceeded: notificationSizeValidator
? notificationSizeValidator(_cloneDeep(encryptedNotification))
: false,
};
}
}
async function encryptAndroidNotificationPayload<T>(
cookieID: string,
unencryptedPayload: T,
payloadSizeValidator?: (T | { +encryptedPayload: string }) => boolean,
): Promise<{
+resultPayload: T | { +encryptedPayload: string },
+payloadSizeExceeded: boolean,
+encryptionOrder?: number,
}> {
try {
const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
if (!unencryptedSerializedPayload) {
return {
resultPayload: unencryptedPayload,
payloadSizeExceeded: payloadSizeValidator
? payloadSizeValidator(unencryptedPayload)
: false,
};
}
let dbPersistCondition;
if (payloadSizeValidator) {
dbPersistCondition = ({ serializedPayload }) =>
payloadSizeValidator({ encryptedPayload: serializedPayload.body });
}
const {
encryptedMessages: { serializedPayload },
dbPersistConditionViolated,
encryptionOrder,
} = await encryptAndUpdateOlmSession(
cookieID,
'notifications',
{
serializedPayload: unencryptedSerializedPayload,
},
dbPersistCondition,
);
return {
resultPayload: { encryptedPayload: serializedPayload.body },
payloadSizeExceeded: !!dbPersistConditionViolated,
encryptionOrder,
};
} catch (e) {
console.log('Notification encryption failed: ' + e);
const resultPayload = {
encryptionFailed: '1',
...unencryptedPayload,
};
return {
resultPayload,
payloadSizeExceeded: payloadSizeValidator
? payloadSizeValidator(resultPayload)
: false,
};
}
}
async function encryptAndroidNotification(
cookieID: string,
notification: AndroidNotification,
notificationSizeValidator?: AndroidNotification => boolean,
): Promise<{
+notification: AndroidNotification,
+payloadSizeExceeded: boolean,
+encryptionOrder?: number,
}> {
const { id, badgeOnly, ...unencryptedPayload } = notification.data;
let payloadSizeValidator;
if (notificationSizeValidator) {
payloadSizeValidator = (
payload: AndroidNotificationPayload | { +encryptedPayload: string },
) => {
return notificationSizeValidator({ data: { id, badgeOnly, ...payload } });
};
}
const { resultPayload, payloadSizeExceeded, encryptionOrder } =
await encryptAndroidNotificationPayload(
cookieID,
unencryptedPayload,
payloadSizeValidator,
);
return {
notification: {
data: {
id,
badgeOnly,
...resultPayload,
},
},
payloadSizeExceeded,
encryptionOrder,
};
}
async function encryptAndroidNotificationRescind(
cookieID: string,
notification: AndroidNotificationRescind,
): Promise<AndroidNotificationRescind> {
// We don't validate payload size for rescind
// since they are expected to be small and
// never exceed any FCM limit
const { resultPayload } = await encryptAndroidNotificationPayload(
cookieID,
notification.data,
);
return {
data: resultPayload,
};
}
async function encryptWebNotification(
cookieID: string,
notification: PlainTextWebNotification,
): Promise<{ +notification: WebNotification, +encryptionOrder?: number }> {
const { id, ...payloadSansId } = notification;
const unencryptedSerializedPayload = JSON.stringify(payloadSansId);
try {
const {
encryptedMessages: { serializedPayload },
encryptionOrder,
} = await encryptAndUpdateOlmSession(cookieID, 'notifications', {
serializedPayload: unencryptedSerializedPayload,
});
return {
notification: { id, encryptedPayload: serializedPayload.body },
encryptionOrder,
};
} catch (e) {
console.log('Notification encryption failed: ' + e);
return {
notification: {
id,
encryptionFailed: '1',
...payloadSansId,
},
};
}
}
function prepareEncryptedIOSNotifications(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: apn.Notification,
codeVersion?: ?number,
notificationSizeValidator?: apn.Notification => boolean,
): Promise<
$ReadOnlyArray<{
+cookieID: string,
+deviceToken: string,
+notification: apn.Notification,
+payloadSizeExceeded: boolean,
+encryptedPayloadHash?: string,
+encryptionOrder?: number,
}>,
> {
const notificationPromises = devices.map(
async ({ cookieID, deviceToken }) => {
const notif = await encryptIOSNotification(
cookieID,
notification,
codeVersion,
notificationSizeValidator,
);
return { cookieID, deviceToken, ...notif };
},
);
return Promise.all(notificationPromises);
}
function prepareEncryptedIOSNotificationRescind(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: apn.Notification,
codeVersion?: ?number,
): Promise<
$ReadOnlyArray<{
+cookieID: string,
+deviceToken: string,
+notification: apn.Notification,
}>,
> {
const notificationPromises = devices.map(
async ({ deviceToken, cookieID }) => {
const { notification: notif } = await encryptIOSNotification(
cookieID,
notification,
codeVersion,
);
return { deviceToken, cookieID, notification: notif };
},
);
return Promise.all(notificationPromises);
}
function prepareEncryptedAndroidNotifications(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: AndroidNotification,
notificationSizeValidator?: (notification: AndroidNotification) => boolean,
): Promise<
$ReadOnlyArray<{
+cookieID: string,
+deviceToken: string,
+notification: AndroidNotification,
+payloadSizeExceeded: boolean,
+encryptionOrder?: number,
}>,
> {
const notificationPromises = devices.map(
async ({ deviceToken, cookieID }) => {
const notif = await encryptAndroidNotification(
cookieID,
notification,
notificationSizeValidator,
);
return { deviceToken, cookieID, ...notif };
},
);
return Promise.all(notificationPromises);
}
function prepareEncryptedAndroidNotificationRescinds(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: AndroidNotificationRescind,
): Promise<
$ReadOnlyArray<{
+cookieID: string,
+deviceToken: string,
+notification: AndroidNotificationRescind,
+encryptionOrder?: number,
}>,
> {
const notificationPromises = devices.map(
async ({ deviceToken, cookieID }) => {
const notif = await encryptAndroidNotificationRescind(
cookieID,
notification,
);
return { deviceToken, cookieID, notification: notif };
},
);
return Promise.all(notificationPromises);
}
function prepareEncryptedWebNotifications(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: PlainTextWebNotification,
): Promise<
$ReadOnlyArray<{
+deviceToken: string,
+notification: WebNotification,
+encryptionOrder?: number,
}>,
> {
const notificationPromises = devices.map(
async ({ deviceToken, cookieID }) => {
const notif = await encryptWebNotification(cookieID, notification);
return { ...notif, deviceToken };
},
);
return Promise.all(notificationPromises);
}
+async function encryptBlobPayload(payload: string): Promise<{
+ +encryptionKey: string,
+ +encryptedPayload: Blob,
+ +encryptedPayloadHash: string,
+}> {
+ const encryptionKey = await generateKey();
+ const encryptedPayload = await encrypt(
+ encryptionKey,
+ new TextEncoder().encode(payload),
+ );
+ const encryptedPayloadBuffer = Buffer.from(encryptedPayload);
+ const blobHashBase64 = await crypto
+ .createHash('sha256')
+ .update(encryptedPayloadBuffer)
+ .digest('base64');
+ const blobHash = toBase64URL(blobHashBase64);
+
+ const payloadBlob = new Blob([encryptedPayloadBuffer]);
+ const encryptionKeyString = Buffer.from(encryptionKey).toString('base64');
+ return {
+ encryptionKey: encryptionKeyString,
+ encryptedPayload: payloadBlob,
+ encryptedPayloadHash: blobHash,
+ };
+}
+
export {
prepareEncryptedIOSNotifications,
prepareEncryptedIOSNotificationRescind,
prepareEncryptedAndroidNotifications,
prepareEncryptedAndroidNotificationRescinds,
prepareEncryptedWebNotifications,
+ encryptBlobPayload,
};
diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js
index 0c5ab92b7..16df511c4 100644
--- a/keyserver/src/push/utils.js
+++ b/keyserver/src/push/utils.js
@@ -1,405 +1,420 @@
// @flow
import type { ResponseFailure } from '@parse/node-apn';
import type { FirebaseApp, FirebaseError } from 'firebase-admin';
import invariant from 'invariant';
import nodeFetch from 'node-fetch';
import type { Response } from 'node-fetch';
+import uuid from 'uuid';
import webpush from 'web-push';
import type { PlatformDetails } from 'lib/types/device-types.js';
import { threadSubscriptions } from 'lib/types/subscription-types.js';
import { threadPermissions } from 'lib/types/thread-permission-types.js';
+import { encryptBlobPayload } from './crypto.js';
import {
getAPNPushProfileForCodeVersion,
getFCMPushProfileForCodeVersion,
getAPNProvider,
getFCMProvider,
ensureWebPushInitialized,
getWNSToken,
} from './providers.js';
import type {
TargetedAPNsNotification,
TargetedAndroidNotification,
TargetedWebNotification,
TargetedWNSNotification,
} from './types.js';
import { dbQuery, SQL } from '../database/database.js';
import { upload } from '../services/blob.js';
const fcmTokenInvalidationErrors = new Set([
'messaging/registration-token-not-registered',
'messaging/invalid-registration-token',
]);
const fcmMaxNotificationPayloadByteSize = 4000;
const apnTokenInvalidationErrorCode = 410;
const apnBadRequestErrorCode = 400;
const apnBadTokenErrorString = 'BadDeviceToken';
const apnMaxNotificationPayloadByteSize = 4096;
const webInvalidTokenErrorCodes = [404, 410];
const wnsInvalidTokenErrorCodes = [404, 410];
const wnsMaxNotificationPayloadByteSize = 5000;
type APNPushResult =
| { +success: true }
| {
+errors: $ReadOnlyArray<ResponseFailure>,
+invalidTokens?: $ReadOnlyArray<string>,
};
async function apnPush({
targetedNotifications,
platformDetails,
}: {
+targetedNotifications: $ReadOnlyArray<TargetedAPNsNotification>,
+platformDetails: PlatformDetails,
}): Promise<APNPushResult> {
const pushProfile = getAPNPushProfileForCodeVersion(platformDetails);
const apnProvider = await getAPNProvider(pushProfile);
if (!apnProvider && process.env.NODE_ENV === 'development') {
console.log(`no keyserver/secrets/${pushProfile}.json so ignoring notifs`);
return { success: true };
}
invariant(apnProvider, `keyserver/secrets/${pushProfile}.json should exist`);
const results = await Promise.all(
targetedNotifications.map(({ notification, deviceToken }) => {
return apnProvider.send(notification, deviceToken);
}),
);
const mergedResults = { sent: [], failed: [] };
for (const result of results) {
mergedResults.sent.push(...result.sent);
mergedResults.failed.push(...result.failed);
}
const errors = [];
const invalidTokens = [];
for (const error of mergedResults.failed) {
errors.push(error);
/* eslint-disable eqeqeq */
if (
error.status == apnTokenInvalidationErrorCode ||
(error.status == apnBadRequestErrorCode &&
error.response.reason === apnBadTokenErrorString)
) {
invalidTokens.push(error.device);
}
/* eslint-enable eqeqeq */
}
if (invalidTokens.length > 0) {
return { errors, invalidTokens };
} else if (errors.length > 0) {
return { errors };
} else {
return { success: true };
}
}
type FCMPushResult = {
+success?: true,
+fcmIDs?: $ReadOnlyArray<string>,
+errors?: $ReadOnlyArray<FirebaseError>,
+invalidTokens?: $ReadOnlyArray<string>,
};
async function fcmPush({
targetedNotifications,
collapseKey,
codeVersion,
}: {
+targetedNotifications: $ReadOnlyArray<TargetedAndroidNotification>,
+codeVersion: ?number,
+collapseKey?: ?string,
}): Promise<FCMPushResult> {
const pushProfile = getFCMPushProfileForCodeVersion(codeVersion);
const fcmProvider = await getFCMProvider(pushProfile);
if (!fcmProvider && process.env.NODE_ENV === 'development') {
console.log(`no keyserver/secrets/${pushProfile}.json so ignoring notifs`);
return { success: true };
}
invariant(fcmProvider, `keyserver/secrets/${pushProfile}.json should exist`);
const options: Object = {
priority: 'high',
};
if (collapseKey) {
options.collapseKey = collapseKey;
}
// firebase-admin is extremely barebones and has a lot of missing or poorly
// thought-out functionality. One of the issues is that if you send a
// multicast messages and one of the device tokens is invalid, the resultant
// won't explain which of the device tokens is invalid. So we're forced to
// avoid the multicast functionality and call it once per deviceToken.
const promises = [];
for (const { notification, deviceToken } of targetedNotifications) {
promises.push(
fcmSinglePush(fcmProvider, notification, deviceToken, options),
);
}
const pushResults = await Promise.all(promises);
const errors = [];
const ids = [];
const invalidTokens = [];
for (let i = 0; i < pushResults.length; i++) {
const pushResult = pushResults[i];
for (const error of pushResult.errors) {
errors.push(error);
if (fcmTokenInvalidationErrors.has(error.errorInfo.code)) {
invalidTokens.push(targetedNotifications[i].deviceToken);
}
}
for (const id of pushResult.fcmIDs) {
ids.push(id);
}
}
const result = {};
if (ids.length > 0) {
result.fcmIDs = ids;
}
if (errors.length > 0) {
result.errors = errors;
} else {
result.success = true;
}
if (invalidTokens.length > 0) {
result.invalidTokens = invalidTokens;
}
return { ...result };
}
async function fcmSinglePush(
provider: FirebaseApp,
notification: Object,
deviceToken: string,
options: Object,
) {
try {
const deliveryResult = await provider
.messaging()
.sendToDevice(deviceToken, notification, options);
const errors = [];
const ids = [];
for (const fcmResult of deliveryResult.results) {
if (fcmResult.error) {
errors.push(fcmResult.error);
} else if (fcmResult.messageId) {
ids.push(fcmResult.messageId);
}
}
return { fcmIDs: ids, errors };
} catch (e) {
return { fcmIDs: [], errors: [e] };
}
}
async function getUnreadCounts(
userIDs: string[],
): Promise<{ [userID: string]: number }> {
const visPermissionExtractString = `$.${threadPermissions.VISIBLE}.value`;
const notificationExtractString = `$.${threadSubscriptions.home}`;
const query = SQL`
SELECT user, COUNT(thread) AS unread_count
FROM memberships
WHERE user IN (${userIDs}) AND last_message > last_read_message
AND role > 0
AND JSON_EXTRACT(permissions, ${visPermissionExtractString})
AND JSON_EXTRACT(subscription, ${notificationExtractString})
GROUP BY user
`;
const [result] = await dbQuery(query);
const usersToUnreadCounts = {};
for (const row of result) {
usersToUnreadCounts[row.user.toString()] = row.unread_count;
}
for (const userID of userIDs) {
if (usersToUnreadCounts[userID] === undefined) {
usersToUnreadCounts[userID] = 0;
}
}
return usersToUnreadCounts;
}
export type WebPushError = {
+statusCode: number,
+headers: { +[string]: string },
+body: string,
};
type WebPushResult = {
+success?: true,
+errors?: $ReadOnlyArray<WebPushError>,
+invalidTokens?: $ReadOnlyArray<string>,
};
async function webPush(
targetedNotifications: $ReadOnlyArray<TargetedWebNotification>,
): Promise<WebPushResult> {
await ensureWebPushInitialized();
const pushResults = await Promise.all(
targetedNotifications.map(
async ({ notification, deviceToken: deviceTokenString }) => {
const deviceToken: PushSubscriptionJSON = JSON.parse(deviceTokenString);
const notificationString = JSON.stringify(notification);
try {
await webpush.sendNotification(deviceToken, notificationString);
} catch (error) {
return { error };
}
return {};
},
),
);
const errors = [];
const invalidTokens = [];
const deviceTokens = targetedNotifications.map(
({ deviceToken }) => deviceToken,
);
for (let i = 0; i < pushResults.length; i++) {
const pushResult = pushResults[i];
if (pushResult.error) {
errors.push(pushResult.error);
if (webInvalidTokenErrorCodes.includes(pushResult.error.statusCode)) {
invalidTokens.push(deviceTokens[i]);
}
}
}
const result = {};
if (errors.length > 0) {
result.errors = errors;
} else {
result.success = true;
}
if (invalidTokens.length > 0) {
result.invalidTokens = invalidTokens;
}
return { ...result };
}
export type WNSPushError = any | string | Response;
type WNSPushResult = {
+success?: true,
+wnsIDs?: $ReadOnlyArray<string>,
+errors?: $ReadOnlyArray<WNSPushError>,
+invalidTokens?: $ReadOnlyArray<string>,
};
async function wnsPush(
targetedNotifications: $ReadOnlyArray<TargetedWNSNotification>,
): Promise<WNSPushResult> {
const token = await getWNSToken();
if (!token && process.env.NODE_ENV === 'development') {
console.log(`no keyserver/secrets/wns_config.json so ignoring notifs`);
return { success: true };
}
invariant(token, `keyserver/secrets/wns_config.json should exist`);
const pushResults = targetedNotifications.map(async targetedNotification => {
const notificationString = JSON.stringify(
targetedNotification.notification,
);
try {
return await wnsSinglePush(
token,
notificationString,
targetedNotification.deviceToken,
);
} catch (error) {
return { error };
}
});
const errors = [];
const notifIDs = [];
const invalidTokens = [];
const deviceTokens = targetedNotifications.map(
({ deviceToken }) => deviceToken,
);
for (let i = 0; i < pushResults.length; i++) {
const pushResult = await pushResults[i];
if (pushResult.error) {
errors.push(pushResult.error);
if (
pushResult.error === 'invalidDomain' ||
wnsInvalidTokenErrorCodes.includes(pushResult.error?.status)
) {
invalidTokens.push(deviceTokens[i]);
}
} else {
notifIDs.push(pushResult.wnsID);
}
}
const result = {};
if (notifIDs.length > 0) {
result.wnsIDs = notifIDs;
}
if (errors.length > 0) {
result.errors = errors;
} else {
result.success = true;
}
if (invalidTokens.length > 0) {
result.invalidTokens = invalidTokens;
}
return { ...result };
}
async function wnsSinglePush(token: string, notification: string, url: string) {
const parsedURL = new URL(url);
const domain = parsedURL.hostname.split('.').slice(-3);
if (
domain[0] !== 'notify' ||
domain[1] !== 'windows' ||
domain[2] !== 'com'
) {
return { error: 'invalidDomain' };
}
try {
const result = await nodeFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'X-WNS-Type': 'wns/raw',
'Authorization': `Bearer ${token}`,
},
body: notification,
});
if (!result.ok) {
return { error: result };
}
const wnsID = result.headers.get('X-WNS-MSG-ID');
invariant(wnsID, 'Missing WNS ID');
return { wnsID };
} catch (err) {
return { error: err };
}
}
async function blobServiceUpload(payload: string): Promise<
| {
+blobHash: string,
+encryptionKey: string,
}
| { +blobUploadError: string },
> {
- return upload(payload);
+ const blobHolder = uuid.v4();
+ try {
+ const { encryptionKey, encryptedPayload, encryptedPayloadHash } =
+ await encryptBlobPayload(payload);
+ await upload(encryptedPayload, encryptedPayloadHash, blobHolder);
+ return {
+ blobHash: encryptedPayloadHash,
+ encryptionKey,
+ };
+ } catch (e) {
+ return {
+ blobUploadError: e.message,
+ };
+ }
}
export {
apnPush,
blobServiceUpload,
fcmPush,
webPush,
wnsPush,
getUnreadCounts,
apnMaxNotificationPayloadByteSize,
fcmMaxNotificationPayloadByteSize,
wnsMaxNotificationPayloadByteSize,
};
diff --git a/keyserver/src/services/blob.js b/keyserver/src/services/blob.js
index 976eb0f0f..271a2285d 100644
--- a/keyserver/src/services/blob.js
+++ b/keyserver/src/services/blob.js
@@ -1,95 +1,61 @@
// @flow
-import crypto from 'crypto';
-import uuid from 'uuid';
-
import blobService from 'lib/facts/blob-service.js';
-import { toBase64URL } from 'lib/utils/base64.js';
import { makeBlobServiceEndpointURL } from 'lib/utils/blob-service.js';
import { getMessageForException } from 'lib/utils/errors.js';
-import { encrypt, generateKey } from '../utils/aes-crypto-utils.js';
-
-async function upload(payload: string): Promise<
- | {
- +blobHash: string,
- +encryptionKey: string,
- }
- | { +blobUploadError: string },
-> {
- const encryptionKey = await generateKey();
- const encryptedPayloadBuffer = Buffer.from(
- await encrypt(encryptionKey, new TextEncoder().encode(payload)),
- );
-
- const blobHolder = uuid.v4();
- const blobHashBase64 = await crypto
- .createHash('sha256')
- .update(encryptedPayloadBuffer)
- .digest('base64');
-
- const blobHash = toBase64URL(blobHashBase64);
-
+async function upload(blob: Blob, hash: string, holder: string): Promise<void> {
const formData = new FormData();
- const payloadBlob = new Blob([encryptedPayloadBuffer]);
-
- formData.append('blob_hash', blobHash);
- formData.append('blob_data', payloadBlob);
+ formData.append('blob_hash', hash);
+ formData.append('blob_data', blob);
const assignHolderPromise = fetch(
makeBlobServiceEndpointURL(blobService.httpEndpoints.ASSIGN_HOLDER),
{
method: blobService.httpEndpoints.ASSIGN_HOLDER.method,
body: JSON.stringify({
- holder: blobHolder,
- blob_hash: blobHash,
+ holder,
+ blob_hash: hash,
}),
headers: {
'content-type': 'application/json',
},
},
);
const uploadHolderPromise = fetch(
makeBlobServiceEndpointURL(blobService.httpEndpoints.UPLOAD_BLOB),
{
method: blobService.httpEndpoints.UPLOAD_BLOB.method,
body: formData,
},
);
+ let assignHolderResponse, uploadBlobResponse;
try {
- const [assignHolderResponse, uploadBlobResponse] = await Promise.all([
+ [assignHolderResponse, uploadBlobResponse] = await Promise.all([
assignHolderPromise,
uploadHolderPromise,
]);
-
- if (!assignHolderResponse.ok) {
- const { status, statusText } = assignHolderResponse;
- return {
- blobUploadError: `Holder assignment failed with HTTP ${status}: ${statusText}`,
- };
- }
-
- if (!uploadBlobResponse.ok) {
- const { status, statusText } = uploadBlobResponse;
- return {
- blobUploadError: `Payload upload failed with HTTP ${status}: ${statusText}`,
- };
- }
} catch (e) {
- return {
- blobUploadError: `Payload upload failed with: ${
+ throw new Error(
+ `Payload upload failed with: ${
getMessageForException(e) ?? 'unknown error'
}`,
- };
+ );
}
- const encryptionKeyString = Buffer.from(encryptionKey).toString('base64');
- return {
- blobHash,
- encryptionKey: encryptionKeyString,
- };
+ if (!assignHolderResponse.ok) {
+ const { status, statusText } = assignHolderResponse;
+ throw new Error(
+ `Holder assignment failed with HTTP ${status}: ${statusText}`,
+ );
+ }
+
+ if (!uploadBlobResponse.ok) {
+ const { status, statusText } = uploadBlobResponse;
+ throw new Error(`Payload upload failed with HTTP ${status}: ${statusText}`);
+ }
}
export { upload };

File Metadata

Mime Type
text/x-diff
Expires
Wed, Dec 25, 5:16 PM (6 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2695111
Default Alt Text
(27 KB)

Event Timeline