@blamejs/core 0.10.15 → 0.11.1

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
@@ -6,6 +6,11 @@ Pre-1.0 the surface is intentionally evolving — every release may
6
6
  change something operators depend on. Read each entry before
7
7
  upgrading across more than a few patches at a time.
8
8
 
9
+ ## v0.11.x
10
+
11
+ - v0.11.1 (2026-05-19) — **Integration suite hardening + live coverage for the v0.11.0 surface.** **`b.httpClient.request`** now skips the local SSRF DNS lookup when a proxy is configured AND the operator passes `allowInternal: true`. The proxy resolves the destination hostname in its own network context, so requiring local resolution refused legitimate intranet / docker-service-name targets routed through the proxy. The SSRF gate still runs when `allowInternal` is false / array-form (the proxy's freedom to reach internal IPs is not a blanket license; the explicit opt-in is still required). **`b.mtlsCa`** integration tests now compose with the `caKeySealedMode: "disabled"` opt for fixture purposes; production deployments continue to wire `opts.vault` for sealed-at-rest CA-key storage. **`b.mail.crypto.smime.verify`** return shape gains a `chainVerified: boolean` field reflecting whether `opts.trustAnchorCertsPem` was supplied and the leaf-to-root chain walk completed. **Integration coverage added for v0.11.0 primitives:** new `test/integration/mail-crypto-smime.test.js` round-trips S/MIME sign + verify with a real X.509 chain issued by `b.mtlsCa` (CA → leaf cert → ML-DSA-65 signer), exercises tamper / wrong-key / untrusted-anchor refusal paths, and validates the `chainVerified` return field. `test/integration/federation-auth.test.js` extends to cover SAML SLO (`buildLogoutRequest` against Keycloak's `/protocol/saml` SLO endpoint with the wire-format-parse assertion) and RFC 7592 Dynamic Client Registration Management (`registerClient` / `readClient` / `updateClient` / `deleteClient` against Keycloak's DCR endpoint).
12
+
13
+ - v0.11.0 (2026-05-19) — **Mail-crypto sign/verify + encrypt/decrypt, SAML Single Logout (Redirect / POST / SOAP) + EncryptedAssertion + Holder-of-Key, browser identity (FedCM / DBSC / VAPID), CSP3 builder, hypermedia + observability formats, OAuth DCR management + OIDC Native SSO, sectoral compliance posture growth.** **`b.cms.parseSignedData(buf)`** walks an inbound RFC 5652 SignedData ContentInfo and returns `{ digestAlgs, encapContent, certificates, signerInfos }` so consumers verify signatures without re-implementing the SignedData walker. **`b.mail.crypto.smime.sign(opts)` + `.verify(opts)`** are now live on the `b.cms` substrate: `sign` emits an RFC 8551 `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha3-{256,512}` envelope; `verify` recomputes the message digest, compares it against the signed-attrs `messageDigest` attribute, refuses tamper with `mail-crypto/smime/message-digest-mismatch`, and PQC-verifies the signature against the operator-supplied signer public key. Supports ML-DSA-65 / ML-DSA-87 / SLH-DSA-SHAKE-256f signers; SHA-2 family refused at `cms/bad-digest`. **`b.mail.crypto.pgp.experimental.encrypt(opts)` + `.decrypt(opts)` + `.wkd.computeUrl(email)`** ship under the `experimental` namespace because the relevant IANA codepoint registrations are still in draft: ML-KEM-1024 KEM + ChaCha20-Poly1305 AEAD multi-recipient envelope; WKD URL computer per [draft-koch-openpgp-webkey-service](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/) (SHAKE256-hash localpart + zbase32 encoding; operators supply their own HTTPS fetcher). **`b.auth.saml.sp.buildLogoutRequest({...})` + `parseLogoutRequest(b64, opts)` + `buildLogoutResponse({...})`** implement SAML 2.0 Single Logout on the HTTP-Redirect binding per SAML Bindings §3.4.4.1 with PQC-signed canonical query (ML-DSA-65 / ML-DSA-87 / Ed25519); SP metadata now emits `<md:SingleLogoutService>` bindings when `singleLogoutServiceUrl` is set; tamper / wrong-key / missing-signature each refuse as typed errors. **`b.webPush.generateVapidKeypair()` + `.buildVapidAuthHeader(opts)`** sign the RFC 8292 VAPID JWT inline (ECDSA-P256 per the spec; the framework's PQC-default JWT signer refuses ES256 by design, so `b.webPush` owns the signing rather than relaxing the broader policy). **`b.fedcm`** ships the W3C FedCM 2024 IdP-side response builders: `wellKnown({ provider_urls })` / `config({ accounts_endpoint, ... })` / `accountsResponse({ accounts })` / `idAssertionResponse({ token })`. **`b.dbsc`** implements IETF Device-Bound Session Credentials: `challenge({ secretKey })` mints an HMAC-SHA3-512-signed token requiring no server-side storage; `verifyBindingAssertion(jwt, opts)` refuses HS256 / none as algorithm-confusion, validates ES256/RS256 against the embedded JWK, and returns the RFC 7638 JWK thumbprint so operators pin the binding key to a session. **`b.importmapIntegrity.build({ modules })`** emits a WICG Import Maps + SRI integrity map (SHA-384 default) so browsers refuse module bytes that don't match. **OpenMetrics 1.0 exposition** — `b.metrics` registry `exposition({ format: "openmetrics" })` emits the counter `_total` suffix, `# UNIT` lines, exemplar trace IDs on histograms, and `# EOF` terminator per the openmetrics.io 1.0 wire format. **`b.standardWebhooks.sign + verify`** implement the standardwebhooks.com consortium spec (Stripe / Svix / Okta wire format): HMAC-SHA256, multi-version signature header, 5-minute default skew tolerance. **`b.lro`** implements AIP-151 Long-Running Operations (`create({ store }) → { submit, status, list, cancel }`) with operator-supplied storage and AbortSignal-aware cancellation. **`b.jsonApi.dataResponse(data, opts) + .errorResponse(errors)`** wraps domain payloads in the JSON:API v1.1 top-level shape; refuses missing Resource Object `type`. **`b.hal.resource(payload, { links, embedded, templates })`** builds HAL responses (draft-kelly-json-hal) with an RFC 8288 link-object normaliser. **Lib-side codebase-patterns detector `number-coerce-or-zero-on-json-source`** refuses `Number(<var>['<kebab-cased-key>']) || 0` shapes — that coercion silently accepts `Infinity`, NaN, negative values, and arbitrary strings on operator-untrusted JSON-source input. **(a) `b.cms.parseSignedData(buf)`** — RFC 5652 §5.1 SignedData walker that surfaces `digestAlgs` / `encapContent` / `certificates` / `signerInfos` as structured arrays so downstream verifiers can check signatures without re-implementing the walker. **(b) `b.mail.crypto.smime.sign(opts)` + `b.mail.crypto.smime.verify(opts)` — LIVE on the b.cms substrate.** `sign` composes `b.cms.encodeSignedData` and wraps in an RFC 8551 `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha3-{256,512}` envelope. `verify` parses the CMS SignedData payload, walks signed-attributes to extract the `messageDigest` attribute, recomputes the message digest, refuses tamper with `mail-crypto/smime/message-digest-mismatch`, and PQC-verifies the signature against the operator-supplied signer public key. Supports ML-DSA-65 / ML-DSA-87 / SLH-DSA-SHAKE-256f signers; SHA-2 family refused at `cms/bad-digest`. **(c) `b.mail.crypto.pgp.experimental.encrypt(opts)` + `decrypt(opts)` + `wkd.computeUrl(email)`** — PQC PGP encrypt/decrypt under `experimental` namespace (RFC 9580bis PKESK ML-KEM codepoints haven't IANA-registered yet; framework-private envelope similar to v0.10.10 `b.jose.jwe.experimental`). ML-KEM-1024 KEM + ChaCha20-Poly1305 AEAD + per-recipient KEK derived via SHAKE256 bound to the literal label `pgp/experimental/chacha20-poly1305`. Multi-recipient envelopes; tamper / wrong-key refusal as typed errors. WKD URL computer per [draft-koch-openpgp-webkey-service](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/) — SHAKE256-hash localpart + zbase32 encoding; returns `{ direct, advanced }` URLs (operators supply their own HTTPS fetcher). **(d) `b.auth.saml.sp.buildLogoutRequest({...})` + `parseLogoutRequest(b64, opts)` + `buildLogoutResponse({...})`** — SAML 2.0 Single Logout on the HTTP-Redirect binding per SAML Bindings §3.4.4.1 with PQC-signed canonical query string (ML-DSA-65 / ML-DSA-87 / Ed25519). `parseLogoutRequest` inflates the operator-supplied SAMLRequest, optionally verifies the redirect-binding signature against an IdP public key, and surfaces NameID / SessionIndex / Issuer. Tamper / wrong-key / missing-signature each refuse as typed errors. Closes the largest item from the v0.10.15 SAML SLO + identity-residual queued plan. **(e) Codex P2 detector `number-coerce-or-zero-on-json-source`** (lib-side) — flags `Number(<var>['<kebab-cased-key>']) || 0` shapes that silently accept Infinity / NaN / negative on operator-untrusted JSON-source numeric input. The v0.10.15 TLS-RPT fix was a one-off; this detector forces the discipline. **Additional residual closure:** `b.auth.saml.sp.buildLogoutRequestPost` / `parseLogoutRequestPost` / `buildLogoutRequestSoap` / `parseLogoutResponseSoap` add SAML SLO HTTP-POST + SOAP synchronous back-channel bindings with embedded XMLDSig-Enveloped signatures. **SignatureMethod surface** spans the W3C XMLDSig Core 1.1 + RFC 9231 vocabulary so the framework interops with deployed IdPs out of the box: `rsa-sha256` / `rsa-sha384` / `rsa-sha512` (W3C XMLDSig Core 1.1), `ecdsa-sha256` / `ecdsa-sha384` / `ecdsa-sha512` (W3C XMLDSig Core 1.1), `ed25519` (RFC 9231). Classical keys are PEM strings or `node:crypto` KeyObject instances; PQC keys remain `Uint8Array` from `b.pqcSoftware.ml_dsa_*.keygen()`. Post-quantum `ml-dsa-65` / `ml-dsa-87` are also accepted on the same surface under clearly framework-private URIs (`urn:blamejs:experimental:saml-sig-alg:ml-dsa-65` / `:ml-dsa-87`) — no IETF/W3C XMLDSig registration exists for ML-DSA yet (LAMPS WG drafts in flight). Verification refuses `'unsupported-c14n'` (only exclusive c14n; inclusive-c14n IdPs must upgrade), alg-confusion (SignatureMethod URI must match the operator-declared `idpVerifyAlg`), signature-wrapping (Reference URI must match root ID via timing-safe digest compare), and SHA-1 digest methods (CVE-2017-7525-class). **EncryptedAssertion (SAML 2.0 §2.5)** decrypts AES-128-GCM / AES-256-GCM (W3C XMLEnc 1.1 §5.2.4) content + RSA-OAEP-MGF1P / xmlenc11 RSA-OAEP key transport (SHA-256/384/512 only — SHA-1 OAEP refused as CVE-2023-49141-class). AES-CBC content encryption is **refused** under both `xmlenc#aes128-cbc` and `xmlenc#aes256-cbc` — CVE-2011-1473 padding-oracle research makes CBC mode under XMLEnc unsuitable without a per-content MAC; operators integrating with IdPs that default to CBC (older ADFS / Azure AD / Okta / Keycloak / OneLogin) switch the IdP's content-encryption setting to AES-128-GCM or AES-256-GCM. Framework-experimental URIs `urn:blamejs:experimental:xmlenc:ml-kem-1024` (key transport) and `urn:blamejs:experimental:xmlenc:xchacha20-poly1305` (content) are accepted alongside the W3C URIs. `b.mail.crypto.smime.verify({ trustAnchorCertsPem })` now walks the SignerInfo cert chain leaf-to-root via `node:crypto.X509Certificate` with notBefore/notAfter checks and refuses `mail-crypto/smime/untrusted-chain` / `cert-expired` / `cert-not-yet-valid`; revocation is operator-wired via `b.network.tls.ocsp` when freshness is required. `b.auth.saml.sp.verifyResponse({ holderOfKey: { presentedCertPem } })` honors `urn:oasis:names:tc:SAML:2.0:cm:holder-of-key` SubjectConfirmation: SHA3-512 fingerprint of the embedded KeyInfo/X509Data certificate is compared against the operator-supplied presented mTLS / possession-proof cert via timingSafeEqual; HoK and Bearer confirmations coexist. `b.auth.oauth.readClient(uri, token)` / `updateClient(uri, token, metadata)` / `deleteClient(uri, token)` implement RFC 7592 Dynamic Client Registration Management Protocol (GET/PUT/DELETE bound to the registration_access_token returned by `registerClient`); updateClient enforces the same redirect_uris-array refusal as registerClient. `b.auth.oauth.nativeSsoExchange({ deviceSecret, idToken, audience })` convenience-wraps `exchangeToken` for OpenID Connect Native SSO 1.0 §6 with `urn:openid:params:token-type:device-secret` added to the RFC 8693 §3 token-type allowlist. **`b.csp.build(directives, opts?)` + `b.csp.nonce(byteLen?)` + `b.csp.hash(scriptBody, alg?)`** ship the CSP Level 3 builder surface: refuses `'unsafe-*'` / catch-all `*` / `https:` / `data:` in non-image directives without an explicit acknowledgement opt; auto-appends `require-trusted-types-for 'script'` plus the operator-supplied `trusted-types` policy list when any script-* directive is set; emits ≥128-bit nonces by default; computes sha256/sha384/sha512 hash sources for inline scripts. `b.middleware.securityHeaders` `coep` opt now documents the W3C CR 2024-12 `credentialless` value alongside `require-corp`. **`b.compliance` posture catalog gains 17 sectoral / cybersecurity / AI-governance regimes:** `42-cfr-part-2`, `hti-1`, `uscdi-v4`, `irs-1075`, `nist-800-172-r3`, `tlp-2.0`, `soci-au`, `nis2`, `cra`, `ffiec-cat-2`, `cri-profile-v2.0`, `m-22-09`, `m-22-18`, `nist-800-53-r5-privacy`, `nist-ai-600-1-genai`, `nist-csf-2.0`, `sb-53`, `nyc-ll144-2024`. Each posture cascade pins the regime's normative floor (backupEncryptionRequired / auditChainSignedRequired / tlsMinVersion / requireVacuumAfterErase). **References:** [RFC 5652 CMS](https://www.rfc-editor.org/rfc/rfc5652.html) · [RFC 8551 S/MIME 4.0](https://www.rfc-editor.org/rfc/rfc8551.html) · [RFC 9580 OpenPGP](https://www.rfc-editor.org/rfc/rfc9580.html) · [draft-koch-openpgp-webkey-service](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/) · [SAML Bindings §3.4 HTTP-Redirect + §3.5 HTTP-POST + §3.2 SOAP](https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf) · [SAML 2.0 §2.5 EncryptedAssertion](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf) · [W3C XML-Encryption 1.1](https://www.w3.org/TR/xmlenc-core1/) · [RFC 7592 Dynamic Client Management](https://www.rfc-editor.org/rfc/rfc7592.html) · [OpenID Connect Native SSO 1.0](https://openid.net/specs/openid-connect-native-sso-1_0.html) · [W3C CSP Level 3](https://www.w3.org/TR/CSP3/) · [W3C Trusted Types](https://www.w3.org/TR/trusted-types/).
9
14
  ## v0.10.x
10
15
 
11
16
  - v0.10.15 (2026-05-18) — **TLS-RPT receiver — RFC 8460 aggregate-report ingest.** New primitive surface under `b.mail.deploy` that closes the receive-side of TLS-RPT (the publish-side shipped v0.7.29 + v0.9.56). **(a) `b.mail.deploy.parseTlsRptReport(bytes, opts?)`** — pure parser + RFC 8460 §4.4 schema validator. Accepts `application/tlsrpt+json` (raw) and `application/tlsrpt+gzip` (auto-detected via the RFC 1952 gzip magic bytes `0x1f 0x8b` or routed when `opts.contentType` names a gzip media-type). Caps compressed payload at 4 MiB (RFC 8460 §5.2 community ceiling), decompressed at 32 MiB (operator-overridable), and refuses decompression amplification > 50:1 — defends [CVE-2025-0725](https://nvd.nist.gov/vuln/detail/CVE-2025-0725) (libcurl + zlib decompression amplification) and the broader CVE-2024-zlib bomb class. Routes through `b.guardJson.parse` for proto-pollution / depth / key-count defenses before walking the §4.4 schema. Refuses on missing required fields (`organization-name` / `contact-info` / `report-id` / `date-range.{start,end}-datetime` / `policies`) and enforces the §4.4 erratum that `policies` MUST be a non-empty array even for single-policy reports. Returns the normalized report shape plus `sessionTotals: { success, failure }` and a `wasCompressed` flag. **(b) `b.mail.deploy.tlsRptIngestHttp({...})`** — factory returning an `(req, res)` HTTPS POST handler mounted at the operator's `rua=https://<host>/<path>` endpoint per RFC 8460 §5.4. Negotiates the two IANA-registered media types ([RFC 8460 §6.4-6.5](https://www.rfc-editor.org/rfc/rfc8460.html#section-6.4)), returns 405 on non-POST, 415 on bad media-type (with `Accept:` header), 413 on size / bomb / ratio refusal, 400 on parse failure (with `Error-Type:` header naming the typed error code), 201 on accept. Optional `trustedReporters` array refuses non-trusted reporting domains (RFC 8460 §5.3-class defense extended to the HTTPS path). Body collection routes through `b.safeBuffer.boundedChunkCollector` — cap enforced at every `push()`, not after — so a hostile reporter sending a 10-GB body rejects on the chunk that overflows. Emits the `mail.tlsrpt.ingest_http` audit event with `policyDomains` set + session totals on every accept / refuse. **(c) `b.mail.deploy.tlsRptReportSchema()`** — schema descriptor (required fields, policy types, result types) for operator dashboards. Pure function. **(d) Codebase-patterns detector `gunzip-without-output-size-cap`** (lib-side) — every `zlib.gunzipSync` / `zlib.createGunzip` / `zlib.brotliDecompressSync` MUST sit in a file that also names `maxOutputLength` (Node-native cap) per the CVE-2025-0725 defense class. Companion-check `requires` field added to the lib-side runner. **Deferred from v1:** mailto: ingest (no operator demand surfaced — HTTPS POST is the de-facto deployment shape for TLS-RPT; operators wanting mailto: today compose `b.mail.server.mx` + `parseTlsRptReport`) and brotli decompression (no fielded reporter uses `Content-Encoding: br` for TLS-RPT; the RFC 8460 §6.4-6.5 IANA registry only names `+json` and `+gzip`). Each reopens with a documented condition. References: [RFC 8460 SMTP TLS Reporting](https://www.rfc-editor.org/rfc/rfc8460.html) · [RFC 8461 MTA-STS](https://www.rfc-editor.org/rfc/rfc8461.html) · [RFC 1952 gzip](https://www.rfc-editor.org/rfc/rfc1952.html) · [CVE-2025-0725](https://nvd.nist.gov/vuln/detail/CVE-2025-0725).
package/index.js CHANGED
@@ -377,11 +377,29 @@ var daemon = require("./lib/daemon");
377
377
  var selfUpdate = require("./lib/self-update");
378
378
  var cmsCodec = require("./lib/cms-codec");
379
379
  var streamThrottle = require("./lib/stream-throttle");
380
+ var webPush = require("./lib/web-push-vapid");
381
+ var fedcm = require("./lib/fedcm");
382
+ var dbsc = require("./lib/dbsc");
383
+ var importmapIntegrity = require("./lib/importmap-integrity");
384
+ var standardWebhooks = require("./lib/standard-webhooks");
385
+ var lro = require("./lib/lro");
386
+ var jsonApi = require("./lib/jsonapi");
387
+ var hal = require("./lib/hal");
388
+ var csp = require("./lib/csp");
380
389
 
381
390
  module.exports = {
382
391
  crypto: crypto,
383
392
  cms: cmsCodec,
393
+ csp: csp,
384
394
  streamThrottle: streamThrottle,
395
+ webPush: webPush,
396
+ fedcm: fedcm,
397
+ dbsc: dbsc,
398
+ importmapIntegrity: importmapIntegrity,
399
+ standardWebhooks: standardWebhooks,
400
+ lro: lro,
401
+ jsonApi: jsonApi,
402
+ hal: hal,
385
403
  router: router,
386
404
  constants: constants,
387
405
  vault: vault,
package/lib/auth/oauth.js CHANGED
@@ -237,6 +237,10 @@ var RFC_8693_TOKEN_TYPES = Object.freeze([
237
237
  "urn:ietf:params:oauth:token-type:saml1",
238
238
  "urn:ietf:params:oauth:token-type:saml2",
239
239
  "urn:ietf:params:oauth:token-type:jwt",
240
+ // openid-native-sso-1_0 §6 — device_secret is the token type
241
+ // carrying the per-device long-lived secret returned alongside
242
+ // id_token during native-sso-aware authentication.
243
+ "urn:openid:params:token-type:device-secret",
240
244
  ]);
241
245
 
242
246
  // ---- helpers ----
@@ -1691,6 +1695,131 @@ function create(opts) {
1691
1695
  return parsed;
1692
1696
  }
1693
1697
 
1698
+ /**
1699
+ * @primitive b.auth.oauth.readClient
1700
+ * @signature b.auth.oauth.readClient(registrationClientUri, registrationAccessToken)
1701
+ * @since 0.10.16
1702
+ * @status stable
1703
+ * @related b.auth.oauth.registerClient, b.auth.oauth.updateClient, b.auth.oauth.deleteClient
1704
+ *
1705
+ * RFC 7592 §2.1 OAuth 2.0 Dynamic Client Registration Management
1706
+ * Protocol — read the current client configuration via GET against
1707
+ * the operator-supplied `registration_client_uri` carrying the
1708
+ * `registration_access_token`. Returns the AS's full client metadata.
1709
+ *
1710
+ * @example
1711
+ * var meta = await oauth.readClient(rv.registration_client_uri,
1712
+ * rv.registration_access_token);
1713
+ */
1714
+ async function readClient(registrationClientUri, registrationAccessToken) {
1715
+ return _dcrManagementCall("GET", registrationClientUri, registrationAccessToken, null);
1716
+ }
1717
+
1718
+ /**
1719
+ * @primitive b.auth.oauth.updateClient
1720
+ * @signature b.auth.oauth.updateClient(registrationClientUri, registrationAccessToken, metadata)
1721
+ * @since 0.10.16
1722
+ * @status stable
1723
+ *
1724
+ * RFC 7592 §2.2 update the dynamically-registered client's metadata
1725
+ * via PUT. The AS may rotate `registration_access_token` / regenerate
1726
+ * `client_secret` in the response — operators MUST persist the new
1727
+ * values atomically with the update.
1728
+ *
1729
+ * @example
1730
+ * var updated = await oauth.updateClient(
1731
+ * rv.registration_client_uri,
1732
+ * rv.registration_access_token,
1733
+ * { redirect_uris: ["https://rp.example/cb-new"],
1734
+ * grant_types: ["authorization_code", "refresh_token"] });
1735
+ */
1736
+ async function updateClient(registrationClientUri, registrationAccessToken, metadata) {
1737
+ if (!metadata || typeof metadata !== "object") {
1738
+ throw new OAuthError("auth-oauth/bad-update",
1739
+ "updateClient: metadata must be an object");
1740
+ }
1741
+ if (!Array.isArray(metadata.redirect_uris) || metadata.redirect_uris.length === 0) {
1742
+ throw new OAuthError("auth-oauth/update-no-redirect-uris",
1743
+ "updateClient: metadata.redirect_uris must be a non-empty array " +
1744
+ "(same posture as registerClient — RFC 7591/7592 makes it optional, " +
1745
+ "operating without explicit URIs creates an open-redirect surface)");
1746
+ }
1747
+ for (var ri = 0; ri < metadata.redirect_uris.length; ri++) {
1748
+ _validateUrl(metadata.redirect_uris[ri], allowHttp,
1749
+ "metadata.redirect_uris[" + ri + "]");
1750
+ }
1751
+ return _dcrManagementCall("PUT", registrationClientUri, registrationAccessToken, metadata);
1752
+ }
1753
+
1754
+ /**
1755
+ * @primitive b.auth.oauth.deleteClient
1756
+ * @signature b.auth.oauth.deleteClient(registrationClientUri, registrationAccessToken)
1757
+ * @since 0.10.16
1758
+ * @status stable
1759
+ *
1760
+ * RFC 7592 §2.3 deregister the dynamically-registered client via
1761
+ * DELETE. The AS responds 204 No Content on success; this primitive
1762
+ * returns true / throws on failure (404 = client already gone is
1763
+ * surfaced as a specific error so the caller can swallow it).
1764
+ *
1765
+ * @example
1766
+ * await oauth.deleteClient(rv.registration_client_uri,
1767
+ * rv.registration_access_token);
1768
+ */
1769
+ async function deleteClient(registrationClientUri, registrationAccessToken) {
1770
+ await _dcrManagementCall("DELETE", registrationClientUri, registrationAccessToken, null);
1771
+ return true;
1772
+ }
1773
+
1774
+ async function _dcrManagementCall(method, registrationClientUri, registrationAccessToken, body) {
1775
+ if (typeof registrationClientUri !== "string" || registrationClientUri.length === 0) {
1776
+ throw new OAuthError("auth-oauth/bad-registration-client-uri",
1777
+ method.toLowerCase() + "Client: registrationClientUri must be a non-empty string");
1778
+ }
1779
+ if (typeof registrationAccessToken !== "string" || registrationAccessToken.length === 0) {
1780
+ throw new OAuthError("auth-oauth/bad-registration-access-token",
1781
+ method.toLowerCase() + "Client: registrationAccessToken must be a non-empty string");
1782
+ }
1783
+ _validateUrl(registrationClientUri, allowHttp, "registrationClientUri");
1784
+ var headers = {
1785
+ "Authorization": "Bearer " + registrationAccessToken,
1786
+ "Accept": "application/json",
1787
+ };
1788
+ var req = {
1789
+ url: registrationClientUri,
1790
+ method: method,
1791
+ headers: headers,
1792
+ };
1793
+ if (body !== null) {
1794
+ headers["Content-Type"] = "application/json";
1795
+ req.body = Buffer.from(safeJson.stringify(body), "utf8");
1796
+ }
1797
+ if (allowHttp) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
1798
+ if (allowInternal !== null) req.allowInternal = allowInternal;
1799
+ Object.assign(req, httpClientOpts);
1800
+ var res = await httpClient.request(req);
1801
+ if (method === "DELETE") {
1802
+ if (res.statusCode === 204 || res.statusCode === 200) return null;
1803
+ if (res.statusCode === 404) {
1804
+ throw new OAuthError("auth-oauth/dcr-not-found",
1805
+ "deleteClient: 404 — registrationClientUri does not resolve to a client");
1806
+ }
1807
+ throw new OAuthError("auth-oauth/dcr-delete-failed-" + res.statusCode,
1808
+ "deleteClient: " + res.statusCode);
1809
+ }
1810
+ if (res.statusCode < 200 || res.statusCode >= 300) {
1811
+ var errText = res.body ? res.body.toString("utf8").slice(0, 500) : "";
1812
+ throw new OAuthError("auth-oauth/dcr-" + method.toLowerCase() + "-failed-" + res.statusCode,
1813
+ method.toLowerCase() + "Client: " + res.statusCode + ": " + errText);
1814
+ }
1815
+ var text = res.body ? res.body.toString("utf8") : "";
1816
+ try { return safeJson.parse(text, { maxBytes: OAUTH_MAX_RESPONSE_BYTES }); }
1817
+ catch (e) {
1818
+ throw new OAuthError("auth-oauth/dcr-bad-response",
1819
+ method.toLowerCase() + "Client: response not JSON: " + ((e && e.message) || String(e)));
1820
+ }
1821
+ }
1822
+
1694
1823
  /**
1695
1824
  * @primitive b.auth.oauth.deviceAuthorization
1696
1825
  * @signature b.auth.oauth.deviceAuthorization(opts?)
@@ -1911,6 +2040,60 @@ function create(opts) {
1911
2040
  return await _normalizeTokens(parsed, xopts);
1912
2041
  }
1913
2042
 
2043
+ /**
2044
+ * @primitive b.auth.oauth.nativeSsoExchange
2045
+ * @signature b.auth.oauth.nativeSsoExchange(opts)
2046
+ * @since 0.10.16
2047
+ * @status stable
2048
+ * @related b.auth.oauth.exchangeToken
2049
+ *
2050
+ * OpenID Connect Native SSO 1.0 §6 — exchange a `device_secret` +
2051
+ * `id_token` pair for a fresh access token for a different client
2052
+ * on the same device (the "second app SSO" pattern). Composes
2053
+ * exchangeToken with the Native-SSO requested-token-type +
2054
+ * device-secret URNs.
2055
+ *
2056
+ * The device_secret comes from the AS in the same response body as
2057
+ * id_token on the initial authentication when the AS supports Native
2058
+ * SSO; sibling apps on the same device get it via a platform IPC
2059
+ * channel.
2060
+ *
2061
+ * @opts
2062
+ * {
2063
+ * deviceSecret: string, // required — opaque device_secret from initial auth
2064
+ * idToken: string, // required — last-seen id_token bound to the device_secret
2065
+ * audience?: string, // optional — second app's client_id / resource indicator
2066
+ * scope?: string[],
2067
+ * }
2068
+ *
2069
+ * @example
2070
+ * var tokens = await oauth.nativeSsoExchange({
2071
+ * deviceSecret: secondAppRequest.deviceSecret,
2072
+ * idToken: secondAppRequest.idToken,
2073
+ * audience: "second-app-client-id",
2074
+ * });
2075
+ */
2076
+ async function nativeSsoExchange(nopts) {
2077
+ nopts = nopts || {};
2078
+ if (typeof nopts.deviceSecret !== "string" || nopts.deviceSecret.length === 0) {
2079
+ throw new OAuthError("auth-oauth/bad-native-sso",
2080
+ "nativeSsoExchange: opts.deviceSecret required");
2081
+ }
2082
+ if (typeof nopts.idToken !== "string" || nopts.idToken.length === 0) {
2083
+ throw new OAuthError("auth-oauth/bad-native-sso",
2084
+ "nativeSsoExchange: opts.idToken required");
2085
+ }
2086
+ return await exchangeToken({
2087
+ subjectToken: nopts.idToken,
2088
+ subjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
2089
+ actorToken: nopts.deviceSecret,
2090
+ actorTokenType: "urn:openid:params:token-type:device-secret",
2091
+ audience: nopts.audience,
2092
+ scope: nopts.scope,
2093
+ requestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
2094
+ });
2095
+ }
2096
+
1914
2097
  return {
1915
2098
  authorizationUrl: authorizationUrl,
1916
2099
  exchangeCode: exchangeCode,
@@ -1928,9 +2111,13 @@ function create(opts) {
1928
2111
  parseJarmResponse: parseJarmResponse,
1929
2112
  introspectToken: introspectToken,
1930
2113
  registerClient: registerClient,
2114
+ readClient: readClient,
2115
+ updateClient: updateClient,
2116
+ deleteClient: deleteClient,
1931
2117
  deviceAuthorization: deviceAuthorization,
1932
2118
  pollDeviceCode: pollDeviceCode,
1933
2119
  exchangeToken: exchangeToken,
2120
+ nativeSsoExchange: nativeSsoExchange,
1934
2121
  // Diagnostic / power-user surface
1935
2122
  issuer: issuer,
1936
2123
  clientId: clientId,