@t4h.framework/hmac 0.3.8 → 0.3.11

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.
@@ -1,244 +1,119 @@
1
1
  ---
2
2
  name: framework-hmac
3
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.
4
+ Verifies shared-secret HMAC signatures with @t4h.framework/hmac. Use when a
5
+ T4H Framework workflow or App needs HMACAuthorization, when an async activity
6
+ such as webhook() declares auth { type: 'hmac' } for its callbacks, or when
7
+ the user mentions a signed webhook body (GitHub, Shopify, Slack style),
8
+ HMACAlgorithm, signatureFormat, signaturePrefix, signedPayload, timestamp
9
+ replay protection or toleranceInSeconds.
10
10
  ---
11
11
 
12
12
  # @t4h.framework/hmac
13
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.
14
+ The package has two uses. Pick the one that matches the task:
15
15
 
16
- **Peer dependency:** `@t4h.framework/core` (same major line).
16
+ - **Protect a workflow or an `App`**: `new HMACAuthorization(context)`.
17
+ - **Verify the callbacks of an async activity**: declare
18
+ `auth: { type: 'hmac', … }` on the activity (for `webhook()`, see the
19
+ **framework-webhook** skill). The host verifies each callback with
20
+ `@t4h.framework/hmac/callback`. You do not import it.
17
21
 
18
- **Node:** `>=22`. No runtime dependencies — verification uses `node:crypto`.
22
+ ## Protect a workflow or an App
19
23
 
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
- | `@t4h.framework/hmac/callback` | Default export `hmacCallback`: `(auth, request) => Promise<boolean>`, the `hmac` authorizer hosts pass to the core `AsyncActivityCallback.authorize` |
24
+ 1. Declare the secret with `type: String` in the `EnvDesign` in
25
+ `src/constants/environments.ts`. `HMACAuthorizationContext.secret` is a
26
+ `string`, not a `Secret`.
27
+ 2. Construct the authorization and attach it to the `authorization` option of
28
+ a `Workflow` or an `App`.
31
29
 
32
30
  ```typescript
33
- import {
34
- HMACAuthorization,
35
- type HMACAuthorizationContext,
36
- HMACAlgorithm,
37
- } from '@t4h.framework/hmac'
38
- ```
39
-
40
- 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`.
41
-
42
- ---
43
-
44
- ## Wiring authorization (core)
45
-
46
- `HMACAuthorization` extends core `Authorization`. Attach to a **Workflow** or **App**; `toManifest()` registers the handler module for the platform.
47
-
48
- ```typescript
49
- import { App, Workflow } from '@t4h.framework/core'
50
31
  import { HMACAuthorization } from '@t4h.framework/hmac'
51
32
 
52
- import { environments } from './constants/environments.js'
33
+ import { environments } from '../constants/environments.js'
53
34
 
54
- const authorization = new HMACAuthorization({
35
+ export const authorization = new HMACAuthorization({
55
36
  secret: environments.HMAC_SECRET,
56
- signature: '<signature-from-runtime>',
57
- rawPayload: '<exact-signed-bytes>',
37
+ signature: '<signature-from-the-request-header>',
38
+ rawPayload: '<the-exact-request-body>',
58
39
  signaturePrefix: 'sha256=',
59
40
  })
60
-
61
- const workflow = new Workflow(
62
- { id: 'protected/v1', authorization },
63
- async () => ({ ok: true }),
64
- )
65
-
66
- const app = new App({
67
- id: 'my-app',
68
- workflows: [workflow],
69
- authorization, // optional app-level default
70
- })
71
41
  ```
72
42
 
73
- Manifest shape (from tests):
74
-
75
- ```typescript
76
- {
77
- pathToModule: '@t4h.framework/hmac/authorize',
78
- context: {
79
- secret: string
80
- signature: string
81
- rawPayload: string
82
- algorithm?: string
83
- signatureFormat?: string
84
- signaturePrefix?: string
85
- timestamp?: string
86
- toleranceInSeconds?: number
87
- },
88
- }
89
- ```
43
+ ## Verify async activity callbacks
90
44
 
91
- Optional manifest fields are still present as `undefined` when omitted (`toManifest` copies all keys).
92
-
93
- Declare the secret in the `EnvDesign` in `src/constants/environments.ts`. The context types `secret` as `string`, so declare the entry with `type: String`, not `type: Secret`. This is an exception to the `type: Secret` rule of **framework-core**.
94
-
95
- ---
96
-
97
- ## Context: `HMACAuthorizationContext`
98
-
99
- | Field | Required | Description |
100
- |-------|----------|-------------|
101
- | `secret` | Yes | Shared secret, used as **UTF-8 bytes** for the HMAC key. A secret delivered hex/base64-encoded must be decoded by the caller before it is passed in. |
102
- | `signature` | Yes | Signature the caller presented. Empty string → handler returns `false`. |
103
- | `rawPayload` | Yes | The **exact** bytes that were signed. Never re-serialize parsed JSON — key order and whitespace change and the digest will not match. |
104
- | `algorithm` | No | `HMACAlgorithm` key: `SHA1`, `SHA256`, `SHA384` or `SHA512` (not `sha256`). Defaults to `SHA256`. Other values throw. |
105
- | `signatureFormat` | No | Encoding of `signature`: `hex`, `base64`, `base64url` or `binary`. Defaults to `hex`. Other values throw. |
106
- | `signaturePrefix` | No | Stripped before comparison, e.g. `sha256=`. If set and absent from `signature` → `false`. |
107
- | `timestamp` | Conditional | Used with `toleranceInSeconds`. All-digit string → Unix **seconds**; otherwise `Date.parse` (ISO 8601). |
108
- | `toleranceInSeconds` | No | Enables replay protection. Symmetric window (past **and** future, for clock skew). |
109
-
110
- ---
111
-
112
- ## Runtime handler (`@t4h.framework/hmac/authorize`)
113
-
114
- Default export:
45
+ The `auth` object names request headers, not values. Its `secret` IS a
46
+ `Secret`: declare it with `type: Secret` and pass it whole.
115
47
 
116
48
  ```typescript
117
- (options: HMACAuthorizationContext) => Promise<boolean>
118
- ```
119
-
120
- Behavior, in order (from `hmac-authorize.ts` and tests):
121
-
122
- 1. Falsy `signature` → **`false`** (no throw).
123
- 2. Check the configuration (see **Return values vs errors**).
124
- 3. If `toleranceInSeconds` is set: `timestamp` outside the window or unparseable → **`false`**.
125
- 4. If `signaturePrefix` is set and `signature` does not start with it → **`false`**; otherwise strip it.
126
- 5. Compute `crypto.createHmac(algorithm, secret).update(rawPayload).digest()` and compare with `crypto.timingSafeEqual`.
127
-
128
- 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).
129
-
130
- ```typescript
131
- export enum HMACAlgorithm {
132
- SHA1 = 'sha1',
133
- SHA256 = 'sha256',
134
- SHA384 = 'sha384',
135
- SHA512 = 'sha512',
136
- }
137
- ```
138
-
139
- ---
140
-
141
- ## Examples
142
-
143
- ### Webhook with a prefixed hex signature (GitHub style)
144
-
145
- ```typescript
146
- new HMACAuthorization({
147
- secret: environments.HMAC_SECRET,
148
- signature: request.headers['x-hub-signature-256'], // 'sha256=9f86d0…'
149
- rawPayload: rawRequestBody, // the untouched body string
150
- signaturePrefix: 'sha256=',
151
- })
152
- ```
153
-
154
- ### Timestamped payload with replay protection (Stripe style)
155
-
156
- ```typescript
157
- new HMACAuthorization({
158
- secret: environments.HMAC_SECRET,
159
- signature: parsedSignatureHeader.v1,
160
- rawPayload: `${timestamp}.${rawRequestBody}`, // timestamp bound INTO the signature
161
- timestamp,
49
+ auth: {
50
+ type: 'hmac',
51
+ secret: environments.PARTNER_SECRET,
52
+ header: 'x-slack-signature',
53
+ signaturePrefix: 'v0=',
54
+ timestampHeader: 'x-slack-request-timestamp',
162
55
  toleranceInSeconds: 300,
163
- })
164
- ```
165
-
166
- `toleranceInSeconds` only proves the timestamp is *recent*. Including the timestamp in `rawPayload` is what stops an attacker from editing it — do both.
167
-
168
- ### Base64 SHA512
169
-
170
- ```typescript
171
- new HMACAuthorization({
172
- secret: environments.HMAC_SECRET,
173
- signature: base64Signature,
174
- rawPayload: canonicalPayloadString,
175
- algorithm: 'SHA512',
176
- signatureFormat: 'base64',
177
- })
56
+ signedPayload: 'v0:{timestamp}:{body}',
57
+ }
178
58
  ```
179
59
 
180
- ---
181
-
182
- ## Return values vs errors
183
-
184
- | Situation | Result |
185
- |-----------|--------|
186
- | Empty `signature` | `false` |
187
- | Digest matches | `true` |
188
- | Wrong secret / tampered payload / wrong algorithm / garbage signature | `false` |
189
- | `signaturePrefix` set but missing from `signature` | `false` |
190
- | Timestamp outside tolerance, or not parseable | `false` |
191
- | Empty `secret`, unknown `algorithm` or `signatureFormat`, `toleranceInSeconds` without `timestamp` | **Throw** (`hmac-authorize.ts`) |
192
-
193
- Design callers so manifest context is complete **before** the handler runs; missing or malformed configuration is a programmer error, not a silent denial.
194
-
195
- ---
196
-
197
- ## Testing the handler locally
198
-
199
- Tests sign with real `node:crypto` HMACs — no mocking needed, since verification is a local computation.
60
+ `signedPayload` is a template with `{body}` (raw body) and `{timestamp}`. It
61
+ defaults to `'{body}'`.
62
+
63
+ ## Traps
64
+
65
+ - `algorithm` is an enum KEY in upper case: `'SHA256'`, not `'sha256'`. Other
66
+ values throw. Defaults: `'SHA256'` and `'hex'`.
67
+ - `rawPayload` is the exact signed bytes. Do not parse JSON and stringify it
68
+ again: key order and spaces change and the digest fails.
69
+ - `secret` is used as UTF-8 bytes. Decode a hex or base64 secret first.
70
+ - A literal string in `secret` fails the lint rule
71
+ `no-literal-secret-in-authorization` of `@t4h.framework/core`.
72
+ - An all-digit `timestamp` is Unix SECONDS. A millisecond value is read as
73
+ seconds and fails the tolerance check. Other strings go through
74
+ `Date.parse`.
75
+ - The tolerance check only proves the timestamp is recent. Put the timestamp in
76
+ the signed bytes too (`rawPayload` or `signedPayload`), or an attacker can
77
+ change it.
78
+ - On a callback, `toleranceInSeconds` without `timestampHeader` makes each
79
+ callback fail with an error.
80
+ - A denied caller gives `false`. A configuration mistake (empty `secret`,
81
+ unknown `algorithm` or `signatureFormat`, tolerance without timestamp)
82
+ throws.
83
+ - `signatureFormat` `'binary'` exists only on `HMACAuthorization`, not on the
84
+ callback `auth`.
85
+
86
+ ## Test
87
+
88
+ Sign with `node:crypto` and call the default export of
89
+ `@t4h.framework/hmac/authorize`. No mock is necessary:
200
90
 
201
91
  ```typescript
202
92
  import crypto from 'node:crypto'
93
+
203
94
  import hmac from '@t4h.framework/hmac/authorize'
204
95
 
205
- const payload = '{"event":"order.created"}'
206
96
  const signature = crypto
207
- .createHmac('sha256', 'super-secret')
97
+ .createHmac('sha256', 'test-secret')
208
98
  .update(payload)
209
99
  .digest('hex')
210
100
 
211
101
  await expect(
212
- hmac({ secret: 'super-secret', signature, rawPayload: payload }),
102
+ hmac({ secret: 'test-secret', signature, rawPayload: payload }),
213
103
  ).resolves.toBe(true)
214
104
  ```
215
105
 
216
- Use `vi.useFakeTimers()` + `vi.setSystemTime(...)` for the tolerance tests — `isWithinTolerance` reads `Date.now()`.
106
+ For a tolerance test, set the clock with `vi.useFakeTimers()` and
107
+ `vi.setSystemTime(...)`.
217
108
 
218
- 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`.
109
+ ## Related skills
219
110
 
220
- ---
111
+ - **framework-jwks**: public-key signatures and JWTs.
112
+ - **framework-webhook**: wait for a partner callback that uses this `auth`.
221
113
 
222
- ## What this package does not do
223
-
224
- - No HTTP header / query / env extraction for signatures or secrets.
225
- - No provider presets (no built-in GitHub/Stripe/Shopify header parsing) — you assemble `rawPayload` and pass the header value in.
226
- - No public-key signatures — for those use **framework-jwks**.
227
- - No secret decoding: `secret` is used as UTF-8 bytes as given.
228
- - Does not define **Claims** or **Activities** — only **Authorization** manifests for core `Workflow` / `App`.
229
-
230
- For workflow orchestration and claims use **framework-core**; for JWKS/JWT verification use **framework-jwks**; for OIDC introspection use **framework-oidc**.
231
-
232
- ---
114
+ ## Reference
233
115
 
234
- ## Quick checklist for agents
235
-
236
- 1. Add `@t4h.framework/hmac` and peer `@t4h.framework/core`.
237
- 2. Build `HMACAuthorizationContext` with `secret`, `signature`, `rawPayload` at minimum.
238
- 3. Attach `new HMACAuthorization(context)` to `Workflow` and/or `App` `authorization` option.
239
- 4. Keep `rawPayload` byte-exact — do not parse and re-stringify JSON.
240
- 5. Set `signaturePrefix` when the provider prefixes the header (`sha256=`).
241
- 6. Set `algorithm` / `signatureFormat` only when they differ from `SHA256` / `hex`.
242
- 7. Add `timestamp` + `toleranceInSeconds` for replay protection, and bind the timestamp into `rawPayload`.
243
- 8. `@t4h.framework/core/no-literal-secret-in-authorization` rejects a literal `secret`. Read it from the `EnvDesign` in `src/constants/environments.ts`.
244
- 9. Expect `false` for deny, throws for misconfiguration.
116
+ - Guide: <https://tech4humans-brasil.github.io/framework/hmac/>
117
+ - Types: `node_modules/@t4h.framework/hmac/dist/hmac.d.ts`
118
+ - Callback `auth` type: `AsyncActivityCallbackAuthHmac` in
119
+ `node_modules/@t4h.framework/core/dist/`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @t4h.framework/hmac
2
2
 
3
+ ## 0.3.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [#276](https://github.com/tech4humans-brasil/framework/pull/276) [`7b38934`](https://github.com/tech4humans-brasil/framework/commit/7b38934e8f9207c98c33afcd76e3a7a4da7e8b14) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The install command gives the `@t4h.framework/core` peer, and the samples import `HMACAuthorization`. The docs tell that `./callback` reads the headers and the secret.
8
+
9
+ ## 0.3.10
10
+
11
+ ## 0.3.9
12
+
13
+ ### Patch Changes
14
+
15
+ - [#257](https://github.com/tech4humans-brasil/framework/pull/257) [`586bfb4`](https://github.com/tech4humans-brasil/framework/commit/586bfb4663f4a478527ee0c8db360603f034d615) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The hmac README is now short and links to the site. The site page now describes the callback `auth`, the results and errors, and how to test the handler.
16
+
3
17
  ## 0.3.8
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -1,61 +1,16 @@
1
1
  # @t4h.framework/hmac
2
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.
3
+ HMAC authorization for the T4H Framework. It verifies a shared-secret signature
4
+ over a raw payload, for a workflow, an app or the callbacks of an async
5
+ activity.
4
6
 
5
- ## Peer dependency
7
+ ## Install
6
8
 
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
- | `@t4h.framework/hmac/callback` | Default export `hmacCallback`: `hmac` callback authorizer for the core `AsyncActivityCallback.authorize` (`pending({ auth: { type: 'hmac' } })`) |
16
-
17
- ## `HMACAuthorization`
18
-
19
- 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`.
20
-
21
- 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.
22
-
23
- ## Context: `HMACAuthorizationContext`
24
-
25
- | Field | Required | Description |
26
- |-------|----------|-------------|
27
- | `secret` | Yes | Shared secret, used as UTF-8 bytes for the HMAC key. Empty string **throws**. |
28
- | `signature` | Yes | The signature the caller presented. Empty string yields `false`. |
29
- | `rawPayload` | Yes | The exact bytes that were signed. |
30
- | `algorithm` | No | A **key** of `HMACAlgorithm` (`'SHA1'`, `'SHA256'`, `'SHA384'`, `'SHA512'`). Defaults to `'SHA256'`. |
31
- | `signatureFormat` | No | Encoding of `signature`: `'hex'`, `'base64'`, `'base64url'` or `'binary'`. Defaults to `'hex'`. |
32
- | `signaturePrefix` | No | Prefix to strip before comparing, e.g. `'sha256='` for GitHub-style headers. |
33
- | `timestamp` | Conditional | When the payload was signed. Required if `toleranceInSeconds` is set. |
34
- | `toleranceInSeconds` | No | Enables replay protection: rejects signatures whose `timestamp` drifts further than this from now. |
35
-
36
- ## Runtime handler (`hmac/authorize`)
37
-
38
- The default export is an async function:
39
-
40
- ```ts
41
- (options: HMACAuthorizationContext) => Promise<boolean>
9
+ ```bash
10
+ npm install @t4h.framework/hmac @t4h.framework/core
42
11
  ```
43
12
 
44
- 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.
45
-
46
- - Returns `false` if `signature` is missing, the digests differ, the expected prefix is absent, or the timestamp is outside the tolerance window.
47
- - Returns `true` if the digests match.
48
- - **Throws** on configuration errors: empty `secret`, an `algorithm` that is not a `HMACAlgorithm` key, an unsupported `signatureFormat`, or a `toleranceInSeconds` set without a `timestamp`.
49
-
50
- The signature is decoded from `signatureFormat` before the comparison, so hex casing and base64 padding differences never cause a false negative.
51
-
52
- ### Replay protection
53
-
54
- 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.
55
-
56
- 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}` ``.
57
-
58
- ## Example
13
+ ## Usage
59
14
 
60
15
  ```typescript
61
16
  import { HMACAuthorization } from '@t4h.framework/hmac'
@@ -67,20 +22,13 @@ const authorization = new HMACAuthorization({
67
22
  signature: '<signature-from-the-request-header>',
68
23
  rawPayload: '<the-exact-request-body>',
69
24
  signaturePrefix: 'sha256=',
70
- timestamp: '<timestamp-from-the-request-header>',
71
- toleranceInSeconds: 300,
72
25
  })
73
26
  ```
74
27
 
75
- Declare `HMAC_SECRET` in the `EnvDesign` in `src/constants/environments.ts` with `type: String`, because the `secret` field is a string:
76
-
77
- ```typescript
78
- // src/constants/environments.ts
79
- import { EnvDesign } from '@t4h.framework/core'
28
+ Declare `HMAC_SECRET` with `type: String` in the `EnvDesign` in
29
+ `src/constants/environments.ts`, and attach `authorization` to a `Workflow` or
30
+ an `App`.
80
31
 
81
- export const environments = EnvDesign({
82
- HMAC_SECRET: { type: String },
83
- })
84
- ```
32
+ ## Documentation
85
33
 
86
- 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.
34
+ <https://tech4humans-brasil.github.io/framework/hmac/>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/hmac",
3
- "version": "0.3.8",
3
+ "version": "0.3.11",
4
4
  "description": "HMAC module for the T4H Framework authorization",
5
5
  "homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/hmac",
6
6
  "bugs": "https://github.com/tech4humans-brasil/framework/issues",
@@ -43,7 +43,7 @@
43
43
  "test:watch": "vitest"
44
44
  },
45
45
  "devDependencies": {
46
- "@t4h.framework/core": "^0.20.0",
46
+ "@t4h.framework/core": "^0.21.2",
47
47
  "typescript": "^5.9.3",
48
48
  "vitest": "^4.0.18"
49
49
  },