@t4h.framework/hmac 0.0.0-experimental-20260807115832
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai/skills/framework-hmac/SKILL.md +247 -0
- package/CHANGELOG.md +9 -0
- package/README.md +72 -0
- package/dist/helpers/isWithinTolerance.d.ts +2 -0
- package/dist/helpers/isWithinTolerance.d.ts.map +1 -0
- package/dist/helpers/isWithinTolerance.js +17 -0
- package/dist/helpers/isWithinTolerance.js.map +1 -0
- package/dist/helpers/timingSafeCompare.d.ts +3 -0
- package/dist/helpers/timingSafeCompare.d.ts.map +1 -0
- package/dist/helpers/timingSafeCompare.js +8 -0
- package/dist/helpers/timingSafeCompare.js.map +1 -0
- package/dist/hmac-authorize.d.ts +11 -0
- package/dist/hmac-authorize.d.ts.map +1 -0
- package/dist/hmac-authorize.js +51 -0
- package/dist/hmac-authorize.js.map +1 -0
- package/dist/hmac.d.ts +3 -0
- package/dist/hmac.d.ts.map +1 -0
- package/dist/hmac.js +3 -0
- package/dist/hmac.js.map +1 -0
- package/dist/models/HMACAuthorization.d.ts +19 -0
- package/dist/models/HMACAuthorization.d.ts.map +1 -0
- package/dist/models/HMACAuthorization.js +24 -0
- package/dist/models/HMACAuthorization.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framework-hmac
|
|
3
|
+
description: >-
|
|
4
|
+
HMAC shared-secret authorization for T4H Framework workflows and apps. Use
|
|
5
|
+
when importing @t4h.framework/hmac, wiring HMACAuthorization on Workflow or
|
|
6
|
+
App, verifying signed webhook bodies (GitHub/Stripe style), configuring
|
|
7
|
+
algorithm/signatureFormat/signaturePrefix, adding timestamp replay
|
|
8
|
+
protection, or implementing the @t4h.framework/hmac/authorize runtime
|
|
9
|
+
handler. Requires @t4h.framework/core. Package path: framework/packages/hmac.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# @t4h.framework/hmac
|
|
13
|
+
|
|
14
|
+
HMAC-backed **Authorization** for workflows and apps. Exposes `HMACAuthorization` (core manifest) and a default runtime handler at `@t4h.framework/hmac/authorize` that recomputes the HMAC over a raw payload with a shared secret and compares it in constant time.
|
|
15
|
+
|
|
16
|
+
**Peer dependency:** `@t4h.framework/core` (same major line).
|
|
17
|
+
|
|
18
|
+
**Node:** `>=22`. No runtime dependencies — verification uses `node:crypto`.
|
|
19
|
+
|
|
20
|
+
Package path: `framework/packages/hmac`. See [README.md](../../../README.md) for overview.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Imports
|
|
25
|
+
|
|
26
|
+
| Entry | Purpose |
|
|
27
|
+
|-------|---------|
|
|
28
|
+
| `@t4h.framework/hmac` | `HMACAuthorization`, `HMACAuthorizationContext`, `HMACAuthorizationManifest`, `HMACAlgorithm`, `HMAC_DEFAULT_ALGORITHM`, `HMAC_DEFAULT_SIGNATURE_FORMAT`, re-exports authorize module |
|
|
29
|
+
| `@t4h.framework/hmac/authorize` | Default async handler: `(context) => Promise<boolean>` |
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
HMACAuthorization,
|
|
34
|
+
type HMACAuthorizationContext,
|
|
35
|
+
HMACAlgorithm,
|
|
36
|
+
} from '@t4h.framework/hmac'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Do **not** assume header parsing, env-variable mapping, or provider-specific presets — this package does not read HTTP or env. Your app or hosting layer supplies `secret`, `signature`, `rawPayload` and `timestamp`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Wiring authorization (core)
|
|
44
|
+
|
|
45
|
+
`HMACAuthorization` extends core `Authorization`. Attach to a **Workflow** or **App**; `toManifest()` registers the handler module for the platform.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { App, Workflow } from '@t4h.framework/core'
|
|
49
|
+
import { HMACAuthorization } from '@t4h.framework/hmac'
|
|
50
|
+
|
|
51
|
+
const authorization = new HMACAuthorization({
|
|
52
|
+
secret: '<shared-secret>',
|
|
53
|
+
signature: '<signature-from-runtime>',
|
|
54
|
+
rawPayload: '<exact-signed-bytes>',
|
|
55
|
+
signaturePrefix: 'sha256=',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const workflow = new Workflow(
|
|
59
|
+
{ id: 'protected/v1', authorization },
|
|
60
|
+
async () => ({ ok: true }),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const app = new App({
|
|
64
|
+
id: 'my-app',
|
|
65
|
+
workflows: [workflow],
|
|
66
|
+
authorization, // optional app-level default
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Manifest shape (from tests):
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
{
|
|
74
|
+
pathToModule: '@t4h.framework/hmac/authorize',
|
|
75
|
+
context: {
|
|
76
|
+
secret: string
|
|
77
|
+
signature: string
|
|
78
|
+
rawPayload: string
|
|
79
|
+
algorithm?: string
|
|
80
|
+
signatureFormat?: string
|
|
81
|
+
signaturePrefix?: string
|
|
82
|
+
timestamp?: string
|
|
83
|
+
toleranceInSeconds?: number
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Optional manifest fields are still present as `undefined` when omitted (`toManifest` copies all keys).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Context: `HMACAuthorizationContext`
|
|
93
|
+
|
|
94
|
+
| Field | Required | Description |
|
|
95
|
+
|-------|----------|-------------|
|
|
96
|
+
| `secret` | Yes | Shared secret, used as **UTF-8 bytes** for the HMAC key. Empty → **throws**. A secret delivered hex/base64-encoded must be decoded by the caller before it is passed in. |
|
|
97
|
+
| `signature` | Yes | Signature the caller presented. Empty string → handler returns `false`. |
|
|
98
|
+
| `rawPayload` | Yes | The **exact** bytes that were signed. Never re-serialize parsed JSON — key order and whitespace change and the digest will not match. |
|
|
99
|
+
| `algorithm` | No | **Enum key** on `HMACAlgorithm` (`SHA1`, `SHA256`, `SHA384`, `SHA512`), not the Node crypto string. Defaults to `SHA256`. |
|
|
100
|
+
| `signatureFormat` | No | Encoding of `signature`: `hex`, `base64`, `base64url`, `binary`. Defaults to `hex`. |
|
|
101
|
+
| `signaturePrefix` | No | Stripped before comparison, e.g. `sha256=`. If set and absent from `signature` → `false`. |
|
|
102
|
+
| `timestamp` | Conditional | Required when `toleranceInSeconds` is set. All-digit string → Unix **seconds**; otherwise `Date.parse` (ISO 8601). |
|
|
103
|
+
| `toleranceInSeconds` | No | Enables replay protection. Symmetric window (past **and** future, for clock skew). |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Runtime handler (`@t4h.framework/hmac/authorize`)
|
|
108
|
+
|
|
109
|
+
Default export:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
(options: HMACAuthorizationContext) => Promise<boolean>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Behavior, in order (from `hmac-authorize.ts` and tests):
|
|
116
|
+
|
|
117
|
+
1. Falsy `signature` → **`false`** (no throw).
|
|
118
|
+
2. Falsy `secret` → **throw** `Secret is required for payload verification`.
|
|
119
|
+
3. Resolve `algorithm` via `HMACAlgorithm[key]`; unknown key → **throw** `Unsupported algorithm: <value>`.
|
|
120
|
+
4. Resolve `signatureFormat`; not one of `hex`/`base64`/`base64url`/`binary` → **throw** `Unsupported signature format: <value>`.
|
|
121
|
+
5. If `toleranceInSeconds` is set: missing `timestamp` → **throw** `Timestamp is required when a tolerance is set`; outside the window or unparseable → **`false`**.
|
|
122
|
+
6. If `signaturePrefix` is set and `signature` does not start with it → **`false`**; otherwise strip it.
|
|
123
|
+
7. Compute `crypto.createHmac(algorithm, secret).update(rawPayload).digest()` and compare with `crypto.timingSafeEqual`.
|
|
124
|
+
|
|
125
|
+
The candidate is decoded from `signatureFormat` into a Buffer before comparing, so **hex casing and base64 padding never cause a false negative**, and a candidate of a different decoded length short-circuits to `false` (`timingSafeEqual` throws on length mismatch).
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
export enum HMACAlgorithm {
|
|
129
|
+
SHA1 = 'sha1',
|
|
130
|
+
SHA256 = 'sha256',
|
|
131
|
+
SHA384 = 'sha384',
|
|
132
|
+
SHA512 = 'sha512',
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Pass `algorithm: 'SHA256'`, not `'sha256'` — the lowercase form is the enum *value* and makes the handler throw.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Examples
|
|
141
|
+
|
|
142
|
+
### Webhook with a prefixed hex signature (GitHub style)
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
new HMACAuthorization({
|
|
146
|
+
secret: env.WEBHOOK_SECRET,
|
|
147
|
+
signature: request.headers['x-hub-signature-256'], // 'sha256=9f86d0…'
|
|
148
|
+
rawPayload: rawRequestBody, // the untouched body string
|
|
149
|
+
signaturePrefix: 'sha256=',
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Timestamped payload with replay protection (Stripe style)
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
new HMACAuthorization({
|
|
157
|
+
secret: env.WEBHOOK_SECRET,
|
|
158
|
+
signature: parsedSignatureHeader.v1,
|
|
159
|
+
rawPayload: `${timestamp}.${rawRequestBody}`, // timestamp bound INTO the signature
|
|
160
|
+
timestamp,
|
|
161
|
+
toleranceInSeconds: 300,
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`toleranceInSeconds` only proves the timestamp is *recent*. Including the timestamp in `rawPayload` is what stops an attacker from editing it — do both.
|
|
166
|
+
|
|
167
|
+
### Base64 SHA512
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
new HMACAuthorization({
|
|
171
|
+
secret: env.WEBHOOK_SECRET,
|
|
172
|
+
signature: base64Signature,
|
|
173
|
+
rawPayload: canonicalPayloadString,
|
|
174
|
+
algorithm: 'SHA512',
|
|
175
|
+
signatureFormat: 'base64',
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Return values vs errors
|
|
182
|
+
|
|
183
|
+
| Situation | Result |
|
|
184
|
+
|-----------|--------|
|
|
185
|
+
| Empty `signature` | `false` |
|
|
186
|
+
| Digest matches | `true` |
|
|
187
|
+
| Wrong secret / tampered payload / wrong algorithm / garbage signature | `false` |
|
|
188
|
+
| `signaturePrefix` set but missing from `signature` | `false` |
|
|
189
|
+
| Timestamp outside tolerance, or not parseable | `false` |
|
|
190
|
+
| Empty `secret` | **Throw** |
|
|
191
|
+
| `algorithm` not a `HMACAlgorithm` key | **Throw** |
|
|
192
|
+
| Unsupported `signatureFormat` | **Throw** |
|
|
193
|
+
| `toleranceInSeconds` without `timestamp` | **Throw** |
|
|
194
|
+
|
|
195
|
+
Design callers so manifest context is complete **before** the handler runs; missing or malformed configuration is a programmer error, not a silent denial.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Testing the handler locally
|
|
200
|
+
|
|
201
|
+
Tests sign with real `node:crypto` HMACs — no mocking needed, since verification is a local computation.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import crypto from 'node:crypto'
|
|
205
|
+
import hmac from '@t4h.framework/hmac/authorize'
|
|
206
|
+
|
|
207
|
+
const payload = '{"event":"order.created"}'
|
|
208
|
+
const signature = crypto
|
|
209
|
+
.createHmac('sha256', 'super-secret')
|
|
210
|
+
.update(payload)
|
|
211
|
+
.digest('hex')
|
|
212
|
+
|
|
213
|
+
await expect(
|
|
214
|
+
hmac({ secret: 'super-secret', signature, rawPayload: payload }),
|
|
215
|
+
).resolves.toBe(true)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Use `vi.useFakeTimers()` + `vi.setSystemTime(...)` for the tolerance tests — `isWithinTolerance` reads `Date.now()`.
|
|
219
|
+
|
|
220
|
+
Test files: `src/__tests__/hmac-authorize.spec.ts`, `src/helpers/__tests__/timingSafeCompare.spec.ts`, `src/helpers/__tests__/isWithinTolerance.spec.ts`, `src/models/__tests__/HMACAuthorization.spec.ts`.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## What this package does not do
|
|
225
|
+
|
|
226
|
+
- No HTTP header / query / env extraction for signatures or secrets.
|
|
227
|
+
- No provider presets (no built-in GitHub/Stripe/Shopify header parsing) — you assemble `rawPayload` and pass the header value in.
|
|
228
|
+
- No public-key signatures — for those use **framework-jwks**.
|
|
229
|
+
- No secret decoding: `secret` is used as UTF-8 bytes as given.
|
|
230
|
+
- Does not define **Claims** or **Activities** — only **Authorization** manifests for core `Workflow` / `App`.
|
|
231
|
+
|
|
232
|
+
For workflow orchestration and claims use **framework-core**; for JWKS/JWT verification use **framework-jwks**; for OIDC introspection use **framework-oidc**.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Quick checklist for agents
|
|
237
|
+
|
|
238
|
+
1. Add `@t4h.framework/hmac` and peer `@t4h.framework/core`.
|
|
239
|
+
2. Build `HMACAuthorizationContext` with `secret`, `signature`, `rawPayload` at minimum.
|
|
240
|
+
3. Attach `new HMACAuthorization(context)` to `Workflow` and/or `App` `authorization` option.
|
|
241
|
+
4. Keep `rawPayload` byte-exact — do not parse and re-stringify JSON.
|
|
242
|
+
5. Set `signaturePrefix` when the provider prefixes the header (`sha256=`).
|
|
243
|
+
6. Set `algorithm` / `signatureFormat` only when they differ from `SHA256` / `hex`; use **enum keys**.
|
|
244
|
+
7. Add `timestamp` + `toleranceInSeconds` for replay protection, and bind the timestamp into `rawPayload`.
|
|
245
|
+
8. Read the secret from `EnvDesign` in `src/constants/environments.ts`, never hard-code it.
|
|
246
|
+
9. Expect `false` for deny, throws for misconfiguration.
|
|
247
|
+
10. Do not reference APIs absent from `src/` and tests.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @t4h.framework/hmac
|
|
2
|
+
|
|
3
|
+
## 0.0.0-experimental-20260807115832
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#128](https://github.com/tech4humans-brasil/framework/pull/128) [`0107c3b`](https://github.com/tech4humans-brasil/framework/commit/0107c3b8a76a0c96f51f21d7c1c8bd5a76c5c69c) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Create HMAC package for authorization
|
|
8
|
+
|
|
9
|
+
- [#128](https://github.com/tech4humans-brasil/framework/pull/128) [`0107c3b`](https://github.com/tech4humans-brasil/framework/commit/0107c3b8a76a0c96f51f21d7c1c8bd5a76c5c69c) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Initialize package
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @t4h.framework/hmac
|
|
2
|
+
|
|
3
|
+
HMAC-backed authorization for the T4H Framework. It exposes a core `Authorization` manifest and a runtime handler that verifies a shared-secret signature over a raw payload.
|
|
4
|
+
|
|
5
|
+
## Peer dependency
|
|
6
|
+
|
|
7
|
+
Requires `@t4h.framework/core` (same major line as this package).
|
|
8
|
+
|
|
9
|
+
## Exports
|
|
10
|
+
|
|
11
|
+
| Entry | Purpose |
|
|
12
|
+
|------------------|---------|
|
|
13
|
+
| `@t4h.framework/hmac` | `HMACAuthorization`, `HMACAuthorizationContext`, `HMACAlgorithm`, and related types |
|
|
14
|
+
| `@t4h.framework/hmac/authorize` | Default async handler invoked by the runtime with the manifest context |
|
|
15
|
+
|
|
16
|
+
## `HMACAuthorization`
|
|
17
|
+
|
|
18
|
+
Subclass of [`Authorization`](https://github.com/tech4humans-brasil/framework/tree/main/packages/core) from core. `toManifest()` points the runtime at `@t4h.framework/hmac/authorize` and passes an `HMACAuthorizationContext`.
|
|
19
|
+
|
|
20
|
+
Construct it with a full `HMACAuthorizationContext`. How you obtain `signature`, `rawPayload` and `timestamp` — headers, query, body, env — is up to your app and deployment pipeline; this package does not read HTTP or env by itself.
|
|
21
|
+
|
|
22
|
+
## Context: `HMACAuthorizationContext`
|
|
23
|
+
|
|
24
|
+
| Field | Required | Description |
|
|
25
|
+
|-------|----------|-------------|
|
|
26
|
+
| `secret` | Yes | Shared secret, used as UTF-8 bytes for the HMAC key. Empty string **throws**. |
|
|
27
|
+
| `signature` | Yes | The signature the caller presented. Empty string yields `false`. |
|
|
28
|
+
| `rawPayload` | Yes | The exact bytes that were signed. |
|
|
29
|
+
| `algorithm` | No | A **key** of `HMACAlgorithm` (`'SHA1'`, `'SHA256'`, `'SHA384'`, `'SHA512'`). Defaults to `'SHA256'`. |
|
|
30
|
+
| `signatureFormat` | No | Encoding of `signature`: `'hex'`, `'base64'`, `'base64url'` or `'binary'`. Defaults to `'hex'`. |
|
|
31
|
+
| `signaturePrefix` | No | Prefix to strip before comparing, e.g. `'sha256='` for GitHub-style headers. |
|
|
32
|
+
| `timestamp` | Conditional | When the payload was signed. Required if `toleranceInSeconds` is set. |
|
|
33
|
+
| `toleranceInSeconds` | No | Enables replay protection: rejects signatures whose `timestamp` drifts further than this from now. |
|
|
34
|
+
|
|
35
|
+
## Runtime handler (`hmac/authorize`)
|
|
36
|
+
|
|
37
|
+
The default export is an async function:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
(options: HMACAuthorizationContext) => Promise<boolean>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It recomputes `HMAC(secret, rawPayload)` with the chosen algorithm and compares it against `signature` using `crypto.timingSafeEqual`, so a wrong signature never leaks how many leading bytes were right.
|
|
44
|
+
|
|
45
|
+
- Returns `false` if `signature` is missing, the digests differ, the expected prefix is absent, or the timestamp is outside the tolerance window.
|
|
46
|
+
- Returns `true` if the digests match.
|
|
47
|
+
- **Throws** on configuration errors: empty `secret`, an `algorithm` that is not a `HMACAlgorithm` key, an unsupported `signatureFormat`, or a `toleranceInSeconds` set without a `timestamp`.
|
|
48
|
+
|
|
49
|
+
The signature is decoded from `signatureFormat` before the comparison, so hex casing and base64 padding differences never cause a false negative.
|
|
50
|
+
|
|
51
|
+
### Replay protection
|
|
52
|
+
|
|
53
|
+
Set `toleranceInSeconds` together with `timestamp` to reject stale signatures. `timestamp` is read as Unix time in seconds when it is all digits, otherwise via `Date.parse` (ISO 8601, for example). The window is symmetric — a sender whose clock runs slightly fast is accepted the same way a slightly slow one is.
|
|
54
|
+
|
|
55
|
+
Note that the timestamp is only checked for freshness. If you want it *bound* to the signature (so an attacker cannot change it), include it in `rawPayload` the same way the sender did — for example `` `${timestamp}.${body}` ``.
|
|
56
|
+
|
|
57
|
+
## Example
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { HMACAuthorization } from '@t4h.framework/hmac'
|
|
61
|
+
|
|
62
|
+
const authorization = new HMACAuthorization({
|
|
63
|
+
secret: '<shared-secret>',
|
|
64
|
+
signature: '<signature-from-the-request-header>',
|
|
65
|
+
rawPayload: '<the-exact-request-body>',
|
|
66
|
+
signaturePrefix: 'sha256=',
|
|
67
|
+
timestamp: '<timestamp-from-the-request-header>',
|
|
68
|
+
toleranceInSeconds: 300,
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Wire `authorization` on an `App` or `Workflow` per the core package. The platform loads `@t4h.framework/hmac/authorize` with the manifest context when a protected workflow runs.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isWithinTolerance.d.ts","sourceRoot":"","sources":["../../src/helpers/isWithinTolerance.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,MAAM,GACzB,OAAO,CAQT"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function isWithinTolerance(timestamp, toleranceInSeconds) {
|
|
2
|
+
const signedAt = parseTimestamp(timestamp);
|
|
3
|
+
if (signedAt === null)
|
|
4
|
+
return false;
|
|
5
|
+
const driftInSeconds = Math.abs(Date.now() - signedAt) / 1000;
|
|
6
|
+
return driftInSeconds <= toleranceInSeconds;
|
|
7
|
+
}
|
|
8
|
+
function parseTimestamp(timestamp) {
|
|
9
|
+
const value = timestamp.trim();
|
|
10
|
+
if (!value)
|
|
11
|
+
return null;
|
|
12
|
+
if (/^\d+$/.test(value))
|
|
13
|
+
return Number(value) * 1000;
|
|
14
|
+
const parsed = Date.parse(value);
|
|
15
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=isWithinTolerance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isWithinTolerance.js","sourceRoot":"","sources":["../../src/helpers/isWithinTolerance.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB,CAC/B,SAAiB,EACjB,kBAA0B;IAE1B,MAAM,QAAQ,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IAE1C,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAEnC,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAA;IAE7D,OAAO,cAAc,IAAI,kBAAkB,CAAA;AAC7C,CAAC;AAED,SAAS,cAAc,CAAC,SAAiB;IACvC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAA;IAE9B,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IAEvB,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAA;IAEpD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAEhC,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;AAC7C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timingSafeCompare.d.ts","sourceRoot":"","sources":["../../src/helpers/timingSafeCompare.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAE/D,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,oBAAoB,GACpC,OAAO,CAMT"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import crypto, {} from 'node:crypto';
|
|
2
|
+
export function timingSafeCompare(expected, candidate, signatureFormat) {
|
|
3
|
+
const provided = Buffer.from(candidate, signatureFormat);
|
|
4
|
+
if (provided.length !== expected.length)
|
|
5
|
+
return false;
|
|
6
|
+
return crypto.timingSafeEqual(expected, provided);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=timingSafeCompare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timingSafeCompare.js","sourceRoot":"","sources":["../../src/helpers/timingSafeCompare.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAA6B,MAAM,aAAa,CAAA;AAE/D,MAAM,UAAU,iBAAiB,CAC/B,QAAgB,EAChB,SAAiB,EACjB,eAAqC;IAErC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAA;IAExD,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAErD,OAAO,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;AACnD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HMACAuthorizationContext } from './models/HMACAuthorization.js';
|
|
2
|
+
export declare enum HMACAlgorithm {
|
|
3
|
+
SHA1 = "sha1",
|
|
4
|
+
SHA256 = "sha256",
|
|
5
|
+
SHA384 = "sha384",
|
|
6
|
+
SHA512 = "sha512"
|
|
7
|
+
}
|
|
8
|
+
export declare const HMAC_DEFAULT_ALGORITHM = "SHA256";
|
|
9
|
+
export declare const HMAC_DEFAULT_SIGNATURE_FORMAT = "hex";
|
|
10
|
+
export default function hmac(options: HMACAuthorizationContext): Promise<boolean>;
|
|
11
|
+
//# sourceMappingURL=hmac-authorize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac-authorize.d.ts","sourceRoot":"","sources":["../src/hmac-authorize.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAE7E,oBAAY,aAAa;IACvB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,MAAM,WAAW;CAClB;AAED,eAAO,MAAM,sBAAsB,WAAW,CAAA;AAE9C,eAAO,MAAM,6BAA6B,QAAQ,CAAA;AASlD,wBAA8B,IAAI,CAChC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,OAAO,CAAC,CAgDlB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import crypto, {} from 'node:crypto';
|
|
2
|
+
import { isWithinTolerance } from './helpers/isWithinTolerance.js';
|
|
3
|
+
import { timingSafeCompare } from './helpers/timingSafeCompare.js';
|
|
4
|
+
export var HMACAlgorithm;
|
|
5
|
+
(function (HMACAlgorithm) {
|
|
6
|
+
HMACAlgorithm["SHA1"] = "sha1";
|
|
7
|
+
HMACAlgorithm["SHA256"] = "sha256";
|
|
8
|
+
HMACAlgorithm["SHA384"] = "sha384";
|
|
9
|
+
HMACAlgorithm["SHA512"] = "sha512";
|
|
10
|
+
})(HMACAlgorithm || (HMACAlgorithm = {}));
|
|
11
|
+
export const HMAC_DEFAULT_ALGORITHM = 'SHA256';
|
|
12
|
+
export const HMAC_DEFAULT_SIGNATURE_FORMAT = 'hex';
|
|
13
|
+
const SIGNATURE_FORMATS = [
|
|
14
|
+
'hex',
|
|
15
|
+
'base64',
|
|
16
|
+
'base64url',
|
|
17
|
+
'binary',
|
|
18
|
+
];
|
|
19
|
+
export default async function hmac(options) {
|
|
20
|
+
const signature = options.signature;
|
|
21
|
+
if (!signature)
|
|
22
|
+
return false;
|
|
23
|
+
const secret = options.secret;
|
|
24
|
+
if (!secret)
|
|
25
|
+
throw new Error('Secret is required for payload verification');
|
|
26
|
+
const algorithmKey = options.algorithm ?? HMAC_DEFAULT_ALGORITHM;
|
|
27
|
+
const algorithm = HMACAlgorithm[algorithmKey];
|
|
28
|
+
if (!algorithm)
|
|
29
|
+
throw new Error(`Unsupported algorithm: ${algorithmKey}`);
|
|
30
|
+
const signatureFormat = options.signatureFormat ?? HMAC_DEFAULT_SIGNATURE_FORMAT;
|
|
31
|
+
if (!SIGNATURE_FORMATS.includes(signatureFormat))
|
|
32
|
+
throw new Error(`Unsupported signature format: ${signatureFormat}`);
|
|
33
|
+
const toleranceInSeconds = options.toleranceInSeconds;
|
|
34
|
+
if (toleranceInSeconds !== undefined) {
|
|
35
|
+
const timestamp = options.timestamp;
|
|
36
|
+
if (!timestamp)
|
|
37
|
+
throw new Error('Timestamp is required when a tolerance is set');
|
|
38
|
+
if (!isWithinTolerance(timestamp, toleranceInSeconds))
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const prefix = options.signaturePrefix;
|
|
42
|
+
if (prefix && !signature.startsWith(prefix))
|
|
43
|
+
return false;
|
|
44
|
+
const candidate = prefix ? signature.slice(prefix.length) : signature;
|
|
45
|
+
const expected = crypto
|
|
46
|
+
.createHmac(algorithm, secret)
|
|
47
|
+
.update(options.rawPayload)
|
|
48
|
+
.digest();
|
|
49
|
+
return timingSafeCompare(expected, candidate, signatureFormat);
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=hmac-authorize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac-authorize.js","sourceRoot":"","sources":["../src/hmac-authorize.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAA6B,MAAM,aAAa,CAAA;AAE/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAGlE,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,8BAAa,CAAA;IACb,kCAAiB,CAAA;IACjB,kCAAiB,CAAA;IACjB,kCAAiB,CAAA;AACnB,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAA;AAE9C,MAAM,CAAC,MAAM,6BAA6B,GAAG,KAAK,CAAA;AAElD,MAAM,iBAAiB,GAA2B;IAChD,KAAK;IACL,QAAQ;IACR,WAAW;IACX,QAAQ;CACT,CAAA;AAED,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,IAAI,CAChC,OAAiC;IAEjC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAA;IAEnC,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAA;IAE5B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IAE7B,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;IAE3E,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,IAAI,sBAAsB,CAAA;IAEhE,MAAM,SAAS,GAAG,aAAa,CAAC,YAA0C,CAAC,CAAA;IAE3E,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,YAAY,EAAE,CAAC,CAAA;IAEzE,MAAM,eAAe,GACnB,OAAO,CAAC,eAAe,IAAI,6BAA6B,CAAA;IAE1D,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,eAAuC,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,iCAAiC,eAAe,EAAE,CAAC,CAAA;IAErE,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAA;IAErD,IAAI,kBAAkB,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAA;QAEnC,IAAI,CAAC,SAAS;YACZ,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;QAElE,IAAI,CAAC,iBAAiB,CAAC,SAAS,EAAE,kBAAkB,CAAC;YAAE,OAAO,KAAK,CAAA;IACrE,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,eAAe,CAAA;IAEtC,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IAEzD,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAErE,MAAM,QAAQ,GAAG,MAAM;SACpB,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC;SAC7B,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;SAC1B,MAAM,EAAE,CAAA;IAEX,OAAO,iBAAiB,CACtB,QAAQ,EACR,SAAS,EACT,eAAuC,CACxC,CAAA;AACH,CAAC"}
|
package/dist/hmac.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac.d.ts","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,+BAA+B,CAAA"}
|
package/dist/hmac.js
ADDED
package/dist/hmac.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hmac.js","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,+BAA+B,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BinaryToTextEncoding } from 'node:crypto';
|
|
2
|
+
import { Authorization, type AuthorizationManifest } from '@t4h.framework/core';
|
|
3
|
+
export type HMACAuthorizationContext = {
|
|
4
|
+
secret: string;
|
|
5
|
+
signature: string;
|
|
6
|
+
rawPayload: string;
|
|
7
|
+
algorithm?: string;
|
|
8
|
+
signatureFormat?: BinaryToTextEncoding | string;
|
|
9
|
+
signaturePrefix?: string;
|
|
10
|
+
timestamp?: string;
|
|
11
|
+
toleranceInSeconds?: number;
|
|
12
|
+
};
|
|
13
|
+
export type HMACAuthorizationManifest = AuthorizationManifest<HMACAuthorizationContext>;
|
|
14
|
+
export declare class HMACAuthorization extends Authorization<HMACAuthorizationContext> {
|
|
15
|
+
private readonly context;
|
|
16
|
+
constructor(context: HMACAuthorizationContext);
|
|
17
|
+
toManifest(): HMACAuthorizationManifest;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=HMACAuthorization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HMACAuthorization.d.ts","sourceRoot":"","sources":["../../src/models/HMACAuthorization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAE/E,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,eAAe,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAAA;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,yBAAyB,GACnC,qBAAqB,CAAC,wBAAwB,CAAC,CAAA;AAEjD,qBAAa,iBAAkB,SAAQ,aAAa,CAAC,wBAAwB,CAAC;IAChE,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,wBAAwB;IAIvD,UAAU,IAAI,yBAAyB;CAe/C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Authorization } from '@t4h.framework/core';
|
|
2
|
+
export class HMACAuthorization extends Authorization {
|
|
3
|
+
context;
|
|
4
|
+
constructor(context) {
|
|
5
|
+
super();
|
|
6
|
+
this.context = context;
|
|
7
|
+
}
|
|
8
|
+
toManifest() {
|
|
9
|
+
return {
|
|
10
|
+
context: {
|
|
11
|
+
secret: this.context.secret,
|
|
12
|
+
signature: this.context.signature,
|
|
13
|
+
rawPayload: this.context.rawPayload,
|
|
14
|
+
algorithm: this.context.algorithm,
|
|
15
|
+
signatureFormat: this.context.signatureFormat,
|
|
16
|
+
signaturePrefix: this.context.signaturePrefix,
|
|
17
|
+
timestamp: this.context.timestamp,
|
|
18
|
+
toleranceInSeconds: this.context.toleranceInSeconds,
|
|
19
|
+
},
|
|
20
|
+
pathToModule: '@t4h.framework/hmac/authorize',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=HMACAuthorization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HMACAuthorization.js","sourceRoot":"","sources":["../../src/models/HMACAuthorization.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAA8B,MAAM,qBAAqB,CAAA;AAgB/E,MAAM,OAAO,iBAAkB,SAAQ,aAAuC;IAC/C;IAA7B,YAA6B,OAAiC;QAC5D,KAAK,EAAE,CAAA;QADoB,YAAO,GAAP,OAAO,CAA0B;IAE9D,CAAC;IAEM,UAAU;QACf,OAAO;YACL,OAAO,EAAE;gBACP,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM;gBAC3B,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS;gBACjC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU;gBACnC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS;gBACjC,eAAe,EAAE,IAAI,CAAC,OAAO,CAAC,eAAe;gBAC7C,eAAe,EAAE,IAAI,CAAC,OAAO,CAAC,eAAe;gBAC7C,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS;gBACjC,kBAAkB,EAAE,IAAI,CAAC,OAAO,CAAC,kBAAkB;aACpD;YACD,YAAY,EAAE,+BAA+B;SAC9C,CAAA;IACH,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@t4h.framework/hmac",
|
|
3
|
+
"version": "0.0.0-experimental-20260807115832",
|
|
4
|
+
"description": "HMAC module for the T4H Framework authorization",
|
|
5
|
+
"homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/hmac",
|
|
6
|
+
"bugs": "https://github.com/tech4humans-brasil/framework/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/tech4humans-brasil/framework.git",
|
|
10
|
+
"directory": "packages/hmac"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Tech4Humans <contact@tech4h.com.br> (https://tech4h.com.br)",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/hmac.d.ts",
|
|
18
|
+
"import": "./dist/hmac.js"
|
|
19
|
+
},
|
|
20
|
+
"./authorize": {
|
|
21
|
+
"types": "./dist/hmac-authorize.d.ts",
|
|
22
|
+
"import": "./dist/hmac-authorize.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"module": "./dist/hmac.js",
|
|
26
|
+
"types": "./dist/hmac.d.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
".ai"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc --project tsconfig.build.json",
|
|
34
|
+
"check-types": "tsc --noEmit",
|
|
35
|
+
"prepublishOnly": "yarn build",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@t4h.framework/core": "^0.9.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@t4h.framework/core": "^0.9.0"
|
|
46
|
+
},
|
|
47
|
+
"packageManager": "yarn@4.12.0",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22"
|
|
50
|
+
}
|
|
51
|
+
}
|