Compile the real jose root entry: string durations, chained builders and navigator (#1407)

This commit is contained in:
Chris Tate
2026-10-19 19:45:13 -05:00
committed by GitHub
192 changed files with 9814 additions and 240 deletions
+1
View File
@@ -35,6 +35,7 @@ output/
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
!tests/fixtures/provenance/node_modules/*/dist/
!tests/fixtures/npm-static/node_modules/*/dist/
!tests/corpus/*/node_modules/*/dist/
.scriptc/
*.log
.DS_Store
+50 -50
View File
@@ -328,10 +328,10 @@
"entries": 92,
"apiEntries": 92,
"static": {
"supported": 0,
"partial": 12,
"supported": 1,
"partial": 15,
"refused": 1,
"not-implemented": 79,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -854,9 +854,9 @@
"apiEntries": 156,
"static": {
"supported": 2,
"partial": 10,
"partial": 21,
"refused": 0,
"not-implemented": 144,
"not-implemented": 133,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -35693,9 +35693,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -36360,9 +36360,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -37165,9 +37165,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -37303,9 +37303,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "supported",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80229,9 +80229,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80252,9 +80252,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80275,9 +80275,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80298,9 +80298,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80321,9 +80321,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80344,9 +80344,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80436,9 +80436,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80574,9 +80574,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80735,9 +80735,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80758,9 +80758,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80804,9 +80804,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -3,12 +3,12 @@
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"summary": {
"taskCount": 3454,
"tierItemCount": 3454,
"taskCount": 3453,
"tierItemCount": 3453,
"tiers": {
"static": {
"audit-partial": 1165,
"verify-gap": 2161,
"audit-partial": 1179,
"verify-gap": 2146,
"replace-refusal": 41,
"classify": 2,
"implement": 85
@@ -17,7 +17,7 @@
"priorities": {
"high": 2287,
"normal": 694,
"low": 473
"low": 472
}
},
"tasks": [
@@ -33201,12 +33201,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:globals",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:globals.CryptoKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -33932,12 +33938,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:globals",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:globals.SubtleCrypto",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -34853,12 +34865,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:globals",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:globals.navigator",
"tests": [
"tests/fixtures/node-types/navigator.ts",
"tests/harness/project-config.test.ts",
"tests/corpus/6331-jose-root-entry/main.ts"
]
}
}
},
@@ -34987,31 +35003,6 @@
}
}
},
{
"id": "globals:fc0a32e8c26f",
"chapter": "globals",
"label": "userAgent",
"signature": "`userAgent` Type: {string}",
"apiSymbol": "userAgent",
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/globals.html#navigator",
"nodeStability": {
"index": "1.1",
"level": 1,
"text": "Active development. Disable this API with the [`--no-experimental-global-navigator`][] CLI flag.",
"inherited": true
},
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:globals",
"tests": []
}
}
},
{
"id": "globals:b412bf9a456d",
"chapter": "globals",
@@ -81024,12 +81015,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81049,12 +81046,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.CryptoKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81074,12 +81075,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.cryptoKey.algorithm",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81099,12 +81104,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.extractable",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81124,12 +81133,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.type",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81149,12 +81162,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.usages",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81249,12 +81266,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.SubtleCrypto",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
}
}
},
@@ -81399,12 +81422,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle.digest",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
}
}
},
@@ -81574,12 +81604,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle.importKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
}
}
},
@@ -81599,12 +81636,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle.sign",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
}
}
},
@@ -81649,12 +81693,19 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webcrypto",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webcrypto.subtle.verify",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
}
}
},
@@ -340,10 +340,10 @@
"entries": 94,
"apiEntries": 92,
"static": {
"supported": 0,
"partial": 12,
"supported": 1,
"partial": 15,
"refused": 1,
"not-implemented": 79,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -911,9 +911,9 @@
"apiEntries": 156,
"static": {
"supported": 2,
"partial": 10,
"partial": 21,
"refused": 0,
"not-implemented": 144,
"not-implemented": 133,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -52242,8 +52242,16 @@
"tests/corpus/5288-blob-validation.js",
"tests/corpus/5290-blob-dynamic.js",
"tests/corpus/5830-timers-promises-signal.ts",
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/6331-jose-root-entry/main.ts",
"tests/corpus/text-codec-values/main.ts",
"tests/harness/fetch-conformance.test.ts"
"tests/fixtures/node-types/navigator.ts",
"tests/harness/fetch-conformance.test.ts",
"tests/harness/project-config.test.ts"
]
},
"anchorSource": "chapter"
@@ -52664,8 +52672,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.CryptoKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -53338,8 +53353,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.SubtleCrypto",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -54244,8 +54266,13 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.navigator",
"tests": [
"tests/fixtures/node-types/navigator.ts",
"tests/harness/project-config.test.ts",
"tests/corpus/6331-jose-root-entry/main.ts"
]
},
"anchorSource": "exact"
},
@@ -54382,8 +54409,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "supported",
"evidence": "compiler-feature:globals.userAgent",
"tests": [
"tests/fixtures/node-types/navigator.ts",
"tests/harness/project-config.test.ts"
]
},
"anchorSource": "ancestor"
},
@@ -106872,10 +106903,16 @@
"tests/corpus/5443-crypto-random-arguments.js",
"tests/corpus/6204-crypto-get-random-values.ts",
"tests/corpus/6205-crypto-get-random-values.js",
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts",
"tests/corpus/node24-crypto-kdf.ts",
"tests/corpus/node24-crypto-sha2.ts",
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/harness/npm-static.test.ts"
]
},
@@ -110411,8 +110448,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "ancestor"
},
@@ -110434,8 +110478,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.CryptoKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -110457,8 +110506,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.cryptoKey.algorithm",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -110480,8 +110534,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.extractable",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "ancestor"
},
@@ -110503,8 +110562,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.type",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "ancestor"
},
@@ -110526,8 +110590,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.usages",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "ancestor"
},
@@ -110618,8 +110687,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.SubtleCrypto",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -110756,8 +110832,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.digest",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
},
"anchorSource": "exact"
},
@@ -110917,8 +111001,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.importKey",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
},
"anchorSource": "exact"
},
@@ -110940,8 +111032,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.sign",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
},
"anchorSource": "exact"
},
@@ -110986,8 +111086,16 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webcrypto"
"status": "partial",
"evidence": "compiler-feature:webcrypto.subtle.verify",
"tests": [
"tests/corpus/6291-webcrypto-hmac-sign-verify.ts",
"tests/corpus/6292-webcrypto-hmac-errors.ts",
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
]
},
"anchorSource": "exact"
},
@@ -179,11 +179,19 @@
"stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"],
"stdlib.headers": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.request": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
"stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
"stdlib.response": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
},
"features": [
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] },
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] },
{ "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] },
{"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]},
{"chapter": "events", "symbols": ["Event"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5241-event-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5247-event-target-subclass-lifetimes.ts"]},
+111 -6
View File
@@ -511,14 +511,119 @@ declare var DOMException: {
readonly prototype: DOMException;
};
/* Capability-gated globals harness code probes behind feature guards
* (globalThis.crypto under a hasCrypto check, the SQLite-backed storage
* pair): typed `unknown` — no lowering exists, the guard is expected to
* be false in a compiled binary, and any reached use fences per site. */
declare var crypto: unknown;
/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for
* raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as
* exactly that surface (the declarations mirror @types/node's webcrypto
* shapes). Every other SubtleCrypto method and crypto member is absent,
* so a use is an honest type error. CryptoKey values are native handles
* whose four members read through the handle. */
declare var crypto: Crypto;
/* The Crypto and SubtleCrypto constructors (feature probes only) stay
* `unknown`; the values keep this declaration order. */
declare var Crypto: unknown;
declare var CryptoKey: unknown;
declare var CryptoKey: {
prototype: CryptoKey;
new (): CryptoKey;
};
declare var SubtleCrypto: unknown;
type KeyUsage =
| "encrypt"
| "decrypt"
| "sign"
| "verify"
| "deriveKey"
| "deriveBits"
| "encapsulateBits"
| "decapsulateBits"
| "encapsulateKey"
| "decapsulateKey"
| "wrapKey"
| "unwrapKey";
type KeyType = "private" | "public" | "secret";
interface Algorithm {
name: string;
}
type AlgorithmIdentifier = Algorithm | string;
type HashAlgorithmIdentifier = AlgorithmIdentifier;
interface HmacImportParams extends Algorithm {
hash: HashAlgorithmIdentifier;
length?: number;
}
interface KeyAlgorithm {
name: string;
}
interface HmacKeyAlgorithm extends KeyAlgorithm {
hash: KeyAlgorithm;
length: number;
}
interface CryptoKey {
readonly algorithm: KeyAlgorithm;
readonly extractable: boolean;
readonly type: KeyType;
readonly usages: KeyUsage[];
}
type KeyFormat =
| "jwk"
| "pkcs8"
| "raw"
| "raw-private"
| "raw-public"
| "raw-secret"
| "raw-seed"
| "spki";
interface JsonWebKey {
alg?: string;
crv?: string;
d?: string;
dp?: string;
dq?: string;
e?: string;
ext?: boolean;
k?: string;
key_ops?: string[];
kty?: string;
n?: string;
p?: string;
q?: string;
qi?: string;
use?: string;
x?: string;
y?: string;
}
/* The 'jwk' form type-checks like @types/node's; the compiler refuses it
* (the JsonWebKey import is not implemented). */
interface SubtleCrypto {
digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise<ArrayBuffer>;
importKey(
format: "jwk",
keyData: JsonWebKey,
algorithm: AlgorithmIdentifier | HmacImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
importKey(
format: Exclude<KeyFormat, "jwk">,
keyData: BufferSource,
algorithm: AlgorithmIdentifier | HmacImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
verify(
algorithm: AlgorithmIdentifier,
key: CryptoKey,
signature: BufferSource,
data: BufferSource,
): Promise<boolean>;
}
interface Crypto {
readonly subtle: SubtleCrypto;
}
/* Capability-gated globals harness code probes behind feature guards
* (the SQLite-backed storage pair): typed `unknown` — no lowering exists,
* the guard is expected to be false in a compiled binary, and any reached
* use fences per site. */
declare var localStorage: unknown;
declare var sessionStorage: unknown;
@@ -40,5 +40,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tls: features.tls,
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
};
}
@@ -52,6 +52,7 @@ export async function compilePackedLibrary(
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
tlsCa: false,
};
const selection = await loadRuntimePack({
@@ -832,6 +832,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "util":
case "bigint":
case "crypto":
case "webcrypto":
case "buffer":
case "bytes":
case "tlsca":
@@ -588,6 +588,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.generateKeySync": "scr_crypto_generate_key_sync",
"crypto.keyObjectInput": "scr_crypto_key_object_input",
"crypto.isKeyObject": "scr_crypto_is_key_object",
"webcrypto.importKey": "scr_webcrypto_import_key",
"webcrypto.sign": "scr_webcrypto_sign",
"webcrypto.verify": "scr_webcrypto_verify",
"webcrypto.digest": "scr_webcrypto_digest",
"webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key",
"crypto.randomUUIDv7": "scr_crypto_random_uuid_v7",
"crypto.pbkdf2": "scr_crypto_pbkdf2",
"crypto.hkdf": "scr_crypto_hkdf",
@@ -1471,6 +1476,10 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"process.emitWarning",
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
"crypto.cipherNew",
// WebCrypto results settle through the deferred crypto queue.
"webcrypto.sign",
"webcrypto.verify",
"webcrypto.digest",
"process.emitWarningText",
// The legacy url entries queue DEP0169/DEP0170 on the next tick.
"url.legacyParse",
@@ -51,6 +51,9 @@ export interface NativeLinkFeatures {
* archive. */
cipher: boolean;
tlsCa: boolean;
/** crypto.subtle importKey/sign/verify/digest and CryptoKey values:
* scr_webcrypto.c. */
webcrypto: boolean;
}
export interface NativeLinkInfo {
@@ -199,6 +199,8 @@ export interface CcOptions {
/** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links
* the mbedTLS archive (shared with tls). */
cipher?: boolean;
/** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */
webcrypto?: boolean;
/** Internal compiler hook: called only after a strict native artifact hit
* or a successful stable build has installed `outPath`. The executable
* frontend cache uses it to publish its stamp after native dependencies
@@ -578,6 +578,7 @@ export async function compileCInternal(
...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []),
...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []),
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
// references its default-set override unconditionally.
...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []),
@@ -119,6 +119,7 @@ const features = {
tls: false,
tlsCa: false,
cipher: false,
webcrypto: false,
};
test.each([
@@ -56,6 +56,7 @@ const BASE: NativeLinkFeatures = {
tls: false,
tlsCa: false,
cipher: false,
webcrypto: false,
};
async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
@@ -61,6 +61,7 @@ export async function warmNativeCaches(
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
tlsCa: false,
},
});
@@ -11,7 +11,8 @@ afterAll(() => rmSync(dir, { recursive: true, force: true }));
test("unchanged and comment-only reruns replay the verdict; code edits relower", () => {
const entry = join(dir, "main.ts");
const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = [1, 2] as number[];\nconsole.log(length);\n`;
// The blocker: an object pattern over a union source (arrays alone compile).
const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = (a > 9 ? [1, 2] : "ab") as number[] | string;\nconsole.log(length);\n`;
writeFileSync(entry, program);
const entries = new Map<string, string>();
const store: VerdictStore = {
@@ -57,6 +57,7 @@ const native: EarlyExecutableNativeFeatures = {
tls: false,
tlsCa: false,
cipher: false,
webcrypto: false,
};
async function fixture(): Promise<{
@@ -80,6 +80,7 @@ export interface EarlyExecutableNativeFeatures {
tls: boolean;
cipher: boolean;
tlsCa: boolean;
webcrypto: boolean;
}
interface EarlyExecutableCacheStamp {
@@ -193,6 +194,7 @@ const BOOLEAN_NATIVE_KEYS = [
"tls",
"tlsCa",
"cipher",
"webcrypto",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
@@ -0,0 +1,60 @@
/* Node 24's `navigator` global (lib/internal/navigator.js), the part
* libraries read to tell runtimes apart: `typeof navigator` is "object"
* and `navigator.userAgent` is `Node.js/<major>`. jose reads exactly
* that at module load (jwks/remote.js). Both fold to string literals, so
* nothing links. The other members (hardwareConcurrency, language,
* languages, platform, locks) and the object as a value keep a named
* refusal. */
import * as ts from "../../ts7/adapter.js";
import { strLit } from "../../../ir/build.js";
import type { IrExpr } from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
/** True when `node` names Node's global `navigator`: the @types/node
* declaration, or, in a JavaScript file, the free name no declaration
* binds (Node defines it on the global object). */
export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolean {
let expr = node;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false;
if (lowerer.isStdlibGlobal(expr, "navigator")) return true;
return (
isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined
);
}
/** `navigator.userAgent` (`Node.js/<major>` of `nodeVersion`, the pinned
* release builtins/process.ts names), or the named refusal for another
* member. Null when the receiver is not the global. */
export function lowerNavigatorMember(
lowerer: Lowerer,
access: ts.PropertyAccessExpression,
nodeVersion: string,
): IrExpr | null {
if (!isNavigatorGlobal(lowerer, access.expression)) return null;
if (access.name.text === "userAgent") {
return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access));
}
return refuseNavigator(
lowerer,
access,
`navigator.${access.name.text}`,
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** The refusal for the `navigator` object itself or an unlowered member. */
export function refuseNavigator(
lowerer: Lowerer,
node: ts.Node,
what: string,
symbol?: ts.Symbol,
): never {
return lowerer.noLowering(
what,
node,
"of Node's navigator global only typeof navigator and navigator.userAgent compile",
symbol ?? null,
);
}
@@ -4,7 +4,19 @@ import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js";
import { lowerToNumberArgument } from "../lower-exprs.js";
import { F64, type IrExpr, STRING, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js";
import {
BOOL,
DYN,
F64,
type IrExpr,
type IrStmt,
NULL_T,
STRING,
arrayOf,
bytesOf,
isUnitType,
typeEquals,
} from "../../../ir/ir.js";
/** `String.fromCharCode/fromCodePoint(...codes)` on THE String global: every argument
* lowers as a number and packs into ONE f64[] array-literal argument
@@ -17,6 +29,8 @@ export function lowerStringStaticCall(
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken) return null;
const applied = lowerFromCharCodeApply(lowerer, call, access);
if (applied) return applied;
const member = lowerer.stdlibGlobalMember(access, "String");
if (member !== "fromCharCode" && member !== "fromCodePoint" && member !== "raw") return null;
const loc = locOf(call);
@@ -108,6 +122,99 @@ export function lowerStringStaticCall(
return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc };
}
/** `String.fromCharCode.apply(thisArg, codes)`, the btoa chunking idiom
* (`String.fromCharCode.apply(null, bytes.subarray(i, j))`): the receiver
* is ignored, and a typed array or number array forwards to the packed
* fromCharCode like a spread. An untyped list reads a Uint8Array the same
* way at run time; any other value keeps Function.prototype.apply's
* generic array-like call. Null for every other call. */
function lowerFromCharCodeApply(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.name.text !== "apply" || access.questionDotToken) return null;
const target = access.expression;
if (
!ts.isPropertyAccessExpression(target) ||
lowerer.stdlibGlobalMember(target, "String") !== "fromCharCode"
) {
return null;
}
if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement))
return null;
const listNode = call.arguments.nodes[1]!;
const checkerT = lowerer.typeOf(listNode);
const untyped = (checkerT.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
const listT = untyped ? DYN : lowerer.mapTypeOf(checkerT);
if (
listT === null ||
(listT.kind !== "bytes" && listT.kind !== "dyn" && !typeEquals(listT, arrayOf(F64)))
) {
return null;
}
const loc = locOf(call);
const thisArg = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN);
const stmts: IrStmt[] = [{ kind: "exprStmt", expr: thisArg, loc: thisArg.loc }];
if (listT.kind !== "dyn") {
const list = lowerer.lowerExprExpecting(listNode, listT);
const result: IrExpr = {
kind: "libCall",
fn: "string.fromCharCode",
args: [list],
type: STRING,
loc,
};
return { kind: "seqExpr", stmts, result, type: STRING, loc };
}
const listLocal = lowerer.declareHiddenLocal("%charCodes", DYN);
stmts.push({
kind: "varDecl",
localId: listLocal.id,
init: lowerer.lowerExprExpecting(listNode, DYN),
loc,
});
const list = (): IrExpr => ({ kind: "varRef", localId: listLocal.id, type: DYN, loc });
const bytes = bytesOf("u8");
const generic: IrExpr = {
kind: "dynCheck",
value: {
kind: "dynInvoke",
recv: lowerer.lowerExprExpecting(target, DYN),
method: "apply",
calleeName: "String.fromCharCode.apply",
args: [
{
kind: "dynFrom",
value: { kind: "unitLit", unit: "null", type: NULL_T, loc },
type: DYN,
loc,
},
list(),
],
type: DYN,
loc,
},
type: STRING,
loc,
};
const result: IrExpr = {
kind: "ternary",
cond: { kind: "dynTest", test: "bytes", bytesElem: "u8", value: list(), type: BOOL, loc },
then: {
kind: "libCall",
fn: "string.fromCharCode",
args: [{ kind: "dynCheck", value: list(), type: bytes, loc }],
type: STRING,
loc,
},
else_: generic,
type: STRING,
loc,
};
return { kind: "seqExpr", stmts, result, type: STRING, loc };
}
/** `s.lastIndexOf(searchValue?, position?)` on string receivers, using UTF-16
* indices. An omitted search value searches for "undefined". Omitted or undefined positions clamp to the string's end;
* MAX_SAFE_INTEGER has the same effect for every representable string.
@@ -0,0 +1,130 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js";
/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`,
* `sign`, `verify` and `digest` called on the crypto global (also through
* globalThis) lower to runtime entry points that take the argument vector
* as one dyn array and answer a promise, so Web IDL conversion, algorithm
* normalization and every error stay Node-shaped at run time. CryptoKey
* values are native handles; their type maps to dyn and their members
* are handle reads. Other SubtleCrypto methods are refused by name. */
const SUBTLE_LOWERED: ReadonlyMap<string, IrLibFn> = new Map([
["importKey", "webcrypto.importKey"],
["sign", "webcrypto.sign"],
["verify", "webcrypto.verify"],
["digest", "webcrypto.digest"],
]);
const SUBTLE_HINT =
"the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle";
/** Whether `node` is `crypto.subtle` on the WebCrypto global. */
function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
return (
ts.isPropertyAccessExpression(node) &&
!node.questionDotToken &&
node.name.text === "subtle" &&
stdlibGlobalNameOf(lowerer, node.expression) === "crypto"
);
}
/** `crypto.subtle.<method>(...args)`: the lowered methods become their
* libCall over the argument vector; every other method is refused. */
export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || !isSubtleReceiver(lowerer, callee.expression)) {
return null;
}
const member = callee.name.text;
const fn = SUBTLE_LOWERED.get(member);
if (fn === undefined) lowerer.noLowering(`crypto.subtle.${member}`, callee, SUBTLE_HINT);
if (callee.questionDotToken || call.questionDotToken) {
lowerer.noLowering(`optional calls of crypto.subtle.${member}`, call, SUBTLE_HINT);
}
if (call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT);
}
// JsonWebKey imports are not implemented: a literal 'jwk' format is
// refused here (the runtime refuses a computed one).
const format = call.arguments.nodes[0];
if (
member === "importKey" &&
format !== undefined &&
ts.isStringLiteralLike(format) &&
format.text === "jwk"
) {
lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT);
}
const loc = locOf(call);
const args = call.arguments.nodes.map((arg) =>
ts.isObjectLiteralExpression(arg)
? lowerDynObjectLiteral(lowerer, arg)
: lowerer.lowerExprExpecting(arg, DYN),
);
return {
kind: "libCall",
fn,
args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }],
type: { kind: "promise", inner: member === "verify" ? BOOL : DYN },
loc,
};
}
/** `v instanceof CryptoKey` against the global. */
export function lowerCryptoKeyInstanceof(
lowerer: Lowerer,
expr: ts.BinaryExpression,
): IrExpr | null {
if (stdlibGlobalNameOf(lowerer, expr.right) !== "CryptoKey") return null;
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return {
kind: "libCall",
fn: "webcrypto.isCryptoKey",
args: [value],
type: BOOL,
loc: locOf(expr),
};
}
/** TypeScript reads of `crypto.subtle` and its members outside a direct
* call: the global's dynamic object does not carry the lowered methods,
* so a detached read is refused rather than answering a value without
* them. JavaScript keeps the dynamic object's reads. */
export function fenceWebCryptoMember(
lowerer: Lowerer,
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
use: "read" | "call",
): IrExpr | null {
if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null;
if (isJsSourceFile(access.getSourceFile())) return null;
if (isSubtleReceiver(lowerer, access)) {
lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT);
}
if (isSubtleReceiver(lowerer, access.expression)) {
lowerer.noLowering(`crypto.subtle.${access.name.text} as a value`, access, SUBTLE_HINT);
}
return null;
}
/** Whether a checker type is one of the WebCrypto interfaces reached
* through the crypto global's members (SubtleCrypto, CryptoKey and the
* algorithm dictionaries). While `crypto` was typed `unknown` those member
* reads were untyped, so JavaScript destructurings of them keep the
* module-global path untyped bindings take. `Crypto` itself is excluded:
* the global was `unknown`, which already mapped to dyn. */
export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean {
const symbol = type.getAliasSymbol() ?? type.getSymbol();
return (
symbol !== undefined &&
symbol.name !== "Crypto" &&
WEBCRYPTO_DYN_TYPES.has(symbol.name) &&
lowerer.isStdlibSymbol(symbol)
);
}
@@ -686,12 +686,23 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
};
// The declared result is the value's own type (the generic's T):
// validate the dyn copy back into it when the type can be checked;
// dyn-typed and unmappable results stay dyn values (JS files).
// dyn-typed and unmappable results stay dyn values (JS files). An
// empty record (`{}` inferred from a JavaScript `payload = {}`
// default) is the checker's supertype of whatever arrives, not a
// layout: checking into it would drop every member of the copy.
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
const resultShape =
resultT?.kind === "record" ? lowerer.shapes.get(resultT.shapeId) : undefined;
const emptyRecord =
resultShape !== undefined &&
!resultShape.tuple &&
resultShape.indexValue === undefined &&
resultShape.fields.length === 0;
if (
resultT !== null &&
resultT.kind !== "dyn" &&
resultT.kind !== "void" &&
!emptyRecord &&
canDynCheckTo(
resultT,
(id) => lowerer.shapes.get(id),
@@ -178,6 +178,39 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
return valid;
}
/** returnsOnlyThis, also accepting a comma expression whose last operand
* is `this` (the minifier's `return a(), this.x = v, this`, jose's fluent
* claim setters). Used only to narrow a call's result back to the
* receiver's class. */
export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean {
const body = member.body;
const last = body?.statements?.nodes.at(-1);
if (!body || !last || !ts.isReturnStatement(last)) return false;
let valid = true;
const visit = (node: ts.Node): void => {
if (ts.isFunctionLike(node)) return;
if (ts.isReturnStatement(node)) {
let value = node.expression;
for (;;) {
while (value && ts.isParenthesizedExpression(value)) value = value.expression;
if (
value &&
ts.isBinaryExpression(value) &&
value.operatorToken.kind === ts.SyntaxKind.CommaToken
) {
value = value.right;
continue;
}
break;
}
if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false;
}
node.forEachChild(visit);
};
body.forEachChild(visit);
return valid;
}
function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
for (
let parent: ts.Node | undefined = decl.parent;
@@ -6970,19 +7003,43 @@ export function lowerClassGenericMethodCall(
}
: upcastTo(lowerer, receiver, declarer.def.name);
const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call);
return {
const result: IrExpr = {
kind: "call",
callee: instance.name,
args: [thisArg, ...args],
type: instance.returnType,
loc,
};
// A fluent JavaScript method (every return ends in `this`, jose's claim
// setters) answers the receiver: the instance returns the declarer's
// class, a used result's checker type is the receiver's subclass.
if (
result.type.kind === "object" &&
!ts.isExpressionStatement(call.parent) &&
ts.isMethodDeclaration(info.decl) &&
isJsSourceFile(info.decl.getSourceFile()) &&
returnsOnlyThisValue(info.decl)
) {
return lowerer.maybeNarrow(result, call);
}
return result;
}
/** Wraps a derived-class expression in an upcast when the target base
* class differs (a no-op reinterpret at runtime; keeps IR types exact). */
export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrExpr {
if (expr.type.kind === "object" && expr.type.className !== className) {
// A value lowered at an ANCESTOR of the class the site needs (a
// fluent `this` result whose lowering kept the method's declaring
// class, the receiver of the next call in a chain): the checker
// vouches for the subclass, so the bridge is the verified downcast,
// never an upcast the validator rejects (an internal error before).
if (
!lowerer.isSubclassOf(expr.type.className, className) &&
lowerer.isSubclassOf(className, expr.type.className)
) {
return { kind: "downcast", value: expr, type: { kind: "object", className }, loc: expr.loc };
}
return { kind: "upcast", value: expr, type: { kind: "object", className }, loc: expr.loc };
}
return expr;
@@ -6,13 +6,14 @@ import { lowerEventObjectProperty, lowerEventObjectPropertyWrite } from "./lower
import { lowerDisposableStackInstanceOf } from "./builtins/disposable-stack.js";
import { lowerEventStaticProperty } from "./builtins/numbers.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { isOptionalProcessStreamProperty } from "./builtins/process.js";
import { isOptionalProcessStreamProperty, NODE_COMPAT_VERSION } from "./builtins/process.js";
import { lowerWorkerMetadata } from "./builtins/workers.js";
import { esmImportBindingKind } from "./builtins/module-bindings.js";
import { lowerReadlineProperty } from "./builtins/readline.js";
import { lowerBlobInstanceof } from "./builtins/blob.js";
import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
import { lowerCryptoKeyInstanceof } from "./builtins/webcrypto.js";
import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
@@ -221,6 +222,7 @@ import { errorPropertyRead, errorPropertyWrite, errorToStringCall } from "./erro
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
import { unionWideningTags } from "../../ir/analysis.js";
import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { isNavigatorGlobal, lowerNavigatorMember, refuseNavigator } from "./builtins/navigator.js";
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
import {
@@ -362,6 +364,26 @@ export function abstractPropertyDeclOf(
* Narrowing a ParseArgsToken exposes one anonymous union arm, so its alias
* identity no longer reaches mapType; declaration ancestry is the stable
* provenance check. */
/** The Uint8Array base64/hex prototype methods (ES2026) Node 24 lacks. */
const UINT8ARRAY_BASE64_METHODS: ReadonlySet<string> = new Set([
"toBase64",
"toHex",
"setFromBase64",
"setFromHex",
]);
/** `Uint8Array.prototype.<base64/hex method>` read off the global. */
function isUint8ArrayBase64Probe(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
if (!UINT8ARRAY_BASE64_METHODS.has(expr.name.text)) return false;
const proto = expr.expression;
return (
ts.isPropertyAccessExpression(proto) &&
!proto.questionDotToken &&
proto.name.text === "prototype" &&
stdlibGlobalNameOf(lowerer, proto.expression) === "Uint8Array"
);
}
function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
const sym = lowerer.checker.getSymbolAtLocation(expr.name);
if (!sym) return false;
@@ -650,6 +672,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
return { kind: "strLit", value: "object", type: STRING, loc };
if (lowerer.isStdlibGlobal(expr.expression, "fetch"))
return { kind: "strLit", value: "function", type: STRING, loc };
if (isNavigatorGlobal(lowerer, expr.expression))
return { kind: "strLit", value: "object", type: STRING, loc };
const performance = lowerPerfHooksTypeof(lowerer, expr.expression);
if (performance) return performance;
if (isNodeModuleValue(lowerer, expr.expression)) {
@@ -1697,6 +1721,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const ns = lowerNsIdentifierValue(lowerer, expr);
if (ns) return ns;
}
if (isNavigatorGlobal(lowerer, expr)) {
refuseNavigator(
lowerer,
expr,
"the navigator object as a value",
lowerer.checker.getSymbolAtLocation(expr),
);
}
// Preflight guarantees no unresolved identifiers; anything else here
// is a blocked declaration's binding (the SC2004 cascade) or a
// binding form we don't model yet.
@@ -1960,6 +1992,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr);
if (ts.isPropertyAccessExpression(expr)) {
if (!expr.questionDotToken) {
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
if (navigator) return navigator;
}
// `require("./x").member` over a program module's export table: the
// module's %init at this position, then the member read exactly like
// a `const m = require("./x")` binding's (cjs-inline-require.ts).
@@ -2158,6 +2194,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
) {
return dynUndefinedExpr(loc);
}
// `Uint8Array.prototype.toBase64` and its siblings, the base64/hex
// feature probes libraries such as jose make: the methods are newer
// than Node 24, whose Uint8Array prototype answers undefined for them.
if (isUint8ArrayBase64Probe(lowerer, expr)) return dynUndefinedExpr(loc);
if (expr.questionDotToken && expr.name.text === "electron") {
const processProperty = lowerer.lowerProcessProperty(expr);
if (processProperty) return processProperty;
@@ -7086,6 +7126,10 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
}
}
}
if (key.type.kind === "f64") {
const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key);
if (safe) return safe;
}
if (key.type.kind === "string" || key.type.kind === "f64") {
const lit = ts.isStringLiteral(expr.argumentExpression)
? expr.argumentExpression.text
@@ -8047,6 +8091,22 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr
}
return null;
}
// `m[2]` on a JavaScript `RegExpExecArray | null` that a previous
// statement ruled non-null without narrowing it (jose's secs()).
if (
receiverT?.kind === "union" &&
isJsSourceFile(expr.getSourceFile()) &&
!expr.questionDotToken &&
!hasOptionalChainGuard(expr.expression)
) {
const arms = lowerer.unions.get(receiverT.unionId)?.arms ?? [];
const present = arms.filter((arm) => !isUnitType(arm));
if (present.length !== 1 || present[0]!.kind !== "array") return null;
const value = lowerer.lowerExpr(expr.expression);
const index = lowerer.lowerExpr(expr.argumentExpression);
if (index.type.kind !== "f64") return null;
return lowerOptionalArrayIndexRead(lowerer, expr, value, index);
}
if (receiverT?.kind !== "array") return null;
const arr = lowerer.lowerExpr(expr.expression);
if (arr.type.kind !== "array") return null;
@@ -12914,6 +12974,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
if (blob) return blob;
const keyObject = lowerKeyObjectInstanceof(lowerer, expr);
if (keyObject) return keyObject;
const cryptoKey = lowerCryptoKeyInstanceof(lowerer, expr);
if (cryptoKey) return cryptoKey;
const netAddress = lowerNetAddressInstanceof(lowerer, expr);
if (netAddress) return netAddress;
}
@@ -15396,6 +15458,62 @@ export function lowerUnionProperty(
);
}
/** A number-keyed read in JavaScript on an array that may be undefined or
* null outside an optional chain (`m[1]` on a `RegExpExecArray | null`
* that an earlier statement ruled non-null without narrowing it): the
* array arm is an ordinary JavaScript element read, so a hole, a present
* undefined (a regex group that did not participate) or an invalid index
* answers undefined, as lowerSafeIndexRead does for a plain array. The
* unit arms keep the keyed read's answer. The receiver and key evaluate
* once, in source order. Null when the union is not exactly one array arm
* beside unit arms; optional-chain tails and TypeScript keep the keyed
* read. */
function lowerOptionalArrayIndexRead(
lowerer: Lowerer,
expr: ts.ElementAccessExpression,
value: IrExpr,
key: IrExpr,
): IrExpr | null {
if (
!isJsSourceFile(expr.getSourceFile()) ||
expr.questionDotToken ||
hasOptionalChainGuard(expr.expression)
)
return null;
if (value.type.kind !== "union") return null;
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms) return null;
const present = arms.filter((arm) => !isUnitType(arm));
const arm = present[0];
if (present.length !== 1 || arm?.kind !== "array" || arm.elem.kind === "void") return null;
const tag = arms.indexOf(arm);
const loc = locOf(expr);
const stmts: IrStmt[] = [];
const stable = (e: IrExpr, name: string): IrExpr => {
if (e.kind === "varRef" || e.kind === "numLit") return e;
const local = lowerer.declareHiddenLocal(name, e.type);
stmts.push({ kind: "varDecl", localId: local.id, init: e, loc });
return varRef(local.id, e.type, loc);
};
if (lowerUnionKeyedRead(lowerer, expr, unionId, value, key, null) === null) return null;
const recv = stable(value, "%indexReceiver");
const index = stable(key, "%indexKey");
const keyed = lowerUnionKeyedRead(lowerer, expr, unionId, recv, index, null)!;
const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: recv, type: arm, loc };
const read = lowerSafeIndexRead(lowerer, narrowed, index, loc);
if (read === null) return null;
const result: IrExpr = {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, value: recv, negated: false, type: BOOL, loc },
then: lowerer.coerceToExpected(read, keyed.type),
else_: keyed,
type: keyed.type,
loc,
};
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: keyed.type, loc };
}
/** The unionDisc generalization: a keyed read on a union receiver whose
* arms answer DIFFERENT (but joinable) types. Each arm contributes its
* declared answer — an array element's type (number keys), a declared field's type (literal keys), an
@@ -93,6 +93,7 @@ import {
import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
import { registerBuiltinClassAlias } from "./builtins/class-values.js";
import { stripTypeCasts } from "./builtins/arguments.js";
import { isWebCryptoMemberType } from "./builtins/webcrypto.js";
import {
bindingContextualGenericFnNodeOf,
bindingGenericFnAliasInfoOf,
@@ -1917,7 +1918,8 @@ export function collectGlobals(
decl.initializer &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
!lowerer.mapTypeOf(lowerer.typeOf(nameNode))
(!lowerer.mapTypeOf(lowerer.typeOf(nameNode)) ||
isWebCryptoMemberType(lowerer, lowerer.typeOf(nameNode)))
) {
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
if (symbol && !lowerer.globalsBySymbol.has(symbol)) {
@@ -2063,6 +2063,43 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool
.some((p) => p.declarations.some(declarationBelongs));
}
/** True when an `any` value comes from a JavaScript function's result:
* a call (possibly awaited) whose callee is declared in a JavaScript file,
* or a const bound to one. A TypeScript program importing a JavaScript
* package sees the package's inferred types (`jwtVerify` from jose
* answers `any`), and its object destructuring reads the members of that
* checked-dynamic value as JavaScript does — member reads on the same
* value already compile. Other `any` sources keep the fence. */
function javaScriptAnyOrigin(lowerer: Lowerer, node: ts.Expression, depth: number): boolean {
let expr = node;
while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression;
if (ts.isCallExpression(expr)) {
const callee = ts.isPropertyAccessExpression(expr.expression)
? expr.expression.name
: expr.expression;
if (!ts.isIdentifier(callee)) return false;
const symbol = lowerer.resolveValueSymbol(callee);
const declarations = symbol ? lowerer.checker.declarationsOf(symbol) : [];
return (
declarations.length > 0 &&
declarations.every((d) => isJsSourceFile(d.getSourceFile()) && ts.isFunctionLike(d))
);
}
if (ts.isIdentifier(expr) && depth === 0) {
const symbol = lowerer.resolveValueSymbol(expr);
const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
return (
declaration !== undefined &&
ts.isVariableDeclaration(declaration) &&
ts.isIdentifier(declaration.name) &&
declaration.initializer !== undefined &&
(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0 &&
javaScriptAnyOrigin(lowerer, declaration.initializer, depth + 1)
);
}
return false;
}
/** Destructuring declarations, desugared as sugar over indexed/field
* reads: the initializer evaluates ONCE into a hidden temp, then each
* bound name declares (or assigns its pre-registered module global) from
@@ -2107,7 +2144,12 @@ export function lowerDestructuringDecl(
lowerer.anyOrigin(decl.initializer) &&
!isJsSourceFile(decl.getSourceFile()) &&
init.type.kind !== "record" &&
init.type.kind !== "array"
init.type.kind !== "array" &&
!(
init.type.kind === "dyn" &&
ts.isObjectBindingPattern(decl.name) &&
javaScriptAnyOrigin(lowerer, decl.initializer, 0)
)
) {
lowerer.anyOpFence("destructuring", decl);
}
@@ -3029,6 +3071,10 @@ export function lowerBindingPattern(
lowerMatchResultObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out);
return;
}
if (srcType.kind === "array" && isArrayPositionPattern(lowerer, pattern)) {
lowerArrayPositionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out);
return;
}
if (srcType.kind !== "record") {
lowerer.unsupported(
"SC1031",
@@ -3359,19 +3405,8 @@ function lowerMatchResultObjectPattern(
);
}
let value: IrExpr;
if (/^(?:0|[1-9]\d{0,8})$/.test(propName)) {
const index = Number(propName);
const position: IrExpr = { kind: "numLit", value: index, type: F64, loc };
value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? {
kind: "arrayGet",
arr: srcRef(),
index: position,
type: srcType.elem,
loc,
};
if (el.initializer) {
value = arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out);
}
if (ARRAY_POSITION_KEY.test(propName)) {
value = arrayPositionBinding(lowerer, el, srcRef, srcType, Number(propName), out);
} else if (propName === "index" || propName === "input") {
if (el.initializer) {
lowerer.unsupported(
@@ -3417,6 +3452,83 @@ function lowerMatchResultObjectPattern(
}
}
/** A canonical array index key (the numeric positions an object pattern
* reads off an array). */
const ARRAY_POSITION_KEY = /^(?:0|[1-9]\d{0,8})$/;
/** The positional read `{ <index>: x }` takes off an array: the element,
* undefined past the end, with the binding default applied. */
function arrayPositionBinding(
lowerer: Lowerer,
el: ts.BindingElement,
srcRef: () => IrExpr,
srcType: IrType & { kind: "array" },
index: number,
out: IrStmt[],
): IrExpr {
const loc = locOf(el);
const position: IrExpr = { kind: "numLit", value: index, type: F64, loc };
const value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? {
kind: "arrayGet",
arr: srcRef(),
index: position,
type: srcType.elem,
loc,
};
return el.initializer
? arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out)
: value;
}
/** Whether every element of an object pattern over an array names a
* numeric position or `length` (`const { 0: a, 1: b, length } =
* s.split(".")`), with no rest element. */
function isArrayPositionPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean {
return pattern.elements.nodes.every((el) => {
if (el.name === undefined) return true;
if (el.dotDotDotToken) return false;
const prop = el.propertyName ?? el.name;
if (
!ts.isIdentifier(prop) &&
!ts.isComputedPropertyName(prop) &&
!ts.isStringLiteralLike(prop) &&
!ts.isNumericLiteral(prop)
)
return false;
const name = patternKeyNameOf(lowerer, prop as ts.PropertyName);
return name !== null && (name === "length" || ARRAY_POSITION_KEY.test(name));
});
}
/** An object pattern of positions and `length` over an array: each
* position is the read the equivalent array pattern makes, and `length`
* the array's length (always present, so its default never runs). */
function lowerArrayPositionObjectPattern(
lowerer: Lowerer,
pattern: ts.ObjectBindingPattern,
srcRef: () => IrExpr,
srcType: IrType & { kind: "array" },
isLet: boolean,
out: IrStmt[],
): void {
for (const el of pattern.elements.nodes) {
if (el.name === undefined) continue;
const name = patternKeyNameOf(lowerer, (el.propertyName ?? el.name) as ts.PropertyName)!;
const value: IrExpr =
name === "length"
? {
kind: "arrIntrinsic",
method: "length",
receiver: srcRef(),
args: [],
type: F64,
loc: locOf(el),
}
: arrayPositionBinding(lowerer, el, srcRef, srcType, Number(name), out);
lowerer.bindPatternTarget(el.name, value, isLet, out);
}
}
/** Decode one non-rest object-assignment property into its static source
* field, destination expression, and optional default initializer. */
function destructuringPropertyOf(
@@ -7435,6 +7547,15 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
if (droppedPureStdlibStatement(lowerer, expr)) {
return { kind: "block", body: [], loc: locOf(expr) };
}
if (
stmtNode !== undefined &&
ts.isExpressionStatement(stmtNode) &&
ts.isBinaryExpression(expr) &&
(expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
expr.operatorToken.kind === ts.SyntaxKind.BarBarToken)
) {
return lowerDiscardedLogical(lowerer, expr);
}
const value = lowerer.lowerExpr(expr);
// A bare unit-literal statement (`undefined;`): a pure no-op in JS,
// and the IR has no bare-unit VALUE outside a union wrap — drop it
@@ -7443,6 +7564,59 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
return { kind: "exprStmt", expr: value, loc: locOf(expr) };
}
/** A discarded `a && b` / `a || b` statement (the minifier's `if`). The
* value form lowers first, exactly as before. Only when it fails on the
* logical's own result type — the operands have no common representation
* (jose's `(!m || m[4] && m[1]) && invalid()` joins a boolean, a string
* and undefined into the checker's `string`) — does the statement lower
* as the control flow it is: `if (a) b;` / `if (!a) b;`, a condition test
* that needs no result value. Diagnostics outside the statement, and
* internal errors, keep the original failure. */
function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt {
const diagsBefore = lowerer.diags.length;
try {
const value = lowerer.lowerExpr(expr);
if (value.kind === "unitLit") return { kind: "block", body: [], loc: locOf(expr) };
return { kind: "exprStmt", expr: value, loc: locOf(expr) };
} catch (e) {
// A probe (a capture buffer is active) keeps its failure: its owner
// picks another lowering, exactly as before.
if (!(e instanceof PoisonError) || lowerer.diagSink !== null) throw e;
const added = lowerer.diags.slice(diagsBefore);
const file = expr.getSourceFile().fileName;
const start = expr.getStart();
const local =
added.length > 0 &&
added.every(
(d) =>
d.code !== "SC9001" &&
d.loc.file === file &&
d.loc.start >= start &&
d.loc.end <= expr.end,
);
if (!local) throw e;
lowerer.diags.length = diagsBefore;
}
const and = expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken;
const cond = lowerer.lowerCondition(expr.left);
if (cond.kind === "boolLit") {
return cond.value === and
? lowerExprStatement(lowerer, expr.right)
: { kind: "block", body: [], loc: locOf(expr) };
}
const right = (): IrStmt[] =>
lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, expr.left, and), () =>
and
? withRuntimeOptionalNarrowed(lowerer, runtimeOptionalTrueIds(lowerer, expr.left), () => [
lowerExprStatement(lowerer, expr.right),
])
: [lowerExprStatement(lowerer, expr.right)],
);
return and
? { kind: "if", cond, then: right(), else_: [], loc: locOf(expr) }
: { kind: "if", cond, then: [], else_: right(), loc: locOf(expr) };
}
/** True for a statement-position expression whose evaluation cannot be
* observed once the value is discarded:
* - a property/element READ (`?.` included) whose receiver IS a
@@ -32,6 +32,7 @@ import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js";
import { resolveRequireRuntime } from "../runtime-resolve.js";
import { lowerKeyObjectCall } from "./builtins/key-object.js";
import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js";
import { hashStreamOf } from "./builtins/hash-stream.js";
import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
@@ -5022,13 +5023,42 @@ export class Lowerer {
return out;
};
const scanPattern = (name: ts.BindingName, init: ts.Expression): boolean => {
if (ts.isObjectBindingPattern(name)) return scanRecordPattern(name, init);
if (ts.isObjectBindingPattern(name)) {
// `const { 0: a, 1: b, length } = parts` reads positions like the
// equivalent array pattern: each may sit past the end.
// Regex match results keep their own pattern lowering.
const arrayType = this.mapTypeOf(this.typeOf(init));
const initType = this.checker.getNonNullableType(this.typeOf(init));
const initSymbol = initType.getAliasSymbol() ?? initType.getSymbol();
const matchResult =
initSymbol !== undefined &&
(initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray");
if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init);
const positions = name.elements.nodes.filter((el) => {
const key = el.propertyName;
return (
key !== undefined && ts.isNumericLiteral(key) && /^(?:0|[1-9]\d{0,8})$/.test(key.text)
);
});
return scanPositions(positions, init, arrayType);
}
if (!ts.isArrayBindingPattern(name)) return false;
const sourceType = this.mapTypeOf(this.typeOf(init));
if (sourceType?.kind !== "array") return false;
return scanPositions(
name.elements.nodes.filter((el): el is ts.BindingElement => !ts.isOmittedExpression(el)),
init,
sourceType,
);
};
const scanPositions = (
elements: readonly ts.BindingElement[],
init: ts.Expression,
sourceType: IrType & { kind: "array" },
): boolean => {
let changed = false;
name.elements.nodes.forEach((el) => {
if (ts.isOmittedExpression(el) || el.name === undefined || el.dotDotDotToken) return;
elements.forEach((el) => {
if (el.name === undefined || el.dotDotDotToken) return;
if (ts.isIdentifier(el.name)) {
const symbol = symbolOf(el.name);
if (symbol && !el.initializer && !optionalSymbols.has(symbol)) {
@@ -7904,7 +7934,11 @@ export class Lowerer {
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
use: "read" | "call",
): IrExpr | null {
return fenceStaticResponseMember(this, access, use) ?? fenceStaticBlobMember(this, access, use);
return (
fenceStaticResponseMember(this, access, use) ??
fenceStaticBlobMember(this, access, use) ??
fenceWebCryptoMember(this, access, use)
);
}
fenceUnsupportedFetchConstructorMember(
@@ -11736,6 +11770,7 @@ export class Lowerer {
lowerStaticReadableStreamReaderCall(this, expr) ??
lowerStaticResponseCall(this, expr) ??
lowerStaticBlobCall(this, expr) ??
lowerWebCryptoCall(this, expr) ??
lowerKeyObjectCall(this, expr) ??
lowerNetAddressCall(this, expr) ??
lowerFetchElementMethodCall(this, expr) ??
@@ -2162,6 +2162,20 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
fns: ["blob.fileNew"],
note: "a Blob with name and lastModified (Date.now() when absent)",
},
{
id: "stdlib.webcrypto.subtle",
kind: "stdlib",
name: "globalThis.crypto.subtle",
fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"],
note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused",
},
{
id: "stdlib.webcrypto.CryptoKey",
kind: "stdlib",
name: "CryptoKey",
fns: ["webcrypto.isCryptoKey"],
note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey",
},
{
id: "stdlib.atomics",
kind: "stdlib",
@@ -125,6 +125,19 @@ export function jsOpenObjectType(
: type;
}
/** The WebCrypto interfaces whose values are checked-dynamic: the crypto
* global, SubtleCrypto, CryptoKey handles and the algorithm dictionaries
* (scr_webcrypto.c). */
export const WEBCRYPTO_DYN_TYPES: ReadonlySet<string> = new Set([
"Crypto",
"SubtleCrypto",
"CryptoKey",
"Algorithm",
"KeyAlgorithm",
"HmacImportParams",
"HmacKeyAlgorithm",
]);
export const FETCH_AMBIENT_TYPES = [
"Response",
"Request",
@@ -2459,6 +2472,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return DYN;
}
// WebCrypto: the crypto global and its subtle object are dynamic
// objects, CryptoKey values are native handles (scr_webcrypto.c), and
// the algorithm dictionaries the methods take and keys report are plain
// checked-dynamic objects.
if (
psym &&
WEBCRYPTO_DYN_TYPES.has(psym.name) &&
checker
.declarationsOf(psym)
.some(
(d) =>
(ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) &&
ctx.isStdlibFile(d.getSourceFile()),
)
) {
return DYN;
}
// node:crypto's KeyObject: secret keys are native handles (scr_lib.c)
// whose getters and methods dispatch through the handle. The JsonWebKey
// that export() returns is a plain checked-dynamic object.
+1
View File
@@ -554,6 +554,7 @@ async function compileExecutableNative(
tls: features.tls,
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
...(onArtifactReady === undefined ? {} : { onArtifactReady }),
...(ffi === null
? {}
@@ -2636,6 +2636,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID },
"crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 },
"crypto.isKeyObject": { argTypes: [DYN], result: BOOL },
/** WebCrypto (scr_webcrypto.c): each method takes its argument vector as
* one dyn array and answers a promise that argument and validation
* failures reject. CryptoKey values are native handles. */
"webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } },
"webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL },
"crypto.randomUUIDv7": { argTypes: [DYN], result: STRING },
"crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 },
"crypto.randomBytesCb": { argTypes: [F64, null], result: VOID },
@@ -49,6 +49,8 @@ export interface RuntimeFeatures {
tls: boolean;
tlsCa: boolean;
cipher: boolean;
/** crypto.subtle importKey/sign/verify/digest: scr_webcrypto.c. */
webcrypto: boolean;
}
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
@@ -407,6 +409,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
tls: false,
tlsCa: false,
cipher: false,
webcrypto: false,
};
const keepGoing = (): boolean => stopAt === undefined || !features[stopAt];
let tables: TypeTables | undefined;
@@ -512,6 +515,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
features.dynAsync = true;
features.inspect = true;
}
// crypto.subtle and CryptoKey: scr_webcrypto.c.
if (fn.startsWith("webcrypto.")) features.webcrypto = true;
} else {
switch (node.kind) {
case "regexLit":
@@ -51,6 +51,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["dns.", "the node:dns surface"],
["tls.", "the node:tls surface"],
["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"],
["webcrypto.", "WebCrypto (crypto.subtle)"],
["fs.watch", "fs.watch"],
["watcher.", "fs.watch"],
["test.", "the node:test surface"],
+1
View File
@@ -100,6 +100,7 @@ async function main(): Promise<number> {
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
tlsCa: false,
},
options.optimization ?? "release",
+1
View File
@@ -155,6 +155,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"tls",
"tlsCa",
"cipher",
"webcrypto",
].every((name) => typeof features[name] === "boolean");
}
@@ -104,6 +104,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
tls: false,
tlsCa: false,
cipher: false,
webcrypto: false,
nativeFetch: false,
netEffective: false,
httpEffective: false,
+14
View File
@@ -5676,6 +5676,20 @@
"code": "SC2020",
"note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)"
},
{
"id": "stdlib.webcrypto.CryptoKey",
"kind": "stdlib",
"name": "CryptoKey",
"status": "static",
"note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey"
},
{
"id": "stdlib.webcrypto.subtle",
"kind": "stdlib",
"name": "globalThis.crypto.subtle",
"status": "static",
"note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused"
},
{
"id": "syntax.async-generators",
"kind": "syntax",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6291-webcrypto-hmac-sign-verify.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6292-webcrypto-hmac-errors.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6293-webcrypto-digest.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6294-webcrypto-js-values.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6295-jose-hs256/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6296-array-position-patterns.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6297-library-runtime-idioms.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6331-jose-root-entry/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6332-fluent-this-declaration/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6333-fluent-this-declaration-js/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6334-discarded-logical-statements.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6335-discarded-logical-statements.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/webcrypto-refusals.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/navigator.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/webcrypto-hmac.ts"
],
"diags": []
}
@@ -113,6 +113,7 @@ const optional = [
["scr_tls_ca.c", "tlsCaEffective"],
["scr_tls.c", "tlsEffective"],
["scr_cipher.c", "cipher"],
["scr_webcrypto.c", "webcrypto"],
["scr_fetch.c", "nativeFetch"],
];
@@ -103,6 +103,7 @@ const OPTIONAL = [
["scr_tls_ca.c", "tlsCaEffective"],
["scr_tls.c", "tlsEffective"],
["scr_cipher.c", "cipher"],
["scr_webcrypto.c", "webcrypto"],
["scr_fetch.c", "nativeFetch"],
];
+18 -1
View File
@@ -1,4 +1,5 @@
/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c). Included by
/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c) and the
* WebCrypto HMAC slice (scr_webcrypto.c). Included by
* scr_runtime.h, which says where new declarations go. */
#ifndef SCR_CRYPTO_H
#define SCR_CRYPTO_H
@@ -193,6 +194,22 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t
unsigned char out[64]);
size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen,
const unsigned char *data, size_t len, unsigned char out[64]);
/* ── WebCrypto (scr_webcrypto.c) ─────────────────────────────────────
* crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and
* digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the
* call's argument vector as one dyn array (borrowed) and answers a +1
* promise that every argument or validation failure rejects with Node's
* TypeError or DOMException; algorithms and formats outside the slice
* reject with a coded SC2020 Error. importKey settles at once with a
* CryptoKey handle; sign and digest fulfill with an ArrayBuffer and
* verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key
* answers `value instanceof CryptoKey`. */
ScrPromise *scr_webcrypto_import_key(ScrDyn *args);
ScrPromise *scr_webcrypto_sign(ScrDyn *args);
ScrPromise *scr_webcrypto_verify(ScrDyn *args);
ScrPromise *scr_webcrypto_digest(ScrDyn *args);
bool scr_webcrypto_is_crypto_key(const ScrDyn *value);
/* The composed `new crypto.X509Certificate(data).fingerprint` read (the
* handle never materializes): the SHA-1 of the DER, uppercase
* colon-separated — PEM or raw-DER input; anything else throws Node's
+1
View File
@@ -137,6 +137,7 @@ typedef enum {
SCR_DYNH_CIPHER = 45, /* node:crypto Cipheriv (scr_cipher.c) */
SCR_DYNH_CONSOLE = 32, /* stored global console value */
SCR_DYNH_CONSOLE_INSTANCE = 38, /* node:console Console instance */
SCR_DYNH_CRYPTO_KEY = 56, /* WebCrypto CryptoKey (scr_webcrypto.c) */
SCR_DYNH_CUSTOM_EVENT = 40, /* ScrEvent with detail — new CustomEvent() */
SCR_DYNH_DATE = 30, /* JavaScript Date with stable native identity */
SCR_DYNH_DECIPHER = 46, /* node:crypto Decipheriv (scr_cipher.c) */
+829
View File
@@ -0,0 +1,829 @@
/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys),
* sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values
* importKey answers.
*
* This unit is only the WebCrypto layer, modeled on Node 24's
* lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument
* conversion, algorithm normalization, the CryptoKey handle, promise
* wrapping and Node's error shapes. The MACs and digests themselves come
* from node:crypto's digest core (scr_crypto_hmac_raw and
* scr_crypto_digest_raw in scr_lib.c), and results settle through the
* deferred crypto queue that node:crypto's callback forms use, so sign,
* verify and digest settle on a later loop turn like Node's thread-pool
* jobs while importKey settles at once.
*
* Every argument and validation failure rejects the returned promise.
* Algorithms Node recognizes but this slice does not implement (RSA,
* ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded
* SC2020 Error instead of silently diverging. The compiler links the unit
* only into programs that call these methods (the webcrypto feature). */
#include "scr_runtime.h"
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ── algorithm registry ───────────────────────────────────────────────
* The names Node 24 registers per operation (util.js's
* kAlgorithmDefinitions with the conditional algorithms its OpenSSL
* build enables), matched case-insensitively. Only HMAC and the SHA-1/
* SHA-2 digests are implemented. */
static const char *const scr_wc_import_names[] = {
"AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB",
"Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305",
"ECDH", "ECDSA", "Ed25519", "Ed448", "HKDF",
"HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65",
"ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256",
"MLKEM768-X25519", "MLKEM1024-P384", "PBKDF2", "RSA-OAEP", "RSA-PSS",
"RSASSA-PKCS1-v1_5", "X25519", "X448",
};
static const char *const scr_wc_sign_names[] = {
"ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256",
"ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5",
};
static const char *const scr_wc_digest_names[] = {
"SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384",
"SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128",
"TurboSHAKE256",
};
#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0]))
/* The implemented hashes: WebCrypto name and node:crypto digest name. */
static const struct {
const char *name;
const char *native;
} scr_wc_hashes[] = {
{"SHA-1", "sha1"},
{"SHA-256", "sha256"},
{"SHA-384", "sha384"},
{"SHA-512", "sha512"},
};
static bool scr_wc_ascii_ieq(const ScrStr *s, const char *name) {
size_t n = strlen(name);
if (s->len != n) return false;
for (size_t i = 0; i < n; i++) {
unsigned char a = (unsigned char)s->data[i], b = (unsigned char)name[i];
if (a >= 'a' && a <= 'z') a = (unsigned char)(a - 32);
if (b >= 'a' && b <= 'z') b = (unsigned char)(b - 32);
if (a != b) return false;
}
return true;
}
/* The canonical registered spelling of `name`, or NULL. */
static const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) {
for (size_t i = 0; i < count; i++)
if (scr_wc_ascii_ieq(name, names[i])) return names[i];
return NULL;
}
static const char *scr_wc_native_hash(const char *name) {
for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_hashes); i++)
if (strcmp(scr_wc_hashes[i].name, name) == 0) return scr_wc_hashes[i].native;
return NULL;
}
/* ── errors ────────────────────────────────────────────────────────── */
static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"};
static void scr_wc_type_error(const char *code, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
static void scr_wc_type_error(const char *code, const char *fmt, ...) {
char message[512];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(message, sizeof message, fmt, ap);
va_end(ap);
if (n < 0) n = 0;
if ((size_t)n >= sizeof message) n = (int)sizeof message - 1;
scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, code);
}
/* An operation or algorithm Node supports that this slice does not. */
static void scr_wc_unsupported(const char *method, const char *what) {
char message[256];
int n = snprintf(message, sizeof message, "crypto.subtle.%s with %s is not supported yet", method,
what);
if ((size_t)n >= sizeof message) n = (int)sizeof message - 1;
scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020");
}
static void scr_wc_unsupported_algorithm(const char *method, const char *name) {
char what[96];
snprintf(what, sizeof what, "the %s algorithm", name);
scr_wc_unsupported(method, what);
}
/* ── Web IDL conversions ──────────────────────────────────────────────
* Each answers NULL/false with the TypeError pending. `prefix` is
* "Failed to execute '<method>' on 'SubtleCrypto'". */
static bool scr_wc_is_object(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_UNDEF:
case SCR_DYN_NULL:
case SCR_DYN_BOOL:
case SCR_DYN_NUM:
case SCR_DYN_STR:
case SCR_DYN_BIGINT:
case SCR_DYN_SYMBOL:
return false;
default:
return true;
}
}
/* A typed reference capsule as its plain view; other values retained. */
static ScrDyn *scr_wc_plain(ScrDyn *v) {
if (v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v);
return scr_dyn_retain(v);
}
/* BufferSource: the bytes of a TypedArray, DataView, Buffer or
* ArrayBuffer as a u8 view (+1). */
static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) {
ScrDyn *plain = scr_wc_plain(value);
if (!plain) return NULL;
ScrBytes *view = NULL;
if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) {
view = scr_bytes_buffer_source(plain);
} else {
scr_wc_type_error("ERR_INVALID_ARG_TYPE",
"%s: %s argument is not instance of ArrayBuffer, Buffer, TypedArray, or "
"DataView.",
prefix, scr_wc_ordinals[index]);
}
scr_dyn_release(plain);
return view;
}
/* DOMString conversion (+1). */
static ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); }
static const char *const scr_wc_key_formats[] = {
"raw", "raw-public", "raw-seed", "raw-secret", "raw-private", "pkcs8", "spki", "jwk",
};
/* KeyFormat: the canonical enum spelling, or NULL. */
static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) {
ScrStr *s = scr_wc_dom_string(value);
if (!s) return NULL;
for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_key_formats); i++) {
if (s->len == strlen(scr_wc_key_formats[i]) &&
memcmp(s->data, scr_wc_key_formats[i], s->len) == 0) {
scr_str_release(s);
return scr_wc_key_formats[i];
}
}
scr_wc_type_error("ERR_INVALID_ARG_VALUE",
"%s: 1st argument '%.*s' is not a valid enum value of type KeyFormat.", prefix,
(int)(s->len > 200 ? 200 : s->len), s->data);
scr_str_release(s);
return NULL;
}
/* Node's canonical usage order; the bit is the index. */
static const char *const scr_wc_usages[] = {
"encrypt", "decrypt", "sign", "verify",
"deriveKey", "deriveBits", "wrapKey", "unwrapKey",
"encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits",
};
enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 };
/* sequence<KeyUsage> over an array: the usage mask (-1 on failure). */
static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) {
ScrDyn *plain = scr_wc_plain(value);
if (!plain) return -1;
if (plain->kind != SCR_DYN_ARR) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s argument cannot be converted to sequence.",
prefix, scr_wc_ordinals[index]);
scr_dyn_release(plain);
return -1;
}
long mask = 0;
double n = scr_dyn_arr_len(plain);
for (double i = 0; i < n; i++) {
ScrDyn *item = scr_dyn_arr_at(plain, i);
ScrStr *s = scr_wc_dom_string(item);
scr_dyn_release(item);
if (!s) {
scr_dyn_release(plain);
return -1;
}
size_t k = 0;
while (k < SCR_WC_COUNT(scr_wc_usages) &&
!(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0))
k++;
if (k == SCR_WC_COUNT(scr_wc_usages)) {
scr_wc_type_error("ERR_INVALID_ARG_VALUE",
"%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.",
prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len),
s->data);
scr_str_release(s);
scr_dyn_release(plain);
return -1;
}
scr_str_release(s);
mask |= 1L << k;
}
scr_dyn_release(plain);
return mask;
}
/* ── algorithm normalization ──────────────────────────────────────────
* normalizeAlgorithm(algorithm, op) for the members this slice reads:
* the canonical name, and for HMAC imports the hash and length. */
typedef struct {
const char *name; /* canonical registered name */
const char *hash; /* HMAC import: the normalized hash name */
bool has_length;
double length;
} ScrWcAlgorithm;
static const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm";
static void scr_wc_missing_member(const char *dict, const char *member) {
scr_wc_type_error("ERR_MISSING_OPTION",
"%s: passed algorithm cannot be converted to '%s' because '%s' is required in "
"'%s'.",
scr_wc_normalize_prefix, dict, member, dict);
}
/* Reads `key` off an algorithm dictionary (+1; undefined when absent). */
static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) {
if (dict->kind != SCR_DYN_OBJ) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *v = scr_dyn_obj_read(dict, key, strlen(key));
return v ? v : scr_dyn_retain(scr_dyn_undefined());
}
/* The name an AlgorithmIdentifier carries (+1), with the Algorithm
* dictionary's missing-name TypeError. `dict` names the dictionary the
* value converts to. */
static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) {
if (!scr_wc_is_object(identifier)) return scr_wc_dom_string(identifier);
ScrDyn *plain = scr_wc_plain(identifier);
if (!plain) return NULL;
ScrDyn *name = scr_wc_member(plain, "name");
scr_dyn_release(plain);
ScrStr *out = NULL;
if (name->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "name");
else out = scr_wc_dom_string(name);
scr_dyn_release(name);
return out;
}
/* The digest a HashAlgorithmIdentifier names: canonical name, or NULL
* with NotSupportedError / the SC2020 refusal pending. */
static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) {
ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm");
if (!name) return NULL;
const char *canonical = scr_wc_lookup(scr_wc_digest_names, SCR_WC_COUNT(scr_wc_digest_names), name);
scr_str_release(name);
if (!canonical) {
scr_throw_domex("NotSupportedError", "Unrecognized algorithm name");
return NULL;
}
if (!scr_wc_native_hash(canonical)) {
scr_wc_unsupported_algorithm(method, canonical);
return NULL;
}
return canonical;
}
/* HmacImportParams over an object: hash (required, a SHA name), then the
* optional [EnforceRange] unsigned long length. */
static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) {
if (!scr_wc_is_object(identifier)) {
scr_wc_missing_member("HmacImportParams", "hash");
return false;
}
ScrDyn *plain = scr_wc_plain(identifier);
if (!plain) return false;
ScrDyn *hash = scr_wc_member(plain, "hash");
bool ok = true;
if (hash->kind == SCR_DYN_UNDEF) {
scr_wc_missing_member("HmacImportParams", "hash");
ok = false;
}
ScrDyn *hash_value = NULL;
if (ok) {
/* HashAlgorithmIdentifier converts to an object or a DOMString, then
* ensureSHA reads the name: a string, or the object's name member. */
ScrStr *hash_name = NULL;
if (scr_wc_is_object(hash)) {
hash_value = scr_dyn_retain(hash);
ScrDyn *hp = scr_wc_plain(hash);
ScrDyn *member = hp ? scr_wc_member(hp, "name") : NULL;
if (hp) scr_dyn_release(hp);
if (!member) ok = false;
else {
if (member->kind == SCR_DYN_STR) hash_name = scr_str_retain(member->v.str);
scr_dyn_release(member);
}
} else {
hash_name = scr_wc_dom_string(hash);
if (!hash_name) ok = false;
else hash_value = scr_dyn_new_str(hash_name);
}
bool sha = hash_name && hash_name->len >= 3 &&
(hash_name->data[0] | 32) == 's' && (hash_name->data[1] | 32) == 'h' &&
(hash_name->data[2] | 32) == 'a';
if (hash_name) scr_str_release(hash_name);
if (ok && !sha) {
scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams");
ok = false;
}
}
if (ok) {
ScrDyn *length = scr_wc_member(plain, "length");
if (length->kind != SCR_DYN_UNDEF) {
double x;
if (!scr_dyn_number_coerce_js(length, &x)) ok = false;
else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE",
"%s: length in passed algorithm is not a finite number.",
scr_wc_normalize_prefix);
ok = false;
} else {
x = trunc(x);
if (x == 0) x = 0;
if (x < 0 || x > 4294967295.0) {
scr_wc_type_error("ERR_OUT_OF_RANGE",
"%s: length in passed algorithm is outside the expected range of 0 to "
"4294967295.",
scr_wc_normalize_prefix);
ok = false;
} else if (x == 0) {
scr_throw_domex("DataError", "HmacImportParams.length cannot be 0");
ok = false;
} else {
out->has_length = true;
out->length = x;
}
}
}
scr_dyn_release(length);
}
if (ok) {
out->hash = scr_wc_digest_algorithm(hash_value, "importKey");
if (!out->hash) ok = false;
}
if (hash_value) scr_dyn_release(hash_value);
scr_dyn_release(hash);
scr_dyn_release(plain);
return ok;
}
/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with
* the error pending. Only HMAC parameters are read; any other recognized
* algorithm is refused by the caller. */
static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count,
bool import, ScrWcAlgorithm *out) {
memset(out, 0, sizeof *out);
ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm");
if (!name) return false;
out->name = scr_wc_lookup(names, count, name);
scr_str_release(name);
if (!out->name) {
scr_throw_domex("NotSupportedError", "Unrecognized algorithm name");
return false;
}
if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out);
return true;
}
/* ── CryptoKey ─────────────────────────────────────────────────────────
* A secret HMAC key: the bytes, the hash, the bit length, the usage mask
* and the extractable flag. The algorithm and usages getters answer the
* same object on every read (Node caches its clones), built on first
* read. The bytes are cleared when the last reference goes. */
typedef struct ScrCryptoKey {
size_t rc;
const char *hash; /* WebCrypto hash name */
double length; /* bits */
unsigned usages;
bool extractable;
ScrDyn *algorithm; /* cached getter results, or NULL */
ScrDyn *usage_list;
size_t len;
unsigned char data[];
} ScrCryptoKey;
static void *scr_crypto_key_retain(void *p) {
((ScrCryptoKey *)p)->rc++;
return p;
}
static void scr_crypto_key_release(void *p) {
ScrCryptoKey *key = p;
if (!key || --key->rc != 0) return;
volatile unsigned char *bytes = key->data;
for (size_t i = 0; i < key->len; i++) bytes[i] = 0;
if (key->algorithm) scr_dyn_release(key->algorithm);
if (key->usage_list) scr_dyn_release(key->usage_list);
free(key);
}
static ScrCryptoKey *scr_crypto_key_of(const ScrDyn *value) {
if (value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_CRYPTO_KEY) return NULL;
return value->v.handle.ptr;
}
bool scr_webcrypto_is_crypto_key(const ScrDyn *value) {
if (value->kind == SCR_DYN_TYPED_REF) return false;
return scr_crypto_key_of(value) != NULL;
}
static ScrDyn *scr_wc_cstr(const char *s) {
ScrStr *str = scr_str_new(s, strlen(s));
ScrDyn *out = scr_dyn_new_str(str);
scr_str_release(str);
return out;
}
static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) {
if (!key->algorithm) {
ScrDyn *hash = scr_dyn_new_obj();
scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash));
ScrDyn *algorithm = scr_dyn_new_obj();
scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC"));
scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length));
scr_dyn_obj_set(algorithm, "hash", 4, hash);
key->algorithm = algorithm;
}
return scr_dyn_retain(key->algorithm);
}
static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) {
if (!key->usage_list) {
ScrDyn *list = scr_dyn_new_arr();
for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++)
if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i]));
key->usage_list = list;
}
return scr_dyn_retain(key->usage_list);
}
static bool scr_wc_name_is(const char *name, size_t len, const char *literal) {
return len == strlen(literal) && memcmp(name, literal, len) == 0;
}
static ScrDyn *scr_crypto_key_get(void *h, const char *name, size_t len) {
ScrCryptoKey *key = h;
if (scr_wc_name_is(name, len, "type")) return scr_wc_cstr("secret");
if (scr_wc_name_is(name, len, "extractable")) return scr_dyn_new_bool(key->extractable);
if (scr_wc_name_is(name, len, "algorithm")) return scr_crypto_key_algorithm(key);
if (scr_wc_name_is(name, len, "usages")) return scr_crypto_key_usages(key);
return NULL;
}
/* The four members are prototype getters without setters: an assignment
* in strict code throws Node's TypeError. */
static bool scr_crypto_key_set(void *h, const char *name, size_t len, const ScrDyn *value) {
(void)h;
(void)value;
if (!scr_wc_name_is(name, len, "type") && !scr_wc_name_is(name, len, "extractable") &&
!scr_wc_name_is(name, len, "algorithm") && !scr_wc_name_is(name, len, "usages"))
return false;
char message[128];
int n = snprintf(message, sizeof message,
"Cannot set property %.*s of #<CryptoKey> which has only a getter", (int)len,
name);
scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n);
return true;
}
static ScrDyn *scr_crypto_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args,
size_t argc, const char *what) {
(void)h;
(void)self;
(void)args;
(void)argc;
if (strcmp(method, "toString") == 0) return scr_wc_cstr("[object CryptoKey]");
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, what);
scr_jb_puts(&b, " is not a function");
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
return NULL;
}
/* key[Symbol.toStringTag] answers "CryptoKey". */
static ScrDyn *scr_crypto_key_symbol_get(void *h, const ScrDyn *key) {
(void)h;
if (key->kind != SCR_DYN_SYMBOL || !key->v.symbol.description) return NULL;
ScrStr *description = key->v.symbol.description(key->v.symbol.value);
bool tag = description && description->len == 18 &&
memcmp(description->data, "Symbol.toStringTag", 18) == 0;
if (description) scr_str_release(description);
return tag ? scr_wc_cstr("CryptoKey") : NULL;
}
/* No inspect_view: Node's custom inspector prints the members one level
* deeper than a plain object would, which the shared handle inspection
* cannot reproduce, so util.inspect refuses a CryptoKey ('not supported
* yet') rather than printing a different shape. */
static const ScrDynHandleOps scr_crypto_key_ops = {
.cls = "CryptoKey",
.retain = scr_crypto_key_retain,
.release = scr_crypto_key_release,
.invoke = scr_crypto_key_invoke,
.get = scr_crypto_key_get,
.set = scr_crypto_key_set,
.json_empty_object = true,
.to_string_tag = "CryptoKey",
.symbol_get = scr_crypto_key_symbol_get,
};
static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash,
double length, unsigned usages, bool extractable) {
static SCR_TL bool installed;
if (!installed) {
installed = true;
scr_dyn_handle_install(SCR_DYNH_CRYPTO_KEY, &scr_crypto_key_ops);
}
ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1));
if (!key) scr_trap("scriptc: out of memory\n");
key->rc = 1;
key->hash = hash;
key->length = length;
key->usages = usages;
key->extractable = extractable;
key->algorithm = NULL;
key->usage_list = NULL;
key->len = len;
if (len) memcpy(key->data, data, len);
ScrDyn *box = scr_dyn_new_handle(key, SCR_DYNH_CRYPTO_KEY);
scr_crypto_key_release(key);
return box;
}
/* ── the operations ───────────────────────────────────────────────────
* Each takes its argument vector as one dyn array (borrowed) and answers
* a +1 promise. */
static ScrPromise *scr_wc_rejected(void) {
ScrPromise *p = scr_promise_new();
scr_promise_reject_pending(p);
return p;
}
/* The argument count check every method starts with (after the receiver
* check the compiler's call shape already guarantees). */
static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) {
size_t argc = (size_t)scr_dyn_arr_len(args);
if (argc >= required) return true;
scr_wc_type_error("ERR_MISSING_ARGS", "%s: %zu arguments required, but only %zu present.", prefix,
required, argc);
return false;
}
typedef struct {
ScrDyn *v[5];
} ScrWcArgs;
static void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) {
for (size_t i = 0; i < n; i++) a->v[i] = scr_dyn_arr_at(args, (double)i);
}
static void scr_wc_args_drop(ScrWcArgs *a, size_t n) {
for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]);
}
/* importKey(format, keyData, algorithm, extractable, keyUsages): Node's
* conversion order, then macImportKey's checks and importKeySync's empty
* usage check. */
static ScrDyn *scr_wc_import(const ScrDyn *args) {
static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'";
if (!scr_wc_required(prefix, args, 5)) return NULL;
ScrWcArgs a;
scr_wc_args_take(&a, args, 5);
ScrDyn *result = NULL;
ScrBytes *data = NULL;
const char *format = scr_wc_key_format(prefix, a.v[0]);
if (!format) goto done;
bool jwk = strcmp(format, "jwk") == 0;
/* The JsonWebKey dictionary is not implemented; that format is refused
* after the remaining arguments convert. */
if (!jwk) {
data = scr_wc_buffer_source(prefix, 1, a.v[1]);
if (!data) goto done;
}
if (!scr_wc_is_object(a.v[2])) {
ScrStr *s = scr_wc_dom_string(a.v[2]);
if (!s) goto done;
scr_str_release(s);
}
bool extractable = scr_dyn_truthy(a.v[3]);
long usages = scr_wc_key_usages(prefix, 4, a.v[4]);
if (usages < 0) goto done;
ScrWcAlgorithm algorithm;
if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true,
&algorithm))
goto done;
if (strcmp(algorithm.name, "HMAC") != 0) {
scr_wc_unsupported_algorithm("importKey", algorithm.name);
goto done;
}
if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) {
scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key");
goto done;
}
if (jwk) {
scr_wc_unsupported("importKey", "the 'jwk' key format");
goto done;
}
if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) {
char message[96];
snprintf(message, sizeof message, "Unable to import HMAC using %s format", format);
scr_throw_domex("NotSupportedError", message);
goto done;
}
{
ScrBytes *view = data;
SCR_BYTES_SNAPSHOT(view);
size_t len = (size_t)scr_bytes_byte_len(view);
if (len == 0) {
scr_throw_domex("DataError", "Zero-length key is not supported");
goto done;
}
double length = (double)len * 8;
unsigned char last = view->data[len - 1];
if (algorithm.has_length) {
double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8);
if (bytes != (double)len) {
scr_throw_domex("DataError", "Invalid key length");
goto done;
}
/* A length that is not a whole number of bytes clears the unused
* low bits of the final byte (truncateToBitLength). */
int extra = (int)fmod(algorithm.length, 8);
if (extra) last = (unsigned char)(last & (0xff << (8 - extra)));
length = algorithm.length;
}
if (usages == 0) {
scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key.");
goto done;
}
result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages,
extractable);
scr_crypto_key_of(result)->data[len - 1] = last;
}
done:
if (data) scr_bytes_release(data);
scr_wc_args_drop(&a, 5);
return result;
}
ScrPromise *scr_webcrypto_import_key(ScrDyn *args) {
ScrDyn *key = scr_wc_import(args);
if (!key) return scr_wc_rejected();
return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL);
}
/* sign(algorithm, key, data) / verify(algorithm, key, signature, data):
* the HMAC over `data`, or NULL with the error pending. *mac_len is the
* digest length. */
static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char mac[64],
size_t *mac_len, ScrBytes **signature) {
const char *prefix = verify ? "Failed to execute 'verify' on 'SubtleCrypto'"
: "Failed to execute 'sign' on 'SubtleCrypto'";
const char *method = verify ? "verify" : "sign";
size_t n = verify ? 4 : 3;
if (!scr_wc_required(prefix, args, n)) return false;
ScrWcArgs a;
scr_wc_args_take(&a, args, n);
bool ok = false;
ScrBytes *data = NULL;
if (!scr_wc_is_object(a.v[0])) {
ScrStr *s = scr_wc_dom_string(a.v[0]);
if (!s) goto done;
scr_str_release(s);
}
ScrCryptoKey *key = scr_crypto_key_of(a.v[1]);
if (!key) {
scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix);
goto done;
}
if (verify) {
*signature = scr_wc_buffer_source(prefix, 2, a.v[2]);
if (!*signature) goto done;
}
data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]);
if (!data) goto done;
ScrWcAlgorithm algorithm;
if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false,
&algorithm))
goto done;
if (strcmp(algorithm.name, "HMAC") != 0) {
scr_wc_unsupported_algorithm(method, algorithm.name);
goto done;
}
if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) {
char message[64];
snprintf(message, sizeof message, "Unable to use this key to %s", method);
scr_throw_domex("InvalidAccessError", message);
goto done;
}
{
ScrBytes *view = data;
SCR_BYTES_SNAPSHOT(view);
*mac_len = scr_crypto_hmac_raw(scr_wc_native_hash(key->hash), key->data, key->len, view->data,
(size_t)scr_bytes_byte_len(view), mac);
}
ok = true;
done:
if (data) scr_bytes_release(data);
scr_wc_args_drop(&a, n);
return ok;
}
ScrPromise *scr_webcrypto_sign(ScrDyn *args) {
unsigned char mac[64];
size_t mac_len = 0;
if (!scr_wc_sign_verify(args, false, mac, &mac_len, NULL)) return scr_wc_rejected();
ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)mac_len);
memcpy(out->data, mac, mac_len);
ScrPromise *p = scr_promise_new();
scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p),
scr_crypto_promise_settle_array_buffer);
return p;
}
/* The verify result's adapter: the deferred value is a dyn boolean. */
static void scr_wc_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *value) {
(void)error;
ScrPromise *p;
memcpy(&p, &cb->caps[0]->slot, sizeof p);
ScrDyn *verified = (ScrDyn *)value;
scr_promise_fulfill_bool(p, scr_dyn_truthy(verified));
scr_dyn_release(verified);
}
ScrPromise *scr_webcrypto_verify(ScrDyn *args) {
unsigned char mac[64];
size_t mac_len = 0;
ScrBytes *signature = NULL;
if (!scr_wc_sign_verify(args, true, mac, &mac_len, &signature)) {
if (signature) scr_bytes_release(signature);
return scr_wc_rejected();
}
bool equal;
{
ScrBytes *view = signature;
SCR_BYTES_SNAPSHOT(view);
/* A signature of another length is simply not valid; equal lengths
* compare in constant time. */
size_t sig_len = (size_t)scr_bytes_byte_len(view);
equal = sig_len == mac_len;
unsigned char diff = 0;
for (size_t i = 0; equal && i < mac_len; i++) diff |= (unsigned char)(view->data[i] ^ mac[i]);
equal = equal && diff == 0;
}
scr_bytes_release(signature);
ScrPromise *p = scr_promise_new();
scr_crypto_defer_dyn(scr_dyn_new_bool(equal), NULL, scr_crypto_promise_callback(p),
scr_wc_settle_verified);
return p;
}
/* digest(algorithm, data). */
ScrPromise *scr_webcrypto_digest(ScrDyn *args) {
static const char prefix[] = "Failed to execute 'digest' on 'SubtleCrypto'";
if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected();
ScrWcArgs a;
scr_wc_args_take(&a, args, 2);
ScrBytes *data = NULL;
ScrBytes *out = NULL;
if (!scr_wc_is_object(a.v[0])) {
ScrStr *s = scr_wc_dom_string(a.v[0]);
if (!s) goto done;
scr_str_release(s);
}
data = scr_wc_buffer_source(prefix, 1, a.v[1]);
if (!data) goto done;
const char *hash = scr_wc_digest_algorithm(a.v[0], "digest");
if (!hash) goto done;
{
ScrBytes *view = data;
SCR_BYTES_SNAPSHOT(view);
unsigned char digest[64];
size_t len = scr_crypto_digest_raw(scr_wc_native_hash(hash), view->data,
(size_t)scr_bytes_byte_len(view), digest);
out = scr_bytes_new(SCR_BYTES_U8, (double)len);
memcpy(out->data, digest, len);
}
done:
if (data) scr_bytes_release(data);
scr_wc_args_drop(&a, 2);
if (!out) return scr_wc_rejected();
ScrPromise *p = scr_promise_new();
scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p),
scr_crypto_promise_settle_array_buffer);
return p;
}
@@ -0,0 +1,94 @@
// WebCrypto HMAC through the crypto global: importKey('raw') for SHA-1 and
// SHA-2 keys, sign and verify (a wrong or short signature answers false),
// the CryptoKey members, instanceof CryptoKey, the key sources importKey
// and sign accept, and that results settle asynchronously.
const encoder = new TextEncoder();
const secret = encoder.encode("scriptc-webcrypto-secret");
const data = encoder.encode("header.payload");
function hex(buffer: ArrayBuffer): string {
return Buffer.from(buffer).toString("hex");
}
const key = await crypto.subtle.importKey(
"raw",
secret,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"],
);
console.log(key.type, key.extractable, key.usages.join(","));
const algorithm = key.algorithm as HmacKeyAlgorithm;
console.log(algorithm.name, algorithm.length, algorithm.hash.name);
console.log(key instanceof CryptoKey, key.algorithm === key.algorithm, key.usages === key.usages);
const signature = await crypto.subtle.sign("HMAC", key, data);
console.log(signature.byteLength, hex(signature));
console.log(await crypto.subtle.verify("HMAC", key, signature, data));
console.log(await crypto.subtle.verify({ name: "HMAC" }, key, signature, encoder.encode("header.payloaD")));
const tampered = new Uint8Array(signature);
tampered[0] ^= 1;
console.log(await crypto.subtle.verify("HMAC", key, tampered, data));
console.log(await crypto.subtle.verify("HMAC", key, signature.slice(0, 16), data));
console.log(await crypto.subtle.verify("HMAC", key, new Uint8Array(0), data));
for (const hash of ["SHA-1", "SHA-384", "SHA-512"]) {
const k = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash }, true, ["sign"]);
const a = k.algorithm as HmacKeyAlgorithm;
const mac = await crypto.subtle.sign({ name: "hmac" }, k, data);
console.log(hash, a.hash.name, a.length, k.extractable, k.usages.join(","), mac.byteLength, hex(mac));
}
// Names normalize case-insensitively; usages come back in Node's order.
const lower = await crypto.subtle.importKey(
"raw",
secret,
{ name: "hmac", hash: { name: "sha-256" } },
true,
["verify", "sign"],
);
const lowerAlgorithm = lower.algorithm as HmacKeyAlgorithm;
console.log(lowerAlgorithm.name, lowerAlgorithm.hash.name, lower.usages.join(","));
// An explicit length in bits must match the key bytes.
const sized = await crypto.subtle.importKey(
"raw",
secret,
{ name: "HMAC", hash: "SHA-256", length: secret.length * 8 },
false,
["verify"],
);
console.log((sized.algorithm as HmacKeyAlgorithm).length, sized.usages.join(","));
// Key and data sources: ArrayBuffer, Buffer, DataView and subarray views.
const fromBuffer = await crypto.subtle.importKey(
"raw",
secret.slice().buffer,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
console.log(hex(await crypto.subtle.sign("HMAC", fromBuffer, Buffer.from("header.payload"))) === hex(signature));
const padded = encoder.encode("xxheader.payloadyy");
console.log(hex(await crypto.subtle.sign("HMAC", key, padded.subarray(2, 16))) === hex(signature));
console.log(hex(await crypto.subtle.sign("HMAC", key, new DataView(data.buffer))) === hex(signature));
// The data is read when sign is called.
const mutable = encoder.encode("header.payload");
const pending = crypto.subtle.sign("HMAC", key, mutable);
mutable[0] = 0;
console.log(hex(await pending) === hex(signature));
// Results settle after the synchronous code that started them.
const order: string[] = [];
const signed = crypto.subtle.sign("HMAC", key, data).then(() => {
order.push("sign");
});
const imported = crypto.subtle
.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"])
.then(() => {
order.push("importKey");
});
order.push("sync");
await Promise.all([signed, imported]);
console.log(order.join(","));
@@ -0,0 +1,83 @@
// WebCrypto HMAC failures reject with Node's TypeErrors (argument count,
// enum values, buffer sources, CryptoKey arguments, required members) and
// DOMExceptions (usages, key length, algorithm names, key access).
const encoder = new TextEncoder();
const secret = encoder.encode("scriptc-webcrypto-secret");
const data = encoder.encode("payload");
function report(label: string, error: Error): void {
if (error instanceof DOMException) {
const dom = error as DOMException;
console.log(label, dom.name, dom.code, dom.message, "DOMException");
} else {
const coded = error as Error & { code?: string };
console.log(label, coded.name, coded.code, coded.message);
}
}
async function attempt(label: string, run: () => Promise<unknown>): Promise<void> {
try {
await run();
console.log(label, "resolved");
} catch (e) {
report(label, e as Error);
}
}
async function attemptVerify(label: string, run: () => Promise<boolean>): Promise<void> {
try {
console.log(label, "resolved", await run());
} catch (e) {
report(label, e as Error);
}
}
const signOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const verifyOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-384" }, false, ["verify"]);
await attempt("usage", () =>
crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "encrypt"]),
);
await attempt("empty usages", () =>
crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, []),
);
await attempt("zero-length key", () =>
crypto.subtle.importKey("raw", new Uint8Array(0), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]),
);
await attempt("length mismatch", () =>
crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 64 }, false, ["sign"]),
);
await attempt("length zero", () =>
crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 0 }, false, ["sign"]),
);
await attempt("missing hash", () =>
crypto.subtle.importKey("raw", secret, "HMAC", false, ["sign"]),
);
await attempt("md5 hash", () =>
crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "MD5" }, false, ["sign"]),
);
await attempt("unknown sha", () =>
crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-999" }, false, ["sign"]),
);
await attempt("unknown algorithm", () =>
crypto.subtle.importKey("raw", secret, { name: "NOPE", hash: "SHA-256" }, false, ["sign"]),
);
await attempt("pkcs8 format", () =>
crypto.subtle.importKey("pkcs8", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]),
);
await attempt("string key", () =>
crypto.subtle.importKey("raw", "secret" as unknown as BufferSource, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]),
);
await attempt("sign with verify key", () => crypto.subtle.sign("HMAC", verifyOnly, data));
await attemptVerify("verify with sign key", () => crypto.subtle.verify("HMAC", signOnly, new Uint8Array(32), data));
await attempt("sign without key", () =>
crypto.subtle.sign("HMAC", secret as unknown as CryptoKey, data),
);
await attempt("sign unknown algorithm", () => crypto.subtle.sign("HMAC-SHA256", signOnly, data));
await attempt("sign string data", () =>
crypto.subtle.sign("HMAC", signOnly, "payload" as unknown as BufferSource),
);
await attemptVerify("verify string signature", () =>
crypto.subtle.verify("HMAC", verifyOnly, "sig" as unknown as BufferSource, data),
);
+35
View File
@@ -0,0 +1,35 @@
// crypto.subtle.digest over the SHA-1 and SHA-2 hashes: string and object
// algorithm identifiers, typed-array, Buffer and ArrayBuffer input, and
// Node's rejections for unknown names and non-buffer data.
const encoder = new TextEncoder();
const input = encoder.encode("scriptc digest input");
function hex(buffer: ArrayBuffer): string {
return Buffer.from(buffer).toString("hex");
}
for (const name of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) {
const digest = await crypto.subtle.digest(name, input);
console.log(name, digest.byteLength, hex(digest));
}
console.log(hex(await crypto.subtle.digest({ name: "sha-256" }, Buffer.from("scriptc digest input"))));
console.log(hex(await crypto.subtle.digest("SHA-256", input.slice().buffer)));
console.log(hex(await crypto.subtle.digest("SHA-256", new Uint8Array(0))));
async function attempt(label: string, run: () => Promise<unknown>): Promise<void> {
try {
await run();
console.log(label, "resolved");
} catch (e) {
if (e instanceof DOMException) {
const error = e as DOMException;
console.log(label, error.name, error.code, error.message, "DOMException");
} else {
const error = e as Error & { code?: string };
console.log(label, error.name, error.code, error.message);
}
}
}
await attempt("md5", () => crypto.subtle.digest("MD5", input));
await attempt("string data", () => crypto.subtle.digest("SHA-256", "text" as unknown as BufferSource));
+52
View File
@@ -0,0 +1,52 @@
// WebCrypto from JavaScript: keys and algorithms carried as dynamic
// values, the CryptoKey brand checks libraries use (Symbol.toStringTag,
// Object.prototype.toString and instanceof), JSON.stringify of a key, and
// rejections for missing arguments and non-key values.
const encoder = new TextEncoder();
const secret = encoder.encode("dynamic-secret");
const table = {
HS256: { name: "HMAC", hash: "SHA-256" },
HS512: { name: "HMAC", hash: "SHA-512" },
};
async function importFor(alg, usage) {
const params = table[alg];
return crypto.subtle.importKey("raw", secret, params, false, [usage]);
}
const key = await importFor("HS256", "sign");
console.log(key[Symbol.toStringTag], Object.prototype.toString.call(key), String(key));
console.log(key instanceof CryptoKey, secret instanceof CryptoKey, {} instanceof CryptoKey);
console.log(JSON.stringify(key), typeof key, key.type, key.algorithm.hash.name, key.usages.includes("sign"));
const sig = new Uint8Array(await crypto.subtle.sign(table.HS256, key, encoder.encode("a.b")));
console.log(sig.length, Buffer.from(sig).toString("base64url"));
const verifier = await importFor("HS256", "verify");
let verified = false;
try {
verified = await crypto.subtle.verify(table.HS256, verifier, sig, encoder.encode("a.b"));
} catch {}
console.log(verified);
const other = await importFor("HS512", "verify");
console.log(await crypto.subtle.verify(table.HS512, other, sig, encoder.encode("a.b")));
async function attempt(label, run) {
try {
await run();
console.log(label, "resolved");
} catch (e) {
console.log(label, e.name, e.code, e.message);
}
}
await attempt("too few", () => crypto.subtle.sign("HMAC", key));
await attempt("import too few", () => crypto.subtle.importKey("raw", secret, table.HS256, false));
await attempt("verify too few", () => crypto.subtle.verify("HMAC", key, sig));
await attempt("digest too few", () => crypto.subtle.digest("SHA-256"));
const lookalike = JSON.parse('{"type":"secret","usages":["sign"]}');
await attempt("not a key", () => crypto.subtle.sign("HMAC", lookalike, secret));
await attempt("bad format", () => crypto.subtle.importKey("nope", secret, table.HS256, false, ["sign"]));
await attempt("bad usage", () => crypto.subtle.importKey("raw", secret, table.HS256, false, ["bogus"]));
await attempt("usages not a sequence", () => crypto.subtle.importKey("raw", secret, table.HS256, false, "sign"));
await attempt("algorithm without name", () => crypto.subtle.sign({ hash: "SHA-256" }, key, secret));
await attempt("hash object without name", () =>
crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: {} }, false, ["sign"]),
);
+103
View File
@@ -0,0 +1,103 @@
// The real jose@6.2.12 HS256 path: SignJWT and jwtVerify from jose's
// public jose/jwt/sign, jose/jwt/verify and jose/errors entry points, over
// crypto.subtle importKey, sign and verify. node_modules/jose vendors the
// dist/webapi files those entry points load and their declarations,
// unmodified (MIT, LICENSE.md), with a package.json that exports only
// those entry points.
import { SignJWT } from "jose/jwt/sign";
import { jwtVerify } from "jose/jwt/verify";
import * as errors from "jose/errors";
const encoder = new TextEncoder();
const secret = encoder.encode("scriptc-jose-hs256-shared-secret");
const now = new Date(1_800_000_000_000);
const iat = 1_799_999_000;
async function sign(claims, alg = "HS256", key = secret) {
const jwt = new SignJWT(claims);
jwt.setProtectedHeader({ alg, typ: "JWT" });
jwt.setIssuedAt(iat);
jwt.setIssuer("urn:scriptc:issuer");
jwt.setAudience("urn:scriptc:audience");
jwt.setSubject("alice");
return jwt.sign(key);
}
async function verify(label, token, options = {}, key = secret) {
try {
const { payload, protectedHeader } = await jwtVerify(token, key, {
algorithms: ["HS256"],
currentDate: now,
...options,
});
console.log(label, "ok", JSON.stringify(protectedHeader), JSON.stringify(payload));
} catch (e) {
console.log(label, e.code, e.message, e instanceof errors.JOSEError, e.name, e.claim, e.reason);
}
}
const token = await sign({ role: "admin", exp: iat + 3600 });
console.log(token);
await verify("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" });
await verify("bytes token", encoder.encode(token));
await verify("wrong secret", token, {}, encoder.encode("another-secret"));
const [header, body, signature] = token.split(".");
const forged = Buffer.from(JSON.stringify({ role: "root", sub: "mallory" })).toString("base64url");
await verify("tampered payload", `${header}.${forged}.${signature}`);
await verify("tampered signature", `${header}.${body}.${signature.slice(0, -2)}AA`);
await verify("malformed", "not-a-jwt");
await verify("bad signature encoding", `${header}.${body}.***`);
await verify("expired", token, { currentDate: new Date((iat + 7200) * 1000) });
await verify("clock tolerance", token, { currentDate: new Date((iat + 3610) * 1000), clockTolerance: 30 });
await verify("not yet valid", await sign({ nbf: iat + 600 }));
await verify("issuer", token, { issuer: "urn:other" });
await verify("audience", token, { audience: "urn:other" });
await verify("required claim", token, { requiredClaims: ["jti"] });
await verify("alg not allowed", token, { algorithms: ["HS512"] });
await verify("max token age", token, { maxTokenAge: 600 });
const hs384 = await sign({ scope: "read" }, "HS384");
await verify("hs384", hs384, { algorithms: ["HS384"] });
const hs512 = await sign({ scope: "write" }, "HS512");
await verify("hs512", hs512, { algorithms: ["HS256", "HS512"] });
await verify("hs512 as hs256 only", hs512);
await verify("none", `${Buffer.from('{"alg":"none"}').toString("base64url")}.${body}.`, { algorithms: ["none"] });
// The same key imported once and reused as a CryptoKey.
const cryptoKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
await verify("crypto key", token, {}, cryptoKey);
const signingKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-512" }, false, ["sign"]);
await verify("wrong key hash", token, {}, signingKey);
// Chained builder calls: jose's declarations type every setter's result as
// `this`, implemented by the JWTClaimsBuilder base SignJWT extends.
const chained = await new SignJWT({ role: "reader" })
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuedAt(iat)
.setIssuer("urn:scriptc:issuer")
.setAudience(["urn:scriptc:audience", "urn:other"])
.setSubject("bob")
.setJti("jti-2")
.setNotBefore(iat - 60)
.setExpirationTime(iat + 60)
.sign(secret);
console.log(chained);
await verify("chained", chained, { audience: "urn:scriptc:audience" });
// String durations: jose's secs() parses them.
for (const maxTokenAge of ["10 minutes", "1 hour", "1.5h", "2 days", "+20m", "1 week", "1y", "900 secs", "3 HOURS", "2h ago", "-2h ago", "10 fortnights", ""]) {
await verify(`max token age ${JSON.stringify(maxTokenAge)}`, token, { maxTokenAge });
}
for (const clockTolerance of ["30s", "5 sec", "1 min from now", "1 min ago"]) {
await verify(`clock tolerance ${clockTolerance}`, token, { currentDate: new Date((iat + 3620) * 1000), clockTolerance });
}
const relative = new SignJWT({ role: "reader" });
relative.setProtectedHeader({ alg: "HS256" }).setIssuedAt("-1 minute").setExpirationTime("2h");
const claims = JSON.parse(Buffer.from((await relative.sign(secret)).split(".")[1], "base64url").toString());
const clock = Math.floor(Date.now() / 1000);
console.log(Math.abs(claims.exp - clock - 7200) <= 2, Math.abs(claims.iat - clock + 60) <= 2);
try {
new SignJWT({}).setExpirationTime("soon");
} catch (e) {
console.log(e.name, e.message);
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Filip Skokan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,21 @@
import type * as t from '../types.d.ts';
/** @param payload Initial JWT Claims Set. Defaults to an empty object. */
declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT;
/** Builds and signs Compact JWS-formatted JSON Web Tokens. */
export declare class SignJWT extends SignJWT_base {
#private;
/**
* Sets the JWS Protected Header. May only be called once.
*
* @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property.
*/
setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this;
/**
* Signs and returns the JWT.
*
* @param key Private key or shared secret to sign the JWT with. See
* {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
*/
sign(key: t.KeyInput, options?: t.SignOptions): Promise<string>;
}
export {};
@@ -0,0 +1,35 @@
import type * as t from '../types.d.ts';
/** JWS verification and JWT Claims Set validation options. */
export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions {
}
/**
* Resolves a JWT verification key. No token components have been authenticated when this function
* is called.
*/
export interface JWTVerifyGetKey<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array> extends t.GetKeyFunction<t.CompactJWSHeaderParameters, t.FlattenedJWSInput, KeyType | t.KeyObject | t.JWK> {
}
/**
* Verifies a Compact JWS-formatted JWT and validates its Claims Set.
*
* @param key Public key or shared secret to verify the JWT with. See
* {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
* @param options JWT Verification and JWT Claims Set validation options.
*/
export declare function jwtVerify<PayloadType = t.JWTPayload>(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise<t.JWTVerifyResult<PayloadType>>;
/**
* Verifies the JWT signature and claims, returning the resolved key.
*
* @param getKey Function resolving a public key or shared secret to verify the JWT with. See
* {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
* @param options JWT Verification and JWT Claims Set validation options.
*/
export declare function jwtVerify<PayloadType = t.JWTPayload, KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array>(jwt: string | Uint8Array, getKey: JWTVerifyGetKey<KeyType>, options?: JWTVerifyOptions): Promise<t.JWTVerifyResult<PayloadType> & t.ResolvedKey<KeyType>>;
/**
* Accepts a key or key resolver. Use this overload when forwarding either form; the result includes
* `key` only when a resolver was used.
*
* @param key Key, or function resolving a key, to verify the JWT with. See
* {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
* @param options JWT Verification and JWT Claims Set validation options.
*/
export declare function jwtVerify<PayloadType = t.JWTPayload>(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise<t.JWTVerifyResult<PayloadType> & Partial<t.ResolvedKey>>;
+736
View File
@@ -0,0 +1,736 @@
/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */
export type JWSAlgorithm =
| 'HS256'
| 'HS384'
| 'HS512'
| 'RS256'
| 'RS384'
| 'RS512'
| 'PS256'
| 'PS384'
| 'PS512'
| 'ES256'
| 'ES384'
| 'ES512'
| 'EdDSA'
| 'Ed25519'
| 'ML-DSA-44'
| 'ML-DSA-65'
| 'ML-DSA-87'
| (string & {})
/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */
export type JWEKeyManagementAlgorithm =
| 'dir'
| 'A128KW'
| 'A192KW'
| 'A256KW'
| 'A128GCMKW'
| 'A192GCMKW'
| 'A256GCMKW'
| 'ECDH-ES'
| 'ECDH-ES+A128KW'
| 'ECDH-ES+A192KW'
| 'ECDH-ES+A256KW'
| 'RSA-OAEP'
| 'RSA-OAEP-256'
| 'RSA-OAEP-384'
| 'RSA-OAEP-512'
| 'PBES2-HS256+A128KW'
| 'PBES2-HS384+A192KW'
| 'PBES2-HS512+A256KW'
| (string & {})
/**
* Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the
* runtime.
*/
export type JWEContentEncryptionAlgorithm =
| 'A128CBC-HS256'
| 'A192CBC-HS384'
| 'A256CBC-HS512'
| 'A128GCM'
| 'A192GCM'
| 'A256GCM'
| (string & {})
/** JWK "kty" (Key Type) Parameter values supported by this module. */
export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {})
/**
* Generic JSON Web Key Parameters.
*
* > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index
* > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`.
*/
export type JWKParameters = {
/** JWK "kty" (Key Type) Parameter */
kty?: JWKKeyType
/** JWK "alg" (Algorithm) Parameter */
alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm
/** Permitted key operations. */
key_ops?: string[]
/** Whether the key may be exported. */
ext?: boolean
/** JWK "use" (Public Key Use) Parameter */
use?: 'sig' | 'enc' | (string & {})
/** X.509 certificate chain. */
x5c?: string[]
/** X.509 certificate SHA-1 thumbprint. */
x5t?: string
/** X.509 certificate SHA-256 thumbprint. */
'x5t#S256'?: string
/** X.509 certificate URL. */
x5u?: string
/** JWK "kid" (Key ID) Parameter */
kid?: string
}
/** Convenience interface for public OKP JSON Web Keys. */
export interface JWK_OKP_Public extends JWKParameters {
/** Key pair subtype. */
crv: string
/** Public key. */
x: string
}
/** Convenience interface for private OKP JSON Web Keys. */
export interface JWK_OKP_Private extends JWK_OKP_Public {
/** Private key. */
d: string
}
/** Convenience interface for public AKP JSON Web Keys. */
export interface JWK_AKP_Public extends JWKParameters {
/** JWK "alg" (Algorithm) Parameter */
alg: string
/** AKP JWK "pub" (The Public key) Parameter */
pub: string
}
/** Convenience interface for private AKP JSON Web Keys. */
export interface JWK_AKP_Private extends JWK_AKP_Public {
/** AKP JWK "priv" (The Private Key) Parameter */
priv: string
}
/** Convenience interface for public EC JSON Web Keys. */
export interface JWK_EC_Public extends JWKParameters {
/** Curve. */
crv: string
/** Public key X coordinate. */
x: string
/** Public key Y coordinate. */
y: string
}
/** Convenience interface for private EC JSON Web Keys. */
export interface JWK_EC_Private extends JWK_EC_Public {
/** Private key. */
d: string
}
/** Convenience interface for public RSA JSON Web Keys. */
export interface JWK_RSA_Public extends JWKParameters {
/** Public exponent. */
e: string
/** Modulus. */
n: string
}
/** Convenience interface for private RSA JSON Web Keys. */
export interface JWK_RSA_Private extends JWK_RSA_Public {
/** Private exponent. */
d: string
/** First factor CRT exponent. */
dp: string
/** Second factor CRT exponent. */
dq: string
/** First prime factor. */
p: string
/** Second prime factor. */
q: string
/** First CRT coefficient. */
qi: string
}
/** Convenience interface for "oct" JSON Web Keys. */
export interface JWK_oct extends JWKParameters {
/** Symmetric key value. */
k: string
}
/**
* JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP",
* and "oct" key types are supported.
*
* > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index
* > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the
* > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented
* > in one place.
*/
export type JWK = {
/** JWK "kty" (Key Type) Parameter */
kty?: JWKKeyType
/** JWK "alg" (Algorithm) Parameter */
alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm
/** Permitted key operations. */
key_ops?: string[]
/** Whether the key may be exported. */
ext?: boolean
/** JWK "use" (Public Key Use) Parameter */
use?: 'sig' | 'enc' | (string & {})
/** X.509 certificate chain. */
x5c?: string[]
/** X.509 certificate SHA-1 thumbprint. */
x5t?: string
/** X.509 certificate SHA-256 thumbprint. */
'x5t#S256'?: string
/** X.509 certificate URL. */
x5u?: string
/** JWK "kid" (Key ID) Parameter */
kid?: string
/** EC curve or OKP key pair subtype. */
crv?: string
/** Private RSA exponent, EC key, or OKP key. */
d?: string
/** RSA first factor CRT exponent. */
dp?: string
/** RSA second factor CRT exponent. */
dq?: string
/** RSA public exponent. */
e?: string
/** Symmetric key value. */
k?: string
/** RSA modulus. */
n?: string
/** RSA first prime factor. */
p?: string
/** RSA second prime factor. */
q?: string
/** RSA first CRT coefficient. */
qi?: string
/** EC public key X coordinate or OKP public key. */
x?: string
/** EC public key Y coordinate. */
y?: string
/** AKP JWK "pub" (Public Key) Parameter */
pub?: string
/** AKP JWK "priv" (Private key) Parameter */
priv?: string
/** Additional RSA prime factors. */
oth?: Array<{
/** Factor CRT exponent. */
d?: string
/** Prime factor. */
r?: string
/** Factor CRT coefficient. */
t?: string
}>
}
/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */
// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`.
export type AnyJWK =
| (JWK_EC_Private & { kty: 'EC' })
| (JWK_EC_Public & { kty: 'EC' })
| (JWK_RSA_Private & { kty: 'RSA' })
| (JWK_RSA_Public & { kty: 'RSA' })
| (JWK_OKP_Private & { kty: 'OKP' })
| (JWK_OKP_Public & { kty: 'OKP' })
| (JWK_AKP_Private & { kty: 'AKP' })
| (JWK_AKP_Public & { kty: 'AKP' })
| (JWK_oct & { kty: 'oct' })
/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */
export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array
/** Dynamic key resolver for consuming operations. */
export interface GetKeyFunction<IProtectedHeader, IToken, KeyTypes extends KeyInput = KeyInput> {
/**
* Resolves a key for an unverified token. Throw if no suitable key can be resolved.
*
* @param token The consumed JWE or JWS token; none of its components have been verified.
*/
(protectedHeader: IProtectedHeader, token: IToken): Promise<KeyTypes> | KeyTypes
}
/** Flattened JWS verification input. */
export interface FlattenedJWSInput {
/** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */
header?: JWSHeaderParameters
/** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */
payload: string | Uint8Array
/** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */
protected?: string
/** Base64url-encoded signature or MAC. */
signature: string
}
/** General JWS verification input. */
export interface GeneralJWSInput {
/** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */
payload: string | Uint8Array
/**
* The "signatures" member value MUST be an array of JSON objects. Each object represents a
* signature or MAC over the JWS Payload and the JWS Protected Header.
*/
signatures: Omit<FlattenedJWSInput, 'payload'>[]
}
/**
* Flattened JWS JSON Serialization token. The payload is an empty string when the
* {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used.
*/
export interface FlattenedJWS extends Partial<FlattenedJWSInput> {
payload: string
signature: string
}
/**
* General JWS JSON Serialization token. The payload is an empty string when the
* {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used.
*/
export interface GeneralJWS {
payload: string
signatures: Omit<FlattenedJWSInput, 'payload'>[]
}
/** Header Parameters common to JWE and JWS. */
export interface JoseHeaderParameters {
/** "kid" (Key ID) Header Parameter */
kid?: string
/** X.509 certificate SHA-1 thumbprint. */
x5t?: string
/** X.509 certificate chain. */
x5c?: string[]
/** X.509 certificate URL. */
x5u?: string
/** JWK Set URL. */
jku?: string
/** Public JWK only; private and symmetric key parameters are not permitted. */
jwk?: Omit<JWK, 'd' | 'dp' | 'dq' | 'k' | 'p' | 'q' | 'qi' | 'priv' | 'oth'>
/** "typ" (Type) Header Parameter */
typ?: string
/** Content type. */
cty?: string
}
/** Recognized JWS Header Parameters; additional members may also be present. */
export interface JWSHeaderParameters extends JoseHeaderParameters {
/** JWS "alg" (Algorithm) Header Parameter */
alg?: JWSAlgorithm
/**
* Controls payload encoding and the JWS signing input as defined by
* {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in
* `crit` to use an unencoded payload.
*/
b64?: boolean
/** Extension parameters that must be recognized. */
crit?: string[]
/** Any other JWS Header member. */
[propName: string]: unknown
}
/** Recognized JWE Key Management-related Header Parameters. */
export interface JWEKeyManagementHeaderParameters {
/** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */
apu?: Uint8Array
/** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */
apv?: Uint8Array
/** PBES2 PBKDF2 iteration count, added to the JOSE header. */
p2c?: number
/** @deprecated For testing and vector validation only. */
p2s?: Uint8Array
/** @deprecated For testing and vector validation only. */
iv?: Uint8Array
/** @deprecated For testing and vector validation only. */
epk?: CryptoKey | KeyObject
}
/** Flattened JWE JSON Serialization token. */
export interface FlattenedJWE {
/**
* Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit
* when empty.
*/
aad?: string
/** Base64url-encoded ciphertext. */
ciphertext: string
/** Base64url-encoded encrypted key. Omit when empty. */
encrypted_key?: string
/**
* JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when
* empty.
*/
header?: JWEHeaderParameters
/** Base64url-encoded initialization vector. Omit when empty. */
iv?: string
/** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */
protected?: string
/** Base64url-encoded authentication tag. Omit when empty. */
tag?: string
/** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */
unprotected?: JWEHeaderParameters
}
/** General JWE JSON Serialization token. */
export interface GeneralJWE extends Omit<FlattenedJWE, 'encrypted_key' | 'header'> {
recipients: Pick<FlattenedJWE, 'encrypted_key' | 'header'>[]
}
/** Recognized JWE Header Parameters; additional members may also be present. */
export interface JWEHeaderParameters extends JoseHeaderParameters {
/** JWE "alg" (Algorithm) Header Parameter */
alg?: JWEKeyManagementAlgorithm
/** JWE "enc" (Encryption Algorithm) Header Parameter */
enc?: JWEContentEncryptionAlgorithm
/** Extension parameters that must be recognized. */
crit?: string[]
/**
* JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's
* `CompressionStream` / `DecompressionStream` APIs.
*/
zip?: 'DEF' | (string & {})
/** Any other JWE Header member. */
[propName: string]: unknown
}
/** Shared "crit" option for signing, verification, encryption, and decryption. */
export interface CritOption {
/**
* Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require
* integrity protection, or `false` when protection is optional. The JWS `b64` extension is always
* recognized and processed.
*
* > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence
* > is not required by this option. You must check their presence and process them according to the
* > profile's validation steps after the operation succeeds.
*/
crit?: {
[propName: string]: boolean
}
}
/** JWE Decryption options. */
export interface DecryptOptions extends CritOption {
/**
* Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable
* to the key or secret except PBES2, which must be explicitly allowed.
*/
keyManagementAlgorithms?: JWEKeyManagementAlgorithm[]
/**
* Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms
* applicable to the key or secret.
*/
contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[]
/**
* Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their
* computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to
* disable the limit.
*/
maxPBES2Count?: number
/**
* Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs,
* and `Infinity` disables the limit. Other values must be positive safe integers.
*/
maxDecompressedLength?: number
}
/** JWE Encryption options. */
export interface EncryptOptions extends CritOption {}
/** JWT Claims Set verification options. */
export interface JWTClaimVerificationOptions {
/** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */
audience?: string | string[]
/**
* Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf"
* (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when
* {@link maxTokenAge} is set.
*/
clockTolerance?: string | number
/** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */
issuer?: string | string[]
/**
* Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2
* hours"). Requires the claim to be present.
*/
maxTokenAge?: string | number
/** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */
subject?: string
/** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */
typ?: string
/** Date for NumericDate comparisons. Defaults to `new Date()`. */
currentDate?: Date
/**
* Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience},
* {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and
* "iat", respectively.
*/
requiredClaims?: string[]
}
/** JWS Verification options. */
export interface VerifyOptions extends CritOption {
/**
* Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable
* to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted.
*/
algorithms?: JWSAlgorithm[]
}
/** JWS Signing options. */
export interface SignOptions extends CritOption {}
/** Recognized JWT Claims Set members; additional members may also be present. */
export interface JWTPayload {
/** JWT Issuer */
iss?: string
/** JWT Subject */
sub?: string
/** JWT Audience */
aud?: string | string[]
/** JWT ID */
jti?: string
/** Not valid before this Unix timestamp in seconds. */
nbf?: number
/** Expiration Unix timestamp in seconds. */
exp?: number
/** Issued-at Unix timestamp in seconds. */
iat?: number
/** Any other JWT Claim Set member. */
[propName: string]: unknown
}
/** Flattened JWE JSON Serialization decryption result. */
export interface FlattenedDecryptResult {
/** JWE Additional Authenticated Data, integrity protected but not encrypted. */
additionalAuthenticatedData?: Uint8Array
/** Plaintext. */
plaintext: Uint8Array
/** JWE Protected Header. */
protectedHeader?: JWEHeaderParameters
/** JWE Shared Unprotected Header. Not integrity protected. */
sharedUnprotectedHeader?: JWEHeaderParameters
/** JWE Per-Recipient Unprotected Header. Not integrity protected. */
unprotectedHeader?: JWEHeaderParameters
}
/** General JWE JSON Serialization decryption result. */
export interface GeneralDecryptResult extends FlattenedDecryptResult {}
/** Compact JWE decryption result. */
export interface CompactDecryptResult {
/** Plaintext. */
plaintext: Uint8Array
/** JWE Protected Header. */
protectedHeader: CompactJWEHeaderParameters
}
/** Flattened JWS JSON Serialization verification result. */
export interface FlattenedVerifyResult {
/** JWS Payload. */
payload: Uint8Array
/** JWS Protected Header. */
protectedHeader?: JWSHeaderParameters
/** JWS Unprotected Header. Not integrity protected. */
unprotectedHeader?: JWSHeaderParameters
}
/** General JWS JSON Serialization verification result. */
export interface GeneralVerifyResult extends FlattenedVerifyResult {}
/** Compact JWS verification result. */
export interface CompactVerifyResult {
/** JWS Payload. */
payload: Uint8Array
/** JWS Protected Header. */
protectedHeader: CompactJWSHeaderParameters
}
/** Signed JSON Web Token (JWT) verification result. */
export interface JWTVerifyResult<PayloadType = JWTPayload> {
/** JWT Claims Set. */
payload: PayloadType &
JWTPayload &
([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never)
/** JWS Protected Header. */
protectedHeader: JWTHeaderParameters
}
/** Encrypted JSON Web Token (JWT) decryption result. */
export interface JWTDecryptResult<PayloadType = JWTPayload> {
/** JWT Claims Set. */
payload: PayloadType &
JWTPayload &
([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never)
/** JWE Protected Header. */
protectedHeader: CompactJWEHeaderParameters
}
/** Key resolver result metadata. */
export interface ResolvedKey<KeyType extends CryptoKey | Uint8Array = CryptoKey | Uint8Array> {
/** The resolved key, present only when a key resolver is used. */
key: KeyType
}
/** Recognized Compact JWS Header Parameters; additional members may also be present. */
export interface CompactJWSHeaderParameters extends JWSHeaderParameters {
alg: JWSAlgorithm
}
/** Recognized signed JWT Header Parameters; additional members may also be present. */
export interface JWTHeaderParameters extends CompactJWSHeaderParameters {
b64?: boolean
}
/** Recognized Compact JWE Header Parameters; additional members may also be present. */
export interface CompactJWEHeaderParameters extends JWEHeaderParameters {
alg: JWEKeyManagementAlgorithm
enc: JWEContentEncryptionAlgorithm
}
/** JSON Web Key Set. */
export interface JSONWebKeySet {
keys: JWK[]
}
/** Node.js {@link !KeyObject} representation accepted as key input. */
export interface KeyObject {
type: string
}
/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */
export type CryptoKey = typeof globalThis extends {
crypto: { subtle: { generateKey(...args: any[]): Promise<infer R> } }
}
? Extract<R, { type: string }>
: CryptoKeyStructuralFallback
/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */
export interface CryptoKeyStructuralFallback {
readonly algorithm: { name: string }
readonly extractable: boolean
readonly type: string
readonly usages: string[]
}
/** Shared fluent API for JWT-producing classes. */
export interface ProduceJWT {
/**
* Set the "iss" (Issuer) Claim.
*
* @param issuer "Issuer" Claim value to set on the JWT Claims Set.
*/
setIssuer(issuer: string): this
/**
* Set the "sub" (Subject) Claim.
*
* @param subject "sub" (Subject) Claim value to set on the JWT Claims Set.
*/
setSubject(subject: string): this
/**
* Set the "aud" (Audience) Claim.
*
* @param audience "aud" (Audience) Claim value to set on the JWT Claims Set.
*/
setAudience(audience: string | string[]): this
/**
* Set the "jti" (JWT ID) Claim.
*
* @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set.
*/
setJti(jwtId: string): this
/**
* Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted
* to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years
* (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration.
*
* @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration.
*/
setNotBefore(input: number | string | Date): this
/**
* Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a
* duration relative to now using the same formats as {@link setNotBefore}.
*
* @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration.
*/
setExpirationTime(input: number | string | Date): this
/**
* Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a
* Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as
* {@link setNotBefore}.
*
* @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration.
*/
setIssuedAt(input?: number | string | Date): this
}
@@ -0,0 +1,182 @@
import type * as t from '../types.d.ts';
/** Stable error codes used by this module. */
export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID';
/** Shared details of JWT claim or header validation failures. */
export interface JWTClaimValidationFailure {
/** Claim or header that failed validation. */
claim: string;
/** Reason code for the validation failure. */
reason: JWTClaimValidationReason;
/** The parsed JWT Claims Set; validation of other claims may be incomplete. */
payload: t.JWTPayload;
}
/** Reason codes produced by JWT Claims Set validation. */
export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {});
/** Base class for JOSE errors. */
export declare class JOSEError extends Error {
/** Stable code identifying the error class. */
static code: JOSEErrorCode | (string & {});
/** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/**
* Thrown when JWT claim or header validation fails. Expiration is reported separately as
* {@link JWTExpired}.
*/
export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
/** The {@link JWTClaimValidationFailure} details carried by this error. */
cause: JWTClaimValidationFailure;
/** Claim or header that failed validation. */
claim: string;
/** Reason code for the validation failure. */
reason: JWTClaimValidationReason;
/**
* The parsed JWT Claims Set; validation of other claims may be incomplete. With
* {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token
* authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not
* authenticate tokens.
*/
payload: t.JWTPayload;
constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason);
}
/** Thrown when a JWT has expired or exceeds the configured maximum token age. */
export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
/** The {@link JWTClaimValidationFailure} details carried by this error. */
cause: JWTClaimValidationFailure;
/** Claim or header that failed validation. */
claim: string;
/** Reason code for the validation failure. */
reason: JWTClaimValidationReason;
/**
* The parsed JWT Claims Set; validation of other claims may be incomplete. With
* {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token
* authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not
* authenticate tokens.
*/
payload: t.JWTPayload;
constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason);
}
/** Thrown when an algorithm is disallowed by configuration. */
export declare class JOSEAlgNotAllowed extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a feature or algorithm is unsupported. */
export declare class JOSENotSupported extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when JWE ciphertext decryption or authentication fails. */
export declare class JWEDecryptionFailed extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Thrown when a JWE is invalid. */
export declare class JWEInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a JWS is invalid. */
export declare class JWSInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a JWT is invalid. */
export declare class JWTInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a JWK is invalid. */
export declare class JWKInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when a JWKS is invalid. */
export declare class JWKSInvalid extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
}
/** Thrown when no keys match in a JWKS. */
export declare class JWKSNoMatchingKey extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Thrown when multiple keys match in a JWKS. */
export declare class JWKSMultipleMatchingKeys extends JOSEError {
/**
* Yields public keys matching the JWS Header so verification can be attempted with each. See the
* {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and
* {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed
* errors.
*/
[Symbol.asyncIterator]: () => AsyncIterableIterator<t.CryptoKey>;
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Thrown when fetching a remote JWKS times out. */
export declare class JWKSTimeout extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Thrown when JWS signature verification fails. */
export declare class JWSSignatureVerificationFailed extends JOSEError {
static code: JOSEErrorCode | (string & {});
code: JOSEErrorCode | (string & {});
constructor(message?: string, options?: {
cause?: unknown;
});
}
/** Errors thrown during JWT Claims Set validation. */
export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired;
/**
* Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base
* {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors.
*/
export type AnyJOSEError = (JOSEAlgNotAllowed & {
code: 'ERR_JOSE_ALG_NOT_ALLOWED';
}) | (JOSENotSupported & {
code: 'ERR_JOSE_NOT_SUPPORTED';
}) | (JWEDecryptionFailed & {
code: 'ERR_JWE_DECRYPTION_FAILED';
}) | (JWEInvalid & {
code: 'ERR_JWE_INVALID';
}) | (JWKInvalid & {
code: 'ERR_JWK_INVALID';
}) | (JWKSInvalid & {
code: 'ERR_JWKS_INVALID';
}) | (JWKSMultipleMatchingKeys & {
code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';
}) | (JWKSNoMatchingKey & {
code: 'ERR_JWKS_NO_MATCHING_KEY';
}) | (JWKSTimeout & {
code: 'ERR_JWKS_TIMEOUT';
}) | (JWSInvalid & {
code: 'ERR_JWS_INVALID';
}) | (JWSSignatureVerificationFailed & {
code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';
}) | (JWTClaimValidationFailed & {
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED';
}) | (JWTExpired & {
code: 'ERR_JWT_EXPIRED';
}) | (JWTInvalid & {
code: 'ERR_JWT_INVALID';
});
+19
View File
@@ -0,0 +1,19 @@
import { createCompactSignature } from "../lib/jws_sign.js";
import { JWTInvalid } from "../util/errors.js";
import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js";
import { assertNotSet } from "../lib/validate.js";
const SignJWT_base = JWTClaimsBuilder;
class SignJWT extends SignJWT_base {
#protectedHeader;
setProtectedHeader(protectedHeader) {
return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this;
}
async sign(key, options) {
return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => {
throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
});
}
}
export {
SignJWT
};
@@ -0,0 +1,13 @@
import { prepareVerify, verifyCompact } from "../lib/jws_verify.js";
import { validateClaimsSet } from "../lib/jwt_claims_set.js";
import { JWTInvalid } from "../util/errors.js";
async function jwtVerify(jwt, key, options) {
const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key);
if (!b64)
throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options);
return { ...verified, payload };
}
export {
jwtVerify
};
@@ -0,0 +1,75 @@
const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32;
function concat(...buffers) {
const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size);
let i = 0;
for (const buffer of buffers)
buf.set(buffer, i), i += buffer.length;
return buf;
}
function writeUInt32BE(buf, value, offset) {
if (value < 0 || value >= MAX_INT32)
throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);
buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset);
}
function uint64be(value) {
const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8);
return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf;
}
function uint32be(value) {
const buf = new Uint8Array(4);
return writeUInt32BE(buf, value), buf;
}
const NON_ASCII = /[^\x00-\x7f]/;
function encode(string) {
if (typeof string == "string" && string.length >= 128) {
if (NON_ASCII.test(string))
throw new TypeError("non-ASCII string encountered in encode()");
return encoder.encode(string);
}
const bytes = new Uint8Array(string.length);
for (let i = 0; i < string.length; i++) {
const code = string.charCodeAt(i);
if (code > 127)
throw new TypeError("non-ASCII string encountered in encode()");
bytes[i] = code;
}
return bytes;
}
function encodeBase64(input, url = !1) {
if (Uint8Array.prototype.toBase64)
return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url });
const CHUNK_SIZE = 32768, arr = [];
for (let i = 0; i < input.length; i += CHUNK_SIZE)
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
const encoded = btoa(arr.join(""));
return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded;
}
function decodeBase64(encoded, url = !1) {
if (Uint8Array.fromBase64)
return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" });
if (url) {
if (encoded.includes("+") || encoded.includes("/"))
throw new TypeError("Invalid base64url");
encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
}
const binary = atob(encoded), bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++)
bytes[i] = binary.charCodeAt(i);
return bytes;
}
async function digest(algorithm, data) {
const subtleDigest = `SHA-${algorithm.slice(-3)}`;
return new Uint8Array(await crypto.subtle.digest(subtleDigest, data));
}
export {
concat,
decodeBase64,
decoder,
digest,
encode,
encodeBase64,
encoder,
strictDecoder,
uint32be,
uint64be
};
@@ -0,0 +1,74 @@
import { JOSENotSupported } from "../util/errors.js";
import { table } from "./key_descriptor.js";
const sig = [["verify"], ["sign"]];
function hmac(bits) {
const subtle = { name: "HMAC", hash: `SHA-${bits}` };
return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig };
}
function rsa(bits, saltLength) {
const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` };
return {
kty: ["RSA"],
subtle,
signing: saltLength ? { ...subtle, saltLength } : subtle,
usages: sig,
minRsaBits: 2048
};
}
function ecdsa(crv, bits) {
return {
kty: ["EC"],
crv,
subtle: { name: "ECDSA", namedCurve: crv },
signing: { name: "ECDSA", hash: `SHA-${bits}` },
usages: sig
};
}
function eddsa() {
const subtle = { name: "Ed25519" };
return {
kty: ["OKP"],
crv: "Ed25519",
subtle,
signing: subtle,
usages: sig
};
}
function mldsa(bits) {
const subtle = { name: `ML-DSA-${bits}` };
return {
kty: ["AKP"],
subtle,
signing: subtle,
usages: sig
};
}
const JWS = table({
HS256: hmac(256),
HS384: hmac(384),
HS512: hmac(512),
RS256: rsa(256),
RS384: rsa(384),
RS512: rsa(512),
PS256: rsa(256, 32),
PS384: rsa(384, 48),
PS512: rsa(512, 64),
ES256: ecdsa("P-256", 256),
ES384: ecdsa("P-384", 384),
ES512: ecdsa("P-521", 512),
EdDSA: eddsa(),
Ed25519: eddsa(),
"ML-DSA-44": mldsa(44),
"ML-DSA-65": mldsa(65),
"ML-DSA-87": mldsa(87)
});
function jwsAlgorithm(alg) {
const entry = typeof alg == "string" ? JWS[alg] : void 0;
if (!entry)
throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
return entry;
}
export {
JWS,
jwsAlgorithm
};
@@ -0,0 +1,46 @@
import { encode as b64u } from "../util/base64url.js";
import { jwsAlgorithm } from "./jws_algorithms.js";
import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js";
import { JWSInvalid } from "../util/errors.js";
import { concat, encode, encoder } from "./buffer_utils.js";
import { prepareKey, rawKey, checkModulusLength } from "./key.js";
async function createSignature(input, key, rejectUnencoded) {
let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = "";
if (protectedHeader !== void 0) {
const normalized = serializeJoseHeader(JWSInvalid, protectedHeader);
protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]);
}
if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader)
throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()");
if (!isDisjoint(protectedHeader, unprotectedHeader))
throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
const joseHeader = { ...protectedHeader, ...unprotectedHeader };
validateCritDuplicates(JWSInvalid, protectedHeader);
const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader));
b64 || rejectUnencoded?.();
const { alg } = joseHeader;
if (typeof alg != "string" || !alg)
throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
const entry = jwsAlgorithm(alg);
let payloadS = "", payloadB = payload, data;
if (b64) {
const encoded = input[4];
encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`));
}
data ??= concat(encode(protectedHeaderString), encode("."), payloadB);
const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign");
entry.minRsaBits && checkModulusLength(entry.alg, k);
const jws = {
signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))),
payload: payloadS
};
return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64];
}
async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) {
const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded);
return `${jws.protected}.${jws.payload}.${jws.signature}`;
}
export {
createCompactSignature,
createSignature
};
@@ -0,0 +1,86 @@
import { jwsAlgorithm } from "./jws_algorithms.js";
import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js";
import { concat, decoder, encoder, encode } from "./buffer_utils.js";
import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js";
import { prepareKey, rawKey, checkModulusLength } from "./key.js";
function snapshotJws(jws, sharedPayload) {
const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader;
let payload = sharedPayload ? sharedPayload[0] : jws.payload;
!sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload));
const signature = jws.signature, snapshot = { payload, signature };
if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0)
throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members');
if (encodedProtected !== void 0 && typeof encodedProtected != "string")
throw new JWSInvalid("JWS Protected Header incorrect type");
if (payload === void 0)
throw new JWSInvalid("JWS Payload missing");
if (typeof signature != "string")
throw new JWSInvalid("JWS Signature missing or incorrect type");
if (header !== void 0 && !isObject(header))
throw new JWSInvalid("JWS Unprotected Header incorrect type");
return snapshot;
}
function prepareVerify(options) {
return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit];
}
function parseProtectedHeader(encodedProtected) {
return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid");
}
function encodeJsonUnencodedPayload(payload) {
const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0];
if (invalid !== void 0)
throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points");
return encoder.encode(payload);
}
function encodeCompactUnencodedPayload(payload) {
try {
return encode(payload);
} catch {
throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters");
}
}
async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) {
const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected);
if (!isDisjoint(parsedProt, header))
throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader;
if (typeof alg != "string" || !alg)
throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
if (shared[0] && !shared[0].has(alg))
throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed');
if (b64) {
if (typeof inputPayload != "string")
throw new JWSInvalid("JWS Payload must be a string");
} else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array))
throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance");
const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload);
let resolvedKey = !1;
typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0);
const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify");
entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey);
let verified = !1;
try {
verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data);
} catch {
}
if (!verified)
throw new JWSSignatureVerificationFailed();
const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload };
return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64];
}
async function verifyCompact(jws, shared, key) {
if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string")
throw new JWSInvalid("Compact JWS must be a string or Uint8Array");
const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split(".");
if (length !== 3)
throw new JWSInvalid("Invalid Compact JWS");
return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload);
}
export {
encodeJsonUnencodedPayload,
parseProtectedHeader,
prepareVerify,
snapshotJws,
verifyCompact,
verifySignature
};
@@ -0,0 +1,146 @@
import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js";
import { encoder, strictDecoder } from "./buffer_utils.js";
import { isObject } from "./validate.js";
const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = {
s: 1,
m: 60,
h: 3600,
d: 86400,
w: 604800,
y: 31557600
}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed";
function invalidDuration() {
throw new TypeError("Invalid time period format");
}
function secs(str) {
typeof str != "string" && invalidDuration();
const matched = REGEX.exec(str);
(!matched || matched[4] && matched[1]) && invalidDuration();
const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2;
}
function validateInput(label, input) {
if (!Number.isFinite(input))
throw new TypeError(`Invalid ${label} input`);
return input;
}
function validateStringClaim(claim, value) {
if (typeof value != "string")
throw new TypeError(`"${claim}" claim must be a string`);
}
function validateAudienceClaim(value) {
if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string")))
throw new TypeError('"aud" claim must be a string or an array of strings');
}
function numericDate(value, label) {
return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value);
}
const normalizeTyp = (value) => {
const normalized = value.toLowerCase();
return value.includes("/") ? normalized : `application/${normalized}`;
}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1;
function validateNumericDate(payload, claim, required = !1) {
const value = payload[claim];
if (!(value === void 0 && !required)) {
if (typeof value != "number")
throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid");
return value;
}
}
function unexpectedClaim(payload, claim) {
throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed);
}
function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
let payload;
try {
payload = JSON.parse(strictDecoder.decode(encodedPayload));
} catch {
}
if (!isObject(payload))
throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
const { typ } = options;
if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ)))
throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed);
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims];
maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss");
for (const claim of new Set(presenceCheck.reverse()))
if (!Object.hasOwn(payload, claim))
throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing");
issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud");
const { clockTolerance } = options;
let tolerance = 0;
if (typeof clockTolerance == "string")
tolerance = secs(clockTolerance);
else if (clockTolerance !== void 0) {
if (typeof clockTolerance != "number")
throw new TypeError("Invalid clockTolerance option type");
tolerance = clockTolerance;
}
validateInput("clockTolerance option", tolerance);
const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf");
if (nbf !== void 0 && nbf > now + tolerance)
throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed);
const exp = validateNumericDate(payload, "exp");
if (exp !== void 0 && exp <= now - tolerance)
throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed);
if (maxTokenAge !== void 0) {
const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge));
if (age - tolerance > max)
throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed);
if (age < -tolerance)
throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed);
}
return payload;
}
let producerPayloads;
function producerPayload(producer) {
return producerPayloads.get(producer);
}
function jwtData(producer) {
const payload = producerPayload(producer);
for (const claim of ["iat", "nbf", "exp"]) {
const value = payload[claim];
if (typeof value == "number" && !Number.isFinite(value))
throw new TypeError(`"${claim}" claim must be a finite number`);
}
return encoder.encode(JSON.stringify(payload));
}
function jwtClaim(producer, claim) {
return producerPayload(producer)[claim];
}
class JWTClaimsBuilder {
constructor(payload = {}) {
if (!isObject(payload))
throw new TypeError("JWT Claims Set MUST be an object");
(producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload));
}
setIssuer(value) {
return validateStringClaim("iss", value), producerPayload(this).iss = value, this;
}
setSubject(value) {
return validateStringClaim("sub", value), producerPayload(this).sub = value, this;
}
setAudience(value) {
return validateAudienceClaim(value), producerPayload(this).aud = value, this;
}
setJti(value) {
return validateStringClaim("jti", value), producerPayload(this).jti = value, this;
}
setNotBefore(value) {
return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this;
}
setExpirationTime(value) {
return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this;
}
setIssuedAt(value) {
const payload = producerPayload(this);
return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this;
}
}
export {
JWTClaimsBuilder,
jwtClaim,
jwtData,
secs,
validateClaimsSet
};
+155
View File
@@ -0,0 +1,155 @@
import { isObject } from "./validate.js";
import { decode } from "../util/base64url.js";
import { JOSENotSupported } from "../util/errors.js";
const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => {
const { alg } = entry;
if (key.use !== void 0) {
const expected = usage === "sign" || usage === "verify" ? "sig" : "enc";
if (key.use !== expected)
throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
}
if (key.alg !== void 0 && key.alg !== alg)
throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
if (Array.isArray(key.key_ops)) {
const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage;
if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp))
throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
}
};
async function prepareKey(entry, key, usage) {
const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign";
if (secret && key instanceof Uint8Array)
return key;
let normalized, keyObject;
if (isObject(key)) {
if (normalized = normalizeJwk(key), typeof normalized.kty != "string")
throw invalidKeyType(alg, key, secret);
if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0)))
throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`);
if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct")
return decode(normalized.k);
if (!Object.isFrozen(key)) {
const { key_ops } = key;
Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key);
}
} else {
if (!isKeyLike(key))
throw invalidKeyType(alg, key, secret);
const expectedType = secret ? "secret" : privateKey ? "private" : "public";
if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type)))
throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`);
if (isCryptoKey(key))
return key;
if (keyObject = key, keyObject.type === "secret")
return keyObject.export();
}
cache ||= /* @__PURE__ */ new WeakMap();
const cacheKey = key;
let cached = cache.get(cacheKey);
if (cached?.[alg])
return cached[alg];
if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") {
const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle;
return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]);
}
return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized);
}
let cache;
const nist = {
__proto__: null,
prime256v1: "P-256",
secp384r1: "P-384",
secp521r1: "P-521"
};
function assertCryptoKey(key) {
if (!isCryptoKey(key))
throw new Error("CryptoKey instance expected");
}
const isCryptoKey = (key) => {
if (key?.[Symbol.toStringTag] === "CryptoKey")
return !0;
try {
return key instanceof CryptoKey;
} catch {
return !1;
}
}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
function message(msg, actual, ...types) {
if (types.length > 2) {
const last = types.pop();
msg += `one of type ${types.join(", ")}, or ${last}.`;
} else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`;
return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg;
}
const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types);
function invalidKeyType(alg, actual, secret) {
const types = ["CryptoKey", "KeyObject", "JSON Web Key"];
return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types));
}
const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
function checkUsage(key, usage) {
if (usage && !key.usages.includes(usage))
throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
}
function checkModulusLength(alg, key) {
const { modulusLength } = key.algorithm;
if (typeof modulusLength != "number" || modulusLength < 2048)
throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
}
function checkCryptoKey(key, expected, usage) {
const algorithm = key.algorithm;
if (algorithm.name !== expected.name)
throw unusable(expected.name);
if (expected.hash && algorithm.hash?.name !== expected.hash)
throw unusable(expected.hash, "algorithm.hash");
if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve)
throw unusable(expected.namedCurve, "algorithm.namedCurve");
if (expected.length !== void 0 && algorithm.length !== expected.length)
throw unusable(expected.length, "algorithm.length");
checkUsage(key, usage);
}
function snapshotJwk(jwk) {
return { __proto__: null, ...jwk };
}
function normalizeJwk(jwk) {
const normalized = snapshotJwk(jwk);
if (normalized.ext !== void 0 && typeof normalized.ext != "boolean")
throw new TypeError('"ext" (Extractable) Parameter must be a boolean');
if (normalized.key_ops !== void 0) {
const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0;
if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length)
throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings');
normalized.key_ops = keyOps;
}
return normalized;
}
function validateExtractableOption(extractable) {
if (extractable !== void 0 && typeof extractable != "boolean")
throw new TypeError('"extractable" option must be a boolean');
return extractable;
}
async function jwkToKey(entry, jwk, extractable) {
if (!entry.kty.includes(jwk.kty))
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext };
return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]);
}
async function rawKey(key, expected, usage, extractable = !1) {
return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key;
}
export {
assertCryptoKey,
checkCryptoKey,
checkModulusLength,
checkUsage,
invalidKeyInput,
isCryptoKey,
isKeyLike,
isKeyObject,
jwkToKey,
normalizeJwk,
prepareKey,
rawKey,
snapshotJwk,
validateExtractableOption
};
@@ -0,0 +1,9 @@
function table(entries) {
const out = { __proto__: null };
for (const alg in entries)
out[alg] = { ...entries[alg], alg };
return out;
}
export {
table
};
@@ -0,0 +1,122 @@
import { JOSENotSupported, JWSInvalid } from "../util/errors.js";
import { decode } from "../util/base64url.js";
import { encode, strictDecoder } from "./buffer_utils.js";
function assertUint8Array(input, label) {
if (!(input instanceof Uint8Array))
throw new TypeError(`${label} must be an instance of Uint8Array`);
}
function isObject(input) {
if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]")
return !1;
const prototype = Object.getPrototypeOf(input);
return prototype === null || Object.getPrototypeOf(prototype) === null;
}
function isJwkSet(input) {
return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject);
}
function isDisjoint(...headers) {
const parameters = /* @__PURE__ */ new Set();
for (const header of headers)
if (header)
for (const parameter of Object.keys(header)) {
if (parameters.has(parameter))
return !1;
parameters.add(parameter);
}
return !0;
}
function assertNotSet(value, name) {
if (value !== void 0)
throw new TypeError(`${name} can only be called once`);
}
function decodeBase64url(value, label, ErrorClass) {
try {
return decode(value);
} catch {
throw new ErrorClass(`Failed to base64url decode the ${label}`);
}
}
function encodeBase64url(value, label, ErrorClass) {
try {
return encode(value);
} catch {
throw new ErrorClass(`The ${label} is not a valid base64url string`);
}
}
function parseJoseHeader(b64, ErrorClass, message) {
let parsed;
try {
parsed = JSON.parse(strictDecoder.decode(decode(b64)));
} catch {
throw new ErrorClass(message);
}
if (!isObject(parsed))
throw new ErrorClass(message);
return parsed;
}
const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null };
function validateAlgorithms(option, algorithms) {
if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string")))
throw new TypeError(`"${option}" option must be an array of strings`);
return algorithms === void 0 ? void 0 : new Set(algorithms);
}
function validateCritDuplicates(Err, protectedHeader) {
const { crit } = protectedHeader ?? {};
if (Array.isArray(crit) && new Set(crit).size !== crit.length)
throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values');
}
function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0)
throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
if (!protectedHeader || protectedHeader.crit === void 0)
return [];
if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0))
throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault };
for (const parameter of protectedHeader.crit) {
if (!(parameter in recognized))
throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0)
throw new Err(`Extension Header Parameter "${parameter}" is missing`);
if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0))
throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
}
return protectedHeader.crit;
}
function validateB64(protectedHeader, extensions) {
if (extensions.includes("b64")) {
const b64 = protectedHeader.b64;
if (typeof b64 != "boolean")
throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
return b64;
}
return !0;
}
function serializeJoseHeader(Err, header) {
let serialized, parsed;
try {
serialized = JSON.stringify(header), parsed = JSON.parse(serialized);
} catch (cause) {
throw new Err("JOSE Header is not valid JSON", { cause });
}
if (!isObject(parsed))
throw new Err("JOSE Header is not a JSON object");
return [parsed, serialized];
}
export {
JWE_RECOGNIZED,
JWS_RECOGNIZED,
assertNotSet,
assertUint8Array,
decodeBase64url,
encodeBase64url,
isDisjoint,
isJwkSet,
isObject,
parseJoseHeader,
serializeJoseHeader,
validateAlgorithms,
validateB64,
validateCrit,
validateCritDuplicates
};
@@ -0,0 +1,16 @@
import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js";
const invalid = "The input to be decoded is not correctly encoded.";
function decode(input) {
try {
return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0);
} catch (cause) {
throw new TypeError(invalid, { cause });
}
}
function encode(input) {
return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0);
}
export {
decode,
encode
};
@@ -0,0 +1,109 @@
class JOSEError extends Error {
static code = "ERR_JOSE_GENERIC";
code = "ERR_JOSE_GENERIC";
constructor(message, options) {
super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor);
}
}
class JWTClaimValidationFailed extends JOSEError {
static code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
claim;
reason;
payload;
constructor(message, payload, claim = "unspecified", reason = "unspecified") {
super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload;
}
}
class JWTExpired extends JOSEError {
static code = "ERR_JWT_EXPIRED";
code = "ERR_JWT_EXPIRED";
claim;
reason;
payload;
constructor(message, payload, claim = "unspecified", reason = "unspecified") {
super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload;
}
}
class JOSEAlgNotAllowed extends JOSEError {
static code = "ERR_JOSE_ALG_NOT_ALLOWED";
code = "ERR_JOSE_ALG_NOT_ALLOWED";
}
class JOSENotSupported extends JOSEError {
static code = "ERR_JOSE_NOT_SUPPORTED";
code = "ERR_JOSE_NOT_SUPPORTED";
}
class JWEDecryptionFailed extends JOSEError {
static code = "ERR_JWE_DECRYPTION_FAILED";
code = "ERR_JWE_DECRYPTION_FAILED";
constructor(message = "decryption operation failed", options) {
super(message, options);
}
}
class JWEInvalid extends JOSEError {
static code = "ERR_JWE_INVALID";
code = "ERR_JWE_INVALID";
}
class JWSInvalid extends JOSEError {
static code = "ERR_JWS_INVALID";
code = "ERR_JWS_INVALID";
}
class JWTInvalid extends JOSEError {
static code = "ERR_JWT_INVALID";
code = "ERR_JWT_INVALID";
}
class JWKInvalid extends JOSEError {
static code = "ERR_JWK_INVALID";
code = "ERR_JWK_INVALID";
}
class JWKSInvalid extends JOSEError {
static code = "ERR_JWKS_INVALID";
code = "ERR_JWKS_INVALID";
}
class JWKSNoMatchingKey extends JOSEError {
static code = "ERR_JWKS_NO_MATCHING_KEY";
code = "ERR_JWKS_NO_MATCHING_KEY";
constructor(message = "no applicable key found in the JSON Web Key Set", options) {
super(message, options);
}
}
class JWKSMultipleMatchingKeys extends JOSEError {
[Symbol.asyncIterator] = async function* () {
};
static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS";
code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS";
constructor(message = "multiple matching keys found in the JSON Web Key Set", options) {
super(message, options);
}
}
class JWKSTimeout extends JOSEError {
static code = "ERR_JWKS_TIMEOUT";
code = "ERR_JWKS_TIMEOUT";
constructor(message = "request timed out", options) {
super(message, options);
}
}
class JWSSignatureVerificationFailed extends JOSEError {
static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
constructor(message = "signature verification failed", options) {
super(message, options);
}
}
export {
JOSEAlgNotAllowed,
JOSEError,
JOSENotSupported,
JWEDecryptionFailed,
JWEInvalid,
JWKInvalid,
JWKSInvalid,
JWKSMultipleMatchingKeys,
JWKSNoMatchingKey,
JWKSTimeout,
JWSInvalid,
JWSSignatureVerificationFailed,
JWTClaimValidationFailed,
JWTExpired,
JWTInvalid
};
+17
View File
@@ -0,0 +1,17 @@
{
"name": "jose",
"version": "6.2.12",
"license": "MIT",
"type": "module",
"sideEffects": false,
"exports": {
"./jwt/*": {
"types": "./dist/types/jwt/*.d.ts",
"default": "./dist/webapi/jwt/*.js"
},
"./errors": {
"types": "./dist/types/util/errors.d.ts",
"default": "./dist/webapi/util/errors.js"
}
}
}
@@ -0,0 +1,23 @@
// Object patterns over arrays that name positions and `length` (jose's
// `const { 0: header, 1: payload, 2: signature, length } = jws.split(".")`):
// each position is the element or undefined past the end, defaults apply
// to missing positions, and `length` is the array's length.
const { 0: header, 1: payload, 2: signature, length } = "aaa.bbb.ccc".split(".");
console.log(header, payload, signature, length);
const short = "aaa.bbb".split(".");
const { 0: first, 2: third, length: count } = short;
console.log(first, third === undefined, count);
const { 1: second = "fallback", 3: fourth = "missing" } = short;
console.log(second, fourth);
function parts(values: number[]): string {
const { 0: a, 1: b, length: n } = values;
return `${a}:${b}:${n}`;
}
console.log(parts([1, 2, 3]), parts([7]));
let renamed = "";
for (const { 0: key, 1: value } of [["x", "1"], ["y", "2"]]) renamed += `${key}=${value};`;
console.log(renamed);
@@ -0,0 +1,34 @@
// Runtime idioms from jose's dist build: the Uint8Array base64/hex
// feature probes (absent in Node 24), String.fromCharCode.apply over byte
// chunks and number lists (the btoa fallback), and structuredClone of a
// parameter whose default is `{}` called with differently shaped objects.
console.log(typeof Uint8Array.prototype.toBase64, typeof Uint8Array.prototype.toHex);
console.log(typeof Uint8Array.prototype.setFromBase64, typeof Uint8Array.prototype.setFromHex);
function encodeBase64(input) {
if (Uint8Array.prototype.toBase64) return input.toBase64();
const CHUNK_SIZE = 4, arr = [];
for (let i = 0; i < input.length; i += CHUNK_SIZE)
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
return btoa(arr.join(""));
}
console.log(encodeBase64(new TextEncoder().encode("hello, base64 chunks")));
console.log(encodeBase64(new Uint8Array([0, 255, 128, 1, 2])));
const bytes = new Uint8Array([104, 105]);
console.log(String.fromCharCode.apply(null, bytes), String.fromCharCode.apply(undefined, [72, 73]));
const untyped = JSON.parse("[79, 75]");
console.log(String.fromCharCode.apply(null, untyped), String.fromCharCode.apply(null, []).length);
let store;
class Builder {
constructor(payload = {}) {
(store ||= new WeakMap()).set(this, structuredClone(payload));
}
claims() {
return JSON.stringify(store.get(this));
}
}
console.log(new Builder({ role: "admin", exp: 5 }).claims());
console.log(new Builder({ scope: "read" }).claims());
console.log(new Builder().claims());
+110
View File
@@ -0,0 +1,110 @@
// The real jose@6.2.12 through its root entry, from TypeScript, the way a
// Next.js proxy uses it: `import { jwtVerify, errors } from "jose"`.
// node_modules/jose vendors the published package's dist/ (every webapi
// module the root entry loads, and its declarations), package.json and
// LICENSE.md unmodified (MIT). Loading the root runs jwks/remote.js, which
// reads Node's navigator global at module load. Covers chained SignJWT
// builders, string durations (jose's secs()), destructured jwtVerify
// results and `instanceof errors.JOSEError` brand checks.
import { SignJWT, jwtVerify, errors, cryptoRuntime, decodeJwt } from "jose";
const encoder = new TextEncoder();
const secret = encoder.encode("scriptc-jose-root-shared-secret");
const now = 1_800_000_000;
const iat = 1_799_999_000;
interface Options {
issuer?: string;
audience?: string;
algorithms?: string[];
at?: number;
maxTokenAge?: string;
clockTolerance?: string;
}
async function check(label: string, token: string, options: Options = {}): Promise<void> {
try {
const { payload, protectedHeader } = await jwtVerify(token, secret, {
algorithms: options.algorithms ?? ["HS256"],
currentDate: new Date((options.at ?? now) * 1000),
issuer: options.issuer,
audience: options.audience,
maxTokenAge: options.maxTokenAge,
clockTolerance: options.clockTolerance,
});
const role = payload.role;
console.log(label, "ok", protectedHeader.alg, payload.sub ?? null, role, payload.exp ?? null);
} catch (error) {
if (error instanceof errors.JOSEError) {
console.log(label, error.code, error.message, error instanceof errors.JWTExpired);
} else if (error instanceof TypeError) {
console.log(label, "TypeError", error.message);
} else {
throw error;
}
}
}
console.log(cryptoRuntime, errors.JWTExpired.code, errors.JWSSignatureVerificationFailed.code);
const token = await new SignJWT({ role: "admin" })
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuedAt(iat)
.setIssuer("urn:scriptc:issuer")
.setAudience("urn:scriptc:audience")
.setSubject("alice")
.setJti("jti-1")
.setNotBefore(iat - 60)
.setExpirationTime(iat + 3600)
.sign(secret);
console.log(token);
console.log(JSON.stringify(decodeJwt(token)));
await check("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" });
await check("issuer", token, { issuer: "urn:other" });
await check("audience", token, { audience: "urn:other" });
await check("expired", token, { at: iat + 7200 });
await check("wrong secret", await new SignJWT({ role: "admin" }).setProtectedHeader({ alg: "HS256" }).sign(encoder.encode("x")));
await check("malformed", "not.a.jwt");
await check("alg", token, { algorithms: ["HS512"] });
// String durations: maxTokenAge and clockTolerance against the fixed date.
await check("max age 10 minutes", token, { maxTokenAge: "10 minutes" });
await check("max age 1 hour", token, { maxTokenAge: "1 hour" });
await check("max age 2 days", token, { maxTokenAge: "2 days" });
await check("max age 1.5h", token, { maxTokenAge: "1.5h" });
await check("max age +20m", token, { maxTokenAge: "+20m" });
await check("max age 1 week", token, { maxTokenAge: "1 week" });
await check("max age 1y", token, { maxTokenAge: "1y" });
await check("max age 900 secs", token, { maxTokenAge: "900 secs" });
await check("max age 3 HOURS", token, { maxTokenAge: "3 HOURS" });
await check("tolerance 30s", token, { at: iat + 3620, clockTolerance: "30s" });
await check("tolerance 5 sec", token, { at: iat + 3620, clockTolerance: "5 sec" });
await check("tolerance 1 min from now", token, { at: iat + 3620, clockTolerance: "1 min from now" });
await check("tolerance 1 min ago", token, { at: iat + 3620, clockTolerance: "1 min ago" });
await check("bad duration", token, { maxTokenAge: "10 fortnights" });
await check("signed ago", token, { maxTokenAge: "-2h ago" });
await check("empty duration", token, { maxTokenAge: "" });
// String durations on the builder are relative to the clock: check the
// distance from now instead of printing it.
const relative = await new SignJWT({ role: "reader" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt("-1 minute")
.setNotBefore("0s")
.setExpirationTime("2h")
.sign(secret);
const claims = decodeJwt(relative);
const clock = Math.floor(Date.now() / 1000);
console.log(
Math.abs((claims.exp ?? 0) - clock - 7200) <= 2,
Math.abs((claims.iat ?? 0) - clock + 60) <= 2,
Math.abs((claims.nbf ?? 0) - clock) <= 2,
);
const { payload } = await jwtVerify(relative, secret, { maxTokenAge: "5 minutes" });
console.log(payload.role);
try {
new SignJWT({}).setExpirationTime("soon");
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Filip Skokan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,55 @@
export { compactDecrypt } from './jwe/compact/decrypt.js';
export type { CompactDecryptGetKey } from './jwe/compact/decrypt.js';
export { flattenedDecrypt } from './jwe/flattened/decrypt.js';
export type { FlattenedDecryptGetKey } from './jwe/flattened/decrypt.js';
export { generalDecrypt } from './jwe/general/decrypt.js';
export type { GeneralDecryptGetKey } from './jwe/general/decrypt.js';
export { GeneralEncrypt } from './jwe/general/encrypt.js';
export type { Recipient } from './jwe/general/encrypt.js';
export { compactVerify } from './jws/compact/verify.js';
export type { CompactVerifyGetKey } from './jws/compact/verify.js';
export { flattenedVerify } from './jws/flattened/verify.js';
export type { FlattenedVerifyGetKey } from './jws/flattened/verify.js';
export { generalVerify } from './jws/general/verify.js';
export type { GeneralVerifyGetKey } from './jws/general/verify.js';
export { jwtVerify } from './jwt/verify.js';
export type { JWTVerifyOptions, JWTVerifyGetKey } from './jwt/verify.js';
export { jwtDecrypt } from './jwt/decrypt.js';
export type { JWTDecryptOptions, JWTDecryptGetKey } from './jwt/decrypt.js';
export { CompactEncrypt } from './jwe/compact/encrypt.js';
export { FlattenedEncrypt } from './jwe/flattened/encrypt.js';
export { CompactSign } from './jws/compact/sign.js';
export { FlattenedSign } from './jws/flattened/sign.js';
export { GeneralSign } from './jws/general/sign.js';
export type { Signature } from './jws/general/sign.js';
export { SignJWT } from './jwt/sign.js';
export { EncryptJWT } from './jwt/encrypt.js';
export { calculateJwkThumbprint, calculateJwkThumbprintUri } from './jwk/thumbprint.js';
export { EmbeddedJWK } from './jwk/embedded.js';
export { createLocalJWKSet } from './jwks/local.js';
export type { LocalJWKSet } from './jwks/local.js';
export { createRemoteJWKSet, jwksCache, customFetch } from './jwks/remote.js';
export type { RemoteJWKSet, RemoteJWKSetOptions, JWKSCacheInput, ExportedJWKSCache, FetchImplementation, } from './jwks/remote.js';
export { UnsecuredJWT } from './jwt/unsecured.js';
export type { UnsecuredResult } from './jwt/unsecured.js';
export { exportPKCS8, exportSPKI, exportJWK } from './key/export.js';
export { importSPKI, importPKCS8, importX509, importJWK } from './key/import.js';
export type { KeyImportOptions } from './key/import.js';
export { decodeProtectedHeader } from './util/decode_protected_header.js';
export { decodeJwt } from './util/decode_jwt.js';
export type { ProtectedHeaderParameters } from './util/decode_protected_header.js';
import * as errors from './util/errors.js';
export { errors };
export { generateKeyPair } from './key/generate_key_pair.js';
export type { GenerateKeyPairAlgorithm, GenerateKeyPairResult, GenerateKeyPairOptions, } from './key/generate_key_pair.js';
export { generateSecret } from './key/generate_secret.js';
export type { GenerateSecretAlgorithm, GenerateSecretOptions } from './key/generate_secret.js';
import * as base64url from './util/base64url.js';
export { base64url };
export type { AnyJWK, CompactDecryptResult, CompactJWEHeaderParameters, CompactJWSHeaderParameters, CompactVerifyResult, CritOption, CryptoKey, DecryptOptions, EncryptOptions, FlattenedDecryptResult, FlattenedJWE, FlattenedJWS, FlattenedJWSInput, FlattenedVerifyResult, GeneralDecryptResult, GeneralJWE, GeneralJWS, GeneralJWSInput, GeneralVerifyResult, GetKeyFunction, JoseHeaderParameters, JSONWebKeySet, JWEContentEncryptionAlgorithm, JWEHeaderParameters, JWEKeyManagementAlgorithm, JWEKeyManagementHeaderParameters, JWK_AKP_Private, JWK_AKP_Public, JWK_EC_Private, JWK_EC_Public, JWK_oct, JWK_OKP_Private, JWK_OKP_Public, JWK_RSA_Private, JWK_RSA_Public, JWK, JWKKeyType, JWKParameters, JWSAlgorithm, JWSHeaderParameters, JWTClaimVerificationOptions, JWTDecryptResult, JWTHeaderParameters, JWTPayload, JWTVerifyResult, KeyInput, KeyObject, ProduceJWT, ResolvedKey, SignOptions, VerifyOptions, } from './types.d.ts';
/**
* Identified the loaded runtime-specific build in prior releases; now always `"WebCryptoAPI"`.
*
* @deprecated Remove any runtime branching on this value; it is always `"WebCryptoAPI"`.
*/
export declare const cryptoRuntime = "WebCryptoAPI";
@@ -0,0 +1,24 @@
import type * as t from '../../types.d.ts';
/** Resolves a key for Compact JWE decryption from unverified headers and token data. */
export interface CompactDecryptGetKey<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array> extends t.GetKeyFunction<t.CompactJWEHeaderParameters, t.FlattenedJWE, KeyType | t.KeyObject | t.JWK> {
}
/**
* Decrypts a Compact JWE.
*
* @param key Private key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput, options?: t.DecryptOptions): Promise<t.CompactDecryptResult>;
/**
* Decrypts a Compact JWE with a dynamically resolved key, included in the result.
*
* @param getKey Resolves a private key or shared secret from unverified token data.
*/
export declare function compactDecrypt<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array>(jwe: string | Uint8Array, getKey: CompactDecryptGetKey<KeyType>, options?: t.DecryptOptions): Promise<t.CompactDecryptResult & t.ResolvedKey<KeyType>>;
/**
* Decrypts a Compact JWE with a key or key resolver. The result includes `key` only when a resolver
* is used.
*
* @param key Private key or shared secret, or a function resolving one.
*/
export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput | CompactDecryptGetKey, options?: t.DecryptOptions): Promise<t.CompactDecryptResult & Partial<t.ResolvedKey>>;
@@ -0,0 +1,40 @@
import type * as t from '../../types.d.ts';
/** Builds and encrypts Compact JWE strings. */
export declare class CompactEncrypt {
#private;
/**
* Creates a Compact JWE encryptor.
*
* @param plaintext Binary representation of the plaintext to encrypt.
*/
constructor(plaintext: Uint8Array);
/**
* Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm.
* May only be called once.
*
* @deprecated Use only for testing and vector validation.
*/
setContentEncryptionKey(cek: Uint8Array): this;
/**
* Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm.
* May only be called once.
*
* @deprecated Use only for testing and vector validation.
*/
setInitializationVector(iv: Uint8Array): this;
/** Sets the JWE Protected Header. May only be called once. */
setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this;
/**
* Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead
* of header setters; the resulting parameters are added to the JOSE header. May only be called
* once.
*/
setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this;
/**
* Encrypts the plaintext as a Compact JWE.
*
* @param key Public key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise<string>;
}
@@ -0,0 +1,24 @@
import type * as t from '../../types.d.ts';
/** Resolves a key for Flattened JWE decryption from unverified headers and token data. */
export interface FlattenedDecryptGetKey<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array> extends t.GetKeyFunction<t.JWEHeaderParameters | undefined, t.FlattenedJWE, KeyType | t.KeyObject | t.JWK> {
}
/**
* Decrypts a Flattened JWE.
*
* @param key Private key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise<t.FlattenedDecryptResult>;
/**
* Decrypts a Flattened JWE with a dynamically resolved key, included in the result.
*
* @param getKey Resolves a private key or shared secret from unverified token data.
*/
export declare function flattenedDecrypt<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array>(jwe: t.FlattenedJWE, getKey: FlattenedDecryptGetKey<KeyType>, options?: t.DecryptOptions): Promise<t.FlattenedDecryptResult & t.ResolvedKey<KeyType>>;
/**
* Decrypts a Flattened JWE with a key or key resolver. The result includes `key` only when a
* resolver is used.
*
* @param key Private key or shared secret, or a function resolving one.
*/
export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput | FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise<t.FlattenedDecryptResult & Partial<t.ResolvedKey>>;
@@ -0,0 +1,46 @@
import type * as t from '../../types.d.ts';
/** Builds and encrypts Flattened JWE objects. */
export declare class FlattenedEncrypt {
#private;
/**
* Creates a Flattened JWE encryptor.
*
* @param plaintext Binary representation of the plaintext to encrypt.
*/
constructor(plaintext: Uint8Array);
/**
* Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead
* of header setters; the resulting parameters are added to the JOSE header. May only be called
* once.
*/
setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this;
/** Sets the JWE Protected Header. May only be called once. */
setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this;
/** Sets the JWE Shared Unprotected Header. May only be called once. */
setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this;
/** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */
setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): this;
/** Sets additional data to authenticate without encrypting it. */
setAdditionalAuthenticatedData(aad: Uint8Array): this;
/**
* Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm.
* May only be called once.
*
* @deprecated Use only for testing and vector validation.
*/
setContentEncryptionKey(cek: Uint8Array): this;
/**
* Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm.
* May only be called once.
*
* @deprecated Use only for testing and vector validation.
*/
setInitializationVector(iv: Uint8Array): this;
/**
* Encrypts the plaintext as a Flattened JWE.
*
* @param key Public key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise<t.FlattenedJWE>;
}
@@ -0,0 +1,28 @@
import type * as t from '../../types.d.ts';
/** Resolves a key for General JWE decryption from unverified headers and token data. */
export interface GeneralDecryptGetKey<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array> extends t.GetKeyFunction<t.JWEHeaderParameters | undefined, t.FlattenedJWE, KeyType | t.KeyObject | t.JWK> {
}
/**
* Decrypts a General JWE.
*
* > Note: Returns plaintext and headers from the first recipient that decrypts successfully. Other
* > recipients may be inspected to enforce serialization rules, but their headers are not included in
* > the result. Rely only on the returned data.
*
* @param key Private key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise<t.GeneralDecryptResult>;
/**
* Decrypts a General JWE with a dynamically resolved key, included in the result.
*
* @param getKey Resolves a private key or shared secret from unverified token data.
*/
export declare function generalDecrypt<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array>(jwe: t.GeneralJWE, getKey: GeneralDecryptGetKey<KeyType>, options?: t.DecryptOptions): Promise<t.GeneralDecryptResult & t.ResolvedKey<KeyType>>;
/**
* Decrypts a General JWE with a key or key resolver. The result includes `key` only when a resolver
* is used.
*
* @param key Private key or shared secret, or a function resolving one.
*/
export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput | GeneralDecryptGetKey, options?: t.DecryptOptions): Promise<t.GeneralDecryptResult & Partial<t.ResolvedKey>>;
@@ -0,0 +1,51 @@
import type * as t from '../../types.d.ts';
/** Configures an individual recipient in a General JWE. */
export interface Recipient {
/** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */
setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): Recipient;
/**
* Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead
* of header setters; the resulting parameters are added to the JOSE header. May only be called
* once.
*/
setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): Recipient;
/**
* Adds another recipient to the enclosing {@link GeneralEncrypt} and returns its configuration.
*
* @param key Public key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient;
/**
* Encrypts for all recipients on the enclosing {@link GeneralEncrypt}, using their configured
* keys.
*/
encrypt(): Promise<t.GeneralJWE>;
/** Returns the enclosing {@link GeneralEncrypt} instance. */
done(): GeneralEncrypt;
}
/** Builds and encrypts General JWE objects. */
export declare class GeneralEncrypt {
#private;
/**
* Creates a General JWE encryptor.
*
* @param plaintext Binary representation of the plaintext to encrypt.
*/
constructor(plaintext: Uint8Array);
/**
* Adds a recipient and returns its configuration.
*
* @param key Public key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}.
*/
addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient;
/** Sets the JWE Protected Header. May only be called once. */
setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this;
/** Sets the JWE Shared Unprotected Header. May only be called once. */
setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this;
/** Sets additional data to authenticate without encrypting it. */
setAdditionalAuthenticatedData(aad: Uint8Array): this;
/** Encrypts the plaintext as a General JWE. */
encrypt(): Promise<t.GeneralJWE>;
}
@@ -0,0 +1,10 @@
import type * as t from '../types.d.ts';
/**
* Resolves a verification key from an embedded "jwk" (JSON Web Key) Header Parameter. This key
* resolver opts JWS and JWT verification into trusting a public key supplied by the token. Use the
* verification function's `algorithms` option to restrict accepted algorithms.
*
* @param token The consumed JWS token.
* @returns The public key from the JWS "jwk" (JSON Web Key) Header Parameter.
*/
export declare function EmbeddedJWK(protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise<t.CryptoKey>;
@@ -0,0 +1,15 @@
import type * as t from '../types.d.ts';
/**
* Calculates a base64url-encoded JWK Thumbprint. CryptoKey inputs must be extractable.
*
* @param key Key to calculate the thumbprint for.
* @param digestAlgorithm Digest algorithm. Defaults to "sha256".
*/
export declare function calculateJwkThumbprint(key: t.JWK | t.CryptoKey | t.KeyObject, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise<string>;
/**
* Calculates a JWK Thumbprint URI. CryptoKey inputs must be extractable.
*
* @param key Key to calculate the thumbprint for.
* @param digestAlgorithm Digest algorithm. Defaults to "sha256".
*/
export declare function calculateJwkThumbprintUri(key: t.CryptoKey | t.KeyObject | t.JWK, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise<string>;
@@ -0,0 +1,17 @@
import type * as t from '../types.d.ts';
/** A key resolver created by {@link createLocalJWKSet}. */
export interface LocalJWKSet {
(protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise<t.CryptoKey>;
/** Returns a structured clone of the original JSON Web Key Set. */
jwks: () => t.JSONWebKeySet;
}
/**
* Creates a resolver for a locally available JSON Web Key Set. Selection uses the header's "alg"
* (Algorithm) and "kid" (Key ID), and respects the JWK's "use" (Public Key Use) and "key_ops" (Key
* Operations). Exactly one key must match.
*
* > Note: Only public signature verification keys are supported, not public encryption keys.
*
* @param jwks JSON Web Key Set formatted object.
*/
export declare function createLocalJWKSet(jwks: t.JSONWebKeySet): LocalJWKSet;
@@ -0,0 +1,94 @@
import type * as t from '../types.d.ts';
/**
* Configures a custom fetch implementation for remote JWKS retrieval.
*
* > Note: Fetch-like libraries may have incompatible TypeScript signatures even when they accept the
* > supplied arguments at runtime.
*/
export declare const customFetch: unique symbol;
/** Custom fetch function. Must return HTTP 200 with a JSON JWKS. See {@link customFetch}. */
export type FetchImplementation = (
/** JWKS URL, passed as the fetch resource. */
url: string,
/** Options passed to fetch. */
options: {
headers: Headers;
method: 'GET';
/** See {@link !Request.redirect} */
redirect: 'manual';
signal: AbortSignal;
}) => Promise<Response>;
/**
* Symbol used to configure an externally persisted remote JWKS cache.
*
* > Warning: Only trusted application code must be allowed to write this cache; its keys are trusted for
* > signature verification.
*/
export declare const jwksCache: unique symbol;
/** Remote JWKS resolver options. */
export interface RemoteJWKSetOptions {
/**
* Timeout (in milliseconds) for the HTTP request. When reached the request will be aborted and
* the verification will fail. Must be a non-negative integer. Default is 5000 (5 seconds).
*/
timeoutDuration?: number;
/**
* Time in milliseconds after a successful fetch before a missing key can trigger another fetch.
* Must not be `NaN`. Defaults to 30000 (30 seconds).
*/
cooldownDuration?: number;
/**
* Maximum age of cached keys in milliseconds. Defaults to 600000 (10 minutes); `Infinity`
* disables expiry. Must not be `NaN`.
*/
cacheMaxAge?: number | typeof Infinity;
/** Headers to be sent with the HTTP request. */
headers?: Record<string, string>;
/** See {@link jwksCache}. */
[jwksCache]?: JWKSCacheInput;
/** See {@link customFetch}. */
[customFetch]?: FetchImplementation;
}
/** Shape of an externally persisted remote JWKS cache. */
export interface ExportedJWKSCache {
/** Current cached JSON Web Key Set */
jwks: t.JSONWebKeySet;
/** Last successful fetch, in milliseconds since Unix epoch. */
uat: number;
}
/** Values accepted by the {@link jwksCache} option. */
export type JWKSCacheInput = ExportedJWKSCache | Record<string, never>;
/** A key resolver created by {@link createRemoteJWKSet}. */
export interface RemoteJWKSet {
(protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise<t.CryptoKey>;
/** Whether the cooldown window following the last successful fetch is still in effect. */
readonly coolingDown: boolean;
/**
* Whether the currently cached JSON Web Key Set is within its
* {@link RemoteJWKSetOptions.cacheMaxAge}.
*/
readonly fresh: boolean;
/** Whether a JSON Web Key Set fetch is currently in flight. */
readonly reloading: boolean;
/**
* Triggers a JSON Web Key Set fetch, bypassing
* {@link RemoteJWKSetOptions.cooldownDuration the cooldown}.
*/
reload: () => Promise<void>;
/**
* Returns a structured clone of the cached JSON Web Key Set, or `undefined` before keys have been
* fetched or seeded via {@link jwksCache}.
*/
jwks: () => t.JSONWebKeySet | undefined;
}
/**
* Creates a resolver for a JSON Web Key Set available at an HTTP(S) URL. Fetches the JSON Web Key
* Set when the cache is missing or stale. An unmatched key triggers another fetch only when
* `cooldownDuration` has elapsed since the last successful fetch. Selection uses the header's "alg"
* and "kid" and respects the JWK's "use" and "key_ops". Exactly one key must match.
*
* > Note: Only public signature verification keys are supported, not public encryption keys.
*
* @param url URL to fetch the JSON Web Key Set from.
*/
export declare function createRemoteJWKSet(url: URL, options?: RemoteJWKSetOptions): RemoteJWKSet;
@@ -0,0 +1,20 @@
import type * as t from '../../types.d.ts';
/** Builds and signs Compact JWS strings. */
export declare class CompactSign {
#private;
/**
* Creates a Compact JWS signer.
*
* @param payload Binary representation of the payload to sign.
*/
constructor(payload: Uint8Array);
/** Sets the JWS Protected Header. May only be called once. */
setProtectedHeader(protectedHeader: t.CompactJWSHeaderParameters): this;
/**
* Signs the payload as a Compact JWS.
*
* @param key Private key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
*/
sign(key: t.KeyInput, options?: t.SignOptions): Promise<string>;
}
@@ -0,0 +1,25 @@
import type * as t from '../../types.d.ts';
/** Resolves a key for Compact JWS verification from unverified headers and token data. */
export interface CompactVerifyGetKey<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array> extends t.GetKeyFunction<t.CompactJWSHeaderParameters, t.FlattenedJWSInput, KeyType | t.KeyObject | t.JWK> {
}
/**
* Verifies a Compact JWS signature and decodes its payload.
*
* @param key Public key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
*/
export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput, options?: t.VerifyOptions): Promise<t.CompactVerifyResult>;
/**
* Verifies a Compact JWS signature and decodes its payload with a dynamically resolved key,
* included in the result.
*
* @param getKey Resolves a public key or shared secret from unverified token data.
*/
export declare function compactVerify<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array>(jws: string | Uint8Array, getKey: CompactVerifyGetKey<KeyType>, options?: t.VerifyOptions): Promise<t.CompactVerifyResult & t.ResolvedKey<KeyType>>;
/**
* Verifies a Compact JWS and decodes its payload using a key or key resolver. The result includes
* `key` only when a resolver is used.
*
* @param key Public key or shared secret, or a function resolving one.
*/
export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput | CompactVerifyGetKey, options?: t.VerifyOptions): Promise<t.CompactVerifyResult & Partial<t.ResolvedKey>>;
@@ -0,0 +1,22 @@
import type * as t from '../../types.d.ts';
/** Builds and signs Flattened JWS objects. */
export declare class FlattenedSign {
#private;
/**
* Creates a Flattened JWS signer.
*
* @param payload Binary representation of the payload to sign.
*/
constructor(payload: Uint8Array);
/** Sets the JWS Protected Header. May only be called once. */
setProtectedHeader(protectedHeader: t.JWSHeaderParameters): this;
/** Sets the JWS Unprotected Header. May only be called once. */
setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): this;
/**
* Signs the payload as a Flattened JWS.
*
* @param key Private key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
*/
sign(key: t.KeyInput, options?: t.SignOptions): Promise<t.FlattenedJWS>;
}
@@ -0,0 +1,25 @@
import type * as t from '../../types.d.ts';
/** Resolves a key for Flattened JWS verification from unverified headers and token data. */
export interface FlattenedVerifyGetKey<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array> extends t.GetKeyFunction<t.JWSHeaderParameters, t.FlattenedJWSInput, KeyType | t.KeyObject | t.JWK> {
}
/**
* Verifies a Flattened JWS signature and decodes its payload.
*
* @param key Public key or shared secret. See
* {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
*/
export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise<t.FlattenedVerifyResult>;
/**
* Verifies a Flattened JWS signature and decodes its payload with a dynamically resolved key,
* included in the result.
*
* @param getKey Resolves a public key or shared secret from unverified token data.
*/
export declare function flattenedVerify<KeyType extends t.CryptoKey | Uint8Array = t.CryptoKey | Uint8Array>(jws: t.FlattenedJWSInput, getKey: FlattenedVerifyGetKey<KeyType>, options?: t.VerifyOptions): Promise<t.FlattenedVerifyResult & t.ResolvedKey<KeyType>>;
/**
* Verifies a Flattened JWS and decodes its payload using a key or key resolver. The result includes
* `key` only when a resolver is used.
*
* @param key Public key or shared secret, or a function resolving one.
*/
export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput | FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise<t.FlattenedVerifyResult & Partial<t.ResolvedKey>>;

Some files were not shown because too many files have changed in this diff Show More