Skip to content

Commit a191016

Browse files
authored
feat: universal-github-app-jwt is now a native ES Module (#65)
BREAKING CHANGE: `universal-github-app-jwt` is now a native ES Module BREAKING CHANGE: `githubAppJwt` is now a default export. Before ```js import { githubAppJwt } from "universal-github-app-jwt"; ``` now ```js import githubAppJwt from "universal-github-app-jwt"; ```
1 parent 8f29a49 commit a191016

22 files changed

+6198
-12983
lines changed

.github/workflows/test.yml

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
1-
"on":
2-
- push
3-
- pull_request
41
name: Test
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
types: [opened, synchronize]
8+
59
jobs:
6-
npmCi:
7-
name: test
10+
node:
811
runs-on: ubuntu-latest
912
steps:
1013
- uses: actions/checkout@v3
1114
- uses: actions/setup-node@v3
1215
with:
13-
node-version: lts/*
16+
node-version: 18
1417
cache: npm
15-
- uses: microsoft/playwright-github-action@v1
1618
- run: npm ci
1719
- run: npm test
20+
deno:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v3
24+
- uses: actions/setup-node@v3
25+
with:
26+
node-version: 18
27+
cache: npm
28+
- uses: denoland/setup-deno@v1
29+
with:
30+
deno-version: v1.x
31+
- run: npm ci
32+
- run: npm run build:default
33+
- run: npm run test:deno

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1+
dist/
12
coverage/
2-
node_modules/
3-
pkg/
3+
node_modules/

README.md

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
> Calculate GitHub App bearer tokens for Node & modern browsers
44
5-
[![@latest](https://img.shields.io/npm/v/universal-github-app-jwt.svg)](https://www.npmjs.com/package/universal-github-app-jwt)
6-
![Build status](https://github.com/gr2m/universal-github-app-jwt/workflows/Test/badge.svg)
7-
[![Greenkeeper](https://badges.greenkeeper.io/gr2m/universal-github-app-jwt.svg)](https://greenkeeper.io/)
5+
[![@latest](https://img.shields.io/npm/vuniversal-github-app-jwt.svg)](https://www.npmjs.com/packageuniversal-github-app-jwt)
6+
[![Build Status](https://github.com/gr2m/universal-github-app-jwt/workflows/Test/badge.svg)](https://github.com/gr2m/universal-github-app-jwt/actions?query=workflow%3ATest+branch%3Amaster)
87

98
⚠ The private keys provide by GitHub are in `PKCS#1` format, but the WebCrypto API only supports `PKCS#8`. You can see the difference in the first line, `PKCS#1` format starts with `-----BEGIN RSA PRIVATE KEY-----` while `PKCS#8` starts with `-----BEGIN PRIVATE KEY-----`. You can convert one format to the other using `oppenssl`:
109

@@ -39,56 +38,54 @@ When using a node, a conversion is not necessary, the implementation is agnostic
3938
<tr><th>
4039
Browsers
4140
</th><td width=100%>
42-
43-
Load `universal-github-app-jwt` directly from [cdn.skypack.dev](https://cdn.skypack.dev)
44-
41+
Load <code>universal-github-app-jwt</code> directly from <a href="https://esm.sh">esm.sh</a>
42+
4543
```html
4644
<script type="module">
47-
import { githubAppJwt } from "https://cdn.skypack.dev/universal-github-app-jwt";
45+
import githubAppJwt from "https://esm.sh/universal-github-app-jwt";
4846
</script>
4947
```
5048

5149
</td></tr>
5250
<tr><th>
53-
Deno
51+
Node
5452
</th><td>
5553

54+
Install with <code>npm install universal-github-app-jwt</code>
55+
5656
```js
57-
import { githubAppJwt } from "https://cdn.skypack.dev/universal-github-app-jwt";
57+
import githubAppJwt from "universal-github-app-jwt";
5858
```
5959

6060
</td></tr>
6161
<tr><th>
62-
Node
62+
Deno
6363
</th><td>
6464

65-
Install with <code>npm install universal-github-app-jwt</code>
65+
Load <code>universal-github-app-jwt</code> directly from <a href="https://esm.sh">esm.sh</a>, including types.
6666

6767
```js
68-
const { githubAppJwt } = require("universal-github-app-jwt");
69-
// or: import { githubAppJwt } from "universal-github-app-jwt";
68+
import githubAppJwt from "https://esm.sh/universal-github-app-jwt";
7069
```
7170

7271
</td></tr>
7372
</tbody>
7473
</table>
7574

7675
```js
77-
(async () => {
78-
const { token, appId, expiration } = await githubAppJwt({
79-
id: APP_ID,
80-
privateKey: PRIVATE_KEY
81-
});
82-
})();
76+
const { token, appId, expiration } = await githubAppJwt({
77+
id: APP_ID,
78+
privateKey: PRIVATE_KEY,
79+
});
8380
```
8481

8582
The retrieved `token` can now be used in Authorization request header, e.g. with [`@octokit/request`](https://github.com/octokit/request.js/#readme):
8683

8784
```js
8885
request("GET /app", {
8986
headers: {
90-
authorization: `bearer ${token}`
91-
}
87+
authorization: `bearer ${token}`,
88+
},
9289
});
9390
```
9491

index.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export type Options = {
2+
id: number;
3+
privateKey: string;
4+
now?: number;
5+
};
6+
7+
export type Result = {
8+
appId: number;
9+
expiration: number;
10+
token: string;
11+
};
12+
13+
export default function githubAppJwt(options: Options): Promise<Result>;

src/index.ts renamed to index.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
1-
import { getToken } from "./get-token";
1+
// @ts-check
22

3-
import { Options, Result } from "./types";
3+
// @ts-ignore - #get-token is defined in "imports" in package.json
4+
import { getToken } from "#get-token";
45

5-
export async function githubAppJwt({
6+
/**
7+
* @param {import(".").Options} options
8+
* @returns {Promise<import(".").Result>}
9+
*/
10+
export default async function githubAppJwt({
611
id,
712
privateKey,
813
now = Math.floor(Date.now() / 1000),
9-
}: Options): Promise<Result> {
14+
}) {
1015
// When creating a JSON Web Token, it sets the "issued at time" (iat) to 30s
1116
// in the past as we have seen people running situations where the GitHub API
1217
// claimed the iat would be in future. It turned out the clocks on the
1318
// different machine were not in sync.
14-
const nowWithSafetyMargin = now - 30
19+
const nowWithSafetyMargin = now - 30;
1520
const expiration = nowWithSafetyMargin + 60 * 10; // JWT expiration time (10 minute maximum)
1621

1722
const payload = {
1823
iat: nowWithSafetyMargin, // Issued at time
1924
exp: expiration,
20-
iss: id
25+
iss: id,
2126
};
2227

2328
const token = await getToken({
2429
privateKey,
25-
payload
30+
payload,
2631
});
2732

2833
return {
2934
appId: id,
3035
expiration,
31-
token
36+
token,
3237
};
3338
}

index.test-d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expectType } from "tsd";
2+
import githubAppJwt from ".";
3+
4+
export async function test() {
5+
const result = await githubAppJwt({
6+
id: 123,
7+
privateKey: "",
8+
});
9+
10+
expectType<number>(result.appId);
11+
expectType<number>(result.expiration);
12+
expectType<string>(result.token);
13+
}

internals.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type Payload = {
2+
iat: number;
3+
exp: number;
4+
iss: number;
5+
};
6+
7+
export type Header = { alg: "RS256"; typ: "JWT" };
8+
9+
export type GetTokenOptions = {
10+
privateKey: string;
11+
payload: Payload;
12+
};
13+
14+
export interface GetToken {
15+
(options: GetTokenOptions): Promise<string>;
16+
}

jsconfig.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"compilerOptions": {
3+
"strictNullChecks": true
4+
}
5+
}

lib/get-token-node.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// @ts-check
2+
3+
import jsonwebtoken from "jsonwebtoken";
4+
5+
/**
6+
* @param {import('../internals').GetTokenOptions} options
7+
* @returns {Promise<string>}
8+
*/
9+
export async function getToken({ privateKey, payload }) {
10+
return jsonwebtoken.sign(payload, privateKey, {
11+
algorithm: "RS256",
12+
});
13+
}

src/get-token-browser.ts renamed to lib/get-token.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import { GetTokenOptions, Token } from "./types";
1+
// we don't @ts-check here because it chokes crypto which is a global API in modern JS runtime environments
2+
23
import {
34
getEncodedMessage,
45
getDERfromPEM,
56
string2ArrayBuffer,
6-
base64encode
7-
} from "./utils";
7+
base64encode,
8+
} from "./utils.js";
89

9-
export const getToken = async ({
10-
privateKey,
11-
payload
12-
}: GetTokenOptions): Promise<Token> => {
10+
/**
11+
* @param {import('../internals').GetTokenOptions} options
12+
* @returns {Promise<string>}
13+
*/
14+
export async function getToken({ privateKey, payload }) {
1315
// WebCrypto only supports PKCS#8, unfortunately
1416
if (/BEGIN RSA PRIVATE KEY/.test(privateKey)) {
1517
throw new Error(
@@ -19,8 +21,10 @@ export const getToken = async ({
1921

2022
const algorithm = {
2123
name: "RSASSA-PKCS1-v1_5",
22-
hash: { name: "SHA-256" }
24+
hash: { name: "SHA-256" },
2325
};
26+
27+
/** @type {import('../internals').Header} */
2428
const header = { alg: "RS256", typ: "JWT" };
2529

2630
const privateKeyDER = getDERfromPEM(privateKey);
@@ -44,4 +48,4 @@ export const getToken = async ({
4448
const encodedSignature = base64encode(signatureArrBuf);
4549

4650
return `${encodedMessage}.${encodedSignature}`;
47-
};
51+
}

lib/utils.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// we don't @ts-check here because it chokes on atob and btoa which are available in all modern JS runtime environments
2+
3+
/**
4+
* @param {string} str
5+
* @returns {ArrayBuffer}
6+
*/
7+
export function string2ArrayBuffer(str) {
8+
const buf = new ArrayBuffer(str.length);
9+
const bufView = new Uint8Array(buf);
10+
for (let i = 0, strLen = str.length; i < strLen; i++) {
11+
bufView[i] = str.charCodeAt(i);
12+
}
13+
return buf;
14+
}
15+
16+
/**
17+
* @param {string} pem
18+
* @returns {ArrayBuffer}
19+
*/
20+
export function getDERfromPEM(pem) {
21+
const pemB64 = pem
22+
.trim()
23+
.split("\n")
24+
.slice(1, -1) // Remove the --- BEGIN / END PRIVATE KEY ---
25+
.join("");
26+
27+
const decoded = atob(pemB64);
28+
return string2ArrayBuffer(decoded);
29+
}
30+
31+
/**
32+
*
33+
* @param {import('../internals').Header} header
34+
* @param {import('../internals').Payload} payload
35+
* @returns {string}
36+
*/
37+
export function getEncodedMessage(header, payload) {
38+
return `${base64encodeJSON(header)}.${base64encodeJSON(payload)}`;
39+
}
40+
41+
/**
42+
* @param {ArrayBuffer} buffer
43+
* @returns {string}
44+
*/
45+
export function base64encode(buffer) {
46+
var binary = "";
47+
var bytes = new Uint8Array(buffer);
48+
var len = bytes.byteLength;
49+
for (var i = 0; i < len; i++) {
50+
binary += String.fromCharCode(bytes[i]);
51+
}
52+
53+
return fromBase64(btoa(binary));
54+
}
55+
56+
/**
57+
* @param {string} base64
58+
* @returns {string}
59+
*/
60+
function fromBase64(base64) {
61+
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
62+
}
63+
64+
/**
65+
*
66+
* @param {Record<string,unknown>} obj
67+
* @returns {string}
68+
*/
69+
function base64encodeJSON(obj) {
70+
return fromBase64(btoa(JSON.stringify(obj)));
71+
}

0 commit comments

Comments
 (0)