@t4h.framework/hmac 0.3.7 → 0.3.9

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,248 +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` |
31
-
32
- ```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.
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`.
47
29
 
48
30
  ```typescript
49
- import { App, Workflow } from '@t4h.framework/core'
50
31
  import { HMACAuthorization } from '@t4h.framework/hmac'
51
32
 
52
- const authorization = new HMACAuthorization({
53
- secret: '<shared-secret>',
54
- signature: '<signature-from-runtime>',
55
- rawPayload: '<exact-signed-bytes>',
56
- signaturePrefix: 'sha256=',
57
- })
58
-
59
- const workflow = new Workflow(
60
- { id: 'protected/v1', authorization },
61
- async () => ({ ok: true }),
62
- )
63
-
64
- const app = new App({
65
- id: 'my-app',
66
- workflows: [workflow],
67
- authorization, // optional app-level default
68
- })
69
- ```
70
-
71
- Manifest shape (from tests):
72
-
73
- ```typescript
74
- {
75
- pathToModule: '@t4h.framework/hmac/authorize',
76
- context: {
77
- secret: string
78
- signature: string
79
- rawPayload: string
80
- algorithm?: string
81
- signatureFormat?: string
82
- signaturePrefix?: string
83
- timestamp?: string
84
- toleranceInSeconds?: number
85
- },
86
- }
87
- ```
88
-
89
- Optional manifest fields are still present as `undefined` when omitted (`toManifest` copies all keys).
90
-
91
- ---
92
-
93
- ## Context: `HMACAuthorizationContext`
94
-
95
- | Field | Required | Description |
96
- |-------|----------|-------------|
97
- | `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. |
98
- | `signature` | Yes | Signature the caller presented. Empty string → handler returns `false`. |
99
- | `rawPayload` | Yes | The **exact** bytes that were signed. Never re-serialize parsed JSON — key order and whitespace change and the digest will not match. |
100
- | `algorithm` | No | **Enum key** on `HMACAlgorithm` (`SHA1`, `SHA256`, `SHA384`, `SHA512`), not the Node crypto string. Defaults to `SHA256`. |
101
- | `signatureFormat` | No | Encoding of `signature`: `hex`, `base64`, `base64url`, `binary`. Defaults to `hex`. |
102
- | `signaturePrefix` | No | Stripped before comparison, e.g. `sha256=`. If set and absent from `signature` → `false`. |
103
- | `timestamp` | Conditional | Required when `toleranceInSeconds` is set. All-digit string → Unix **seconds**; otherwise `Date.parse` (ISO 8601). |
104
- | `toleranceInSeconds` | No | Enables replay protection. Symmetric window (past **and** future, for clock skew). |
105
-
106
- ---
107
-
108
- ## Runtime handler (`@t4h.framework/hmac/authorize`)
109
-
110
- Default export:
111
-
112
- ```typescript
113
- (options: HMACAuthorizationContext) => Promise<boolean>
114
- ```
115
-
116
- Behavior, in order (from `hmac-authorize.ts` and tests):
33
+ import { environments } from '../constants/environments.js'
117
34
 
118
- 1. Falsy `signature` → **`false`** (no throw).
119
- 2. Falsy `secret` → **throw** `Secret is required for payload verification`.
120
- 3. Resolve `algorithm` via `HMACAlgorithm[key]`; unknown key → **throw** `Unsupported algorithm: <value>`.
121
- 4. Resolve `signatureFormat`; not one of `hex`/`base64`/`base64url`/`binary` → **throw** `Unsupported signature format: <value>`.
122
- 5. If `toleranceInSeconds` is set: missing `timestamp` → **throw** `Timestamp is required when a tolerance is set`; outside the window or unparseable → **`false`**.
123
- 6. If `signaturePrefix` is set and `signature` does not start with it → **`false`**; otherwise strip it.
124
- 7. Compute `crypto.createHmac(algorithm, secret).update(rawPayload).digest()` and compare with `crypto.timingSafeEqual`.
125
-
126
- 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).
127
-
128
- ```typescript
129
- export enum HMACAlgorithm {
130
- SHA1 = 'sha1',
131
- SHA256 = 'sha256',
132
- SHA384 = 'sha384',
133
- SHA512 = 'sha512',
134
- }
135
- ```
136
-
137
- Pass `algorithm: 'SHA256'`, not `'sha256'` — the lowercase form is the enum *value* and makes the handler throw.
138
-
139
- ---
140
-
141
- ## Examples
142
-
143
- ### Webhook with a prefixed hex signature (GitHub style)
144
-
145
- ```typescript
146
- new HMACAuthorization({
147
- secret: env.WEBHOOK_SECRET,
148
- signature: request.headers['x-hub-signature-256'], // 'sha256=9f86d0…'
149
- rawPayload: rawRequestBody, // the untouched body string
35
+ export const authorization = new HMACAuthorization({
36
+ secret: environments.HMAC_SECRET,
37
+ signature: '<signature-from-the-request-header>',
38
+ rawPayload: '<the-exact-request-body>',
150
39
  signaturePrefix: 'sha256=',
151
40
  })
152
41
  ```
153
42
 
154
- ### Timestamped payload with replay protection (Stripe style)
155
-
156
- ```typescript
157
- new HMACAuthorization({
158
- secret: env.WEBHOOK_SECRET,
159
- signature: parsedSignatureHeader.v1,
160
- rawPayload: `${timestamp}.${rawRequestBody}`, // timestamp bound INTO the signature
161
- timestamp,
162
- 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.
43
+ ## Verify async activity callbacks
167
44
 
168
- ### Base64 SHA512
45
+ The `auth` object names request headers, not values. Its `secret` IS a
46
+ `Secret`: declare it with `type: Secret` and pass it whole.
169
47
 
170
48
  ```typescript
171
- new HMACAuthorization({
172
- secret: env.WEBHOOK_SECRET,
173
- signature: base64Signature,
174
- rawPayload: canonicalPayloadString,
175
- algorithm: 'SHA512',
176
- signatureFormat: 'base64',
177
- })
49
+ auth: {
50
+ type: 'hmac',
51
+ secret: environments.PARTNER_SECRET,
52
+ header: 'x-slack-signature',
53
+ signaturePrefix: 'v0=',
54
+ timestampHeader: 'x-slack-request-timestamp',
55
+ toleranceInSeconds: 300,
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` | **Throw** |
192
- | `algorithm` not a `HMACAlgorithm` key | **Throw** |
193
- | Unsupported `signatureFormat` | **Throw** |
194
- | `toleranceInSeconds` without `timestamp` | **Throw** |
195
-
196
- Design callers so manifest context is complete **before** the handler runs; missing or malformed configuration is a programmer error, not a silent denial.
197
-
198
- ---
199
-
200
- ## Testing the handler locally
201
-
202
- 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:
203
90
 
204
91
  ```typescript
205
92
  import crypto from 'node:crypto'
93
+
206
94
  import hmac from '@t4h.framework/hmac/authorize'
207
95
 
208
- const payload = '{"event":"order.created"}'
209
96
  const signature = crypto
210
- .createHmac('sha256', 'super-secret')
97
+ .createHmac('sha256', 'test-secret')
211
98
  .update(payload)
212
99
  .digest('hex')
213
100
 
214
101
  await expect(
215
- hmac({ secret: 'super-secret', signature, rawPayload: payload }),
102
+ hmac({ secret: 'test-secret', signature, rawPayload: payload }),
216
103
  ).resolves.toBe(true)
217
104
  ```
218
105
 
219
- Use `vi.useFakeTimers()` + `vi.setSystemTime(...)` for the tolerance tests — `isWithinTolerance` reads `Date.now()`.
220
-
221
- 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`.
106
+ For a tolerance test, set the clock with `vi.useFakeTimers()` and
107
+ `vi.setSystemTime(...)`.
222
108
 
223
- ---
224
-
225
- ## What this package does not do
109
+ ## Related skills
226
110
 
227
- - No HTTP header / query / env extraction for signatures or secrets.
228
- - No provider presets (no built-in GitHub/Stripe/Shopify header parsing) — you assemble `rawPayload` and pass the header value in.
229
- - No public-key signatures — for those use **framework-jwks**.
230
- - No secret decoding: `secret` is used as UTF-8 bytes as given.
231
- - Does not define **Claims** or **Activities** — only **Authorization** manifests for core `Workflow` / `App`.
111
+ - **framework-jwks**: public-key signatures and JWTs.
112
+ - **framework-webhook**: wait for a partner callback that uses this `auth`.
232
113
 
233
- For workflow orchestration and claims use **framework-core**; for JWKS/JWT verification use **framework-jwks**; for OIDC introspection use **framework-oidc**.
234
-
235
- ---
114
+ ## Reference
236
115
 
237
- ## Quick checklist for agents
238
-
239
- 1. Add `@t4h.framework/hmac` and peer `@t4h.framework/core`.
240
- 2. Build `HMACAuthorizationContext` with `secret`, `signature`, `rawPayload` at minimum.
241
- 3. Attach `new HMACAuthorization(context)` to `Workflow` and/or `App` `authorization` option.
242
- 4. Keep `rawPayload` byte-exact — do not parse and re-stringify JSON.
243
- 5. Set `signaturePrefix` when the provider prefixes the header (`sha256=`).
244
- 6. Set `algorithm` / `signatureFormat` only when they differ from `SHA256` / `hex`; use **enum keys**.
245
- 7. Add `timestamp` + `toleranceInSeconds` for replay protection, and bind the timestamp into `rawPayload`.
246
- 8. Read the secret from `EnvDesign` in `src/constants/environments.ts`, never hard-code it.
247
- 9. Expect `false` for deny, throws for misconfiguration.
248
- 10. Do not reference APIs absent from `src/` and tests.
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,36 @@
1
1
  # @t4h.framework/hmac
2
2
 
3
+ ## 0.3.9
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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.
8
+
9
+ ## 0.3.8
10
+
11
+ ### Patch Changes
12
+
13
+ - [#241](https://github.com/tech4humans-brasil/framework/pull/241) [`f6b8454`](https://github.com/tech4humans-brasil/framework/commit/f6b845484d22e7c8e7240fe2e113f1cdea7c8ce3) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Adds seven rules to `@t4h.framework/core/eslint` and turns them on in `configs.recommended`:
14
+ - `no-io-in-workflow` reports `fetch` and calls to network, file, database and mail modules in a workflow and in the `constructor`, `toInput` and `toOutput` of an activity. It also reports `zlib` and `stream` calls there, because they work on the content in memory on each replay.
15
+ - `no-binary-subclass` reports a class that extends `Binary`.
16
+ - `no-framework-activity-in-app` reports `new` and `extends` on an activity of a framework package other than the core.
17
+ - `no-testing-import-in-source` reports an import of a `@t4h.framework/<package>/testing` entry out of a test file.
18
+ - `require-workflow-id` reports a `Workflow` with no `id`.
19
+ - `no-secret-to-string` reports a `Secret` that becomes a string.
20
+ - `no-literal-secret-in-authorization` reports a string literal in the `secret` or `clientSecret` field of an `Authorization`.
21
+
22
+ The package skills now name these rules in place of the text that the rules enforce. The docs and skills give each `Workflow` an `id`, read the HMAC secret from `EnvDesign`, and do not document a custom HTTP activity.
23
+
24
+ - [#243](https://github.com/tech4humans-brasil/framework/pull/243) [`0ffd663`](https://github.com/tech4humans-brasil/framework/commit/0ffd663ce8369edd7b54206cb3216568ad1de471) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The README examples read each value from `environments`, an `EnvDesign` in `src/constants/environments.ts`, and pass each credential as a `Secret`. They no longer use `process.env`, the raw `env` from the core or a literal in a `Secret` field. The jwks README describes the real context fields (`signature`, `rawPayload`) and says that a token that `jwt.verify` rejects throws.
25
+
26
+ - [#240](https://github.com/tech4humans-brasil/framework/pull/240) [`0606115`](https://github.com/tech4humans-brasil/framework/commit/0606115e2e0baa994e9f0ed3559ee54ecac88e44) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - The skills do not repeat a rule that a lint rule, `tsc` or a runtime error already enforces. Each such rule is now one line that names the gate. The examples that broke a gate (`process.env`, `as any`, an inline secret, an `EnvDesign` in a workflow file, `ref.output` before `wait()`) are correct now.
27
+
28
+ - [#228](https://github.com/tech4humans-brasil/framework/pull/228) [`d29e1ab`](https://github.com/tech4humans-brasil/framework/commit/d29e1ab29a039b13c3e4b075472bd615566ae75e) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Uses exact types and `unknown` in place of `any`, and checks external data with guards.
29
+ - harness: the fix-issues prompt lists each sensor as `- checkTypes: PASS`. Before, it printed `- 0: [object Object]`.
30
+ - hmac: an algorithm name such as `constructor` throws `Unsupported algorithm`.
31
+ - oidc: a discovery response with no `introspection_endpoint`, or a null introspection body, throws a clear error.
32
+ - `HttpRequestActivity.toOutput` takes `HttpRequestActivityOutput` in place of `any`.
33
+
3
34
  ## 0.3.7
4
35
 
5
36
  ## 0.3.6
package/README.md CHANGED
@@ -1,73 +1,34 @@
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'
62
17
 
18
+ import { environments } from './constants/environments.js'
19
+
63
20
  const authorization = new HMACAuthorization({
64
- secret: '<shared-secret>',
21
+ secret: environments.HMAC_SECRET,
65
22
  signature: '<signature-from-the-request-header>',
66
23
  rawPayload: '<the-exact-request-body>',
67
24
  signaturePrefix: 'sha256=',
68
- timestamp: '<timestamp-from-the-request-header>',
69
- toleranceInSeconds: 300,
70
25
  })
71
26
  ```
72
27
 
73
- 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.
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`.
31
+
32
+ ## Documentation
33
+
34
+ <https://tech4humans-brasil.github.io/framework/hmac/>
@@ -1 +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"}
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;AAgElD,MAAM,CAAC,OAAO,UAAU,IAAI,CAC1B,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,OAAO,CAAC,CAIlB"}
@@ -16,7 +16,13 @@ const SIGNATURE_FORMATS = [
16
16
  'base64url',
17
17
  'binary',
18
18
  ];
19
- export default async function hmac(options) {
19
+ function isAlgorithmKey(key) {
20
+ return Object.hasOwn(HMACAlgorithm, key);
21
+ }
22
+ function isSignatureFormat(format) {
23
+ return SIGNATURE_FORMATS.some(supported => supported === format);
24
+ }
25
+ function verifyHmac(options) {
20
26
  const signature = options.signature;
21
27
  if (!signature)
22
28
  return false;
@@ -24,11 +30,11 @@ export default async function hmac(options) {
24
30
  if (!secret)
25
31
  throw new Error('Secret is required for payload verification');
26
32
  const algorithmKey = options.algorithm ?? HMAC_DEFAULT_ALGORITHM;
27
- const algorithm = HMACAlgorithm[algorithmKey];
28
- if (!algorithm)
33
+ if (!isAlgorithmKey(algorithmKey))
29
34
  throw new Error(`Unsupported algorithm: ${algorithmKey}`);
35
+ const algorithm = HMACAlgorithm[algorithmKey];
30
36
  const signatureFormat = options.signatureFormat ?? HMAC_DEFAULT_SIGNATURE_FORMAT;
31
- if (!SIGNATURE_FORMATS.includes(signatureFormat))
37
+ if (!isSignatureFormat(signatureFormat))
32
38
  throw new Error(`Unsupported signature format: ${signatureFormat}`);
33
39
  const toleranceInSeconds = options.toleranceInSeconds;
34
40
  if (toleranceInSeconds !== undefined) {
@@ -48,4 +54,9 @@ export default async function hmac(options) {
48
54
  .digest();
49
55
  return timingSafeCompare(expected, candidate, signatureFormat);
50
56
  }
57
+ export default function hmac(options) {
58
+ return new Promise(resolve => {
59
+ resolve(verifyHmac(options));
60
+ });
61
+ }
51
62
  //# sourceMappingURL=hmac-authorize.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"hmac-authorize.js","sourceRoot":"","sources":["../src/hmac-authorize.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@ export type HMACAuthorizationContext = {
5
5
  signature: string;
6
6
  rawPayload: string;
7
7
  algorithm?: string;
8
- signatureFormat?: BinaryToTextEncoding | string;
8
+ signatureFormat?: BinaryToTextEncoding | (string & {});
9
9
  signaturePrefix?: string;
10
10
  timestamp?: string;
11
11
  toleranceInSeconds?: number;
@@ -1 +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"}
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,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IACtD,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/hmac",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
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",
@@ -36,12 +36,14 @@
36
36
  "scripts": {
37
37
  "build": "tsc --project tsconfig.build.json",
38
38
  "check-types": "tsc --noEmit",
39
+ "check-package": "publint --strict && attw --pack . --profile esm-only",
40
+ "lint": "eslint . --max-warnings 0",
39
41
  "prepublishOnly": "yarn build",
40
42
  "test": "vitest run",
41
43
  "test:watch": "vitest"
42
44
  },
43
45
  "devDependencies": {
44
- "@t4h.framework/core": "^0.19.0",
46
+ "@t4h.framework/core": "^0.21.0",
45
47
  "typescript": "^5.9.3",
46
48
  "vitest": "^4.0.18"
47
49
  },