@eva_solutions/auth-sdk 0.0.0-stage → 3.0.0-dev-20261109133200
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/CHANGELOG.md +290 -0
- package/LICENSE +21 -0
- package/README.md +481 -2
- package/dist/admin/index.cjs +4 -0
- package/dist/admin/index.d.cts +2 -0
- package/dist/admin/index.d.ts +2 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin-DjvuvuI8.js +408 -0
- package/dist/admin-DjvuvuI8.js.map +1 -0
- package/dist/admin-Q6lbWqLM.cjs +419 -0
- package/dist/admin-Q6lbWqLM.cjs.map +1 -0
- package/dist/api-path-2Ulruz7R.cjs +34 -0
- package/dist/api-path-2Ulruz7R.cjs.map +1 -0
- package/dist/api-path-B-vSkL2H.js +29 -0
- package/dist/api-path-B-vSkL2H.js.map +1 -0
- package/dist/auth-19r-Q6X2.js +84 -0
- package/dist/auth-19r-Q6X2.js.map +1 -0
- package/dist/auth-DX1N5JA4.cjs +131 -0
- package/dist/auth-DX1N5JA4.cjs.map +1 -0
- package/dist/auth-handlers-B3iJaPln.js +343 -0
- package/dist/auth-handlers-B3iJaPln.js.map +1 -0
- package/dist/auth-handlers-BTbXKJms.d.ts +15 -0
- package/dist/auth-handlers-BTbXKJms.d.ts.map +1 -0
- package/dist/auth-handlers-CDzGFDTu.cjs +404 -0
- package/dist/auth-handlers-CDzGFDTu.cjs.map +1 -0
- package/dist/auth-handlers-DRUUTEtW.d.cts +15 -0
- package/dist/auth-handlers-DRUUTEtW.d.cts.map +1 -0
- package/dist/catalog-CHPqldcj.cjs +172 -0
- package/dist/catalog-CHPqldcj.cjs.map +1 -0
- package/dist/catalog-CIqpzgsM.d.cts +75 -0
- package/dist/catalog-CIqpzgsM.d.cts.map +1 -0
- package/dist/catalog-CIqpzgsM.d.ts +75 -0
- package/dist/catalog-CIqpzgsM.d.ts.map +1 -0
- package/dist/catalog-DqPhtCKZ.js +95 -0
- package/dist/catalog-DqPhtCKZ.js.map +1 -0
- package/dist/config-3avoUa9e.d.cts +95 -0
- package/dist/config-3avoUa9e.d.cts.map +1 -0
- package/dist/config-COX7rGAP.d.ts +95 -0
- package/dist/config-COX7rGAP.d.ts.map +1 -0
- package/dist/constants-BE1_9Tod.cjs +52 -0
- package/dist/constants-BE1_9Tod.cjs.map +1 -0
- package/dist/constants-BY7QZokc.js +29 -0
- package/dist/constants-BY7QZokc.js.map +1 -0
- package/dist/constants-CcbhOaAP.cjs +153 -0
- package/dist/constants-CcbhOaAP.cjs.map +1 -0
- package/dist/constants-rNuD5qJn.js +94 -0
- package/dist/constants-rNuD5qJn.js.map +1 -0
- package/dist/contracts/index.cjs +1006 -0
- package/dist/contracts/index.cjs.map +1 -0
- package/dist/contracts/index.d.cts +488 -0
- package/dist/contracts/index.d.cts.map +1 -0
- package/dist/contracts/index.d.ts +488 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +817 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/cookies-DCIcr7uy.cjs +94 -0
- package/dist/cookies-DCIcr7uy.cjs.map +1 -0
- package/dist/cookies-DKEC7AWv.js +71 -0
- package/dist/cookies-DKEC7AWv.js.map +1 -0
- package/dist/delegated-token-CVNbMNfy.js +426 -0
- package/dist/delegated-token-CVNbMNfy.js.map +1 -0
- package/dist/delegated-token-DHSK4rHI.cjs +461 -0
- package/dist/delegated-token-DHSK4rHI.cjs.map +1 -0
- package/dist/error-helpers-0woh13Wd.d.cts +105 -0
- package/dist/error-helpers-0woh13Wd.d.cts.map +1 -0
- package/dist/error-helpers-Ct3AUK1I.d.ts +105 -0
- package/dist/error-helpers-Ct3AUK1I.d.ts.map +1 -0
- package/dist/error-helpers-XCh4ZJ-g.cjs +282 -0
- package/dist/error-helpers-XCh4ZJ-g.cjs.map +1 -0
- package/dist/error-helpers-lnwgNOw9.js +145 -0
- package/dist/error-helpers-lnwgNOw9.js.map +1 -0
- package/dist/error-messages-CGpW1fsg.d.cts +48 -0
- package/dist/error-messages-CGpW1fsg.d.cts.map +1 -0
- package/dist/error-messages-CGpW1fsg.d.ts +48 -0
- package/dist/error-messages-CGpW1fsg.d.ts.map +1 -0
- package/dist/event-codes-BBJHon-I.cjs +48 -0
- package/dist/event-codes-BBJHon-I.cjs.map +1 -0
- package/dist/event-codes-BgcJWsov.d.cts +42 -0
- package/dist/event-codes-BgcJWsov.d.cts.map +1 -0
- package/dist/event-codes-BgcJWsov.d.ts +42 -0
- package/dist/event-codes-BgcJWsov.d.ts.map +1 -0
- package/dist/event-codes-DR5_VlaR.js +43 -0
- package/dist/event-codes-DR5_VlaR.js.map +1 -0
- package/dist/generic/index.cjs +91 -0
- package/dist/generic/index.cjs.map +1 -0
- package/dist/generic/index.d.cts +27 -0
- package/dist/generic/index.d.cts.map +1 -0
- package/dist/generic/index.d.ts +27 -0
- package/dist/generic/index.d.ts.map +1 -0
- package/dist/generic/index.js +80 -0
- package/dist/generic/index.js.map +1 -0
- package/dist/hono/index.cjs +309 -0
- package/dist/hono/index.cjs.map +1 -0
- package/dist/hono/index.d.cts +272 -0
- package/dist/hono/index.d.cts.map +1 -0
- package/dist/hono/index.d.ts +272 -0
- package/dist/hono/index.d.ts.map +1 -0
- package/dist/hono/index.js +290 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/hono-openapi/index.cjs +326 -0
- package/dist/hono-openapi/index.cjs.map +1 -0
- package/dist/hono-openapi/index.d.cts +14 -0
- package/dist/hono-openapi/index.d.cts.map +1 -0
- package/dist/hono-openapi/index.d.ts +14 -0
- package/dist/hono-openapi/index.d.ts.map +1 -0
- package/dist/hono-openapi/index.js +307 -0
- package/dist/hono-openapi/index.js.map +1 -0
- package/dist/http-client-BVF3iktR.cjs +211 -0
- package/dist/http-client-BVF3iktR.cjs.map +1 -0
- package/dist/http-client-Da0-OMfc.js +206 -0
- package/dist/http-client-Da0-OMfc.js.map +1 -0
- package/dist/index-CIH_2t7Y.d.ts +240 -0
- package/dist/index-CIH_2t7Y.d.ts.map +1 -0
- package/dist/index-CZru7c5T.d.cts +1711 -0
- package/dist/index-CZru7c5T.d.cts.map +1 -0
- package/dist/index-CZru7c5T.d.ts +1711 -0
- package/dist/index-CZru7c5T.d.ts.map +1 -0
- package/dist/index-DMVe5IJA.d.cts +240 -0
- package/dist/index-DMVe5IJA.d.cts.map +1 -0
- package/dist/index-DRWKobSp.d.ts +312 -0
- package/dist/index-DRWKobSp.d.ts.map +1 -0
- package/dist/index-DU9wJ-P2.d.cts +312 -0
- package/dist/index-DU9wJ-P2.d.cts.map +1 -0
- package/dist/index-DbiXllNy.d.cts +66 -0
- package/dist/index-DbiXllNy.d.cts.map +1 -0
- package/dist/index-z9r98qlk.d.ts +66 -0
- package/dist/index-z9r98qlk.d.ts.map +1 -0
- package/dist/index.cjs +89 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +114 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt-BBNqk0TV.cjs +155 -0
- package/dist/jwt-BBNqk0TV.cjs.map +1 -0
- package/dist/jwt-DhqgH94B.js +150 -0
- package/dist/jwt-DhqgH94B.js.map +1 -0
- package/dist/key-resolver-CESqj4-P.cjs +45 -0
- package/dist/key-resolver-CESqj4-P.cjs.map +1 -0
- package/dist/key-resolver-D56I2Zpz.js +34 -0
- package/dist/key-resolver-D56I2Zpz.js.map +1 -0
- package/dist/parse-error-response-6qOQS5y9.cjs +316 -0
- package/dist/parse-error-response-6qOQS5y9.cjs.map +1 -0
- package/dist/parse-error-response-BM9-kjs0.js +239 -0
- package/dist/parse-error-response-BM9-kjs0.js.map +1 -0
- package/dist/path-segment-C25i6aaH.cjs +29 -0
- package/dist/path-segment-C25i6aaH.cjs.map +1 -0
- package/dist/path-segment-CyqnaFrP.js +24 -0
- package/dist/path-segment-CyqnaFrP.js.map +1 -0
- package/dist/react/index.cjs +336 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +151 -0
- package/dist/react/index.d.cts.map +1 -0
- package/dist/react/index.d.ts +151 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +300 -0
- package/dist/react/index.js.map +1 -0
- package/dist/refresh-dedup-CKaHHe7m.js +14 -0
- package/dist/refresh-dedup-CKaHHe7m.js.map +1 -0
- package/dist/refresh-dedup-CLdACQXR.cjs +19 -0
- package/dist/refresh-dedup-CLdACQXR.cjs.map +1 -0
- package/dist/rolldown-runtime-CmQyd0Gu.cjs +27 -0
- package/dist/routes-DpAzgY1V.cjs +424 -0
- package/dist/routes-DpAzgY1V.cjs.map +1 -0
- package/dist/routes-F1g5aVqP.js +347 -0
- package/dist/routes-F1g5aVqP.js.map +1 -0
- package/dist/s2s/index.cjs +24 -0
- package/dist/s2s/index.d.cts +5 -0
- package/dist/s2s/index.d.ts +5 -0
- package/dist/s2s/index.js +5 -0
- package/dist/s2s-6wbotr78.cjs +926 -0
- package/dist/s2s-6wbotr78.cjs.map +1 -0
- package/dist/s2s-B_0SIQzn.js +897 -0
- package/dist/s2s-B_0SIQzn.js.map +1 -0
- package/dist/sign-C7y6xdvT.cjs +324 -0
- package/dist/sign-C7y6xdvT.cjs.map +1 -0
- package/dist/sign-HFtcYwa-.js +259 -0
- package/dist/sign-HFtcYwa-.js.map +1 -0
- package/dist/tokens-C1nzkn21.cjs +232 -0
- package/dist/tokens-C1nzkn21.cjs.map +1 -0
- package/dist/tokens-C3Z40qd7.js +107 -0
- package/dist/tokens-C3Z40qd7.js.map +1 -0
- package/dist/types-Ai3iIFlB.d.cts +538 -0
- package/dist/types-Ai3iIFlB.d.cts.map +1 -0
- package/dist/types-Bxg4hCMc.d.cts +52 -0
- package/dist/types-Bxg4hCMc.d.cts.map +1 -0
- package/dist/types-CEABh1Zf.d.ts +96 -0
- package/dist/types-CEABh1Zf.d.ts.map +1 -0
- package/dist/types-Ci0Jq0xB.d.cts +96 -0
- package/dist/types-Ci0Jq0xB.d.cts.map +1 -0
- package/dist/types-DXg8BvEh.d.ts +52 -0
- package/dist/types-DXg8BvEh.d.ts.map +1 -0
- package/dist/types-_zY7QbM5.d.ts +538 -0
- package/dist/types-_zY7QbM5.d.ts.map +1 -0
- package/dist/webhooks/index.cjs +8 -0
- package/dist/webhooks/index.d.cts +5 -0
- package/dist/webhooks/index.d.ts +5 -0
- package/dist/webhooks/index.js +4 -0
- package/dist/webhooks-B5sAWC9W.js +74 -0
- package/dist/webhooks-B5sAWC9W.js.map +1 -0
- package/dist/webhooks-Cx11psg1.cjs +79 -0
- package/dist/webhooks-Cx11psg1.cjs.map +1 -0
- package/package.json +192 -3
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
const require_catalog = require("./catalog-CHPqldcj.cjs");
|
|
2
|
+
const require_tokens = require("./tokens-C1nzkn21.cjs");
|
|
3
|
+
const require_constants = require("./constants-BE1_9Tod.cjs");
|
|
4
|
+
const require_key_resolver = require("./key-resolver-CESqj4-P.cjs");
|
|
5
|
+
const require_sign = require("./sign-C7y6xdvT.cjs");
|
|
6
|
+
let jose = require("jose");
|
|
7
|
+
// src/s2s/verify.ts
|
|
8
|
+
/**
|
|
9
|
+
* Primitiva de verificación HMAC-SHA-256 para requests S2S entrantes.
|
|
10
|
+
*
|
|
11
|
+
* Framework-agnostic — acepta Web API `Request`.
|
|
12
|
+
*
|
|
13
|
+
* REQ-VERIFY-HEADERS-*, REQ-VERIFY-TS-*, REQ-VERIFY-SIG-*, REQ-VERIFY-HMAC-*,
|
|
14
|
+
* REQ-VERIFY-TIMING-SAFE-*, REQ-VERIFY-BODY-*, REQ-API-01.
|
|
15
|
+
*
|
|
16
|
+
* D-12 LOCKED: `hexToBytes` se duplica aquí (NO se importa de sign.ts)
|
|
17
|
+
* para que el entry point ./s2s sea completamente tree-shakeable.
|
|
18
|
+
*/
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
const HEX_REGEX = /^[0-9a-f]+$/i;
|
|
21
|
+
/**
|
|
22
|
+
* Decodifica hex string a Uint8Array.
|
|
23
|
+
* Retorna null si la longitud es impar o contiene chars no-hex.
|
|
24
|
+
* D-12 LOCKED: duplicado de sign.ts para tree-shakeability.
|
|
25
|
+
*/
|
|
26
|
+
function hexToBytes(hex) {
|
|
27
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !HEX_REGEX.test(hex)) return null;
|
|
28
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
29
|
+
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
30
|
+
return bytes;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Dummy HMAC key hex (64 zeros = 32 bytes).
|
|
34
|
+
* Used when `secretStore` returns null to keep timing equivalent to the valid-secret path.
|
|
35
|
+
*
|
|
36
|
+
* @security WHATWG Web Crypto spec does NOT explicitly guarantee constant-time for
|
|
37
|
+
* `crypto.subtle.verify`. In practice Node 22+ (OpenSSL) and Chromium (BoringSSL)
|
|
38
|
+
* use `CRYPTO_memcmp` which IS constant-time. If that changes, migrate to the
|
|
39
|
+
* double-HMAC verification pattern (HMAC(secret, expected) === HMAC(secret, received)).
|
|
40
|
+
* See: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
|
41
|
+
*
|
|
42
|
+
* Q-E05 LOCKED: value is '00'.repeat(32) (64 hex zeros).
|
|
43
|
+
*/
|
|
44
|
+
const DUMMY_HMAC_KEY_HEX = "00".repeat(32);
|
|
45
|
+
/** Regex: timestamp must be 1–11 decimal digits (unsigned Unix epoch in seconds). */
|
|
46
|
+
const TIMESTAMP_REGEX = /^\d{1,11}$/;
|
|
47
|
+
/**
|
|
48
|
+
* Regex: signature must be `sha256=` followed by exactly 64 hex chars. Case-insensitive on
|
|
49
|
+
* purpose (`/i`): the verifier accepts upper or lower case; signers always emit lowercase.
|
|
50
|
+
*/
|
|
51
|
+
const SIGNATURE_REGEX = /^sha256=([0-9a-f]{64})$/i;
|
|
52
|
+
/** Human-readable messages for each reason. */
|
|
53
|
+
const VERIFY_MESSAGES = {
|
|
54
|
+
header_missing: "Required authentication headers are missing",
|
|
55
|
+
timestamp_invalid: "Timestamp header is not a valid integer",
|
|
56
|
+
timestamp_expired: "Timestamp is outside the allowed window",
|
|
57
|
+
nonce_malformed: "Nonce header is malformed",
|
|
58
|
+
signature_malformed: "Signature header is malformed",
|
|
59
|
+
client_unknown: "Authentication failed",
|
|
60
|
+
signature_invalid: "Authentication failed",
|
|
61
|
+
replay_detected: "Signature already used"
|
|
62
|
+
};
|
|
63
|
+
function verifyErr(reason, messages) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: {
|
|
67
|
+
reason,
|
|
68
|
+
message: messages[reason]
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function okResult(data) {
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
data
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Verify an inbound S2S request signed with HMAC-SHA-256.
|
|
80
|
+
*
|
|
81
|
+
* The function reads the body into memory exactly once (via `request.arrayBuffer()`).
|
|
82
|
+
* Callers that need to read the body again after calling this function MUST clone the
|
|
83
|
+
* request before passing it here (e.g., Hono's `s2sAuth` uses `cloneRawRequest`).
|
|
84
|
+
*
|
|
85
|
+
* @param request - Web API `Request` to verify. Body is read and buffered internally.
|
|
86
|
+
* @param opts.secretStore - Callback returning the plaintext secret (64 hex chars) for
|
|
87
|
+
* the given clientId, or null if unknown. Return null — do NOT throw — for unknown clients.
|
|
88
|
+
* @param opts.timestampWindowSeconds - Override timestamp window (default: 120 s).
|
|
89
|
+
*
|
|
90
|
+
* @throws If `secretStore` returns a non-null value that is not valid hex (empty, odd length
|
|
91
|
+
* or non-hex chars). This is a server misconfiguration, so it is NOT mapped to a 401; the
|
|
92
|
+
* error message never contains the secret.
|
|
93
|
+
*
|
|
94
|
+
* @remarks A query string with malformed percent-encoding (e.g. `?a=%ZZ`) cannot be
|
|
95
|
+
* canonicalized; it fails closed with `signature_invalid`, following the same path as an
|
|
96
|
+
* invalid signature (secret lookup and HMAC included, `replayCache` not touched).
|
|
97
|
+
*
|
|
98
|
+
* @returns `S2SVerifyResult<{ clientId: string }>` — discriminated union.
|
|
99
|
+
* `ok: true` → `data.clientId` is the authenticated service client slug.
|
|
100
|
+
* `ok: false` → `error.reason` narrows the failure; `error.message` is human-readable.
|
|
101
|
+
*
|
|
102
|
+
* @security Timing-safe: whether `secretStore` returns null (client_unknown path) or a
|
|
103
|
+
* valid secret with an invalid signature (signature_invalid path), this function
|
|
104
|
+
* ALWAYS reaches `crypto.subtle.verify` with the same execution path — using
|
|
105
|
+
* `DUMMY_HMAC_KEY_HEX` when the secret is null. The verify result is discarded in
|
|
106
|
+
* the client_unknown path, but the operation always executes. See AC-10.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const result = await verifyS2SRequest(request, {
|
|
111
|
+
* secretStore: async (clientId) => {
|
|
112
|
+
* const client = await db.serviceClients.findById(clientId)
|
|
113
|
+
* return client?.secret ?? null
|
|
114
|
+
* },
|
|
115
|
+
* })
|
|
116
|
+
* if (!result.ok) {
|
|
117
|
+
* return new Response(
|
|
118
|
+
* JSON.stringify({ error: result.error }),
|
|
119
|
+
* { status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
120
|
+
* )
|
|
121
|
+
* }
|
|
122
|
+
* const { clientId } = result.data
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
async function verifyS2SRequest(request, opts) {
|
|
126
|
+
const { secretStore, timestampWindowSeconds = 120, replayCache } = opts;
|
|
127
|
+
const effectiveMessages = {
|
|
128
|
+
...VERIFY_MESSAGES,
|
|
129
|
+
...opts.messages
|
|
130
|
+
};
|
|
131
|
+
const clientId = request.headers.get("x-eva-client-id");
|
|
132
|
+
const timestampStr = request.headers.get("x-eva-timestamp");
|
|
133
|
+
const nonce = request.headers.get("x-eva-nonce");
|
|
134
|
+
const signatureHeader = request.headers.get("x-eva-signature");
|
|
135
|
+
if (clientId === null || timestampStr === null || nonce === null || signatureHeader === null) return verifyErr("header_missing", effectiveMessages);
|
|
136
|
+
if (!TIMESTAMP_REGEX.test(timestampStr)) return verifyErr("timestamp_invalid", effectiveMessages);
|
|
137
|
+
const timestamp = Number(timestampStr);
|
|
138
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
139
|
+
if (Math.abs(now - timestamp) > timestampWindowSeconds) return verifyErr("timestamp_expired", effectiveMessages);
|
|
140
|
+
if (!require_sign.S2S_NONCE_REGEX.test(nonce)) return verifyErr("nonce_malformed", effectiveMessages);
|
|
141
|
+
const sigMatch = signatureHeader.match(SIGNATURE_REGEX);
|
|
142
|
+
if (sigMatch === null) return verifyErr("signature_malformed", effectiveMessages);
|
|
143
|
+
const signatureHex = sigMatch[1];
|
|
144
|
+
const signatureBytesNN = hexToBytes(signatureHex);
|
|
145
|
+
const bodyArrayBuffer = await request.arrayBuffer();
|
|
146
|
+
const bodyBytes = new Uint8Array(bodyArrayBuffer);
|
|
147
|
+
const url = new URL(request.url);
|
|
148
|
+
const parts = {
|
|
149
|
+
method: request.method,
|
|
150
|
+
path: url.pathname,
|
|
151
|
+
rawQuery: url.search.length > 0 ? url.search.slice(1) : "",
|
|
152
|
+
timestamp: timestampStr,
|
|
153
|
+
nonce,
|
|
154
|
+
clientId,
|
|
155
|
+
bodyBytes
|
|
156
|
+
};
|
|
157
|
+
let canonical = "";
|
|
158
|
+
let canonicalizable = true;
|
|
159
|
+
try {
|
|
160
|
+
canonical = await require_sign.buildS2SCanonicalString(parts);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (!(err instanceof require_sign.S2SMalformedQueryError)) throw err;
|
|
163
|
+
canonicalizable = false;
|
|
164
|
+
}
|
|
165
|
+
const canonicalBytes = encoder.encode(canonical);
|
|
166
|
+
const secretHex = await secretStore(clientId);
|
|
167
|
+
const keyBytes = hexToBytes(secretHex ?? DUMMY_HMAC_KEY_HEX);
|
|
168
|
+
if (keyBytes === null) throw new Error("[eva-auth-sdk] s2s: secretStore devolvió un secreto con formato inválido (se requiere hex, longitud par)");
|
|
169
|
+
const keyBytesNN = keyBytes;
|
|
170
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBytesNN.buffer, {
|
|
171
|
+
name: "HMAC",
|
|
172
|
+
hash: "SHA-256"
|
|
173
|
+
}, false, ["verify"]);
|
|
174
|
+
const verified = await crypto.subtle.verify("HMAC", cryptoKey, signatureBytesNN.buffer, canonicalBytes);
|
|
175
|
+
if (secretHex === null) return verifyErr("client_unknown", effectiveMessages);
|
|
176
|
+
if (!verified || !canonicalizable) return verifyErr("signature_invalid", effectiveMessages);
|
|
177
|
+
if (replayCache) {
|
|
178
|
+
if (!await replayCache.setIfAbsent(`${clientId}:${nonce}`, 2 * timestampWindowSeconds + 1)) return verifyErr("replay_detected", effectiveMessages);
|
|
179
|
+
}
|
|
180
|
+
return okResult({ clientId });
|
|
181
|
+
}
|
|
182
|
+
// src/s2s/service-token.ts
|
|
183
|
+
/**
|
|
184
|
+
* Verificador de los tokens de servicio (client credentials) que emite el Auth Service y que un
|
|
185
|
+
* servicio recibe como `Authorization: Bearer` de otro servicio (o del propio Auth, en el OTP).
|
|
186
|
+
*
|
|
187
|
+
* Comparte el JWKS con los access tokens de usuario, así que lo que impide confundir un token de
|
|
188
|
+
* usuario con uno de servicio son chequeos independientes: `typ: 'svc-at+jwt'`, `kid` con prefijo
|
|
189
|
+
* `svc-` (un token sin `kid` o con `kid` `usr-` ni siquiera llega a una clave), `aud` igual a la
|
|
190
|
+
* audiencia propia del servicio receptor y el `scope` pedido.
|
|
191
|
+
*
|
|
192
|
+
* Framework-agnostic: recibe el token ya extraído del header.
|
|
193
|
+
*/
|
|
194
|
+
/** Tolerancia por defecto al desfase de relojes, en segundos. */
|
|
195
|
+
const SERVICE_TOKEN_CLOCK_TOLERANCE_SECONDS = 5;
|
|
196
|
+
const SERVICE_TOKEN_MESSAGES = {
|
|
197
|
+
token_missing: "Bearer token is missing",
|
|
198
|
+
token_malformed: "Token is malformed",
|
|
199
|
+
token_expired: "Token has expired",
|
|
200
|
+
signature_invalid: "Authentication failed",
|
|
201
|
+
claims_invalid: "Authentication failed",
|
|
202
|
+
scope_insufficient: "Token does not carry the required scope"
|
|
203
|
+
};
|
|
204
|
+
function fail$1(reason, messages) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
error: {
|
|
208
|
+
reason,
|
|
209
|
+
message: messages[reason]
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** Traduce un error de jose; los que no son del token (fallo de red del JWKS, config) se relanzan. */
|
|
214
|
+
function reasonFromJoseError(err) {
|
|
215
|
+
switch (err?.code) {
|
|
216
|
+
case "ERR_JWT_EXPIRED": return "token_expired";
|
|
217
|
+
case "ERR_JWT_CLAIM_VALIDATION_FAILED":
|
|
218
|
+
case "ERR_JWT_INVALID": return "claims_invalid";
|
|
219
|
+
case "ERR_JWS_INVALID": return "token_malformed";
|
|
220
|
+
case "ERR_JOSE_ALG_NOT_ALLOWED":
|
|
221
|
+
case "ERR_JOSE_NOT_SUPPORTED":
|
|
222
|
+
case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED":
|
|
223
|
+
case "ERR_JWKS_NO_MATCHING_KEY":
|
|
224
|
+
case "ERR_JWKS_MULTIPLE_MATCHING_KEYS": return "signature_invalid";
|
|
225
|
+
default: return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Valida las opciones (audiencia del catálogo, permisos de esa audiencia). Lanza `TypeError`.
|
|
230
|
+
* `serviceTokenAuth` la llama al construirse; `verifyServiceToken` en cada llamada.
|
|
231
|
+
*/
|
|
232
|
+
function assertServiceTokenOptions(opts) {
|
|
233
|
+
const { audience, requiredScope, anyScope } = opts;
|
|
234
|
+
if (!require_catalog.isServiceAudience(audience)) throw new TypeError(`[eva-auth-sdk] verifyServiceToken: audience debe ser una audiencia de servicio del catálogo (${require_catalog.SERVICE_AUDIENCES.join(", ")})`);
|
|
235
|
+
if ([
|
|
236
|
+
requiredScope !== void 0,
|
|
237
|
+
anyScope !== void 0,
|
|
238
|
+
opts.scopeCheck !== void 0
|
|
239
|
+
].filter(Boolean).length !== 1 || opts.scopeCheck !== void 0 && opts.scopeCheck !== "deferred") throw new TypeError("[eva-auth-sdk] verifyServiceToken: indica exactamente uno de 'requiredScope', 'anyScope' o scopeCheck: 'deferred'");
|
|
240
|
+
if (opts.scopeCheck !== void 0) return;
|
|
241
|
+
const wanted = requiredScope !== void 0 ? [requiredScope] : anyScope;
|
|
242
|
+
if (wanted.length === 0) throw new TypeError("[eva-auth-sdk] verifyServiceToken: 'anyScope' no puede ser vacío");
|
|
243
|
+
for (const scope of wanted) if (!require_catalog.isScopeOfAudience(audience, scope)) throw new TypeError(`[eva-auth-sdk] verifyServiceToken: "${scope}" no es un permiso de la audiencia "${audience}"`);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Verifica un token de servicio.
|
|
247
|
+
*
|
|
248
|
+
* Contrato del token (lo emite el Auth Service):
|
|
249
|
+
* - Header: `alg: 'ES256'`, `typ: 'svc-at+jwt'`, `kid` con prefijo `svc-`.
|
|
250
|
+
* - Claims: `iss: 'auth-service'`, `aud` (una sola, la del receptor), `sub` (id del service
|
|
251
|
+
* client), `client_id` (su slug), `scope` (separado por espacios), `iat`, `exp` (con
|
|
252
|
+
* `exp - iat` <= `maxLifetimeSeconds`) y `jti`.
|
|
253
|
+
*
|
|
254
|
+
* Un token de servicio es reutilizable dentro de su vida: no hay replay cache.
|
|
255
|
+
*
|
|
256
|
+
* @throws `TypeError` si `audience` no es del catálogo, si no se indica exactamente uno de
|
|
257
|
+
* `requiredScope` / `anyScope`, o si un permiso no pertenece a la audiencia.
|
|
258
|
+
* @throws Error de configuración (`EvaAuthConfigError`) si falta `authUrl` y no se inyecta `jwks`, y
|
|
259
|
+
* los fallos de red del JWKS: son errores de infraestructura, no un token inválido, así que no
|
|
260
|
+
* se degradan a un rechazo.
|
|
261
|
+
*/
|
|
262
|
+
async function verifyServiceToken(token, opts) {
|
|
263
|
+
assertServiceTokenOptions(opts);
|
|
264
|
+
const { audience, maxLifetimeSeconds = 300, clockToleranceSeconds = 5 } = opts;
|
|
265
|
+
const messages = {
|
|
266
|
+
...SERVICE_TOKEN_MESSAGES,
|
|
267
|
+
...opts.messages
|
|
268
|
+
};
|
|
269
|
+
let payload;
|
|
270
|
+
try {
|
|
271
|
+
({payload} = await (0, jose.jwtVerify)(token, require_key_resolver.withKidPrefix(opts.jwks ?? require_key_resolver.getJwksKeyFn(), require_tokens.SERVICE_KID_PREFIX), {
|
|
272
|
+
algorithms: [require_constants.JWT_CONFIG.ALGORITHM],
|
|
273
|
+
typ: require_tokens.SERVICE_TOKEN_TYP,
|
|
274
|
+
issuer: require_constants.JWT_CONFIG.ISSUER,
|
|
275
|
+
audience,
|
|
276
|
+
clockTolerance: clockToleranceSeconds,
|
|
277
|
+
requiredClaims: [
|
|
278
|
+
"iat",
|
|
279
|
+
"exp",
|
|
280
|
+
"sub"
|
|
281
|
+
]
|
|
282
|
+
}));
|
|
283
|
+
} catch (err) {
|
|
284
|
+
const reason = reasonFromJoseError(err);
|
|
285
|
+
if (reason === null) throw err;
|
|
286
|
+
return fail$1(reason, messages);
|
|
287
|
+
}
|
|
288
|
+
const { iat, exp, sub, client_id: clientId, scope, jti } = payload;
|
|
289
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
290
|
+
if (payload.aud !== audience || typeof iat !== "number" || typeof exp !== "number" || exp - iat > maxLifetimeSeconds || iat > now + clockToleranceSeconds || typeof sub !== "string" || sub === "" || typeof clientId !== "string" || clientId === "") return fail$1("claims_invalid", messages);
|
|
291
|
+
const scopes = typeof scope === "string" ? scope.split(" ").filter(Boolean) : [];
|
|
292
|
+
if (!(opts.scopeCheck !== void 0 || (opts.requiredScope !== void 0 ? scopes.includes(opts.requiredScope) : opts.anyScope.some((wanted) => scopes.includes(wanted))))) return fail$1("scope_insufficient", messages);
|
|
293
|
+
return {
|
|
294
|
+
ok: true,
|
|
295
|
+
data: {
|
|
296
|
+
serviceClientId: sub,
|
|
297
|
+
serviceClientSlug: clientId,
|
|
298
|
+
scopes,
|
|
299
|
+
jti: typeof jti === "string" ? jti : void 0
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// src/s2s/delegated-token.ts
|
|
304
|
+
/**
|
|
305
|
+
* Verificador de los tokens delegados (token exchange, RFC 8693) que un servicio recibe como
|
|
306
|
+
* `Authorization: Bearer` de otro servicio que actúa en nombre de un usuario.
|
|
307
|
+
*
|
|
308
|
+
* El token delegado se firma con una clave de SERVICIO (`kid` `svc-`) pero tiene su propio tipo:
|
|
309
|
+
* `typ: 'dlg-at+jwt'`. Ningún verificador acepta el tipo de otro: este exige `typ` + prefijo de
|
|
310
|
+
* `kid` + una sola `aud` (la del receptor, nunca `auth-api`) + empresa, producto y sesión de
|
|
311
|
+
* origen; `verifyServiceToken` exige `svc-at+jwt` y `verifyAccessToken` exige `at+jwt` + `kid` `usr-`.
|
|
312
|
+
*
|
|
313
|
+
* Framework-agnostic: recibe el token ya extraído del header.
|
|
314
|
+
*/
|
|
315
|
+
function fail(reason, messages) {
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
error: {
|
|
319
|
+
reason,
|
|
320
|
+
message: messages[reason]
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Valida las opciones (audiencia delegable, permisos delegables de esa audiencia). Lanza
|
|
326
|
+
* `TypeError`. `delegatedTokenAuth` la llama al construirse; `verifyDelegatedToken` en cada llamada.
|
|
327
|
+
*/
|
|
328
|
+
function assertDelegatedTokenOptions(opts) {
|
|
329
|
+
const { audience, requiredScope, anyScope } = opts;
|
|
330
|
+
if (!require_catalog.isDelegableAudience(audience)) throw new TypeError("[eva-auth-sdk] verifyDelegatedToken: audience debe ser una audiencia delegable del catálogo (auth-api nunca lo es)");
|
|
331
|
+
if ([
|
|
332
|
+
requiredScope !== void 0,
|
|
333
|
+
anyScope !== void 0,
|
|
334
|
+
opts.scopeCheck !== void 0
|
|
335
|
+
].filter(Boolean).length !== 1 || opts.scopeCheck !== void 0 && opts.scopeCheck !== "deferred") throw new TypeError("[eva-auth-sdk] verifyDelegatedToken: indica exactamente uno de 'requiredScope', 'anyScope' o scopeCheck: 'deferred'");
|
|
336
|
+
if (opts.scopeCheck !== void 0) return;
|
|
337
|
+
const wanted = requiredScope !== void 0 ? [requiredScope] : anyScope;
|
|
338
|
+
if (wanted.length === 0) throw new TypeError("[eva-auth-sdk] verifyDelegatedToken: 'anyScope' no puede ser vacío");
|
|
339
|
+
for (const scope of wanted) if (!require_catalog.isDelegableScope(audience, scope)) throw new TypeError(`[eva-auth-sdk] verifyDelegatedToken: "${scope}" no es un permiso delegable de la audiencia "${audience}"`);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Lee la cadena `act`: cada nivel con `sub` y `client_id` no vacíos, al menos uno y como máximo
|
|
343
|
+
* `DELEGATED_TOKEN_MAX_DEPTH`. `null` si algo no cumple.
|
|
344
|
+
*/
|
|
345
|
+
function readActorChain(act) {
|
|
346
|
+
const chain = [];
|
|
347
|
+
let node = act;
|
|
348
|
+
while (node !== void 0) {
|
|
349
|
+
if (chain.length >= 3) return null;
|
|
350
|
+
if (typeof node !== "object" || node === null || typeof node.sub !== "string" || node.sub === "" || typeof node.client_id !== "string" || node.client_id === "") return null;
|
|
351
|
+
chain.push({
|
|
352
|
+
serviceClientId: node.sub,
|
|
353
|
+
serviceClientSlug: node.client_id
|
|
354
|
+
});
|
|
355
|
+
node = node.act;
|
|
356
|
+
}
|
|
357
|
+
return chain.length > 0 ? chain : null;
|
|
358
|
+
}
|
|
359
|
+
const isNonEmptyString = (value) => typeof value === "string" && value !== "";
|
|
360
|
+
/**
|
|
361
|
+
* Verifica un token delegado.
|
|
362
|
+
*
|
|
363
|
+
* Contrato del token (lo emite el Auth Service en `POST /internal/token-exchange`):
|
|
364
|
+
* - Header: `alg: 'ES256'`, `typ: 'dlg-at+jwt'`, `kid` con prefijo `svc-`.
|
|
365
|
+
* - Claims: `iss: 'auth-service'`, `aud` (una sola, la del receptor), `sub` (el usuario),
|
|
366
|
+
* `client_id`, `act` (cadena de actores), `empresa`, `producto` y `sid` (obligatorios: la
|
|
367
|
+
* empresa es inmutable en la cadena), `scope`, `iat`, `exp` (con `exp - iat` <=
|
|
368
|
+
* `maxLifetimeSeconds`) y `jti`.
|
|
369
|
+
*
|
|
370
|
+
* Un token delegado sin empresa se rechaza (`claims_invalid`): la delegación sin empresa no existe.
|
|
371
|
+
* Es reutilizable dentro de su vida (120 s): no hay replay cache.
|
|
372
|
+
*
|
|
373
|
+
* @throws `TypeError` si `audience` no es delegable o los permisos exigidos no lo son.
|
|
374
|
+
* @throws Error de configuración si falta `authUrl` y no se inyecta `jwks`, y los fallos de red del
|
|
375
|
+
* JWKS (son de infraestructura, no un token inválido).
|
|
376
|
+
*/
|
|
377
|
+
async function verifyDelegatedToken(token, opts) {
|
|
378
|
+
assertDelegatedTokenOptions(opts);
|
|
379
|
+
const { audience, maxLifetimeSeconds = 120, clockToleranceSeconds = 5 } = opts;
|
|
380
|
+
const messages = {
|
|
381
|
+
...SERVICE_TOKEN_MESSAGES,
|
|
382
|
+
...opts.messages
|
|
383
|
+
};
|
|
384
|
+
let payload;
|
|
385
|
+
try {
|
|
386
|
+
({payload} = await (0, jose.jwtVerify)(token, require_key_resolver.withKidPrefix(opts.jwks ?? require_key_resolver.getJwksKeyFn(), require_tokens.SERVICE_KID_PREFIX), {
|
|
387
|
+
algorithms: [require_constants.JWT_CONFIG.ALGORITHM],
|
|
388
|
+
typ: require_tokens.DELEGATED_TOKEN_TYP,
|
|
389
|
+
issuer: require_constants.JWT_CONFIG.ISSUER,
|
|
390
|
+
audience,
|
|
391
|
+
clockTolerance: clockToleranceSeconds,
|
|
392
|
+
requiredClaims: [
|
|
393
|
+
"iat",
|
|
394
|
+
"exp",
|
|
395
|
+
"sub",
|
|
396
|
+
"jti"
|
|
397
|
+
]
|
|
398
|
+
}));
|
|
399
|
+
} catch (err) {
|
|
400
|
+
const reason = reasonFromJoseError(err);
|
|
401
|
+
if (reason === null) throw err;
|
|
402
|
+
return fail(reason, messages);
|
|
403
|
+
}
|
|
404
|
+
const { iat, exp, sub, client_id: clientId, empresa, producto, sid, scope, jti } = payload;
|
|
405
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
406
|
+
const actorChain = readActorChain(payload.act);
|
|
407
|
+
if (payload.aud !== audience || typeof iat !== "number" || typeof exp !== "number" || exp - iat > maxLifetimeSeconds || iat > now + clockToleranceSeconds || !isNonEmptyString(sub) || !isNonEmptyString(clientId) || !isNonEmptyString(empresa) || !isNonEmptyString(producto) || !isNonEmptyString(sid) || !isNonEmptyString(jti) || actorChain === null || clientId !== actorChain[0].serviceClientSlug) return fail("claims_invalid", messages);
|
|
408
|
+
const scopes = typeof scope === "string" ? scope.split(" ").filter(Boolean) : [];
|
|
409
|
+
if (!(opts.scopeCheck !== void 0 || (opts.requiredScope !== void 0 ? scopes.includes(opts.requiredScope) : opts.anyScope.some((wanted) => scopes.includes(wanted))))) return fail("scope_insufficient", messages);
|
|
410
|
+
return {
|
|
411
|
+
ok: true,
|
|
412
|
+
data: {
|
|
413
|
+
userId: sub,
|
|
414
|
+
empresaId: empresa,
|
|
415
|
+
producto,
|
|
416
|
+
sessionId: sid,
|
|
417
|
+
actorChain,
|
|
418
|
+
clientId,
|
|
419
|
+
scopes,
|
|
420
|
+
jti
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
Object.defineProperty(exports, "SERVICE_TOKEN_CLOCK_TOLERANCE_SECONDS", {
|
|
425
|
+
enumerable: true,
|
|
426
|
+
get: function() {
|
|
427
|
+
return SERVICE_TOKEN_CLOCK_TOLERANCE_SECONDS;
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
Object.defineProperty(exports, "assertDelegatedTokenOptions", {
|
|
431
|
+
enumerable: true,
|
|
432
|
+
get: function() {
|
|
433
|
+
return assertDelegatedTokenOptions;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
Object.defineProperty(exports, "assertServiceTokenOptions", {
|
|
437
|
+
enumerable: true,
|
|
438
|
+
get: function() {
|
|
439
|
+
return assertServiceTokenOptions;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
Object.defineProperty(exports, "verifyDelegatedToken", {
|
|
443
|
+
enumerable: true,
|
|
444
|
+
get: function() {
|
|
445
|
+
return verifyDelegatedToken;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
Object.defineProperty(exports, "verifyS2SRequest", {
|
|
449
|
+
enumerable: true,
|
|
450
|
+
get: function() {
|
|
451
|
+
return verifyS2SRequest;
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
Object.defineProperty(exports, "verifyServiceToken", {
|
|
455
|
+
enumerable: true,
|
|
456
|
+
get: function() {
|
|
457
|
+
return verifyServiceToken;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
//# sourceMappingURL=delegated-token-DHSK4rHI.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delegated-token-DHSK4rHI.cjs","names":["S2S_NONCE_REGEX","buildS2SCanonicalString","S2SMalformedQueryError","fail","isServiceAudience","SERVICE_AUDIENCES","isScopeOfAudience","jwtVerify","withKidPrefix","getJwksKeyFn","SERVICE_KID_PREFIX","JWT_CONFIG","SERVICE_TOKEN_TYP","isDelegableAudience","isDelegableScope","jwtVerify","withKidPrefix","getJwksKeyFn","SERVICE_KID_PREFIX","JWT_CONFIG","DELEGATED_TOKEN_TYP"],"sources":["../src/s2s/verify.ts","../src/s2s/service-token.ts","../src/s2s/delegated-token.ts"],"sourcesContent":["/**\n * Primitiva de verificación HMAC-SHA-256 para requests S2S entrantes.\n *\n * Framework-agnostic — acepta Web API `Request`.\n *\n * REQ-VERIFY-HEADERS-*, REQ-VERIFY-TS-*, REQ-VERIFY-SIG-*, REQ-VERIFY-HMAC-*,\n * REQ-VERIFY-TIMING-SAFE-*, REQ-VERIFY-BODY-*, REQ-API-01.\n *\n * D-12 LOCKED: `hexToBytes` se duplica aquí (NO se importa de sign.ts)\n * para que el entry point ./s2s sea completamente tree-shakeable.\n */\n\nimport { S2SMalformedQueryError } from './errors'\nimport { buildS2SCanonicalString } from './sign'\nimport { S2S_NONCE_REGEX, S2S_TIMESTAMP_WINDOW_SECONDS } from './constants'\nimport type {\n S2SCanonicalParts,\n S2SVerifyError,\n S2SVerifyOptions,\n S2SVerifyReason,\n S2SVerifyResult,\n} from './types'\n\n// ---------------------------------------------------------------------------\n// Helpers criptográficos locales (D-12 LOCKED — duplicado de sign.ts)\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder()\nconst HEX_REGEX = /^[0-9a-f]+$/i\n\n/**\n * Decodifica hex string a Uint8Array.\n * Retorna null si la longitud es impar o contiene chars no-hex.\n * D-12 LOCKED: duplicado de sign.ts para tree-shakeability.\n */\nfunction hexToBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || !HEX_REGEX.test(hex)) {\n return null\n }\n const bytes = new Uint8Array(hex.length / 2)\n for (let i = 0; i < hex.length; i += 2) {\n bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16)\n }\n return bytes\n}\n\n// ---------------------------------------------------------------------------\n// Constantes internas\n// ---------------------------------------------------------------------------\n\n/**\n * Dummy HMAC key hex (64 zeros = 32 bytes).\n * Used when `secretStore` returns null to keep timing equivalent to the valid-secret path.\n *\n * @security WHATWG Web Crypto spec does NOT explicitly guarantee constant-time for\n * `crypto.subtle.verify`. In practice Node 22+ (OpenSSL) and Chromium (BoringSSL)\n * use `CRYPTO_memcmp` which IS constant-time. If that changes, migrate to the\n * double-HMAC verification pattern (HMAC(secret, expected) === HMAC(secret, received)).\n * See: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy\n *\n * Q-E05 LOCKED: value is '00'.repeat(32) (64 hex zeros).\n */\nconst DUMMY_HMAC_KEY_HEX = '00'.repeat(32)\n\n/** Regex: timestamp must be 1–11 decimal digits (unsigned Unix epoch in seconds). */\nconst TIMESTAMP_REGEX = /^\\d{1,11}$/\n\n/**\n * Regex: signature must be `sha256=` followed by exactly 64 hex chars. Case-insensitive on\n * purpose (`/i`): the verifier accepts upper or lower case; signers always emit lowercase.\n */\nconst SIGNATURE_REGEX = /^sha256=([0-9a-f]{64})$/i\n\n/** Human-readable messages for each reason. */\nconst VERIFY_MESSAGES: Record<S2SVerifyReason, string> = {\n header_missing: 'Required authentication headers are missing',\n timestamp_invalid: 'Timestamp header is not a valid integer',\n timestamp_expired: 'Timestamp is outside the allowed window',\n nonce_malformed: 'Nonce header is malformed',\n signature_malformed: 'Signature header is malformed',\n client_unknown: 'Authentication failed',\n signature_invalid: 'Authentication failed',\n replay_detected: 'Signature already used',\n}\n\n// ---------------------------------------------------------------------------\n// Helpers privados\n// ---------------------------------------------------------------------------\n\nfunction verifyErr(\n reason: S2SVerifyReason,\n messages: Record<S2SVerifyReason, string>,\n): S2SVerifyResult<never> {\n const error: S2SVerifyError = { reason, message: messages[reason] }\n return { ok: false, error }\n}\n\nfunction okResult<T>(data: T): S2SVerifyResult<T> {\n return { ok: true, data }\n}\n\n// ---------------------------------------------------------------------------\n// API pública\n// ---------------------------------------------------------------------------\n\n/**\n * Verify an inbound S2S request signed with HMAC-SHA-256.\n *\n * The function reads the body into memory exactly once (via `request.arrayBuffer()`).\n * Callers that need to read the body again after calling this function MUST clone the\n * request before passing it here (e.g., Hono's `s2sAuth` uses `cloneRawRequest`).\n *\n * @param request - Web API `Request` to verify. Body is read and buffered internally.\n * @param opts.secretStore - Callback returning the plaintext secret (64 hex chars) for\n * the given clientId, or null if unknown. Return null — do NOT throw — for unknown clients.\n * @param opts.timestampWindowSeconds - Override timestamp window (default: 120 s).\n *\n * @throws If `secretStore` returns a non-null value that is not valid hex (empty, odd length\n * or non-hex chars). This is a server misconfiguration, so it is NOT mapped to a 401; the\n * error message never contains the secret.\n *\n * @remarks A query string with malformed percent-encoding (e.g. `?a=%ZZ`) cannot be\n * canonicalized; it fails closed with `signature_invalid`, following the same path as an\n * invalid signature (secret lookup and HMAC included, `replayCache` not touched).\n *\n * @returns `S2SVerifyResult<{ clientId: string }>` — discriminated union.\n * `ok: true` → `data.clientId` is the authenticated service client slug.\n * `ok: false` → `error.reason` narrows the failure; `error.message` is human-readable.\n *\n * @security Timing-safe: whether `secretStore` returns null (client_unknown path) or a\n * valid secret with an invalid signature (signature_invalid path), this function\n * ALWAYS reaches `crypto.subtle.verify` with the same execution path — using\n * `DUMMY_HMAC_KEY_HEX` when the secret is null. The verify result is discarded in\n * the client_unknown path, but the operation always executes. See AC-10.\n *\n * @example\n * ```ts\n * const result = await verifyS2SRequest(request, {\n * secretStore: async (clientId) => {\n * const client = await db.serviceClients.findById(clientId)\n * return client?.secret ?? null\n * },\n * })\n * if (!result.ok) {\n * return new Response(\n * JSON.stringify({ error: result.error }),\n * { status: 401, headers: { 'Content-Type': 'application/json' } },\n * )\n * }\n * const { clientId } = result.data\n * ```\n */\nexport async function verifyS2SRequest(\n request: Request,\n opts: S2SVerifyOptions,\n): Promise<S2SVerifyResult<{ clientId: string }>> {\n const {\n secretStore,\n timestampWindowSeconds = S2S_TIMESTAMP_WINDOW_SECONDS,\n replayCache,\n } = opts\n\n // PASO PRE — i18n merge (D12): spread natural respeta '' como override explícito\n const effectiveMessages = { ...VERIFY_MESSAGES, ...opts.messages }\n\n // PASO 1 — Extract headers (Web API Headers normalizes case automatically per spec)\n const clientId = request.headers.get('x-eva-client-id')\n const timestampStr = request.headers.get('x-eva-timestamp')\n const nonce = request.headers.get('x-eva-nonce')\n const signatureHeader = request.headers.get('x-eva-signature')\n\n if (\n clientId === null ||\n timestampStr === null ||\n nonce === null ||\n signatureHeader === null\n ) {\n return verifyErr('header_missing', effectiveMessages)\n }\n\n // PASO 2 — Validate timestamp format (unsigned decimal integer, up to 11 digits)\n if (!TIMESTAMP_REGEX.test(timestampStr)) {\n return verifyErr('timestamp_invalid', effectiveMessages)\n }\n const timestamp = Number(timestampStr)\n\n // PASO 3 — Validate timestamp window\n const now = Math.floor(Date.now() / 1000)\n if (Math.abs(now - timestamp) > timestampWindowSeconds) {\n return verifyErr('timestamp_expired', effectiveMessages)\n }\n\n // PASO 3b — Validate nonce format (canonical 32 lowercase hex, no normalization).\n // Does not depend on the secret, so it is decided before any HMAC work.\n if (!S2S_NONCE_REGEX.test(nonce)) {\n return verifyErr('nonce_malformed', effectiveMessages)\n }\n\n // PASO 4 — Parse signature header: `sha256=<64 hex chars>`\n const sigMatch = signatureHeader.match(SIGNATURE_REGEX)\n if (sigMatch === null) {\n return verifyErr('signature_malformed', effectiveMessages)\n }\n const signatureHex = sigMatch[1] as string\n const signatureBytes = hexToBytes(signatureHex)\n // hexToBytes cannot return null here: SIGNATURE_REGEX guarantees 64 valid hex chars.\n // Non-null assertion avoids runtime overhead.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const signatureBytesNN = signatureBytes!\n\n // PASO 5 — Read body (buffers the stream into memory once)\n const bodyArrayBuffer = await request.arrayBuffer()\n const bodyBytes = new Uint8Array(bodyArrayBuffer)\n\n // PASO 6 — Build canonical string (reuses existing algorithm — D-01: timestampStr as string,\n // D-02: clientId included)\n const url = new URL(request.url)\n const parts: S2SCanonicalParts = {\n method: request.method,\n path: url.pathname,\n rawQuery: url.search.length > 0 ? url.search.slice(1) : '', // strip leading '?'\n timestamp: timestampStr, // D-01: pass string directly — no numeric conversion\n nonce, // validated in PASO 3b, bound into canonical\n clientId, // D-02: extracted from header, bound into canonical\n bodyBytes,\n }\n // Un query con percent-encoding malformado (`%ZZ`) no se puede canonicalizar. Falla cerrado\n // como `signature_invalid` (401), pero sigue el mismo camino que una firma inválida:\n // consulta el secreto y ejecuta el HMAC (sobre un canonical vacío, cuyo resultado se descarta).\n let canonical = ''\n let canonicalizable = true\n try {\n canonical = await buildS2SCanonicalString(parts)\n } catch (err) {\n // Solo la malformación del query es un 401; cualquier otro fallo (p. ej. crypto) se relanza.\n if (!(err instanceof S2SMalformedQueryError)) throw err\n canonicalizable = false\n }\n const canonicalBytes = encoder.encode(canonical)\n\n // PASO 7 — Secret lookup (timing-safe: always proceed to HMAC regardless of result)\n const secretHex = await secretStore(clientId)\n const keyHex = secretHex ?? DUMMY_HMAC_KEY_HEX\n const keyBytes = hexToBytes(keyHex)\n if (keyBytes === null) {\n // Misconfiguration of the caller (the contract requires hex): fail loudly instead of\n // answering 401. The message never includes the secret or the (attacker-supplied) clientId.\n throw new Error(\n '[eva-auth-sdk] s2s: secretStore devolvió un secreto con formato inválido (se requiere hex, longitud par)',\n )\n }\n const keyBytesNN = keyBytes\n\n // PASO 8 — HMAC verify — ALWAYS reaches this point (timing-safe invariant AC-10)\n const cryptoKey = await crypto.subtle.importKey(\n 'raw',\n keyBytesNN.buffer as BufferSource,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['verify'],\n )\n const verified = await crypto.subtle.verify(\n 'HMAC',\n cryptoKey,\n signatureBytesNN.buffer as BufferSource,\n canonicalBytes as BufferSource,\n )\n\n // PASO 9 — Result (checked AFTER verify — no early return before PASO 8)\n if (secretHex === null) {\n // Discard verify result — client_unknown takes precedence for timing-safe path\n return verifyErr('client_unknown', effectiveMessages)\n }\n if (!verified || !canonicalizable) {\n return verifyErr('signature_invalid', effectiveMessages)\n }\n\n // PASO 9b — replayCache check: always post-HMAC verify (D16 timing-safe)\n if (replayCache) {\n // The key is unambiguous because the nonce is a fixed-length 32-hex suffix without ':',\n // so `clientId:nonce` cannot collide across clients even if clientId contains ':'. TTL = 2W + 1:\n // `now` is floored to seconds and the window is inclusive (±W), so a request is\n // acceptable for 2W + 1 seconds and the key must outlive that.\n const inserted = await replayCache.setIfAbsent(\n `${clientId}:${nonce}`,\n 2 * timestampWindowSeconds + 1,\n )\n if (!inserted) return verifyErr('replay_detected', effectiveMessages)\n }\n\n return okResult({ clientId })\n}\n","/**\n * Verificador de los tokens de servicio (client credentials) que emite el Auth Service y que un\n * servicio recibe como `Authorization: Bearer` de otro servicio (o del propio Auth, en el OTP).\n *\n * Comparte el JWKS con los access tokens de usuario, así que lo que impide confundir un token de\n * usuario con uno de servicio son chequeos independientes: `typ: 'svc-at+jwt'`, `kid` con prefijo\n * `svc-` (un token sin `kid` o con `kid` `usr-` ni siquiera llega a una clave), `aud` igual a la\n * audiencia propia del servicio receptor y el `scope` pedido.\n *\n * Framework-agnostic: recibe el token ya extraído del header.\n */\n\nimport { jwtVerify } from 'jose'\nimport type { JWTVerifyGetKey } from 'jose'\nimport {\n SERVICE_AUDIENCES,\n SERVICE_KID_PREFIX,\n SERVICE_TOKEN_LIFETIME_SECONDS,\n SERVICE_TOKEN_TYP,\n isScopeOfAudience,\n isServiceAudience,\n} from '@eva_solutions/auth-contracts'\nimport type { ServiceAudience } from '@eva_solutions/auth-contracts'\nimport { JWT_CONFIG } from '../constants'\nimport { getJwksKeyFn } from '../jwks'\nimport { withKidPrefix } from '../key-resolver'\n\n/** Tolerancia por defecto al desfase de relojes, en segundos. */\nexport const SERVICE_TOKEN_CLOCK_TOLERANCE_SECONDS = 5\n\n/**\n * Motivos de rechazo de `verifyServiceToken`.\n *\n * - `token_missing` — no hay `Authorization: Bearer <jwt>` (solo lo produce el middleware).\n * - `token_malformed` — no es un JWT bien formado.\n * - `token_expired` — `exp` ya pasó (más la tolerancia).\n * - `signature_invalid` — firma inválida, `kid` ausente, de otro tipo o desconocido, o algoritmo distinto de ES256.\n * - `claims_invalid` — `typ`, `iss`, `aud`, `sub`, `client_id`, `iat`/`exp` ausentes o fuera de rango, o vida mayor al máximo.\n * - `scope_insufficient` — el claim `scope` no cubre el permiso requerido.\n */\nexport type ServiceTokenVerifyReason =\n | 'token_missing'\n | 'token_malformed'\n | 'token_expired'\n | 'signature_invalid'\n | 'claims_invalid'\n | 'scope_insufficient'\n\nexport type ServiceTokenVerifyError = {\n reason: ServiceTokenVerifyReason\n message: string\n}\n\n/** Datos verificados del token de servicio. */\nexport type ServiceTokenClaims = {\n /** `sub`: id interno estable del service client. Es la identidad; no cambia ni se reutiliza. */\n serviceClientId: string\n /** `client_id`: slug del service client. Dato visible (logs, UI); no usarlo como identidad. */\n serviceClientSlug: string\n /** Permisos del claim `scope` (separado por espacios). */\n scopes: readonly string[]\n /** `jti` del token, si lo trae. */\n jti: string | undefined\n}\n\nexport type ServiceTokenVerifyResult =\n | { ok: true; data: ServiceTokenClaims }\n | { ok: false; error: ServiceTokenVerifyError }\n\n/**\n * Permiso exigido: uno concreto (`requiredScope`), al menos uno de varios (`anyScope`) o\n * `scopeCheck: 'deferred'`, que no exige ninguno aquí y deja el permiso por ruta a `requireScope`\n * (cadena Bearer + permiso por ruta). Es explícito para que olvidar el permiso no abra la ruta.\n */\nexport type ServiceTokenScopeRequirement =\n | { requiredScope: string; anyScope?: never; scopeCheck?: never }\n | { anyScope: readonly string[]; requiredScope?: never; scopeCheck?: never }\n | { scopeCheck: 'deferred'; requiredScope?: never; anyScope?: never }\n\nexport type ServiceTokenVerifyOptions = ServiceTokenScopeRequirement & {\n /**\n * Audiencia propia del servicio receptor (`'whatsapp-api'`), del catálogo de `auth-contracts`.\n * Un slug de producto o cualquier otro valor lanza `TypeError`.\n */\n audience: ServiceAudience\n /**\n * Resolutor de claves. Por defecto el JWKS remoto del Auth Service (`getJwksKeyFn()`, requiere\n * `configureEvaAuth({ authUrl })`). Inyectable para tests. Siempre se filtra por el prefijo\n * `svc-`, también el inyectado.\n */\n jwks?: JWTVerifyGetKey\n /** Vida máxima aceptada (`exp - iat`). Default: `SERVICE_TOKEN_LIFETIME_SECONDS` (300). */\n maxLifetimeSeconds?: number\n /** Tolerancia de reloj. Default: `SERVICE_TOKEN_CLOCK_TOLERANCE_SECONDS` (5). */\n clockToleranceSeconds?: number\n /** Overrides de mensajes (i18n). Las keys no provistas usan el default en inglés. */\n messages?: Partial<Record<ServiceTokenVerifyReason, string>>\n}\n\nexport const SERVICE_TOKEN_MESSAGES: Record<ServiceTokenVerifyReason, string> =\n {\n token_missing: 'Bearer token is missing',\n token_malformed: 'Token is malformed',\n token_expired: 'Token has expired',\n signature_invalid: 'Authentication failed',\n claims_invalid: 'Authentication failed',\n scope_insufficient: 'Token does not carry the required scope',\n }\n\nfunction fail(\n reason: ServiceTokenVerifyReason,\n messages: Record<ServiceTokenVerifyReason, string>,\n): ServiceTokenVerifyResult {\n return { ok: false, error: { reason, message: messages[reason] } }\n}\n\n/** Traduce un error de jose; los que no son del token (fallo de red del JWKS, config) se relanzan. */\nexport function reasonFromJoseError(\n err: unknown,\n): ServiceTokenVerifyReason | null {\n const code = (err as { code?: unknown } | null)?.code\n switch (code) {\n case 'ERR_JWT_EXPIRED':\n return 'token_expired'\n case 'ERR_JWT_CLAIM_VALIDATION_FAILED':\n case 'ERR_JWT_INVALID':\n return 'claims_invalid'\n case 'ERR_JWS_INVALID':\n return 'token_malformed'\n case 'ERR_JOSE_ALG_NOT_ALLOWED':\n case 'ERR_JOSE_NOT_SUPPORTED':\n case 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED':\n case 'ERR_JWKS_NO_MATCHING_KEY':\n case 'ERR_JWKS_MULTIPLE_MATCHING_KEYS':\n return 'signature_invalid'\n default:\n return null\n }\n}\n\n/**\n * Valida las opciones (audiencia del catálogo, permisos de esa audiencia). Lanza `TypeError`.\n * `serviceTokenAuth` la llama al construirse; `verifyServiceToken` en cada llamada.\n */\nexport function assertServiceTokenOptions(\n opts: ServiceTokenVerifyOptions,\n): void {\n const { audience, requiredScope, anyScope } = opts\n if (!isServiceAudience(audience)) {\n throw new TypeError(\n `[eva-auth-sdk] verifyServiceToken: audience debe ser una audiencia de servicio del catálogo (${SERVICE_AUDIENCES.join(', ')})`,\n )\n }\n const given = [\n requiredScope !== undefined,\n anyScope !== undefined,\n opts.scopeCheck !== undefined,\n ].filter(Boolean).length\n if (\n given !== 1 ||\n (opts.scopeCheck !== undefined && opts.scopeCheck !== 'deferred')\n ) {\n throw new TypeError(\n \"[eva-auth-sdk] verifyServiceToken: indica exactamente uno de 'requiredScope', 'anyScope' o scopeCheck: 'deferred'\",\n )\n }\n if (opts.scopeCheck !== undefined) return\n const wanted = requiredScope !== undefined ? [requiredScope] : anyScope!\n if (wanted.length === 0) {\n throw new TypeError(\n \"[eva-auth-sdk] verifyServiceToken: 'anyScope' no puede ser vacío\",\n )\n }\n for (const scope of wanted) {\n if (!isScopeOfAudience(audience, scope)) {\n throw new TypeError(\n `[eva-auth-sdk] verifyServiceToken: \"${scope}\" no es un permiso de la audiencia \"${audience}\"`,\n )\n }\n }\n}\n\n/**\n * Verifica un token de servicio.\n *\n * Contrato del token (lo emite el Auth Service):\n * - Header: `alg: 'ES256'`, `typ: 'svc-at+jwt'`, `kid` con prefijo `svc-`.\n * - Claims: `iss: 'auth-service'`, `aud` (una sola, la del receptor), `sub` (id del service\n * client), `client_id` (su slug), `scope` (separado por espacios), `iat`, `exp` (con\n * `exp - iat` <= `maxLifetimeSeconds`) y `jti`.\n *\n * Un token de servicio es reutilizable dentro de su vida: no hay replay cache.\n *\n * @throws `TypeError` si `audience` no es del catálogo, si no se indica exactamente uno de\n * `requiredScope` / `anyScope`, o si un permiso no pertenece a la audiencia.\n * @throws Error de configuración (`EvaAuthConfigError`) si falta `authUrl` y no se inyecta `jwks`, y\n * los fallos de red del JWKS: son errores de infraestructura, no un token inválido, así que no\n * se degradan a un rechazo.\n */\nexport async function verifyServiceToken(\n token: string,\n opts: ServiceTokenVerifyOptions,\n): Promise<ServiceTokenVerifyResult> {\n assertServiceTokenOptions(opts)\n const {\n audience,\n maxLifetimeSeconds = SERVICE_TOKEN_LIFETIME_SECONDS,\n clockToleranceSeconds = SERVICE_TOKEN_CLOCK_TOLERANCE_SECONDS,\n } = opts\n const messages = { ...SERVICE_TOKEN_MESSAGES, ...opts.messages }\n\n let payload\n try {\n ;({ payload } = await jwtVerify(\n token,\n withKidPrefix(opts.jwks ?? getJwksKeyFn(), SERVICE_KID_PREFIX),\n {\n algorithms: [JWT_CONFIG.ALGORITHM],\n typ: SERVICE_TOKEN_TYP,\n issuer: JWT_CONFIG.ISSUER,\n audience,\n clockTolerance: clockToleranceSeconds,\n requiredClaims: ['iat', 'exp', 'sub'],\n },\n ))\n } catch (err) {\n const reason = reasonFromJoseError(err)\n if (reason === null) throw err\n return fail(reason, messages)\n }\n\n const { iat, exp, sub, client_id: clientId, scope, jti } = payload\n const now = Math.floor(Date.now() / 1000)\n if (\n // Una sola audiencia: un token con varias (`aud` arreglo) no es de este formato.\n payload.aud !== audience ||\n typeof iat !== 'number' ||\n typeof exp !== 'number' ||\n exp - iat > maxLifetimeSeconds ||\n iat > now + clockToleranceSeconds ||\n typeof sub !== 'string' ||\n sub === '' ||\n typeof clientId !== 'string' ||\n clientId === ''\n ) {\n return fail('claims_invalid', messages)\n }\n\n const scopes =\n typeof scope === 'string' ? scope.split(' ').filter(Boolean) : []\n const satisfied =\n opts.scopeCheck !== undefined ||\n (opts.requiredScope !== undefined\n ? scopes.includes(opts.requiredScope)\n : opts.anyScope!.some((wanted) => scopes.includes(wanted)))\n if (!satisfied) return fail('scope_insufficient', messages)\n\n return {\n ok: true,\n data: {\n serviceClientId: sub,\n serviceClientSlug: clientId,\n scopes,\n jti: typeof jti === 'string' ? jti : undefined,\n },\n }\n}\n","/**\n * Verificador de los tokens delegados (token exchange, RFC 8693) que un servicio recibe como\n * `Authorization: Bearer` de otro servicio que actúa en nombre de un usuario.\n *\n * El token delegado se firma con una clave de SERVICIO (`kid` `svc-`) pero tiene su propio tipo:\n * `typ: 'dlg-at+jwt'`. Ningún verificador acepta el tipo de otro: este exige `typ` + prefijo de\n * `kid` + una sola `aud` (la del receptor, nunca `auth-api`) + empresa, producto y sesión de\n * origen; `verifyServiceToken` exige `svc-at+jwt` y `verifyAccessToken` exige `at+jwt` + `kid` `usr-`.\n *\n * Framework-agnostic: recibe el token ya extraído del header.\n */\n\nimport { jwtVerify } from 'jose'\nimport type { JWTVerifyGetKey } from 'jose'\nimport {\n DELEGATED_TOKEN_LIFETIME_SECONDS,\n DELEGATED_TOKEN_MAX_DEPTH,\n DELEGATED_TOKEN_TYP,\n SERVICE_KID_PREFIX,\n isDelegableAudience,\n isDelegableScope,\n} from '@eva_solutions/auth-contracts'\nimport type {\n DelegableAudience,\n DelegatedActorClaim,\n} from '@eva_solutions/auth-contracts'\nimport { JWT_CONFIG } from '../constants'\nimport { getJwksKeyFn } from '../jwks'\nimport { withKidPrefix } from '../key-resolver'\nimport {\n SERVICE_TOKEN_CLOCK_TOLERANCE_SECONDS,\n SERVICE_TOKEN_MESSAGES,\n reasonFromJoseError,\n} from './service-token'\nimport type {\n ServiceTokenScopeRequirement,\n ServiceTokenVerifyReason,\n} from './service-token'\n\n/** Motivos de rechazo: los mismos que los del token de servicio. */\nexport type DelegatedTokenVerifyReason = ServiceTokenVerifyReason\n\nexport type DelegatedTokenVerifyError = {\n reason: DelegatedTokenVerifyReason\n message: string\n}\n\n/** Un actor de la cadena: el servicio que intercambió el token. */\nexport type DelegatedActor = {\n /** `act.sub`: id interno estable del service client. */\n serviceClientId: string\n /** `act.client_id`: slug del service client. Dato visible, no identidad. */\n serviceClientSlug: string\n}\n\n/** Datos verificados del token delegado. */\nexport type DelegatedTokenClaims = {\n /** `sub`: el usuario en cuyo nombre se actúa. */\n userId: string\n /** `empresa`: obligatoria e inmutable en toda la cadena. */\n empresaId: string\n /** `producto`: producto de origen de la cadena. */\n producto: string\n /** `sid`: sesión de origen. */\n sessionId: string\n /**\n * Cadena de actores, del último (quien llama a este servicio) al primero (quien partió del\n * access token del usuario). Siempre tiene al menos uno y como máximo `DELEGATED_TOKEN_MAX_DEPTH`.\n */\n actorChain: readonly DelegatedActor[]\n /** `client_id` del token: slug del actor, para logs y rate limit. */\n clientId: string\n /** Permisos del claim `scope` (separado por espacios). */\n scopes: readonly string[]\n /** `jti` del token (obligatorio). */\n jti: string\n}\n\nexport type DelegatedTokenVerifyResult =\n | { ok: true; data: DelegatedTokenClaims }\n | { ok: false; error: DelegatedTokenVerifyError }\n\nexport type DelegatedTokenVerifyOptions = ServiceTokenScopeRequirement & {\n /**\n * Audiencia propia del servicio receptor, del catálogo delegable de `auth-contracts`. `auth-api`\n * (o cualquier otro valor) lanza `TypeError`: Auth nunca es destino de un token delegado.\n */\n audience: DelegableAudience\n /** Resolutor de claves; por defecto el JWKS remoto. Siempre se filtra por el prefijo `svc-`. */\n jwks?: JWTVerifyGetKey\n /** Vida máxima aceptada (`exp - iat`). Default: `DELEGATED_TOKEN_LIFETIME_SECONDS` (120). */\n maxLifetimeSeconds?: number\n /** Tolerancia de reloj. Default: 5 s. */\n clockToleranceSeconds?: number\n /** Overrides de mensajes (i18n). */\n messages?: Partial<Record<DelegatedTokenVerifyReason, string>>\n}\n\nfunction fail(\n reason: DelegatedTokenVerifyReason,\n messages: Record<DelegatedTokenVerifyReason, string>,\n): DelegatedTokenVerifyResult {\n return { ok: false, error: { reason, message: messages[reason] } }\n}\n\n/**\n * Valida las opciones (audiencia delegable, permisos delegables de esa audiencia). Lanza\n * `TypeError`. `delegatedTokenAuth` la llama al construirse; `verifyDelegatedToken` en cada llamada.\n */\nexport function assertDelegatedTokenOptions(\n opts: DelegatedTokenVerifyOptions,\n): void {\n const { audience, requiredScope, anyScope } = opts\n if (!isDelegableAudience(audience)) {\n throw new TypeError(\n '[eva-auth-sdk] verifyDelegatedToken: audience debe ser una audiencia delegable del catálogo (auth-api nunca lo es)',\n )\n }\n const given = [\n requiredScope !== undefined,\n anyScope !== undefined,\n opts.scopeCheck !== undefined,\n ].filter(Boolean).length\n if (\n given !== 1 ||\n (opts.scopeCheck !== undefined && opts.scopeCheck !== 'deferred')\n ) {\n throw new TypeError(\n \"[eva-auth-sdk] verifyDelegatedToken: indica exactamente uno de 'requiredScope', 'anyScope' o scopeCheck: 'deferred'\",\n )\n }\n if (opts.scopeCheck !== undefined) return\n const wanted = requiredScope !== undefined ? [requiredScope] : anyScope!\n if (wanted.length === 0) {\n throw new TypeError(\n \"[eva-auth-sdk] verifyDelegatedToken: 'anyScope' no puede ser vacío\",\n )\n }\n for (const scope of wanted) {\n if (!isDelegableScope(audience, scope)) {\n throw new TypeError(\n `[eva-auth-sdk] verifyDelegatedToken: \"${scope}\" no es un permiso delegable de la audiencia \"${audience}\"`,\n )\n }\n }\n}\n\n/**\n * Lee la cadena `act`: cada nivel con `sub` y `client_id` no vacíos, al menos uno y como máximo\n * `DELEGATED_TOKEN_MAX_DEPTH`. `null` si algo no cumple.\n */\nfunction readActorChain(act: unknown): DelegatedActor[] | null {\n const chain: DelegatedActor[] = []\n let node = act as DelegatedActorClaim | undefined\n while (node !== undefined) {\n if (chain.length >= DELEGATED_TOKEN_MAX_DEPTH) return null\n if (\n typeof node !== 'object' ||\n node === null ||\n typeof node.sub !== 'string' ||\n node.sub === '' ||\n typeof node.client_id !== 'string' ||\n node.client_id === ''\n ) {\n return null\n }\n chain.push({ serviceClientId: node.sub, serviceClientSlug: node.client_id })\n node = node.act\n }\n return chain.length > 0 ? chain : null\n}\n\nconst isNonEmptyString = (value: unknown): value is string =>\n typeof value === 'string' && value !== ''\n\n/**\n * Verifica un token delegado.\n *\n * Contrato del token (lo emite el Auth Service en `POST /internal/token-exchange`):\n * - Header: `alg: 'ES256'`, `typ: 'dlg-at+jwt'`, `kid` con prefijo `svc-`.\n * - Claims: `iss: 'auth-service'`, `aud` (una sola, la del receptor), `sub` (el usuario),\n * `client_id`, `act` (cadena de actores), `empresa`, `producto` y `sid` (obligatorios: la\n * empresa es inmutable en la cadena), `scope`, `iat`, `exp` (con `exp - iat` <=\n * `maxLifetimeSeconds`) y `jti`.\n *\n * Un token delegado sin empresa se rechaza (`claims_invalid`): la delegación sin empresa no existe.\n * Es reutilizable dentro de su vida (120 s): no hay replay cache.\n *\n * @throws `TypeError` si `audience` no es delegable o los permisos exigidos no lo son.\n * @throws Error de configuración si falta `authUrl` y no se inyecta `jwks`, y los fallos de red del\n * JWKS (son de infraestructura, no un token inválido).\n */\nexport async function verifyDelegatedToken(\n token: string,\n opts: DelegatedTokenVerifyOptions,\n): Promise<DelegatedTokenVerifyResult> {\n assertDelegatedTokenOptions(opts)\n const {\n audience,\n maxLifetimeSeconds = DELEGATED_TOKEN_LIFETIME_SECONDS,\n clockToleranceSeconds = SERVICE_TOKEN_CLOCK_TOLERANCE_SECONDS,\n } = opts\n const messages = { ...SERVICE_TOKEN_MESSAGES, ...opts.messages }\n\n let payload\n try {\n ;({ payload } = await jwtVerify(\n token,\n withKidPrefix(opts.jwks ?? getJwksKeyFn(), SERVICE_KID_PREFIX),\n {\n algorithms: [JWT_CONFIG.ALGORITHM],\n typ: DELEGATED_TOKEN_TYP,\n issuer: JWT_CONFIG.ISSUER,\n audience,\n clockTolerance: clockToleranceSeconds,\n requiredClaims: ['iat', 'exp', 'sub', 'jti'],\n },\n ))\n } catch (err) {\n const reason = reasonFromJoseError(err)\n if (reason === null) throw err\n return fail(reason, messages)\n }\n\n const {\n iat,\n exp,\n sub,\n client_id: clientId,\n empresa,\n producto,\n sid,\n scope,\n jti,\n } = payload\n const now = Math.floor(Date.now() / 1000)\n const actorChain = readActorChain(payload.act)\n if (\n // Una sola audiencia: un token con varias (`aud` arreglo) no es de este formato.\n payload.aud !== audience ||\n typeof iat !== 'number' ||\n typeof exp !== 'number' ||\n exp - iat > maxLifetimeSeconds ||\n iat > now + clockToleranceSeconds ||\n !isNonEmptyString(sub) ||\n !isNonEmptyString(clientId) ||\n // La empresa es obligatoria: el token delegado sin empresa no existe.\n !isNonEmptyString(empresa) ||\n !isNonEmptyString(producto) ||\n !isNonEmptyString(sid) ||\n !isNonEmptyString(jti) ||\n actorChain === null ||\n // `client_id` es el slug del actor del primer nivel: debe ser el mismo de `act`.\n clientId !== actorChain[0]!.serviceClientSlug\n ) {\n return fail('claims_invalid', messages)\n }\n\n const scopes =\n typeof scope === 'string' ? scope.split(' ').filter(Boolean) : []\n const satisfied =\n opts.scopeCheck !== undefined ||\n (opts.requiredScope !== undefined\n ? scopes.includes(opts.requiredScope)\n : opts.anyScope!.some((wanted) => scopes.includes(wanted)))\n if (!satisfied) return fail('scope_insufficient', messages)\n\n return {\n ok: true,\n data: {\n userId: sub,\n empresaId: empresa,\n producto,\n sessionId: sid,\n actorChain,\n clientId,\n scopes,\n jti,\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { a as EvaError, h as SdkErrorReason, o as EvaSdkError, p as ErrorCode, r as EvaApiError } from "./types-Ci0Jq0xB.cjs";
|
|
2
|
+
/**
|
|
3
|
+
* Narrows EvaError a EvaApiError (kind: 'api').
|
|
4
|
+
* Use para acceder a `.code`, `.status`, `.message` del error del API.
|
|
5
|
+
*/
|
|
6
|
+
declare function isApiError(err: EvaError): err is EvaApiError;
|
|
7
|
+
/**
|
|
8
|
+
* Narrows EvaError a EvaSdkError (kind: 'sdk').
|
|
9
|
+
* Use para acceder a `.reason`, `.status`, `.message` del error interno del SDK.
|
|
10
|
+
*/
|
|
11
|
+
declare function isSdkError(err: EvaError): err is EvaSdkError;
|
|
12
|
+
/**
|
|
13
|
+
* Comprueba si err es un EvaApiError con el code exacto indicado.
|
|
14
|
+
* Útil para codes feature-specific que no tienen shortcut propio.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* if (matchError(r.error, ERROR_CODES.conflict)) { ... }
|
|
18
|
+
* if (matchError(r.error, 'service_client_already_exists')) { ... }
|
|
19
|
+
*/
|
|
20
|
+
declare function matchError<TCode extends ErrorCode>(err: EvaError, code: TCode): err is EvaApiError & {
|
|
21
|
+
code: TCode;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Comprueba si err es un EvaSdkError con el reason exacto indicado.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* if (matchSdkReason(r.error, SDK_ERROR_REASONS.network)) { ... }
|
|
28
|
+
*/
|
|
29
|
+
declare function matchSdkReason<TReason extends SdkErrorReason>(err: EvaError, reason: TReason): err is EvaSdkError & {
|
|
30
|
+
reason: TReason;
|
|
31
|
+
};
|
|
32
|
+
/** El recurso solicitado no existe (HTTP 404). */
|
|
33
|
+
declare function isNotFound(err: EvaError): err is EvaApiError & {
|
|
34
|
+
code: 'not_found';
|
|
35
|
+
};
|
|
36
|
+
/** Token o credenciales inválidos (HTTP 401). */
|
|
37
|
+
declare function isUnauthorized(err: EvaError): err is EvaApiError & {
|
|
38
|
+
code: 'unauthorized';
|
|
39
|
+
};
|
|
40
|
+
/** Acción no permitida para el usuario autenticado (HTTP 403). */
|
|
41
|
+
declare function isForbidden(err: EvaError): err is EvaApiError & {
|
|
42
|
+
code: 'forbidden';
|
|
43
|
+
};
|
|
44
|
+
/** El recurso ya existe o hay conflicto de estado (HTTP 409). */
|
|
45
|
+
declare function isConflict(err: EvaError): err is EvaApiError & {
|
|
46
|
+
code: 'conflict';
|
|
47
|
+
};
|
|
48
|
+
/** Los datos de entrada no superaron la validación (HTTP 422). */
|
|
49
|
+
declare function isValidationError(err: EvaError): err is EvaApiError & {
|
|
50
|
+
code: 'validation_error';
|
|
51
|
+
};
|
|
52
|
+
/** Límite de requests excedido (HTTP 429). */
|
|
53
|
+
declare function isRateLimited(err: EvaError): err is EvaApiError & {
|
|
54
|
+
code: 'rate_limited';
|
|
55
|
+
};
|
|
56
|
+
/** El recurso fue eliminado permanentemente (HTTP 410). */
|
|
57
|
+
declare function isGone(err: EvaError): err is EvaApiError & {
|
|
58
|
+
code: 'gone';
|
|
59
|
+
};
|
|
60
|
+
/** La entidad no puede procesarse según las reglas de negocio (HTTP 422). */
|
|
61
|
+
declare function isUnprocessableEntity(err: EvaError): err is EvaApiError & {
|
|
62
|
+
code: 'unprocessable_entity';
|
|
63
|
+
};
|
|
64
|
+
/** Request malformada o con parámetros inválidos (HTTP 400). */
|
|
65
|
+
declare function isBadRequest(err: EvaError): err is EvaApiError & {
|
|
66
|
+
code: 'bad_request';
|
|
67
|
+
};
|
|
68
|
+
/** Error interno del servidor (HTTP 500). */
|
|
69
|
+
declare function isInternalError(err: EvaError): err is EvaApiError & {
|
|
70
|
+
code: 'internal_error';
|
|
71
|
+
};
|
|
72
|
+
/** El servicio no está disponible (HTTP 503). */
|
|
73
|
+
declare function isServiceUnavailable(err: EvaError): err is EvaApiError & {
|
|
74
|
+
code: 'service_unavailable';
|
|
75
|
+
};
|
|
76
|
+
/** La cuenta está bloqueada por estado (HTTP 403). */
|
|
77
|
+
declare function isAccountStateLocked(err: EvaError): err is EvaApiError & {
|
|
78
|
+
code: 'account_state_locked';
|
|
79
|
+
};
|
|
80
|
+
/** Fallo de red o fetch (no se pudo contactar al servidor). */
|
|
81
|
+
declare function isNetworkError(err: EvaError): err is EvaSdkError & {
|
|
82
|
+
reason: 'network';
|
|
83
|
+
};
|
|
84
|
+
/** Token presente pero inválido (signature/format/expiry). */
|
|
85
|
+
declare function isTokenInvalid(err: EvaError): err is EvaSdkError & {
|
|
86
|
+
reason: 'token_invalid';
|
|
87
|
+
};
|
|
88
|
+
/** No hay tokens en el request (sin cookies de autenticación). */
|
|
89
|
+
declare function isAuthRequired(err: EvaError): err is EvaSdkError & {
|
|
90
|
+
reason: 'auth_required';
|
|
91
|
+
};
|
|
92
|
+
/** Refresh exitoso pero el servidor no devolvió tokens nuevos. */
|
|
93
|
+
declare function isRefreshNoTokens(err: EvaError): err is EvaSdkError & {
|
|
94
|
+
reason: 'refresh_no_tokens';
|
|
95
|
+
};
|
|
96
|
+
/** Fallo de verificación no clasificable (post-refresh). */
|
|
97
|
+
declare function isVerifyFailed(err: EvaError): err is EvaSdkError & {
|
|
98
|
+
reason: 'verify_failed';
|
|
99
|
+
};
|
|
100
|
+
/** Respuesta HTTP no parseable o sin el shape esperado. */
|
|
101
|
+
declare function isMalformed(err: EvaError): err is EvaSdkError & {
|
|
102
|
+
reason: 'malformed';
|
|
103
|
+
};
|
|
104
|
+
export { matchSdkReason as S, isUnauthorized as _, isConflict as a, isVerifyFailed as b, isInternalError as c, isNotFound as d, isRateLimited as f, isTokenInvalid as g, isServiceUnavailable as h, isBadRequest as i, isMalformed as l, isSdkError as m, isApiError as n, isForbidden as o, isRefreshNoTokens as p, isAuthRequired as r, isGone as s, isAccountStateLocked as t, isNetworkError as u, isUnprocessableEntity as v, matchError as x, isValidationError as y };
|
|
105
|
+
//# sourceMappingURL=error-helpers-0woh13Wd.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-helpers-0woh13Wd.d.cts","names":[],"sources":["../src/error-helpers.ts"],"mappings":";;;;;iBA4BgB,WAAW,KAAK,WAAW,OAAO;;;;;iBAQlC,WAAW,KAAK,WAAW,OAAO;;;;;;;;;iBAgBlC,WAAW,cAAc,WACvC,KAAK,UACL,MAAM,QACL,OAAO;EAAgB,MAAM;;;;;;;;iBAUhB,eAAe,gBAAgB,gBAC7C,KAAK,UACL,QAAQ,UACP,OAAO;EAAgB,QAAQ;;;iBASlB,WACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,eACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,YACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,WACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,kBACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,cACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,OAAO,KAAK,WAAW,OAAO;EAAgB;;;iBAK9C,sBACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,aACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,gBACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,qBACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,qBACd,KAAK,WACJ,OAAO;EAAgB;;;iBASV,eACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,eACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,eACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,kBACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,eACd,KAAK,WACJ,OAAO;EAAgB;;;iBAKV,YACd,KAAK,WACJ,OAAO;EAAgB"}
|