Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/add-async-result-utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@gregoiref/result": minor
---

Add async Result utilities: `resultAll`, `resultSettled`, `withTimeout`, and `withRetry` with exponential backoff.

- `resultAll` — collect all Ok values or short-circuit on the first Err
- `resultSettled` — collect every Result regardless of outcome
- `withTimeout` — race a Result promise against a configurable deadline (`OperationTimeoutError`)
- `withRetry` — retry a fallible operation with exponential backoff and a custom `shouldRetry` predicate
13 changes: 13 additions & 0 deletions .changeset/add-crypto-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@gregoiref/crypto": minor
---

New package: `@gregoiref/crypto` — Web Crypto API utilities with zero dependencies.

Works in Node.js 22+, Cloudflare Workers, Deno, Bun, and modern browsers.

- **Encoding** — `hex` and `base64url` encode/decode for strings and `BufferSource`
- **Hashing** — `digest` (SHA-1/256/384/512) and HMAC (`hmac`, `hmacVerify`) with constant-time verification
- **Random** — `randomBytes`, `randomHex`, `randomBase64url` backed by `crypto.getRandomValues`
- **AES-GCM** — `generateAesKey`, `encryptAes`, `decryptAes`, `exportAesKey`, `importAesKey`
- **PBKDF2** — `deriveKey` for key derivation and `hashPassword` / `verifyPassword` for password storage (600 000 iterations, OWASP 2023)
1 change: 0 additions & 1 deletion .github/CODEOWNERS

This file was deleted.

17 changes: 0 additions & 17 deletions .github/pull_request_template.md

This file was deleted.

15 changes: 0 additions & 15 deletions packages/crypto/CHANGELOG.md

This file was deleted.

17 changes: 14 additions & 3 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
{
"name": "@gregoiref/crypto",
"version": "0.2.0",
"version": "0.1.0",
"description": "Web Crypto API utilities — hashing, HMAC, AES-GCM, PBKDF2, random tokens — zero dependencies",
"keywords": ["typescript", "crypto", "webcrypto", "hmac", "aes", "pbkdf2", "zero-dependencies"],
"keywords": [
"typescript",
"crypto",
"webcrypto",
"hmac",
"aes",
"pbkdf2",
"zero-dependencies"
],
"homepage": "https://github.com/GregoireF/utils/tree/main/packages/crypto",
"repository": {
"type": "git",
Expand All @@ -22,7 +30,10 @@
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "!dist/**/*.test.*"],
"files": [
"dist",
"!dist/**/*.test.*"
],
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "public"
Expand Down
7 changes: 0 additions & 7 deletions packages/http-client/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
# @gregoiref/http-client

## 1.0.1

### Patch Changes

- 📦 Updated dependencies:
- `@gregoiref/result@1.1.0`

## 1.0.0

### Major Changes
Expand Down
15 changes: 12 additions & 3 deletions packages/http-client/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
{
"name": "@gregoiref/http-client",
"version": "1.0.1",
"version": "1.0.0",
"description": "Typed fetch wrapper with interceptors, timeout, and Result<T, E> error handling",
"keywords": ["typescript", "fetch", "http", "client", "result"],
"keywords": [
"typescript",
"fetch",
"http",
"client",
"result"
],
"homepage": "https://github.com/GregoireF/utils/tree/main/packages/http-client",
"repository": {
"type": "git",
Expand All @@ -22,7 +28,10 @@
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "!dist/**/*.test.*"],
"files": [
"dist",
"!dist/**/*.test.*"
],
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "public"
Expand Down
11 changes: 0 additions & 11 deletions packages/result/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
# @gregoiref/result

## 1.1.0

### Minor Changes

- ✨ Add async Result utilities: `resultAll`, `resultSettled`, `withTimeout`, and `withRetry` with exponential backoff.

- `resultAll` — collect all Ok values or short-circuit on the first Err
- `resultSettled` — collect every Result regardless of outcome
- `withTimeout` — race a Result promise against a configurable deadline (`OperationTimeoutError`)
- `withRetry` — retry a fallible operation with exponential backoff and a custom `shouldRetry` predicate

## 1.0.0

### Major Changes
Expand Down
15 changes: 12 additions & 3 deletions packages/result/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
{
"name": "@gregoiref/result",
"version": "1.1.0",
"version": "1.0.0",
"description": "Type-safe Result<T, E> pattern for TypeScript — zero dependencies",
"keywords": ["typescript", "result", "error-handling", "functional", "zero-dependencies"],
"keywords": [
"typescript",
"result",
"error-handling",
"functional",
"zero-dependencies"
],
"homepage": "https://github.com/GregoireF/utils/tree/main/packages/result",
"repository": {
"type": "git",
Expand All @@ -22,7 +28,10 @@
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist", "!dist/**/*.test.*"],
"files": [
"dist",
"!dist/**/*.test.*"
],
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "public"
Expand Down
2 changes: 2 additions & 0 deletions wiki/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Zero-dependency TypeScript utilities for Node.js, Cloudflare Workers, and modern

```bash
pnpm add @gregoiref/result
pnpm add @gregoiref/crypto
pnpm add @gregoiref/http-client
pnpm add @gregoiref/logger
pnpm add @gregoiref/env-validator
Expand All @@ -39,6 +40,7 @@ pnpm add @gregoiref/date
| Package | Description |
|---|---|
| [`@gregoiref/result`](https://github.com/GregoireF/utils/tree/main/packages/result) | Typed `Result<T, E>` — no-throw error handling |
| [`@gregoiref/crypto`](https://github.com/GregoireF/utils/tree/main/packages/crypto) | Web Crypto API — hashing, HMAC, AES-GCM, PBKDF2, random |
| [`@gregoiref/http-client`](https://github.com/GregoireF/utils/tree/main/packages/http-client) | Fetch wrapper returning `Result` — no try/catch |
| [`@gregoiref/logger`](https://github.com/GregoireF/utils/tree/main/packages/logger) | Structured JSON logger with child context |
| [`@gregoiref/env-validator`](https://github.com/GregoireF/utils/tree/main/packages/env-validator) | Runtime env validation with types — fail fast at startup |
Expand Down
170 changes: 170 additions & 0 deletions wiki/Package-crypto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# @gregoiref/crypto

Web Crypto API utilities — hashing, HMAC, AES-GCM, PBKDF2, random tokens. Zero dependencies.

Works in Node.js 22+, Cloudflare Workers, Deno, Bun, and modern browsers — anywhere the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is available.

```bash
pnpm add @gregoiref/crypto
```

> Requires GitHub Packages. Add to `.npmrc`:
> ```ini
> @gregoiref:registry=https://npm.pkg.github.com
> ```

---

## Encoding

```typescript
import { hex, base64url } from '@gregoiref/crypto'

hex.encode('hello') // '68656c6c6f'
hex.encode(new Uint8Array([0xff, 0x00])) // 'ff00'
hex.decode('68656c6c6f') // Uint8Array([104, 101, 108, 108, 111])

base64url.encode('hello') // 'aGVsbG8'
base64url.decode('aGVsbG8') // Uint8Array (UTF-8 bytes of 'hello')
```

Both accept `string` (UTF-8 encoded) or any `BufferSource` (ArrayBuffer, TypedArray, DataView).

---

## Hashing

```typescript
import { digest } from '@gregoiref/crypto'

const sha256 = await digest('SHA-256', 'hello')
// '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
```

Supported algorithms: `'SHA-1'` | `'SHA-256'` | `'SHA-384'` | `'SHA-512'`.

Always returns a lowercase hex string.

---

## HMAC (webhook validation)

```typescript
import { hmac, hmacVerify } from '@gregoiref/crypto'

// Compute a signature
const signature = await hmac('SHA-256', process.env.WEBHOOK_SECRET, payload)

// Validate an incoming webhook (GitHub, Stripe, etc.)
const trusted = `sha256=${signature}`
const incoming = request.headers.get('x-hub-signature-256') ?? ''
if (trusted !== incoming) throw new Error('Invalid webhook signature')

// Or use hmacVerify for constant-time comparison
const valid = await hmacVerify('SHA-256', secret, payload, receivedHex)
```

The `key` parameter accepts a `string`, `BufferSource`, or an existing `CryptoKey`.

`hmacVerify` uses the Web Crypto API's built-in constant-time comparison — safe against timing attacks.

---

## Random values

```typescript
import { randomBytes, randomHex, randomBase64url } from '@gregoiref/crypto'

randomBytes(32) // Uint8Array(32) — cryptographically secure
randomHex() // 64-char hex string (32 bytes)
randomHex(16) // 32-char hex string (16 bytes)
randomBase64url() // ~43-char URL-safe base64 string (32 bytes)
```

Backed by `crypto.getRandomValues` — no `Math.random()` involved.

---

## AES-GCM encryption

```typescript
import { generateAesKey, encryptAes, decryptAes, exportAesKey, importAesKey } from '@gregoiref/crypto'

// Generate and persist a key
const key = await generateAesKey() // 256-bit by default
const stored = await exportAesKey(key) // base64url string — store in env or KV

// Later: restore the key
const key = await importAesKey(stored)

// Encrypt / decrypt
const ciphertext = await encryptAes(key, 'sensitive data')
// 'iv.ciphertext' — base64url(12-byte IV) + '.' + base64url(AES-GCM output)

const plaintext = await decryptAes(key, ciphertext)
// 'sensitive data'
```

Each call to `encryptAes` uses a freshly generated random IV — the same plaintext produces a different ciphertext every time. Decryption fails with a `DOMException` if the key is wrong or the ciphertext is tampered.

---

## PBKDF2 key derivation

```typescript
import { deriveKey } from '@gregoiref/crypto'

// Derive an AES-GCM key from a password (with auto-generated salt)
const { key, salt } = await deriveKey({ password: 'user-passphrase' })
// Store salt (hex.encode(salt)) alongside your encrypted data

// Reproduce the exact same key later (for decryption)
const { key } = await deriveKey({ password: 'user-passphrase', salt })
```

Uses PBKDF2-SHA256 with 600,000 iterations ([OWASP 2023 recommendation](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)).

---

## Password hashing

```typescript
import { hashPassword, verifyPassword } from '@gregoiref/crypto'

// Hash at registration
const stored = await hashPassword('user-password')
// 'pbkdf2-sha256$600000$<hex_salt>$<hex_hash>'
await db.users.update({ id }, { passwordHash: stored })

// Verify at login
const valid = await verifyPassword(inputPassword, user.passwordHash)
if (!valid) throw new Error('Invalid credentials')
```

`verifyPassword` uses constant-time comparison (via Web Crypto HMAC) to prevent timing attacks.

> **Note:** For dedicated Node.js password storage at scale, consider `argon2` — it's memory-hard and more resistant to GPU attacks. PBKDF2 is used here because it's the only password KDF available in the standard Web Crypto API (required for Cloudflare Workers / browser compatibility).

---

## Usage with `@gregoiref/result`

Wrap crypto calls in `Result` for no-throw error handling:

```typescript
import { encryptAes, decryptAes } from '@gregoiref/crypto'
import { tryAsync } from '@gregoiref/result'

const encrypted = await tryAsync(() => encryptAes(key, 'secret data'))
// Result<string, Error>

const decrypted = await tryAsync(() => decryptAes(key, encrypted.value))
// Result<string, DOMException | Error>
```

---

## Related

- [`@gregoiref/result`](https://github.com/GregoireF/utils/tree/main/packages/result) — wrap crypto errors in `Result<T, E>` for no-throw error handling
- [`@gregoiref/env-validator`](https://github.com/GregoireF/utils/tree/main/packages/env-validator) — validate `WEBHOOK_SECRET`, `ENCRYPTION_KEY`, etc. at startup