-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
middleware-e2e-encryption
middleware example (#402)
## Summary <!-- Succinctly describe your change, providing context, what you've changed, and why. --> Adds an example middleware, [`middleware-e2e-encryption`](https://github.com/inngest/inngest-js/tree/middleware-encryption-example/examples/middleware-e2e-encryption). ## Checklist <!-- Tick these items off as you progress. --> <!-- If an item isn't applicable, ideally please strikeout the item by wrapping it in "~~"" and suffix it with "N/A My reason for skipping this." --> <!-- e.g. "- [ ] ~~Added tests~~ N/A Only touches docs" --> - [ ] ~~Added a [docs PR](https://github.com/inngest/website) that references this PR~~ N/A - [ ] ~~Added unit/integration tests~~ N/A - [x] Added changesets if applicable
- Loading branch information
1 parent
c77f6d7
commit dcfee74
Showing
7 changed files
with
219 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# E2E Encryption | ||
|
||
We can use middleware to encrypt data before it's shipped to Inngest and decrypt it as it comes back in to functions. | ||
|
||
In [`stepEncryptionMiddleware.ts`](./stepEncryptionMiddleware.ts), we provide an example of encrypting and decrypting all step state as it is passed to and from Inngest. This example's "encryption" is just stringifying and reversing the value - in practice you'll want to replace this with your own method using something like [`node:crypto`](https://nodejs.org/api/crypto.html). | ||
|
||
> [!WARNING] | ||
> If you encrypt your step data and lose your encryption key, you'll lose access to all encrypted state. Be careful! In addition, seeing step results in the Inngest dashboard will no longer be possible. | ||
```ts | ||
const inngest = new Inngest({ | ||
id: "my-app", | ||
middleware: [stepEncryptionMiddleware()], | ||
}); | ||
|
||
inngest.createFunction( | ||
{ id: "example-function" }, | ||
{ event: "app/user.created" }, | ||
async ({ event, step }) => { | ||
/** | ||
* The return value of `db.get()` - and therefore the value of `user` is now | ||
* silently encrypted and decrypted by the middleware; no plain-text step | ||
* data leaves your server or is stored in Inngest Cloud. | ||
*/ | ||
const user = await step.run("get-user", () => | ||
db.get("user", event.data.userId) | ||
); | ||
} | ||
); | ||
``` | ||
|
||
It's also easily possible to also encrypt all event data, too, with [`fullEncryptionMiddleware.ts`](./fullEncryptionMiddlware.ts). | ||
|
||
> [!WARNING] | ||
> Encrypting event data means that using features of Inngest such as `step.waitForEvent()` with expressions and browsing event data in the dashboard are no longer possible. | ||
Be aware that, unlike step data, event data is much more commonly shared between systems; think about if you need to also encrypt your event data before doing so. |
86 changes: 86 additions & 0 deletions
86
examples/middleware-e2e-encryption/fullEncryptionMiddlware.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { InngestMiddleware } from "inngest"; | ||
|
||
const encryptionMarker = "__ENCRYPTED__"; | ||
type EncryptedValue = { [encryptionMarker]: true; data: string }; | ||
|
||
export const encryptionMiddleware = ( | ||
key: string = process.env.INNGEST_ENCRYPTION_KEY as string | ||
) => { | ||
if (!key) { | ||
throw new Error("Missing INNGEST_ENCRYPTION_KEY environment variable"); | ||
} | ||
|
||
// Some internal functions that we'll use to encrypt and decrypt values. | ||
// In practice, you'll want to use the `key` passed in to handle encryption | ||
// properly. | ||
const isEncryptedValue = (value: unknown): value is EncryptedValue => { | ||
return ( | ||
typeof value === "object" && | ||
value !== null && | ||
encryptionMarker in value && | ||
value[encryptionMarker] === true && | ||
"data" in value && | ||
typeof value["data"] === "string" | ||
); | ||
}; | ||
|
||
const encrypt = (value: unknown): EncryptedValue => { | ||
return { | ||
[encryptionMarker]: true, | ||
data: JSON.stringify(value).split("").reverse().join(""), | ||
}; | ||
}; | ||
|
||
const decrypt = <T>(value: T): T => { | ||
if (isEncryptedValue(value)) { | ||
return JSON.parse(value.data.split("").reverse().join("")) as T; | ||
} | ||
|
||
return value; | ||
}; | ||
|
||
return new InngestMiddleware({ | ||
name: "Full Encryption Middleware", | ||
init: () => ({ | ||
onSendEvent: () => ({ | ||
transformInput: ({ payloads }) => ({ | ||
payloads: payloads.map((payload) => ({ | ||
...payload, | ||
data: payload.data && encrypt(payload.data), | ||
})), | ||
}), | ||
}), | ||
onFunctionRun: () => ({ | ||
transformInput: ({ ctx, steps }) => ({ | ||
steps: steps.map((step) => ({ | ||
...step, | ||
data: step.data && decrypt(step.data), | ||
})), | ||
ctx: { | ||
event: ctx.event && { | ||
...ctx.event, | ||
data: ctx.event.data && decrypt(ctx.event.data), | ||
}, | ||
events: | ||
ctx.events && | ||
ctx.events?.map((event) => ({ | ||
...event, | ||
data: event.data && decrypt(event.data), | ||
})), | ||
} as {}, | ||
}), | ||
transformOutput: (ctx) => { | ||
if (!ctx.step) { | ||
return; | ||
} | ||
|
||
return { | ||
result: { | ||
data: ctx.result.data && encrypt(ctx.result.data), | ||
}, | ||
}; | ||
}, | ||
}), | ||
}), | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "middleware-e2e-encryption", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "ISC", | ||
"dependencies": { | ||
"inngest": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20.9.1" | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
examples/middleware-e2e-encryption/stepEncryptionMiddleware.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { InngestMiddleware } from "inngest"; | ||
|
||
const encryptionMarker = "__ENCRYPTED__"; | ||
type EncryptedValue = { [encryptionMarker]: true; data: string }; | ||
|
||
export const encryptionMiddleware = ( | ||
key: string = process.env.INNGEST_ENCRYPTION_KEY as string | ||
) => { | ||
if (!key) { | ||
throw new Error("Missing INNGEST_ENCRYPTION_KEY environment variable"); | ||
} | ||
|
||
// Some internal functions that we'll use to encrypt and decrypt values. | ||
// In practice, you'll want to use the `key` passed in to handle encryption | ||
// properly. | ||
const isEncryptedValue = (value: unknown): value is EncryptedValue => { | ||
return ( | ||
typeof value === "object" && | ||
value !== null && | ||
encryptionMarker in value && | ||
value[encryptionMarker] === true && | ||
"data" in value && | ||
typeof value["data"] === "string" | ||
); | ||
}; | ||
|
||
const encrypt = (value: unknown): EncryptedValue => { | ||
return { | ||
[encryptionMarker]: true, | ||
data: JSON.stringify(value).split("").reverse().join(""), | ||
}; | ||
}; | ||
|
||
const decrypt = <T>(value: T): T => { | ||
if (isEncryptedValue(value)) { | ||
return JSON.parse(value.data.split("").reverse().join("")) as T; | ||
} | ||
|
||
return value; | ||
}; | ||
|
||
return new InngestMiddleware({ | ||
name: "Step Encryption Middleware", | ||
init: () => ({ | ||
onSendEvent: () => ({ | ||
transformInput: ({ payloads }) => ({ | ||
payloads: payloads.map((payload) => ({ | ||
...payload, | ||
data: payload.data && encrypt(payload.data), | ||
})), | ||
}), | ||
}), | ||
onFunctionRun: () => ({ | ||
transformInput: ({ ctx, steps }) => ({ | ||
steps: steps.map((step) => ({ | ||
...step, | ||
data: step.data && decrypt(step.data), | ||
})), | ||
}), | ||
transformOutput: (ctx) => { | ||
if (!ctx.step) { | ||
return; | ||
} | ||
|
||
return { | ||
result: { | ||
data: ctx.result.data && encrypt(ctx.result.data), | ||
}, | ||
}; | ||
}, | ||
}), | ||
}), | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters