@blamejs/core 0.8.72 → 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 CHANGED
@@ -8,6 +8,11 @@ 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.
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).
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.
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.
15
+ - v0.8.73 (2026-05-10) — ClusterFuzzLite + OSS-Fuzz integration replaces the hand-rolled fuzz harness. Every `fuzz/*.fuzz.js` is now a jazzer.js / libFuzzer entry-point (`module.exports.fuzz = function (data) { ... }`) so the engine drives the target with coverage-guided mutation instead of random bytes. Same 15 targets shipped in v0.8.72 (`b.safeJson.parse`, `b.safeUrl.parse`, `b.safeJsonPath.validateExpression`, the seven `b.guardX` surfaces, `b.guardEmail.validateMessage`, the four `b.parsers.X.parse` parsers); each gets a `fuzz/<name>_seed_corpus/` directory with realistic bootstrap inputs that libFuzzer mutates from. The shared `_expected.js` classifies operator-friendly framework throws (codes matching `<domain>/<error>` or `<domain>.<error>` shape; node-builtin error subclasses with input-shape messaging) as expected outcomes; anything else escapes as a finding the engine records + minimizes into a regression-corpus entry. **ClusterFuzzLite (local, free)**: `.clusterfuzzlite/Dockerfile` + `build.sh` + `project.yaml` ride alongside the framework source. Two GH Actions workflows wire it in — `cflite_pr.yml` runs 300s of coverage-guided fuzzing per target on every PR touching `lib/` or `fuzz/`; `cflite_batch.yml` runs the deeper 1800s batch + 600s coverage measurement on a daily 05:17 UTC schedule. Findings surface as PR annotations + SARIF in the Security tab. **OSS-Fuzz (upstream, continuous on Google's infrastructure)**: `oss-fuzz/projects/blamejs/{Dockerfile,build.sh,project.yaml,README.md}` is the submission-ready project config that gets copy-pasted into `projects/blamejs/` in the `google/oss-fuzz` upstream repo. Once accepted, ClusterFuzz fuzzes 24/7 on Google Cloud with permanent corpus persistence, stack-trace dedup, automatic regression testing against every commit, and a public coverage dashboard. The OSS-Fuzz `build.sh` mirrors `.clusterfuzzlite/build.sh` byte-for-byte (modulo comment block) so findings reproduce identically locally. **Coverage gate**: `testParserPrimitivesHaveFuzzHarness` in `test/layer-0-primitives/codebase-patterns.test.js` now verifies the jazzer.js shape (`module.exports.fuzz = ...`) in addition to the missing-harness check — a future parser primitive lands either with a coverage-guided harness or an audited `FUZZ_NOT_REQUIRED` entry. `npm run fuzz` switched to invoke jazzer.js for one-target local dev (`npx @jazzer.js/core fuzz/safe-json.fuzz.js -- -max_total_time=60`); the previous `fuzz/_runner.js` + `fuzz/_run-all.js` random-fuzzer + the standalone `.github/workflows/fuzz.yml` are removed. SECURITY.md threat-model + operator-checklist updated with the new dual-pipeline posture.
11
16
  - v0.8.72 (2026-05-10) — fuzz harness against the parser / validator surface + smoke-time fuzz-coverage gate. New `fuzz/` directory ships hand-rolled fuzz harnesses against the 11 highest-value adversarial-input primitives — `b.safeJson.parse`, `b.safeUrl.parse`, `b.safeJsonPath.validateExpression`, `b.guardCsv.validate`, `b.guardHtml.validate`, `b.guardJson.parse`, `b.guardYaml.parse`, `b.guardXml.validate`, `b.guardSvg.validate`, `b.guardMarkdown.validate`, `b.guardEmail.validateMessage`. Each harness generates random / mutated / bidi-salted / control-char-salted inputs against a per-target seed corpus, runs until `FUZZ_BUDGET_MS` elapses (default 30s; CI: 60s on PR / 300s on schedule), and fails with a reproducer when the target throws an unexpected error (vs. an operator-friendly framework error code in the documented `domain/error` or `domain.error` shape). Native `TypeError` with input-shape messaging, `SyntaxError`, and `RangeError` matching the depth/length/cap contract are accepted; everything else is a finding. New `.github/workflows/fuzz.yml` runs the harness in matrix on every PR touching `lib/` or `fuzz/` and on a daily 05:17 UTC schedule. New Layer 0 detector `testParserPrimitivesHaveFuzzHarness` in `test/layer-0-primitives/codebase-patterns.test.js` enforces that every `lib/safe-*.js` and `lib/guard-*.js` file has a corresponding `fuzz/<name>.fuzz.js` OR an explicit `FUZZ_NOT_REQUIRED` allowlist entry with reason — so a future parser primitive can't silently ship without fuzz coverage. `npm run fuzz` runs every harness sequentially via `fuzz/_run-all.js` for local dev. README OpenSSF Scorecard badge URL fixed (`api.scorecards.dev` → `api.scorecard.dev` — plural-singular typo).
12
17
  - v0.8.71 (2026-05-10) — CI green-up for v0.8.70. The v0.8.70 npm-publish workflow's cosign-sign-blob step couldn't resolve `sigstore/cosign-installer@d7d6e07b3e89342f1d8bcd4f76c2fa5a9d1a1f7e` — the SHA was a typo, not a real commit on the action's repo. Replaced with the actual v3.7.0 commit SHA `dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da`. No primitive surface change versus v0.8.70.
13
18
  - v0.8.70 (2026-05-10) — six-batch additive surface across OAuth/OIDC, FAPI 2.0, browser hardening, MCP safety, compliance postures, and supply-chain. **OAuth/OIDC**: `b.auth.oauth.parseCallback(query, opts?)` validates RFC 9207 AS Issuer Identifier (refuses iss-mismatch and OP `error=` redirects, optional `requireIssParam` to refuse missing iss), `parseJarmResponse(jwt, opts?)` decodes OAuth 2.0 JARM signed authorization responses, and `refreshAccessToken(token, { seen })` accepts an operator-supplied callback that refuses replayed refresh tokens before any HTTP call (RFC 9700 §4.13 / OAuth 2.1 §6.1 one-time-use rotation; returns `refreshTokenRotated: true` on success). **FAPI 2.0 runtime**: `b.fapi2.assertCallback(query)` refuses missing iss when `fapi-2.0` posture is set (and refuses bare-param when `fapi-2.0-message-signing` is set, requiring JARM `response`); `b.fapi2.assertAuthzRequest(authzParams)` refuses non-JAR (bare-param) authorization requests under FAPI 2.0. New `fapi-2.0-message-signing` posture registered. **Browser hardening**: `Permissions-Policy` defaults extend with `storage-access=()`, `browsing-topics=()`, `private-aggregation=()`, `controlled-frame=()`, `captured-surface-control=()`; `b.middleware.cors` gains `allowPrivateNetwork` opt + Private Network Access preflight handling (refuses `Access-Control-Request-Private-Network` by default, sets `Access-Control-Allow-Private-Network: true` when opted in); `b.middleware.requireAuth` / `requireAal` / `requireStepUp` 401 responses now set `Cache-Control: no-store` (RFC 9111 §5.2.2.5). **MCP safety + LLM07/08**: `b.mcp.toolResult.sanitize(result, opts?)` runs prompt-injection regex + dangerous-HTML detection + URL-allowlist on tool outputs (modes `refuse` / `sanitize` / `audit-only`); `b.mcp.capability.create(scopes)` + `satisfiedBy(granted)` formalize least-privilege capability checks; `b.mcp.validateToolInput(toolName, input, schema)` enforces a JSON Schema 2020-12 subset on tool inputs (`type` / `properties` / `required` / `items` / `enum` / `const` / `minLength` / `maxLength` / `minimum` / `maximum`). **Compliance postures**: `modpa` (Maryland Online Data Privacy Act, US-MD privacy), `nydfs-500` (NY DFS Cybersecurity Regulation, US-NY financial), `hipaa-2026` (HHS Final Rule effective 2026, US health), `quebec-25` (Quebec Law 25, CA-QC privacy), and `fapi-2.0-message-signing` (INTL financial). **EU Data Act** (Regulation 2023/2854): new `b.dataAct` primitive — `declareProduct`, `recordUserAccess`, `shareWithThirdParty` (Art 32 §1 refuses sharing with DMA designated gatekeepers without an audited override `acceptGatekeeper.reason`), `recordSwitchRequest` (Art 28 §3 caps notice period at 30 days). **Supply chain**: SBOM bumped to CycloneDX 1.6; npm-publish workflow now runs OSV-Scanner with `--fail-on-vuln=HIGH`, signs the SBOM via Sigstore cosign keyless flow (attaches `.sigstore` bundle to the GH release alongside the JSON); `scripts/publish-dep-confusion-placeholder.sh` claims unscoped names (`blamejs`, `blame-js`, `blamejs-core`) on npm with placeholder packages that exit-1 + redirect to canonical `@blamejs/core` (defends against dependency-confusion typosquats — manual, run on maintainer rotation, refuses overwrite when a different owner already holds the name).
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,