Skip to content

Commit

Permalink
chore: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
maxholman committed Apr 22, 2023
0 parents commit 6128f5e
Show file tree
Hide file tree
Showing 49 changed files with 7,717 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# top-most EditorConfig file
root = true

[*]
end_of_line = lf
charset = utf-8
insert_final_newline = true
indent_style = space
indent_size = 2

[Makefile]
indent_style = tab
indent_size = 4
insert_final_newline = false
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
node_modules
build
tmp
10 changes: 10 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
root: true,
extends: ['@block65/eslint-config'],
parserOptions: {
project: './tsconfig-eslint.json',
},
rules: {
'no-console': 'off',
},
};
20 changes: 20 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Deploy

on:
workflow_dispatch: {}
release:
types: [published]

jobs:
publish-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: https://registry.npmjs.org/
- run: make
- run: npm publish --access=public
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
16 changes: 16 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Unit Tests (PR)

on:
pull_request:
branches: ['master']

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: https://registry.npmjs.org/
- run: make test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
coverage
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18
18 changes: 18 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Copyright 2023 Block65 Pte Ltd

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

SRCS = $(wildcard lib/**)

all: dist

.PHONY: deps
deps: node_modules

.PHONY: clean
clean:
pnpm tsc -b --clean

.PHONY: test
test:
pnpm vitest

node_modules: package.json
pnpm install

dist: node_modules tsconfig.json $(SRCS)
pnpm tsc

.PHONY: dist-watch
dist-watch:
pnpm tsc -w --preserveWatchOutput

.PHONY: pretty
pretty: node_modules
pnpm eslint --fix .
pnpm prettier --write .
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# @block65/webcrypto-web-push

Send notifications using Web Push Protocol and Web Crypto APIs (compatible with
Cloudflare Workers)

## Installation

Using yarn:

```
yarn add @block65/webcrypto-web-push
```

Using pnpm:

```
pnpm add @block65/webcrypto-web-push
```

## Usage

```typescript
import {
type PushSubscription,
type Notification,
type VapidKeys
sendPushNotification,
} from '@block65/webcrypto-web-push';

const vapid: VapidKeys = {
subject: env.VAPID_SUBJECT,
publicKey: env.VAPID_SERVER_PUBLIC_KEY,
privateKey: env.VAPID_SERVER_PRIVATE_KEY,
};

// You would probably get a subscription object from the datastore
const subscription: PushSubscription = {
endpoint: 'https://fcm.googleapis.com/fcm/send/...',
expirationTime: null,
keys: {
p256dh: '...',
auth: '...',
},
};

const notification: Notification = {
body: 'You have a new message!',
options: {
ttl: 60,
},
};

// send the payload using your favourite fetch library
const init = await buildPushPayload(notification, subscription, vapid);
const res = await fetch(subscription.endpoint, init);
```

## License

This package is licensed under the MIT license. See the LICENSE file for more information.
8 changes: 8 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
modulePathIgnorePatterns: ['<rootDir>/dist'],
testMatch: ['<rootDir>/__tests__/**/*.test.[jt]s?(x)'],
moduleNameMapper: {
'^(\\..*)\\.jsx?$': '$1', // support for ts imports with .js extensions
},
extensionsToTreatAsEsm: ['.ts', '.tsx'],
};
29 changes: 29 additions & 0 deletions lib/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
decode as decodeBase64,
encode as encodeBase64,
} from 'base64-arraybuffer';

export { decodeBase64, encodeBase64 };

export function decodeBase64Url(str: string): ArrayBuffer {
return decodeBase64(str.replace(/-/g, '+').replace(/_/g, '/'));
}

export function encodeBase64Url(arr: ArrayBuffer): string {
return encodeBase64(arr)
.replace(/\//g, '_')
.replace(/\+/g, '-')
.replace(/=+$/, '');
}

export function base64UrlToObject<T extends Record<string, unknown>>(
str: string,
): T {
return JSON.parse(new TextDecoder().decode(decodeBase64Url(str))) as T;
}

export function objectToBase64Url<T extends Record<string, unknown>>(
obj: T,
): string {
return encodeBase64Url(new TextEncoder().encode(JSON.stringify(obj)));
}
50 changes: 50 additions & 0 deletions lib/cf-jwt/assert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export function assertString<T extends string>(
value: unknown,
): asserts value is T {
if (typeof value !== 'string') {
throw new Error('value must be a string');
}
}

export function assertArray(value: unknown): asserts value is Array<unknown> {
if (!Array.isArray(value)) {
throw new Error('value must be an array');
}
}

export function assertObject(
value: unknown,
): asserts value is Record<string, unknown> {
if (typeof value !== 'object' || value === null) {
throw new Error('value must be an object');
}
}

export function assertTruthy<T>(
value: T,
): asserts value is Exclude<T, false | null | ''> {
if (!value) {
throw new Error('value must be truthy');
}
}

export function assertKeyInObject<T extends string>(
obj: { [key: string]: unknown },
keyName: T,
): asserts obj is {
[key in T]: unknown;
} {
if (!(keyName in obj)) {
throw new Error(`obj must have a ${keyName} property`);
}
}

export function assertStringKeyInObject<T extends string>(
obj: { [key: string]: unknown },
keyName: T,
): asserts obj is {
[key in T]: string;
} {
assertKeyInObject(obj, keyName);
assertString(obj[keyName]);
}
29 changes: 29 additions & 0 deletions lib/cf-jwt/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
decode as decodeBase64,
encode as encodeBase64,
} from 'base64-arraybuffer';

export { decodeBase64, encodeBase64 };

export function decodeBase64Url(str: string): ArrayBuffer {
return decodeBase64(str.replace(/-/g, '+').replace(/_/g, '/'));
}

export function encodeBase64Url(arr: ArrayBuffer): string {
return encodeBase64(arr)
.replace(/\//g, '_')
.replace(/\+/g, '-')
.replace(/=+$/, '');
}

export function base64UrlToObject<T extends Record<string, unknown>>(
str: string,
): T {
return JSON.parse(new TextDecoder().decode(decodeBase64Url(str))) as T;
}

export function objectToBase64Url<T extends Record<string, unknown>>(
obj: T,
): string {
return encodeBase64Url(new TextEncoder().encode(JSON.stringify(obj)));
}
25 changes: 25 additions & 0 deletions lib/cf-jwt/decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { base64UrlToObject, decodeBase64Url } from './base64.js';
import { ValidationError } from './errors.js';
import { type JwtHeader, type JwtPayload } from './jwt.js';

export function decode(token: string) {
const tokenParts = token.split('.') as [string, string, string];

if (tokenParts.length !== 3) {
throw new ValidationError('token must consist of 3 parts').addDetail({
reason: 'token-format',
metadata: {
parts: tokenParts.length,
},
});
}

const [headerEncoded, payloadEncoded, signatureEncoded] = tokenParts;

return {
header: base64UrlToObject<JwtHeader>(headerEncoded),
payload: base64UrlToObject<JwtPayload>(payloadEncoded),
signature: decodeBase64Url(signatureEncoded),
signedData: new TextEncoder().encode(`${headerEncoded}.${payloadEncoded}`),
};
}
10 changes: 10 additions & 0 deletions lib/cf-jwt/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable max-classes-per-file */
import { CustomError, Status } from '@block65/custom-error';

export class PermissionError extends CustomError {
public override code = Status.PERMISSION_DENIED;
}

export class ValidationError extends CustomError {
public override code = Status.PERMISSION_DENIED;
}
45 changes: 45 additions & 0 deletions lib/cf-jwt/jwt-algorithms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export type JwtAlgorithm =
| 'ES256'
| 'ES384'
| 'ES512'
| 'HS256'
| 'HS384'
| 'HS512'
| 'RS256'
| 'RS384'
| 'RS512';

interface Algorithm {
name: string;
}

type NamedCurve = string;
interface EcKeyImportParams extends Algorithm {
namedCurve: NamedCurve;
}

type AlgorithmIdentifier = Algorithm | string;
type HashAlgorithmIdentifier = AlgorithmIdentifier;
interface RsaHashedImportParams extends Algorithm {
hash: HashAlgorithmIdentifier;
}

interface HmacImportParams extends Algorithm {
hash: HashAlgorithmIdentifier;
length?: number;
}

export const algorithms: Record<
JwtAlgorithm,
RsaHashedImportParams | EcKeyImportParams | HmacImportParams
> = {
ES256: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } },
ES384: { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-384' } },
ES512: { name: 'ECDSA', namedCurve: 'P-521', hash: { name: 'SHA-512' } },
HS256: { name: 'HMAC', hash: { name: 'SHA-256' } },
HS384: { name: 'HMAC', hash: { name: 'SHA-384' } },
HS512: { name: 'HMAC', hash: { name: 'SHA-512' } },
RS256: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
RS384: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-384' } },
RS512: { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-512' } },
};
Loading

0 comments on commit 6128f5e

Please sign in to comment.