@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.
- package/.ai/skills/framework-hmac/SKILL.md +78 -207
- package/CHANGELOG.md +31 -0
- package/README.md +17 -56
- package/dist/hmac-authorize.d.ts.map +1 -1
- package/dist/hmac-authorize.js +15 -4
- package/dist/hmac-authorize.js.map +1 -1
- package/dist/models/HMACAuthorization.d.ts +1 -1
- package/dist/models/HMACAuthorization.d.ts.map +1 -1
- package/package.json +4 -2
|
@@ -1,248 +1,119 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: framework-hmac
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
+
The package has two uses. Pick the one that matches the task:
|
|
15
15
|
|
|
16
|
-
**
|
|
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
|
-
|
|
22
|
+
## Protect a workflow or an App
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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', '
|
|
97
|
+
.createHmac('sha256', 'test-secret')
|
|
211
98
|
.update(payload)
|
|
212
99
|
.digest('hex')
|
|
213
100
|
|
|
214
101
|
await expect(
|
|
215
|
-
hmac({ secret: '
|
|
102
|
+
hmac({ secret: 'test-secret', signature, rawPayload: payload }),
|
|
216
103
|
).resolves.toBe(true)
|
|
217
104
|
```
|
|
218
105
|
|
|
219
|
-
|
|
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
|
-
-
|
|
228
|
-
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
---
|
|
114
|
+
## Reference
|
|
236
115
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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
|
-
##
|
|
7
|
+
## Install
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/hmac-authorize.js
CHANGED
|
@@ -16,7 +16,13 @@ const SIGNATURE_FORMATS = [
|
|
|
16
16
|
'base64url',
|
|
17
17
|
'binary',
|
|
18
18
|
];
|
|
19
|
-
|
|
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
|
-
|
|
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 (!
|
|
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,
|
|
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;
|
|
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.
|
|
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.
|
|
46
|
+
"@t4h.framework/core": "^0.21.0",
|
|
45
47
|
"typescript": "^5.9.3",
|
|
46
48
|
"vitest": "^4.0.18"
|
|
47
49
|
},
|