Skip to content

Commit

Permalink
Add middleware-e2e-encryption middleware example (#402)
Browse files Browse the repository at this point in the history
## 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
jpwilliams authored Nov 17, 2023
1 parent c77f6d7 commit dcfee74
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
- id: matrix
run: |
blocklist=$(grep -v '^#' examples/.inngestignore | jq -R . | jq -s .)
echo "matrix=$(find examples -mindepth 1 -maxdepth 1 -type d | grep -v -f <(echo $blocklist | jq -r '.[]') | xargs -n 1 basename | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
echo "matrix=$(find examples -mindepth 1 -maxdepth 1 -type d -name 'framework-*' | grep -v -f <(echo $blocklist | jq -r '.[]') | xargs -n 1 basename | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
examples:
name: Test examples
Expand Down
37 changes: 37 additions & 0 deletions examples/middleware-e2e-encryption/README.md
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 examples/middleware-e2e-encryption/fullEncryptionMiddlware.ts
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),
},
};
},
}),
}),
});
};
18 changes: 18 additions & 0 deletions examples/middleware-e2e-encryption/package.json
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 examples/middleware-e2e-encryption/stepEncryptionMiddleware.ts
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),
},
};
},
}),
}),
});
};
3 changes: 2 additions & 1 deletion packages/inngest/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ We can create new examples using the following formula:
1. Clone or create a new example in [examples/](../../examples/) using one of the following naming conventions:
- `framework-<name>` - bare-bones framework example
- `with-<external-tool>` - using another library or service
- `middleware-<name>` - a single-file example of middleware
- `<generic-use-case>-<concrete-implementation>` - e.g. `email-drip-campaign`
- `<pattern>-<concrete-use-case>` - e.g. `fan-out-weekly-digest`, `parallel-<xyz>`
2. Run the example using `pnpm dev:example` and confirm it works
2. If it's a runnable example, run the example using `pnpm dev:example` and confirm it works
3. Ensure the `inngest` version in `package.json` is set to the latest major version, e.g. `^3.0.0`
4. Remove all lock files, e.g. `package-lock.json`
5. Adapt a `README.md` from an existing example, which should include:
Expand Down
2 changes: 1 addition & 1 deletion packages/inngest/scripts/runExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const examplesPath = path.join(__dirname, "..", "..", "..", "examples");

const examples: string[] = fs
.readdirSync(examplesPath, { withFileTypes: true })
.filter((file) => file.isDirectory())
.filter((file) => file.isDirectory() && file.name.startsWith("framework-"))
.map((file) => file.name);

const exampleFromFlag: string = (argv.example as string) ?? "";
Expand Down

0 comments on commit dcfee74

Please sign in to comment.