Skip to content

Commit

Permalink
feat: v5 (#236)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: output ESM instead of CommonJS
BREAKING CHANGE: remove SHA1 support
BREAKING CHANGE: The `verify()` and `sign()` methods no longer accept an options object
  • Loading branch information
wolfy1339 committed Feb 23, 2024
1 parent 6b1d989 commit fead924
Show file tree
Hide file tree
Showing 13 changed files with 45 additions and 171 deletions.
6 changes: 1 addition & 5 deletions README.md
Expand Up @@ -56,11 +56,7 @@ Node
Install with `npm install @octokit/core @octokit/webhooks-methods`

```js
const {
sign,
verify,
verifyWithFallback,
} = require("@octokit/webhooks-methods");
import { sign, verify, verifyWithFallback } from "@octokit/webhooks-methods";
```

</td></tr>
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions package.json
Expand Up @@ -3,6 +3,7 @@
"publishConfig": {
"access": "public"
},
"type": "module",
"version": "0.0.0-development",
"description": "Methods to handle GitHub Webhook requests",
"scripts": {
Expand All @@ -11,7 +12,7 @@
"lint:fix": "prettier --write '{src,test,scripts}/**/*' README.md package.json",
"pretest": "npm run -s lint",
"test": "npm run -s test:node && npm run -s test:web",
"test:node": "jest --coverage",
"test:node": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest --coverage",
"test:web": "npm run test:deno && npm run test:browser",
"pretest:web": "npm run -s build",
"test:deno": "cd test/deno && deno test",
Expand All @@ -27,7 +28,7 @@
"author": "Gregor Martynus (https://dev.to/gr2m)",
"license": "MIT",
"devDependencies": {
"@octokit/tsconfig": "^2.0.0",
"@octokit/tsconfig": "^3.0.0",
"@types/jest": "^29.0.0",
"@types/node": "^20.0.0",
"esbuild": "^0.20.0",
Expand All @@ -41,11 +42,15 @@
"typescript": "^5.0.0"
},
"jest": {
"extensionsToTreatAsEsm": [
".ts"
],
"transform": {
"^.+\\.(ts|tsx)$": [
"ts-jest",
{
"tsconfig": "test/tsconfig.test.json"
"tsconfig": "test/tsconfig.test.json",
"useESM": true
}
]
},
Expand Down
23 changes: 18 additions & 5 deletions scripts/build.mjs
Expand Up @@ -32,7 +32,7 @@ async function main() {
bundle: true,
platform: "node",
target: "node18",
format: "cjs",
format: "esm",
...sharedOptions,
}),
// Build an ESM browser bundle
Expand All @@ -56,16 +56,29 @@ async function main() {
delete pkg.scripts;
delete pkg.prettier;
delete pkg.release;
delete pkg.jest;
await writeFile(
"pkg/package.json",
JSON.stringify(
{
...pkg,
files: ["dist-*/**"],
main: "dist-node/index.js",
browser: "dist-web/index.js",
types: "dist-types/index.d.ts",
module: "dist-src/index.js",
exports: {
".": {
node: {
types: "./dist-types/index.d.ts",
import: "./dist-node/index.js",
},
browser: {
types: "./dist-types/web.d.ts",
import: "./dist-web/index.js",
},
default: {
types: "./dist-types/index.d.ts",
import: "./dist-node/index.js",
},
},
},
sideEffects: false,
},
null,
Expand Down
20 changes: 2 additions & 18 deletions src/node/sign.ts
@@ -1,19 +1,7 @@
import { createHmac } from "node:crypto";
import { Algorithm, type SignOptions } from "../types.js";
import { VERSION } from "../version.js";

export async function sign(
options: SignOptions | string,
payload: string,
): Promise<string> {
const { secret, algorithm } =
typeof options === "object"
? {
secret: options.secret,
algorithm: options.algorithm || Algorithm.SHA256,
}
: { secret: options, algorithm: Algorithm.SHA256 };

export async function sign(secret: string, payload: string): Promise<string> {
if (!secret || !payload) {
throw new TypeError(
"[@octokit/webhooks-methods] secret & payload required for sign()",
Expand All @@ -24,11 +12,7 @@ export async function sign(
throw new TypeError("[@octokit/webhooks-methods] payload must be a string");
}

if (!Object.values(Algorithm).includes(algorithm as Algorithm)) {
throw new TypeError(
`[@octokit/webhooks] Algorithm ${algorithm} is not supported. Must be 'sha1' or 'sha256'`,
);
}
const algorithm = "sha256";

return `${algorithm}=${createHmac(algorithm, secret)
.update(payload)
Expand Down
6 changes: 1 addition & 5 deletions src/node/verify.ts
Expand Up @@ -3,7 +3,6 @@ import { Buffer } from "node:buffer";

import { sign } from "./sign.js";
import { VERSION } from "../version.js";
import { getAlgorithm } from "../utils.js";

export async function verify(
secret: string,
Expand All @@ -23,11 +22,8 @@ export async function verify(
}

const signatureBuffer = Buffer.from(signature);
const algorithm = getAlgorithm(signature);

const verificationBuffer = Buffer.from(
await sign({ secret, algorithm }, eventPayload),
);
const verificationBuffer = Buffer.from(await sign(secret, eventPayload));

if (signatureBuffer.length !== verificationBuffer.length) {
return false;
Expand Down
11 changes: 0 additions & 11 deletions src/types.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/utils.ts

This file was deleted.

39 changes: 7 additions & 32 deletions src/web.ts
@@ -1,6 +1,3 @@
import { Algorithm, type AlgorithmLike, type SignOptions } from "./types.js";
import { getAlgorithm } from "./utils.js";

const enc = new TextEncoder();

function hexToUInt8Array(string: string) {
Expand All @@ -21,39 +18,22 @@ function UInt8ArrayToHex(signature: ArrayBuffer) {
.join("");
}

function getHMACHashName(algorithm: AlgorithmLike) {
return (
{
[Algorithm.SHA1]: "SHA-1",
[Algorithm.SHA256]: "SHA-256",
} as { [key in Algorithm]: string }
)[algorithm];
}

async function importKey(secret: string, algorithm: AlgorithmLike) {
async function importKey(secret: string) {
// ref: https://developer.mozilla.org/en-US/docs/Web/API/HmacImportParams
return crypto.subtle.importKey(
"raw", // raw format of the key - should be Uint8Array
enc.encode(secret),
{
// algorithm details
name: "HMAC",
hash: { name: getHMACHashName(algorithm) },
hash: { name: "SHA-256" },
},
false, // export = false
["sign", "verify"], // what this key can do
);
}

export async function sign(options: SignOptions | string, payload: string) {
const { secret, algorithm } =
typeof options === "object"
? {
secret: options.secret,
algorithm: options.algorithm || Algorithm.SHA256,
}
: { secret: options, algorithm: Algorithm.SHA256 };

export async function sign(secret: string, payload: string): Promise<string> {
if (!secret || !payload) {
throw new TypeError(
"[@octokit/webhooks-methods] secret & payload required for sign()",
Expand All @@ -64,15 +44,10 @@ export async function sign(options: SignOptions | string, payload: string) {
throw new TypeError("[@octokit/webhooks-methods] payload must be a string");
}

if (!Object.values(Algorithm).includes(algorithm as Algorithm)) {
throw new TypeError(
`[@octokit/webhooks] Algorithm ${algorithm} is not supported. Must be 'sha1' or 'sha256'`,
);
}

const algorithm = "sha256";
const signature = await crypto.subtle.sign(
"HMAC",
await importKey(secret, algorithm),
await importKey(secret),
enc.encode(payload),
);

Expand All @@ -96,10 +71,10 @@ export async function verify(
);
}

const algorithm = getAlgorithm(signature);
const algorithm = "sha256";
return await crypto.subtle.verify(
"HMAC",
await importKey(secret, algorithm),
await importKey(secret),
hexToUInt8Array(signature.replace(`${algorithm}=`, "")),
enc.encode(eventPayload),
);
Expand Down
6 changes: 3 additions & 3 deletions test/browser-test.js
@@ -1,7 +1,7 @@
const { strictEqual } = require("node:assert");
import { strictEqual } from "node:assert";

const { readFile } = require("node:fs/promises");
const puppeteer = require("puppeteer");
import { readFile } from "node:fs/promises";
import puppeteer from "puppeteer";

runTests();

Expand Down
38 changes: 1 addition & 37 deletions test/sign.test.ts
Expand Up @@ -35,50 +35,14 @@ describe("sign", () => {
);
});

test("sign({secret, algorithm}) throws with invalid algorithm", async () => {
await expect(() =>
// @ts-expect-error
sign({ secret, algorithm: "sha2" }, JSON.stringify(eventPayload)),
).rejects.toThrow(
"[@octokit/webhooks] Algorithm sha2 is not supported. Must be 'sha1' or 'sha256'",
);
});

describe("with eventPayload as string", () => {
describe("returns expected sha1 signature", () => {
describe("returns expected sha256 signature", () => {
test("sign(secret, eventPayload)", async () => {
const signature = await sign(secret, JSON.stringify(eventPayload));
expect(signature).toBe(
"sha256=4864d2759938a15468b5df9ade20bf161da9b4f737ea61794142f3484236bda3",
);
});

test("sign({secret}, eventPayload)", async () => {
const signature = await sign({ secret }, JSON.stringify(eventPayload));
expect(signature).toBe(
"sha256=4864d2759938a15468b5df9ade20bf161da9b4f737ea61794142f3484236bda3",
);
});

test("sign({secret, algorithm: 'sha1'}, eventPayload)", async () => {
const signature = await sign(
{ secret, algorithm: "sha1" },
JSON.stringify(eventPayload),
);
expect(signature).toBe("sha1=d03207e4b030cf234e3447bac4d93add4c6643d8");
});
});

describe("returns expected sha256 signature", () => {
test("sign({secret, algorithm: 'sha256'}, eventPayload)", async () => {
const signature = await sign(
{ secret, algorithm: "sha256" },
JSON.stringify(eventPayload),
);
expect(signature).toBe(
"sha256=4864d2759938a15468b5df9ade20bf161da9b4f737ea61794142f3484236bda3",
);
});
});
});

Expand Down
1 change: 0 additions & 1 deletion test/tsconfig.test.json
Expand Up @@ -3,7 +3,6 @@
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"verbatimModuleSyntax": false,
"allowImportingTsExtensions": true
},
"include": ["src/**/*"]
Expand Down

0 comments on commit fead924

Please sign in to comment.