@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.
@@ -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,2 @@
1
+ export declare function isWithinTolerance(timestamp: string, toleranceInSeconds: number): boolean;
2
+ //# sourceMappingURL=isWithinTolerance.d.ts.map
@@ -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,3 @@
1
+ import { type BinaryToTextEncoding } from 'node:crypto';
2
+ export declare function timingSafeCompare(expected: Buffer, candidate: string, signatureFormat: BinaryToTextEncoding): boolean;
3
+ //# sourceMappingURL=timingSafeCompare.d.ts.map
@@ -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,3 @@
1
+ export * from './hmac-authorize.js';
2
+ export * from './models/HMACAuthorization.js';
3
+ //# sourceMappingURL=hmac.d.ts.map
@@ -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
@@ -0,0 +1,3 @@
1
+ export * from './hmac-authorize.js';
2
+ export * from './models/HMACAuthorization.js';
3
+ //# sourceMappingURL=hmac.js.map
@@ -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
+ }