@inngest/middleware-encryption
Version:
E2E encryption middleware for Inngest.
257 lines (256 loc) • 12.1 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isV0EncryptedValue = exports.isEncryptedValue = exports.EncryptionService = exports.encryptionMiddleware = void 0;
const inngest_1 = require("inngest");
const legacy_1 = require("./strategies/legacy");
const libSodium_1 = require("./strategies/libSodium");
/**
* Encrypts and decrypts data sent to and from Inngest.
*/
const encryptionMiddleware = (
/**
* Options used to configure the encryption middleware. If a custom
* `encryptionService` is not provided, the `key` option is required.
*/
opts) => {
var _a;
const keys = [opts.key, ...((_a = opts.fallbackDecryptionKeys) !== null && _a !== void 0 ? _a : [])].filter(Boolean);
const service = opts.encryptionService || new libSodium_1.LibSodiumEncryptionService(keys);
let __v0LegacyService;
const getV0LegacyService = () => {
var _a;
if (!__v0LegacyService) {
__v0LegacyService = new legacy_1.LEGACY_V0Service(Object.assign({ key: keys, forceEncryptWithV0: Boolean((_a = opts.legacyV0Service) === null || _a === void 0 ? void 0 : _a.forceEncryptWithV0) }, opts.legacyV0Service));
}
return __v0LegacyService;
};
const encryptValue = (value) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
if ((0, exports.isEncryptedValue)(value) || (0, exports.isV0EncryptedValue)(value)) {
console.warn("Encryption middleware is encrypting a value that appears to be already encrypted. Did you add the middleware twice?");
}
if ((_a = opts.legacyV0Service) === null || _a === void 0 ? void 0 : _a.forceEncryptWithV0) {
return {
[EncryptionService.ENCRYPTION_MARKER]: true,
data: getV0LegacyService().service.encrypt(value),
};
}
return {
[EncryptionService.ENCRYPTION_MARKER]: true,
[EncryptionService.STRATEGY_MARKER]: service.identifier,
data: yield service.encrypt(value),
};
});
const decryptValue = (value) => __awaiter(void 0, void 0, void 0, function* () {
if ((0, exports.isEncryptedValue)(value)) {
return service.decrypt(value.data);
}
if ((0, exports.isV0EncryptedValue)(value)) {
return getV0LegacyService().service.decrypt(value.data);
}
return value;
});
const fieldShouldBeEncrypted = (field) => {
if (typeof opts.eventEncryptionField === "undefined") {
return field === EncryptionService.DEFAULT_ENCRYPTED_EVENT_FIELD;
}
return opts.eventEncryptionField === field;
};
const encryptEventData = (eventData) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
if ((_a = opts.legacyV0Service) === null || _a === void 0 ? void 0 : _a.forceEncryptWithV0) {
const result = getV0LegacyService().encryptEventData(eventData);
if (!isRecord(result)) {
return eventData;
}
return result;
}
const encryptedEntries = yield Promise.all(Object.keys(eventData).map((key) => __awaiter(void 0, void 0, void 0, function* () {
let value = eventData[key];
if (fieldShouldBeEncrypted(key)) {
value = yield encryptValue(eventData[key]);
}
return [key, value];
})));
const encryptedData = encryptedEntries.reduce((acc, [key, value]) => {
return Object.assign(Object.assign({}, acc), { [key]: value });
}, {});
return encryptedData;
});
const decryptEventData = (eventData) => __awaiter(void 0, void 0, void 0, function* () {
const decryptedEntries = yield Promise.all(Object.keys(eventData).map((key) => __awaiter(void 0, void 0, void 0, function* () {
return [key, yield decryptValue(eventData[key])];
})));
const decryptedData = decryptedEntries.reduce((acc, [key, value]) => {
return Object.assign(Object.assign({}, acc), { [key]: value });
}, {});
return decryptedData;
});
class EncryptionMiddleware extends inngest_1.Middleware.BaseMiddleware {
constructor() {
super(...arguments);
this.id = "inngest:encryption";
}
// Decrypt event data before it reaches the function handler.
// IMPORTANT: Do not decrypt step data here. That won't work with
// checkpointing because we don't "enter" the function once per step.
transformFunctionInput(arg) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
let decryptedEvent = arg.ctx.event;
if ((_a = arg.ctx.event) === null || _a === void 0 ? void 0 : _a.data) {
decryptedEvent = Object.assign(Object.assign({}, arg.ctx.event), { data: yield decryptEventData(arg.ctx.event.data) });
}
let decryptedEvents = arg.ctx.events;
if (arg.ctx.events) {
// @ts-expect-error - Promise.all returns T[] but events is a non-empty tuple
decryptedEvents = yield Promise.all(arg.ctx.events.map((event) => __awaiter(this, void 0, void 0, function* () {
if (!event.data) {
return event;
}
return Object.assign(Object.assign({}, event), { data: yield decryptEventData(event.data) });
})));
}
return Object.assign(Object.assign({}, arg), { ctx: Object.assign(Object.assign({}, arg.ctx), { event: decryptedEvent, events: decryptedEvents }) });
});
}
// Encrypt the `step.invoke` input data.
transformStepInput(arg) {
return __awaiter(this, void 0, void 0, function* () {
if (opts.decryptOnly) {
return arg;
}
// `step.invoke` is the only step whose input needs to be encrypted. We'll
// encrypt `step.sendEvent` using the `transformSendEvent` hook.
if (arg.stepInfo.stepType !== "invoke") {
return arg;
}
const encryptedInput = yield Promise.all(arg.input.map((item) => __awaiter(this, void 0, void 0, function* () {
if (!isRecord(item)) {
return item;
}
const { payload } = item;
if (!isRecord(payload)) {
return item;
}
const { data } = payload;
if (!isRecord(data)) {
return item;
}
return Object.assign(Object.assign({}, item), { payload: Object.assign(Object.assign({}, payload), {
// Encrypt invoke data as if it's an event (i.e. only encrypt the
// specific field)
data: yield encryptEventData(data) }) });
})));
return Object.assign(Object.assign({}, arg), { input: encryptedInput });
});
}
// Encrypt the Inngest function's return value before it's sent to the server.
wrapFunctionHandler(_a) {
return __awaiter(this, arguments, void 0, function* ({ next, }) {
const output = yield next();
if (opts.decryptOnly) {
return output;
}
if (output !== undefined && output !== null) {
return encryptValue(output);
}
return output;
});
}
// Encrypt step output before it's sent to the server.
wrapStepHandler(_a) {
return __awaiter(this, arguments, void 0, function* ({ next }) {
const output = yield next();
if (opts.decryptOnly) {
return output;
}
// No need to encrypt `undefined` or `null` values, since they inherently
// do not contain any sensitive data
if (output !== undefined && output !== null) {
return encryptValue(output);
}
return output;
});
}
// Decrypt memoized step data before it's returned into the Inngest function
// handler.
wrapStep(_a) {
return __awaiter(this, arguments, void 0, function* ({ next }) {
return decryptValue(yield next());
});
}
// Encrypt event data before sending to the server.
transformSendEvent(arg) {
return __awaiter(this, void 0, void 0, function* () {
if (opts.decryptOnly) {
return arg;
}
const encryptedEvents = yield Promise.all(arg.events.map((event) => __awaiter(this, void 0, void 0, function* () {
if (!event.data) {
return event;
}
return Object.assign(Object.assign({}, event), { data: yield encryptEventData(event.data) });
})));
return Object.assign(Object.assign({}, arg), { events: encryptedEvents });
});
}
}
return EncryptionMiddleware;
};
exports.encryptionMiddleware = encryptionMiddleware;
/**
* A service that encrypts and decrypts data. You can implement this abstract
* class to provide your own encryption service, or use the default encryption
* service provided by this package.
*/
class EncryptionService {
}
exports.EncryptionService = EncryptionService;
(function (EncryptionService) {
/**
* A marker used to identify encrypted values without having to guess.
*/
EncryptionService.ENCRYPTION_MARKER = "__ENCRYPTED__";
/**
* A marker used to identify the strategy used for encryption.
*/
EncryptionService.STRATEGY_MARKER = "__STRATEGY__";
/**
* The default field used to store encrypted values in events.
*/
EncryptionService.DEFAULT_ENCRYPTED_EVENT_FIELD = "encrypted";
})(EncryptionService || (exports.EncryptionService = EncryptionService = {}));
const isRecord = (value) => {
return typeof value === "object" && value !== null && !Array.isArray(value);
};
const isEncryptedValue = (value) => {
return (typeof value === "object" &&
value !== null &&
EncryptionService.ENCRYPTION_MARKER in value &&
value[EncryptionService.ENCRYPTION_MARKER] === true &&
"data" in value &&
typeof value["data"] === "string" &&
EncryptionService.STRATEGY_MARKER in value &&
typeof value[EncryptionService.STRATEGY_MARKER] === "string");
};
exports.isEncryptedValue = isEncryptedValue;
const isV0EncryptedValue = (value) => {
return (typeof value === "object" &&
value !== null &&
EncryptionService.ENCRYPTION_MARKER in value &&
value[EncryptionService.ENCRYPTION_MARKER] === true &&
"data" in value &&
typeof value["data"] === "string" &&
!(EncryptionService.STRATEGY_MARKER in value));
};
exports.isV0EncryptedValue = isV0EncryptedValue;