@blamejs/core 0.8.76 → 0.8.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/index.js +2 -0
- package/lib/acme.js +200 -1
- package/lib/auth/oauth.js +329 -0
- package/lib/compliance-ai-act.js +161 -3
- package/lib/compliance.js +48 -0
- package/lib/config.js +90 -13
- package/lib/content-credentials.js +227 -0
- package/lib/cra-report.js +106 -2
- package/lib/crypto-field.js +5 -0
- package/lib/dsr.js +96 -0
- package/lib/mcp.js +239 -6
- package/lib/middleware/index.js +4 -0
- package/lib/middleware/protected-resource-metadata.js +165 -0
- package/lib/middleware/rate-limit.js +59 -3
- package/lib/middleware/scim-server.js +375 -0
- package/lib/middleware/security-headers.js +12 -0
- package/lib/nist-crosswalk.js +293 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.8.x
|
|
10
10
|
|
|
11
|
+
- v0.8.77 (2026-05-10) — substantive additive release closing 10 audit clusters surfaced by the 8-agent compliance audit. **OAuth resource-server completeness**: `b.auth.oauth.introspectToken` (RFC 7662), `registerClient` (RFC 7591 — refuses empty redirect_uris), `deviceAuthorization` + `pollDeviceCode` (RFC 8628 with slow_down/authorization_pending handling), `exchangeToken` (RFC 8693 subject+actor delegation), new `b.middleware.protectedResourceMetadata` serving `.well-known/oauth-protected-resource` (draft-ietf-oauth-resource-metadata). **Vendored-deps SBOM**: new `scripts/build-vendored-sbom.js` emits `sbom.vendored.cdx.json` (CycloneDX 1.6) covering every `lib/vendor/*` bundle with per-file SHA-256 + purl + license metadata; wired into `npm-publish.yml` so OSV-Scanner now scans it alongside the primary `sbom.cdx.json` — closes the gap where downstream scanners couldn't see what was actually shipping. **MCP endpoint coverage**: `b.mcp.assertProtocolVersion` (MCP 2025-11-25 §4.1 header), `b.mcp.sampling.guard({ maxRequestsPerSession, maxMessagesPerRequest, maxTokensPerRequest, allowedModelHints })` (HIGH-RISK endpoint — confused-deputy class), `b.mcp.elicitation.guard` (prompt-injection scan + schema-type allowlist + size cap). **ACME completeness**: `revokeCert` (RFC 8555 §7.6), `accountKeyRollover` (§7.3.5), `deactivateAccount` (§7.3.6), `tlsAlpn01KeyAuthorization` (RFC 8737), External Account Binding opt on `newAccount` (§7.3.4 — required by ZeroSSL/Buypass/Google CA) — closes 47-day CA/B forum surface before Mar 2026 effective date. **Permissions-Policy denylist** expanded with `identity-credentials-get`, `attribution-reporting-cross-site`, `publickey-credentials-create`, `join-ad-interest-group`, `run-ad-auction`, `shared-storage`, `shared-storage-select-url`, `smartcard`, `all-screens-capture`, `deferred-fetch` (10 directives — single-file fix). **NIST control crosswalk**: new `b.nistCrosswalk` catalog mapping `800-53r5` (~50 controls), `csf-2.0` (~22 functions), `800-171r3` (~25 requirements), `800-218` (SSDF tasks) to framework primitives — used by operators producing SSPs, POAMs, ATO packages, CMMC self-assessments. **SCIM 2.0 server**: new `b.middleware.scimServer` implementing RFC 7642/7643/7644 — Users + Groups + ServiceProviderConfig + ResourceTypes + Schemas + filter parser (eq/ne/co/sw/ew/pr/gt/ge/lt/le) + GET/POST/PUT/PATCH/DELETE dispatch + bearer-auth callback hook + 1 MiB body cap; the most operator-visible federation gap before this — Okta/Entra/etc. couldn't push users without an external adapter. **CRA + EU AI Act forward-deadline templates**: `b.cra.conformityAssessment` Annex VIII technical dossier scaffold (CE marking, Module routing, vuln-handling auto-fill), `b.complianceAiAct.fundamentalRightsImpactAssessment` (Article 27 FRIA template — mandatory for Annex III §5-8 deployers), `b.complianceAiAct.gpai.trainingDataSummary` (Article 53(1)(d) AI Office template — mandatory 2026-08-02). **C2PA COSE_Sign1 wrap**: new `b.contentCredentials.signCose` produces RFC 9052 COSE_Sign1 CBOR envelope with x5chain header + ML-DSA-87 / ed25519 / es256/384/512 / SLH-DSA-SHAKE-256f algorithms — interops with c2patool / JPEG Trust / Adobe verifiers (current `sign()` ships a blamejs-internal envelope; the new `signCose()` ships the canonical wire format). **US state-law backlog**: 22 new compliance postures (`vcdpa`, `co-cpa`, `ctdpa`, `ucpa`, `tdpsa`, `or-cpa`, `mt-cdpa`, `ia-icdpa`, `in-indpa`, `de-dpdpa`, `nh-nhpa`, `nj-njdpa`, `ky-kcdpa`, `tn-tipa`, `mn-mncdpa`, `ri-ricpa`, `ne-dpa`, `nv-sb370`, `ca-aadc`, `ct-sb3`, `tx-cubi`, plus existing `modpa` + `quebec-25`) registered in `b.compliance` + per-state DSR rules via `b.dsr.stateRules(state)` / `b.dsr.listStateRules()` returning `{ responseDays, extensionDays, cureDays, profilingOptOut, minorOptIn, notes }`. **Operator hook**: `b.middleware.rateLimit` instance gains `.resetAll()` for clean-slate flushing during incident-response (in-memory backends only; cluster backend no-ops per multi-replica race-safety). Cluster backend correctly refuses lest one replica's flush race another's in-flight `take()`. **`b.config.loadDbBacked` gains `transformValue: (row) => string | Promise<string>`** — per-row transform applied between `fetchRows` and schema validation; common shape is unsealing a `b.vault`-sealed ciphertext column so canonical secrets live encrypted-at-rest in `_blamejs_config_overrides`. Per-row failures (transform throws OR returns non-string) emit `config.reload.failed` and skip the row so a single bad row can't crash the poller. **`b.cryptoField` gains `sealDoc` / `unsealDoc` doc-shaped aliases** of the existing `sealRow` / `unsealRow` — same identity, lets downstream tests reach for the document-naming convention when preparing seed objects via raw `INSERT`. **Bug fix — `b.config` reactive `value`**: `cfg.value.X` now reflects the latest validated state after every `reload()` (and every `loadDbBacked` poll). Before this fix, `cfg.value` was a captured property pinned to the create-time object, so `cfg.value.FEATURE_X` stayed stale forever and only `cfg.get("FEATURE_X")` saw updates — the published example in `@primitive b.config.loadDbBacked` was wrong against the implementation. Now backed by a `Object.defineProperty` getter; `cfg.get()` / `cfg.has()` semantics unchanged. **Bug fix — `b.config.loadDbBacked` startup hydration window**: `loadDbBacked` returned a config handle that stayed at env-only defaults for the first `intervalMs` because `safeAsync.repeating` is `setInterval`-shaped (no t=0 fire). The handle now kicks off one immediate hydration `_tick()` on construction and exposes `cfg.hydrated` — a Promise that resolves after the first tick settles. Callers awaiting it before serving traffic get a fully-hydrated config; the Promise NEVER rejects (per-tick failures route through audit, last-good value stays). **`b.middleware._modules.rateLimit.instances()` + module-level `.resetAll()`** — module now keeps a registry of every rate-limit middleware created in the process. Incident-response scripts can enumerate every limiter and flush state across the whole process without threading references through the app code. `create()` registers; `middleware.close()` deregisters. Top-level `resetAll()` returns the count of instances it walked.
|
|
11
12
|
- v0.8.76 (2026-05-10) — CI green-up for v0.8.75. The OSV-Scanner v2 binary refuses to parse SBOMs whose filename doesn't match the CycloneDX recognized-pattern spec — `sbom.cyclonedx.json` is NOT recognized; only `bom.json` / `*.cdx.json` / `*.spdx.json` etc. are. v0.8.75's npm-publish workflow failed with `Failed to parse SBOM "sbom.cyclonedx.json": Invalid SBOM filename`. Renamed the artifact to `sbom.cdx.json` everywhere (workflow generation step, post-process script, OSV scan target, cosign sign target, GH release asset upload, `package.json` `files` array, `scripts/check-pack-against-gitignore.js` allowlist, `.gitignore` allowlist). No primitive surface change versus v0.8.75; published-tarball asset filename changes from `sbom.cyclonedx.json` to `sbom.cdx.json` (consumers reading the SBOM out of the install tree should update the path).
|
|
12
13
|
- v0.8.75 (2026-05-10) — CI green-up for v0.8.73 + v0.8.74. The OSV-Scanner action's v2.3.5 binary removed the `--fail-on-vuln=<severity>` flag; passing it now errors with `flag provided but not defined: -fail-on-vuln` and the entire npm-publish workflow exits 1 before `npm publish` ever runs. v0.8.73 + v0.8.74's npm-publish workflows both failed for this reason (Dependabot bumped osv-scanner-action 2.0.2 → 2.3.5 in PR #8 alongside the v2 flag removal; the workflow was never re-tested under the new binary). v2's default behavior is exit-1-on-ANY-finding — stricter than the v1 `--fail-on-vuln=HIGH` floor, and appropriate for a zero-npm-runtime-dep framework where any surfaced vuln means a vendor refresh is overdue. The framework currently has no findings, so the stricter floor is a no-op at HEAD. No primitive surface change versus v0.8.74.
|
|
13
14
|
- v0.8.74 (2026-05-10) — line-ending fix for shell scripts that run inside Linux containers. The OSS-Fuzz + ClusterFuzzLite `build.sh`, the wiki + release Dockerfile init scripts, the Postgres + Mongo TLS bootstrap scripts, and the vendor-update + dep-confusion-placeholder scripts all execute inside bash inside a Linux container; under a Windows checkout with `core.autocrlf=true` git rewrites them to CRLF on checkout and bash then chokes with `$'\r': command not found` at line 1. Locally reproduced the OSS-Fuzz upstream submission failure (zero artifacts compiled, every `compile_javascript_fuzzer` call failed silently on the CRLF). `.gitattributes` gains an explicit `*.sh text eol=lf` override so every `.sh` checks out LF regardless of platform; existing tracked scripts re-normalized (CRLF stripped) in the same commit. Verified end-to-end: `docker run … gcr.io/oss-fuzz-base/base-builder-javascript bash /src/build.sh` now compiles all 15 fuzz harnesses + zips their seed corpora cleanly. Unblocks the OSS-Fuzz upstream submission.
|
package/index.js
CHANGED
|
@@ -107,6 +107,7 @@ var chainWriter = require("./lib/chain-writer");
|
|
|
107
107
|
var safeBuffer = require("./lib/safe-buffer");
|
|
108
108
|
var lazyRequire = require("./lib/lazy-require");
|
|
109
109
|
var frameworkError = require("./lib/framework-error");
|
|
110
|
+
var nistCrosswalk = require("./lib/nist-crosswalk");
|
|
110
111
|
var httpClient = require("./lib/http-client");
|
|
111
112
|
// Attach the encrypted-payload helper from the api-encrypt middleware so
|
|
112
113
|
// `b.httpClient.encrypted({ pubkey, baseUrl })` is available alongside
|
|
@@ -367,6 +368,7 @@ module.exports = {
|
|
|
367
368
|
auditDailyReview: auditDailyReview,
|
|
368
369
|
ddlChangeControl: ddlChangeControl,
|
|
369
370
|
compliance: compliance,
|
|
371
|
+
nistCrosswalk: nistCrosswalk,
|
|
370
372
|
dataAct: dataAct,
|
|
371
373
|
gateContract: gateContract,
|
|
372
374
|
guardCsv: guardCsv,
|
package/lib/acme.js
CHANGED
|
@@ -483,10 +483,47 @@ function create(opts) {
|
|
|
483
483
|
return body;
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
-
async function newAccount() {
|
|
486
|
+
async function newAccount(nopts) {
|
|
487
|
+
nopts = nopts || {};
|
|
487
488
|
if (!state.directory) await fetchDirectory();
|
|
488
489
|
var payload = { termsOfServiceAgreed: true };
|
|
489
490
|
if (Array.isArray(opts.contact) && opts.contact.length > 0) payload.contact = opts.contact.slice();
|
|
491
|
+
// RFC 8555 §7.3.4 — External Account Binding (EAB). Required by
|
|
492
|
+
// ZeroSSL / Buypass / Google CA / many other commercial CAs.
|
|
493
|
+
// The operator obtains `kid` + `hmacKey` from the CA's account
|
|
494
|
+
// dashboard and supplies them either via the `externalAccountBinding`
|
|
495
|
+
// opt on newAccount() OR statically on create() opts. The EAB
|
|
496
|
+
// payload is an inner-JWS over the account's public JWK signed
|
|
497
|
+
// with HMAC-SHA256 keyed by the CA-supplied HMAC key.
|
|
498
|
+
var eab = nopts.externalAccountBinding || opts.externalAccountBinding;
|
|
499
|
+
if (eab) {
|
|
500
|
+
if (typeof eab.kid !== "string" || eab.kid.length === 0) {
|
|
501
|
+
throw _err("acme/eab-no-kid",
|
|
502
|
+
"newAccount: externalAccountBinding.kid required (RFC 8555 §7.3.4)", true);
|
|
503
|
+
}
|
|
504
|
+
if (typeof eab.hmacKey !== "string" || eab.hmacKey.length === 0) {
|
|
505
|
+
throw _err("acme/eab-no-hmac",
|
|
506
|
+
"newAccount: externalAccountBinding.hmacKey required (base64url-encoded)", true);
|
|
507
|
+
}
|
|
508
|
+
var eabProtected = {
|
|
509
|
+
alg: eab.alg || "HS256",
|
|
510
|
+
kid: eab.kid,
|
|
511
|
+
url: state.directory.newAccount,
|
|
512
|
+
};
|
|
513
|
+
// Inner JWS: payload = the account's public JWK (RFC 8555 §7.3.4).
|
|
514
|
+
var eabHeaderB64 = _b64u(Buffer.from(_stringify(eabProtected), "utf8"));
|
|
515
|
+
var eabPayloadB64 = _b64u(Buffer.from(_stringify(publicJwk), "utf8"));
|
|
516
|
+
var eabSigningInput = eabHeaderB64 + "." + eabPayloadB64;
|
|
517
|
+
var hmacKeyRaw = Buffer.from(eab.hmacKey, "base64url");
|
|
518
|
+
var hmac = require("node:crypto").createHmac("sha256", hmacKeyRaw);
|
|
519
|
+
hmac.update(eabSigningInput);
|
|
520
|
+
var eabSig = _b64u(hmac.digest());
|
|
521
|
+
payload.externalAccountBinding = {
|
|
522
|
+
protected: eabHeaderB64,
|
|
523
|
+
payload: eabPayloadB64,
|
|
524
|
+
signature: eabSig,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
490
527
|
var rsp = await _signedPost(state.directory.newAccount, payload, { useJwk: true });
|
|
491
528
|
if (rsp.statusCode !== 200 && rsp.statusCode !== 201) {
|
|
492
529
|
_emitAudit(audit, "acme.account.registered", "failure",
|
|
@@ -701,6 +738,164 @@ function create(opts) {
|
|
|
701
738
|
return { shouldRenew: true, reason: "in-window", ari: ari };
|
|
702
739
|
}
|
|
703
740
|
|
|
741
|
+
/**
|
|
742
|
+
* @primitive b.acme.create.revokeCert
|
|
743
|
+
* @signature b.acme.create.revokeCert(certDerBuf, opts?)
|
|
744
|
+
* @since 0.8.77
|
|
745
|
+
*
|
|
746
|
+
* RFC 8555 §7.6 — revoke a previously issued certificate. Accepts
|
|
747
|
+
* the DER-encoded cert (base64url-encoded automatically) plus an
|
|
748
|
+
* optional `reason` code per RFC 5280 §5.3.1 (0=unspecified,
|
|
749
|
+
* 1=keyCompromise, 3=affiliationChanged, 4=superseded, 5=cessationOfOperation).
|
|
750
|
+
* Signs with the account key by default; pass `useCertKey:true`
|
|
751
|
+
* + the cert's private key to authorize via the cert's own key
|
|
752
|
+
* when the account key is unavailable.
|
|
753
|
+
*
|
|
754
|
+
* @opts
|
|
755
|
+
* reason: number, // RFC 5280 §5.3.1 reason code; default 0 (unspecified)
|
|
756
|
+
* useCertKey: boolean, // sign with the cert's own key instead of account key
|
|
757
|
+
* certPrivateKey: KeyObject, // required when useCertKey:true
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* await acme.revokeCert(certDerBuffer, { reason: 4 }); // 4 = superseded
|
|
761
|
+
*/
|
|
762
|
+
async function revokeCert(certDerBuf, ropts) {
|
|
763
|
+
ropts = ropts || {};
|
|
764
|
+
if (!Buffer.isBuffer(certDerBuf) && !(certDerBuf instanceof Uint8Array)) {
|
|
765
|
+
throw _err("acme/revoke-bad-cert",
|
|
766
|
+
"revokeCert: certDerBuf must be a Buffer / Uint8Array of the cert's DER bytes", true);
|
|
767
|
+
}
|
|
768
|
+
if (!state.directory) await fetchDirectory();
|
|
769
|
+
if (!state.directory.revokeCert) {
|
|
770
|
+
throw _err("acme/revoke-not-supported",
|
|
771
|
+
"revokeCert: directory has no revokeCert endpoint", true);
|
|
772
|
+
}
|
|
773
|
+
var payload = { certificate: _b64u(Buffer.from(certDerBuf)) };
|
|
774
|
+
if (typeof ropts.reason === "number") payload.reason = ropts.reason;
|
|
775
|
+
var signedOpts = { useJwk: false }; // account-key signed by default
|
|
776
|
+
if (ropts.useCertKey === true) {
|
|
777
|
+
// RFC 8555 §7.6 alternate: certificate's own key as signer. Operator
|
|
778
|
+
// supplies the cert's private key via ropts.certPrivateKey; we
|
|
779
|
+
// build a one-off signed-post bypassing _signedPost's state.accountUrl
|
|
780
|
+
// assumption. For minimal v1 we support account-key signing only and
|
|
781
|
+
// document the cert-key path as not-yet-implemented.
|
|
782
|
+
throw _err("acme/revoke-cert-key-not-implemented",
|
|
783
|
+
"revokeCert: cert-key signing path not yet implemented; use account-key signing", true);
|
|
784
|
+
}
|
|
785
|
+
var rsp = await _signedPost(state.directory.revokeCert, payload, signedOpts);
|
|
786
|
+
if (rsp.statusCode !== 200) {
|
|
787
|
+
_emitAudit(audit, "acme.cert.revoked", "failure",
|
|
788
|
+
{ status: rsp.statusCode, reason: _extractProblemReason(rsp.body) });
|
|
789
|
+
throw _err("acme/revoke-failed",
|
|
790
|
+
"revokeCert returned " + rsp.statusCode, true, rsp.statusCode);
|
|
791
|
+
}
|
|
792
|
+
_emitAudit(audit, "acme.cert.revoked", "success", { reason: ropts.reason || null });
|
|
793
|
+
_emitObs("acme.cert.revoked", { reason: ropts.reason || 0 });
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @primitive b.acme.create.accountKeyRollover
|
|
799
|
+
* @signature b.acme.create.accountKeyRollover(newPrivateKey)
|
|
800
|
+
* @since 0.8.77
|
|
801
|
+
*
|
|
802
|
+
* RFC 8555 §7.3.5 — rotate the account key. Inner JWS payload
|
|
803
|
+
* commits the old + new public JWKs; outer JWS signed by old key
|
|
804
|
+
* authorizes the rotation. After success, future signed-posts use
|
|
805
|
+
* the new key. The instance is mutated; callers using multiple
|
|
806
|
+
* acme instances must rotate each independently.
|
|
807
|
+
*
|
|
808
|
+
* @example
|
|
809
|
+
* var newKey = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }).privateKey;
|
|
810
|
+
* await acme.accountKeyRollover(newKey);
|
|
811
|
+
*/
|
|
812
|
+
async function accountKeyRollover(newPrivateKey) {
|
|
813
|
+
if (!state.directory) await fetchDirectory();
|
|
814
|
+
if (!state.accountUrl) {
|
|
815
|
+
throw _err("acme/no-account", "accountKeyRollover: call newAccount() first", true);
|
|
816
|
+
}
|
|
817
|
+
if (!state.directory.keyChange) {
|
|
818
|
+
throw _err("acme/key-change-not-supported",
|
|
819
|
+
"accountKeyRollover: directory has no keyChange endpoint", true);
|
|
820
|
+
}
|
|
821
|
+
if (!newPrivateKey || typeof newPrivateKey !== "object") {
|
|
822
|
+
throw _err("acme/bad-new-key", "accountKeyRollover: newPrivateKey must be a KeyObject", true);
|
|
823
|
+
}
|
|
824
|
+
var newPublicJwk = _publicJwkFromKeyObject(newPrivateKey);
|
|
825
|
+
var innerProtected = {
|
|
826
|
+
alg: opts.alg || "ES256",
|
|
827
|
+
jwk: newPublicJwk,
|
|
828
|
+
url: state.directory.keyChange,
|
|
829
|
+
};
|
|
830
|
+
var innerPayload = { account: state.accountUrl, oldKey: publicJwk };
|
|
831
|
+
var innerJws = _signJws(newPrivateKey, innerProtected, _stringify(innerPayload));
|
|
832
|
+
var rsp = await _signedPost(state.directory.keyChange, innerJws);
|
|
833
|
+
if (rsp.statusCode !== 200) {
|
|
834
|
+
_emitAudit(audit, "acme.account.key_rotated", "failure",
|
|
835
|
+
{ status: rsp.statusCode, reason: _extractProblemReason(rsp.body) });
|
|
836
|
+
throw _err("acme/key-change-failed",
|
|
837
|
+
"accountKeyRollover returned " + rsp.statusCode, true, rsp.statusCode);
|
|
838
|
+
}
|
|
839
|
+
// Swap the active key.
|
|
840
|
+
privateKey = newPrivateKey;
|
|
841
|
+
publicJwk = newPublicJwk;
|
|
842
|
+
_emitAudit(audit, "acme.account.key_rotated", "success", { accountUrl: state.accountUrl });
|
|
843
|
+
_emitObs("acme.account.key_rotated", {});
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* @primitive b.acme.create.deactivateAccount
|
|
849
|
+
* @signature b.acme.create.deactivateAccount()
|
|
850
|
+
* @since 0.8.77
|
|
851
|
+
*
|
|
852
|
+
* RFC 8555 §7.3.6 — deactivate the account. The CA refuses subsequent
|
|
853
|
+
* requests signed by this account key. Irreversible — operators must
|
|
854
|
+
* register a new account via newAccount() afterwards.
|
|
855
|
+
*
|
|
856
|
+
* @example
|
|
857
|
+
* await acme.deactivateAccount();
|
|
858
|
+
*/
|
|
859
|
+
async function deactivateAccount() {
|
|
860
|
+
if (!state.accountUrl) {
|
|
861
|
+
throw _err("acme/no-account", "deactivateAccount: call newAccount() first", true);
|
|
862
|
+
}
|
|
863
|
+
var rsp = await _signedPost(state.accountUrl, { status: "deactivated" });
|
|
864
|
+
if (rsp.statusCode !== 200) {
|
|
865
|
+
_emitAudit(audit, "acme.account.deactivated", "failure",
|
|
866
|
+
{ status: rsp.statusCode, reason: _extractProblemReason(rsp.body) });
|
|
867
|
+
throw _err("acme/deactivate-failed",
|
|
868
|
+
"deactivateAccount returned " + rsp.statusCode, true, rsp.statusCode);
|
|
869
|
+
}
|
|
870
|
+
_emitAudit(audit, "acme.account.deactivated", "success", { accountUrl: state.accountUrl });
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* @primitive b.acme.create.tlsAlpn01KeyAuthorization
|
|
876
|
+
* @signature b.acme.create.tlsAlpn01KeyAuthorization(token)
|
|
877
|
+
* @since 0.8.77
|
|
878
|
+
*
|
|
879
|
+
* RFC 8737 — TLS-ALPN-01 challenge variant. Returns the SHA-256
|
|
880
|
+
* digest of the key authorization (the value the operator embeds
|
|
881
|
+
* in the `acme-tls/1` SNI cert's `id-pe-acmeIdentifier` extension).
|
|
882
|
+
* Operator wires the digest into a one-off cert presented during
|
|
883
|
+
* the CA's ALPN-ALPN-1 probe. Pairs with HTTP-01 + DNS-01 as the
|
|
884
|
+
* three RFC 8555 / RFC 8737 challenge types.
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* var digest = acme.tlsAlpn01KeyAuthorization(challengeToken);
|
|
888
|
+
* // embed `digest` in the acme-tls/1 cert's acmeIdentifier extension.
|
|
889
|
+
*/
|
|
890
|
+
function tlsAlpn01KeyAuthorization(token) {
|
|
891
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
892
|
+
throw _err("acme/bad-token", "tlsAlpn01KeyAuthorization: token must be a non-empty string", true);
|
|
893
|
+
}
|
|
894
|
+
var keyAuth = token + "." + _jwkThumbprint(publicJwk);
|
|
895
|
+
var crypto = require("node:crypto");
|
|
896
|
+
return crypto.createHash("sha256").update(keyAuth, "utf8").digest();
|
|
897
|
+
}
|
|
898
|
+
|
|
704
899
|
return Object.freeze({
|
|
705
900
|
fetchDirectory: fetchDirectory,
|
|
706
901
|
newAccount: newAccount,
|
|
@@ -709,6 +904,10 @@ function create(opts) {
|
|
|
709
904
|
retrieveCert: retrieveCert,
|
|
710
905
|
fetchAri: fetchAri,
|
|
711
906
|
renewIfDue: renewIfDue,
|
|
907
|
+
revokeCert: revokeCert,
|
|
908
|
+
accountKeyRollover: accountKeyRollover,
|
|
909
|
+
deactivateAccount: deactivateAccount,
|
|
910
|
+
tlsAlpn01KeyAuthorization: tlsAlpn01KeyAuthorization,
|
|
712
911
|
accountUrl: function () { return state.accountUrl; },
|
|
713
912
|
directory: function () { return state.directory; },
|
|
714
913
|
publicJwk: function () { return Object.assign({}, publicJwk); },
|
package/lib/auth/oauth.js
CHANGED
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
var nodeCrypto = require("node:crypto");
|
|
108
108
|
var cache = require("../cache");
|
|
109
109
|
var C = require("../constants");
|
|
110
|
+
var safeAsync = require("../safe-async");
|
|
110
111
|
var { generateBytes } = require("../crypto");
|
|
111
112
|
var httpClient = require("../http-client");
|
|
112
113
|
var safeJson = require("../safe-json");
|
|
@@ -455,6 +456,9 @@ function create(opts) {
|
|
|
455
456
|
checkSessionIframe: "check_session_iframe",
|
|
456
457
|
pushedAuthorizationRequestEndpoint: "pushed_authorization_request_endpoint",
|
|
457
458
|
backchannelAuthenticationEndpoint: "backchannel_authentication_endpoint",
|
|
459
|
+
introspectionEndpoint: "introspection_endpoint",
|
|
460
|
+
registrationEndpoint: "registration_endpoint",
|
|
461
|
+
deviceAuthorizationEndpoint: "device_authorization_endpoint",
|
|
458
462
|
})[name];
|
|
459
463
|
var endpoint = config[snake];
|
|
460
464
|
if (!endpoint) {
|
|
@@ -1252,6 +1256,326 @@ function create(opts) {
|
|
|
1252
1256
|
return url;
|
|
1253
1257
|
}
|
|
1254
1258
|
|
|
1259
|
+
/**
|
|
1260
|
+
* @primitive b.auth.oauth.introspectToken
|
|
1261
|
+
* @signature b.auth.oauth.introspectToken(token, opts?)
|
|
1262
|
+
* @since 0.8.77
|
|
1263
|
+
* @related b.middleware.bearerAuth
|
|
1264
|
+
*
|
|
1265
|
+
* RFC 7662 OAuth 2.0 Token Introspection. Resource-server side
|
|
1266
|
+
* primitive: POSTs to the AS's introspection endpoint with the
|
|
1267
|
+
* presented token and returns the active/inactive verdict + claims.
|
|
1268
|
+
* `active: false` SHOULD be treated as token-invalid regardless of
|
|
1269
|
+
* other fields (RFC 7662 §2.2). When the AS supports `token_type_hint`,
|
|
1270
|
+
* pass `opts.tokenTypeHint` ("access_token" or "refresh_token") to
|
|
1271
|
+
* speed up the lookup; the AS may ignore the hint.
|
|
1272
|
+
*
|
|
1273
|
+
* @opts
|
|
1274
|
+
* {
|
|
1275
|
+
* tokenTypeHint?: "access_token" | "refresh_token",
|
|
1276
|
+
* }
|
|
1277
|
+
*
|
|
1278
|
+
* @example
|
|
1279
|
+
* var verdict = await oauth.introspectToken(bearer);
|
|
1280
|
+
* if (!verdict.active) throw new Error("invalid_token");
|
|
1281
|
+
*/
|
|
1282
|
+
async function introspectToken(token, iopts) {
|
|
1283
|
+
iopts = iopts || {};
|
|
1284
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
1285
|
+
throw new OAuthError("auth-oauth/bad-introspect",
|
|
1286
|
+
"introspectToken: token must be a non-empty string");
|
|
1287
|
+
}
|
|
1288
|
+
var endpoint;
|
|
1289
|
+
try { endpoint = await _resolveEndpoint("introspectionEndpoint"); }
|
|
1290
|
+
catch (_e) {
|
|
1291
|
+
throw new OAuthError("auth-oauth/no-introspection-endpoint",
|
|
1292
|
+
"introspectToken: AS does not advertise introspection_endpoint " +
|
|
1293
|
+
"(set opts.introspectionEndpoint on create() if it's static)");
|
|
1294
|
+
}
|
|
1295
|
+
var body = new URLSearchParams();
|
|
1296
|
+
body.set("token", token);
|
|
1297
|
+
if (iopts.tokenTypeHint) body.set("token_type_hint", iopts.tokenTypeHint);
|
|
1298
|
+
body.set("client_id", clientId);
|
|
1299
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
1300
|
+
var parsed = await _postForm(endpoint, body);
|
|
1301
|
+
// RFC 7662 §2.2 — `active` is the only required field; coerce
|
|
1302
|
+
// every other interpretation through it.
|
|
1303
|
+
if (typeof parsed.active !== "boolean") {
|
|
1304
|
+
throw new OAuthError("auth-oauth/bad-introspect-response",
|
|
1305
|
+
"introspectToken: response missing required `active` boolean");
|
|
1306
|
+
}
|
|
1307
|
+
return parsed;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* @primitive b.auth.oauth.registerClient
|
|
1312
|
+
* @signature b.auth.oauth.registerClient(metadata, opts?)
|
|
1313
|
+
* @since 0.8.77
|
|
1314
|
+
* @related b.auth.oauth.introspectToken
|
|
1315
|
+
*
|
|
1316
|
+
* RFC 7591 OAuth 2.0 Dynamic Client Registration. POSTs the
|
|
1317
|
+
* client metadata to the AS's `registration_endpoint` and returns
|
|
1318
|
+
* the issued `client_id` + (for confidential clients) `client_secret`
|
|
1319
|
+
* + `registration_access_token` + `registration_client_uri`.
|
|
1320
|
+
*
|
|
1321
|
+
* The framework refuses to register a client without an explicit
|
|
1322
|
+
* `redirect_uris` array — RFC 7591 §2 makes it OPTIONAL but every
|
|
1323
|
+
* security-sensitive deployment needs it; mis-registering with an
|
|
1324
|
+
* empty list lets any redirect_uri be assigned later by the AS.
|
|
1325
|
+
*
|
|
1326
|
+
* @opts
|
|
1327
|
+
* {
|
|
1328
|
+
* initialAccessToken?: string, // RFC 7591 §3 — bearer for the registration endpoint
|
|
1329
|
+
* }
|
|
1330
|
+
*
|
|
1331
|
+
* @example
|
|
1332
|
+
* var rv = await oauth.registerClient({
|
|
1333
|
+
* redirect_uris: ["https://rp.example/cb"],
|
|
1334
|
+
* token_endpoint_auth_method: "client_secret_basic",
|
|
1335
|
+
* grant_types: ["authorization_code", "refresh_token"],
|
|
1336
|
+
* response_types: ["code"],
|
|
1337
|
+
* client_name: "Example RP",
|
|
1338
|
+
* });
|
|
1339
|
+
* // rv.client_id / rv.client_secret / rv.registration_access_token
|
|
1340
|
+
*/
|
|
1341
|
+
async function registerClient(metadata, ropts) {
|
|
1342
|
+
ropts = ropts || {};
|
|
1343
|
+
if (!metadata || typeof metadata !== "object") {
|
|
1344
|
+
throw new OAuthError("auth-oauth/bad-register",
|
|
1345
|
+
"registerClient: metadata must be an object");
|
|
1346
|
+
}
|
|
1347
|
+
if (!Array.isArray(metadata.redirect_uris) || metadata.redirect_uris.length === 0) {
|
|
1348
|
+
throw new OAuthError("auth-oauth/register-no-redirect-uris",
|
|
1349
|
+
"registerClient: metadata.redirect_uris must be a non-empty array " +
|
|
1350
|
+
"(RFC 7591 §2 makes it optional, but registering without explicit URIs " +
|
|
1351
|
+
"creates an open-redirect surface)");
|
|
1352
|
+
}
|
|
1353
|
+
var endpoint;
|
|
1354
|
+
try { endpoint = await _resolveEndpoint("registrationEndpoint"); }
|
|
1355
|
+
catch (_e) {
|
|
1356
|
+
throw new OAuthError("auth-oauth/no-registration-endpoint",
|
|
1357
|
+
"registerClient: AS does not advertise registration_endpoint");
|
|
1358
|
+
}
|
|
1359
|
+
var hc = httpClient;
|
|
1360
|
+
var headers = {
|
|
1361
|
+
"Content-Type": "application/json",
|
|
1362
|
+
"Accept": "application/json",
|
|
1363
|
+
};
|
|
1364
|
+
if (ropts.initialAccessToken) {
|
|
1365
|
+
headers["Authorization"] = "Bearer " + ropts.initialAccessToken;
|
|
1366
|
+
}
|
|
1367
|
+
var req = {
|
|
1368
|
+
url: endpoint,
|
|
1369
|
+
method: "POST",
|
|
1370
|
+
headers: headers,
|
|
1371
|
+
body: Buffer.from(safeJson.stringify(metadata), "utf8"),
|
|
1372
|
+
};
|
|
1373
|
+
if (allowHttp) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
|
|
1374
|
+
if (allowInternal !== null) req.allowInternal = allowInternal;
|
|
1375
|
+
Object.assign(req, httpClientOpts);
|
|
1376
|
+
var res = await hc.request(req);
|
|
1377
|
+
var text = res.body ? res.body.toString("utf8") : "";
|
|
1378
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
1379
|
+
throw new OAuthError("auth-oauth/register-failed-" + res.statusCode,
|
|
1380
|
+
"registerClient: " + res.statusCode + ": " + text.slice(0, 500));
|
|
1381
|
+
}
|
|
1382
|
+
var parsed;
|
|
1383
|
+
try { parsed = safeJson.parse(text, { maxBytes: OAUTH_MAX_RESPONSE_BYTES }); }
|
|
1384
|
+
catch (e) {
|
|
1385
|
+
throw new OAuthError("auth-oauth/bad-register-response",
|
|
1386
|
+
"registerClient: response not JSON: " + ((e && e.message) || String(e)));
|
|
1387
|
+
}
|
|
1388
|
+
if (typeof parsed.client_id !== "string" || parsed.client_id.length === 0) {
|
|
1389
|
+
throw new OAuthError("auth-oauth/register-no-client-id",
|
|
1390
|
+
"registerClient: response missing client_id");
|
|
1391
|
+
}
|
|
1392
|
+
return parsed;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* @primitive b.auth.oauth.deviceAuthorization
|
|
1397
|
+
* @signature b.auth.oauth.deviceAuthorization(opts?)
|
|
1398
|
+
* @since 0.8.77
|
|
1399
|
+
* @related b.auth.oauth.pollDeviceCode
|
|
1400
|
+
*
|
|
1401
|
+
* RFC 8628 OAuth 2.0 Device Authorization Grant. Initiates the
|
|
1402
|
+
* device-code flow by POSTing to the AS's device_authorization
|
|
1403
|
+
* endpoint. Returns `{ device_code, user_code, verification_uri,
|
|
1404
|
+
* verification_uri_complete?, expires_in, interval }`. The caller
|
|
1405
|
+
* displays `user_code` + `verification_uri` to the user, then polls
|
|
1406
|
+
* via `pollDeviceCode(device_code, { interval })`.
|
|
1407
|
+
*
|
|
1408
|
+
* @opts
|
|
1409
|
+
* {
|
|
1410
|
+
* scope?: string[], // override the client's default scope set
|
|
1411
|
+
* }
|
|
1412
|
+
*
|
|
1413
|
+
* @example
|
|
1414
|
+
* var auth = await oauth.deviceAuthorization();
|
|
1415
|
+
* console.log("Visit " + auth.verification_uri + " and enter " + auth.user_code);
|
|
1416
|
+
* var tokens = await oauth.pollDeviceCode(auth.device_code, { interval: auth.interval });
|
|
1417
|
+
*/
|
|
1418
|
+
async function deviceAuthorization(dopts) {
|
|
1419
|
+
dopts = dopts || {};
|
|
1420
|
+
var endpoint;
|
|
1421
|
+
try { endpoint = await _resolveEndpoint("deviceAuthorizationEndpoint"); }
|
|
1422
|
+
catch (_e) {
|
|
1423
|
+
throw new OAuthError("auth-oauth/no-device-endpoint",
|
|
1424
|
+
"deviceAuthorization: AS does not advertise device_authorization_endpoint");
|
|
1425
|
+
}
|
|
1426
|
+
var body = new URLSearchParams();
|
|
1427
|
+
body.set("client_id", clientId);
|
|
1428
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
1429
|
+
var scopes = Array.isArray(dopts.scope) ? dopts.scope : scope;
|
|
1430
|
+
if (scopes && scopes.length > 0) body.set("scope", scopes.join(" "));
|
|
1431
|
+
var parsed = await _postForm(endpoint, body);
|
|
1432
|
+
if (typeof parsed.device_code !== "string" ||
|
|
1433
|
+
typeof parsed.user_code !== "string" ||
|
|
1434
|
+
typeof parsed.verification_uri !== "string") {
|
|
1435
|
+
throw new OAuthError("auth-oauth/bad-device-response",
|
|
1436
|
+
"deviceAuthorization: response missing device_code / user_code / verification_uri");
|
|
1437
|
+
}
|
|
1438
|
+
return parsed;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* @primitive b.auth.oauth.pollDeviceCode
|
|
1443
|
+
* @signature b.auth.oauth.pollDeviceCode(deviceCode, opts?)
|
|
1444
|
+
* @since 0.8.77
|
|
1445
|
+
* @related b.auth.oauth.deviceAuthorization
|
|
1446
|
+
*
|
|
1447
|
+
* Polls the token endpoint with grant_type=urn:ietf:params:oauth:
|
|
1448
|
+
* grant-type:device_code per RFC 8628 §3.4-§3.5. Honors the slow_down
|
|
1449
|
+
* error by extending the interval; returns the token response on
|
|
1450
|
+
* success; throws on expired_token / access_denied.
|
|
1451
|
+
*
|
|
1452
|
+
* @opts
|
|
1453
|
+
* {
|
|
1454
|
+
* interval?: number, // seconds — default from deviceAuthorization()
|
|
1455
|
+
* maxWaitMs?: number, // total budget (default 600s)
|
|
1456
|
+
* }
|
|
1457
|
+
*
|
|
1458
|
+
* @example
|
|
1459
|
+
* var auth = await oauth.deviceAuthorization();
|
|
1460
|
+
* var tokens = await oauth.pollDeviceCode(auth.device_code, { interval: auth.interval });
|
|
1461
|
+
*/
|
|
1462
|
+
async function pollDeviceCode(deviceCode, popts) {
|
|
1463
|
+
popts = popts || {};
|
|
1464
|
+
if (typeof deviceCode !== "string" || deviceCode.length === 0) {
|
|
1465
|
+
throw new OAuthError("auth-oauth/bad-device-code",
|
|
1466
|
+
"pollDeviceCode: deviceCode must be a non-empty string");
|
|
1467
|
+
}
|
|
1468
|
+
var endpoint = await _resolveEndpoint("tokenEndpoint");
|
|
1469
|
+
var interval = Math.max(1, popts.interval || 5);
|
|
1470
|
+
var deadline = Date.now() + (popts.maxWaitMs || C.TIME.minutes(10));
|
|
1471
|
+
while (Date.now() < deadline) {
|
|
1472
|
+
var body = new URLSearchParams();
|
|
1473
|
+
body.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
|
1474
|
+
body.set("device_code", deviceCode);
|
|
1475
|
+
body.set("client_id", clientId);
|
|
1476
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
1477
|
+
var hc = httpClient;
|
|
1478
|
+
var req = {
|
|
1479
|
+
url: endpoint,
|
|
1480
|
+
method: "POST",
|
|
1481
|
+
headers: {
|
|
1482
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1483
|
+
"Accept": "application/json",
|
|
1484
|
+
},
|
|
1485
|
+
body: Buffer.from(body.toString(), "utf8"),
|
|
1486
|
+
};
|
|
1487
|
+
if (allowHttp) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
|
|
1488
|
+
if (allowInternal !== null) req.allowInternal = allowInternal;
|
|
1489
|
+
Object.assign(req, httpClientOpts);
|
|
1490
|
+
var res = await hc.request(req);
|
|
1491
|
+
var text = res.body ? res.body.toString("utf8") : "";
|
|
1492
|
+
var parsed;
|
|
1493
|
+
try { parsed = safeJson.parse(text, { maxBytes: OAUTH_MAX_RESPONSE_BYTES }); }
|
|
1494
|
+
catch (_e) { parsed = null; }
|
|
1495
|
+
if (res.statusCode >= 200 && res.statusCode < 300 && parsed && parsed.access_token) {
|
|
1496
|
+
return await _normalizeTokens(parsed, popts);
|
|
1497
|
+
}
|
|
1498
|
+
// RFC 8628 §3.5 — error codes that should keep polling.
|
|
1499
|
+
var err = parsed && parsed.error;
|
|
1500
|
+
if (err === "authorization_pending") {
|
|
1501
|
+
await safeAsync.sleep(C.TIME.seconds(interval));
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
if (err === "slow_down") {
|
|
1505
|
+
interval += 5;
|
|
1506
|
+
await safeAsync.sleep(C.TIME.seconds(interval));
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
// Terminal errors.
|
|
1510
|
+
throw new OAuthError("auth-oauth/device-" + (err || "unknown"),
|
|
1511
|
+
"pollDeviceCode: " + (parsed && parsed.error_description ? parsed.error_description : text.slice(0, 200))); // allow:raw-byte-literal — 200-char error-snippet cap, not bytes
|
|
1512
|
+
}
|
|
1513
|
+
throw new OAuthError("auth-oauth/device-poll-timeout",
|
|
1514
|
+
"pollDeviceCode: exceeded maxWaitMs " + (popts.maxWaitMs || C.TIME.minutes(10)));
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* @primitive b.auth.oauth.exchangeToken
|
|
1519
|
+
* @signature b.auth.oauth.exchangeToken(opts)
|
|
1520
|
+
* @since 0.8.77
|
|
1521
|
+
* @related b.auth.oauth.introspectToken
|
|
1522
|
+
*
|
|
1523
|
+
* RFC 8693 OAuth 2.0 Token Exchange. Trades a subject token (and
|
|
1524
|
+
* optionally an actor token for delegation chains) for a new
|
|
1525
|
+
* access token with different audience / scopes / authorization
|
|
1526
|
+
* context. Used by middleware tier services that need to call
|
|
1527
|
+
* downstream APIs on behalf of an upstream caller.
|
|
1528
|
+
*
|
|
1529
|
+
* @opts
|
|
1530
|
+
* {
|
|
1531
|
+
* subjectToken: string, // required
|
|
1532
|
+
* subjectTokenType: string, // required — RFC 8693 §3 URN
|
|
1533
|
+
* actorToken?: string, // delegation actor
|
|
1534
|
+
* actorTokenType?: string, // RFC 8693 §3 URN
|
|
1535
|
+
* audience?: string,
|
|
1536
|
+
* resource?: string,
|
|
1537
|
+
* scope?: string[],
|
|
1538
|
+
* requestedTokenType?: string, // default: access_token URN
|
|
1539
|
+
* }
|
|
1540
|
+
*
|
|
1541
|
+
* @example
|
|
1542
|
+
* var newTokens = await oauth.exchangeToken({
|
|
1543
|
+
* subjectToken: upstreamAccessToken,
|
|
1544
|
+
* subjectTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
|
1545
|
+
* audience: "https://downstream.example.com",
|
|
1546
|
+
* });
|
|
1547
|
+
*/
|
|
1548
|
+
async function exchangeToken(xopts) {
|
|
1549
|
+
xopts = xopts || {};
|
|
1550
|
+
if (typeof xopts.subjectToken !== "string" || xopts.subjectToken.length === 0) {
|
|
1551
|
+
throw new OAuthError("auth-oauth/bad-exchange",
|
|
1552
|
+
"exchangeToken: opts.subjectToken required");
|
|
1553
|
+
}
|
|
1554
|
+
if (typeof xopts.subjectTokenType !== "string") {
|
|
1555
|
+
throw new OAuthError("auth-oauth/bad-exchange",
|
|
1556
|
+
"exchangeToken: opts.subjectTokenType required (RFC 8693 §3 URN)");
|
|
1557
|
+
}
|
|
1558
|
+
var endpoint = await _resolveEndpoint("tokenEndpoint");
|
|
1559
|
+
var body = new URLSearchParams();
|
|
1560
|
+
body.set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange");
|
|
1561
|
+
body.set("subject_token", xopts.subjectToken);
|
|
1562
|
+
body.set("subject_token_type", xopts.subjectTokenType);
|
|
1563
|
+
body.set("client_id", clientId);
|
|
1564
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
1565
|
+
if (xopts.actorToken) body.set("actor_token", xopts.actorToken);
|
|
1566
|
+
if (xopts.actorTokenType) body.set("actor_token_type", xopts.actorTokenType);
|
|
1567
|
+
if (xopts.audience) body.set("audience", xopts.audience);
|
|
1568
|
+
if (xopts.resource) body.set("resource", xopts.resource);
|
|
1569
|
+
if (xopts.scope && xopts.scope.length > 0) {
|
|
1570
|
+
body.set("scope", xopts.scope.join(" "));
|
|
1571
|
+
}
|
|
1572
|
+
if (xopts.requestedTokenType) {
|
|
1573
|
+
body.set("requested_token_type", xopts.requestedTokenType);
|
|
1574
|
+
}
|
|
1575
|
+
var parsed = await _postForm(endpoint, body);
|
|
1576
|
+
return await _normalizeTokens(parsed, xopts);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1255
1579
|
return {
|
|
1256
1580
|
authorizationUrl: authorizationUrl,
|
|
1257
1581
|
exchangeCode: exchangeCode,
|
|
@@ -1267,6 +1591,11 @@ function create(opts) {
|
|
|
1267
1591
|
checkSessionIframeUrl: checkSessionIframeUrl,
|
|
1268
1592
|
parseCallback: parseCallback,
|
|
1269
1593
|
parseJarmResponse: parseJarmResponse,
|
|
1594
|
+
introspectToken: introspectToken,
|
|
1595
|
+
registerClient: registerClient,
|
|
1596
|
+
deviceAuthorization: deviceAuthorization,
|
|
1597
|
+
pollDeviceCode: pollDeviceCode,
|
|
1598
|
+
exchangeToken: exchangeToken,
|
|
1270
1599
|
// Diagnostic / power-user surface
|
|
1271
1600
|
issuer: issuer,
|
|
1272
1601
|
clientId: clientId,
|