🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@openparachute/scope-guard

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@openparachute/scope-guard - npm Package Compare versions

Comparing version
0.4.1
to
0.5.0
+36
-0
dist/validate.d.ts

@@ -91,2 +91,38 @@ import { type JwksGetter, type JwksOptions } from "./jwks.js";

/**
* The SET of additional origins to accept in the token's `iss` claim,
* beyond `hubOrigin`. **Optional and purely additive** — when omitted (the
* default), the `iss` check is byte-identical to before this seam existed:
* exact match against the single resolved `hubOrigin`.
*
* Motivation (multi-origin iss-set, onboarding-streamline 2026-06-25): one
* box reachable on several URLs at once (loopback + `<ip>.sslip.io` + a
* custom domain behind Caddy) mints tokens whose `iss` is whichever origin
* the request arrived on. The hub's signing KEY is stable and
* origin-independent — the only thing the origin drives is the `iss` string —
* so a token minted under URL-A must validate when the resource is reached
* via URL-B on the SAME box. jose's `issuer` option already accepts
* `string | string[]`; this option supplies the extra members of that set.
*
* SECURITY INVARIANT (load-bearing — do not loosen): the values returned
* here MUST be the hub's own legitimate origins ONLY — i.e. the output of
* the hub's `buildHubBoundOrigins` (configured issuer ∪ loopback aliases ∪
* expose-state public origin ∪ platform origin), published to the resource
* server out-of-band (env / config) by the HUB/OPERATOR. They must NEVER
* derive from an unvalidated request `Host` / `X-Forwarded-Host`. Accepting
* `iss ∈ this-set` is safe ONLY because the JWKS signature verify
* (`jwtVerify`, which runs FIRST and unconditionally) already proves THIS
* hub minted the token. The set is a belt-and-suspenders allowlist layered
* on top of the signature gate, never a substitute for it. A token signed by
* a foreign key fails the signature step regardless of its `iss`.
*
* Shape: a resolver function returning the set, re-evaluated per call so an
* operator widening the box's origins (a new domain via the env var) is
* picked up without a resource-server restart. The resolved `hubOrigin` is
* always added to whatever this returns, so a caller need not (but may)
* include it. Empty / undefined → identical to omitting the option (single
* `hubOrigin` only). Trailing slashes are stripped; malformed entries are
* dropped.
*/
allowedIssuers?: () => readonly string[] | undefined;
/**
* Origin to FETCH the JWKS from, when it must differ from the `hubOrigin`

@@ -93,0 +129,0 @@ * used to validate the token's `iss`. Same shape as `hubOrigin` — a literal

+61
-9

@@ -13,2 +13,8 @@ import { jwtVerify } from "jose";

* verification (jose verifies the signature, not who issued the token).
* With `allowedIssuers`, the pin widens from a single origin to a
* hub-CONTROLLED SET of the hub's own legitimate origins (one box reached
* via several URLs — loopback ∪ sslip.io ∪ custom domain). This is still a
* pin against the hub's origins, NOT a relaxation: the signature verify
* runs first and proves only this hub minted the token; the set never
* includes an unvalidated request Host. See `allowedIssuers` jsdoc.
* - JWKS is fetched from `<jwksOrigin>/.well-known/jwks.json`, defaulting

@@ -57,3 +63,3 @@ * to the same `hubOrigin` used for the `iss` pin. Co-located resource

export function createScopeGuard(opts) {
const { hubOrigin, jwksOrigin, jwks: jwksOpts, jwksGetter: injected, allowMissingJti = false, missingJtiLogger, } = opts;
const { hubOrigin, allowedIssuers, jwksOrigin, jwks: jwksOpts, jwksGetter: injected, allowMissingJti = false, missingJtiLogger, } = opts;
// The origin the JWKS is FETCHED from. Defaults to `hubOrigin` (zero change

@@ -128,9 +134,50 @@ // for existing callers) — when supplied, it decouples the fetch origin from

/**
* Resolve the accepted-`iss` value passed to jose's `issuer` option.
*
* Default (no `allowedIssuers`): the single canonical `origin` — byte-
* identical exact-match behavior to before the multi-origin seam existed.
*
* With `allowedIssuers`: the UNION of `origin` and the (sanitized) extra
* origins, deduped, returned as a `string[]`. jose accepts `string[]` for
* `issuer` and passes a token whose `iss` matches ANY member — this is the
* "same box, two URLs" tolerance. The canonical `origin` is always present
* so a single-member set behaves exactly like the default.
*
* SECURITY: this set is the hub's own legitimate-origin allowlist (see
* `allowedIssuers` jsdoc). The signature verify in `jwtVerify` runs FIRST
* and proves provenance unconditionally; widening the accepted `iss` from
* one string to this hub-controlled set never accepts a foreign-key token.
*/
function resolveAcceptedIssuers(origin) {
if (!allowedIssuers)
return origin;
const extra = allowedIssuers();
if (!extra || extra.length === 0)
return origin;
const set = new Set([origin]);
for (const raw of extra) {
if (typeof raw !== "string")
continue;
const trimmed = raw.replace(/\/$/, "");
if (trimmed.length > 0)
set.add(trimmed);
}
// A single-member set collapses to the bare string so the
// default/single-origin path stays observably identical (jose treats
// `string` and `[string]` the same, but the string form keeps the
// configured-shape obvious in logs / tests).
return set.size === 1 ? origin : Array.from(set);
}
/**
* Verify the JWT signature + issuer, with a force-reload-and-retry-once on
* rotation-class failures. Returns the verified payload or throws a wrapped
* `HubJwtError`. See the inline comment for the recovery rationale.
*
* `acceptedIssuers` is what jose checks `iss` against — a single string
* (default / single-origin) or a string[] (the multi-origin set). jose
* passes a token whose `iss` matches any member.
*/
async function verifyWithRotationRetry(token, origin, getter) {
async function verifyWithRotationRetry(token, acceptedIssuers, getter) {
try {
const verified = await jwtVerify(token, getter, { issuer: origin });
const verified = await jwtVerify(token, getter, { issuer: acceptedIssuers });
return verified.payload;

@@ -157,3 +204,3 @@ }

try {
const verified = await jwtVerify(token, getter, { issuer: origin });
const verified = await jwtVerify(token, getter, { issuer: acceptedIssuers });
return verified.payload;

@@ -172,10 +219,15 @@ }

async validateHubJwt(token, validateOpts = {}) {
// `origin` pins the `iss` check (and the revocation-list endpoint, which
// lives on the issuer). `jwksFetchOrigin` is where the keys are fetched —
// identical to `origin` unless the caller supplied `jwksOrigin`. Both are
// resolved per call so env changes propagate without a restart.
// `origin` is the CANONICAL hub origin. It pins (a) the revocation-list
// endpoint, which lives on the canonical issuer, and (b) the default
// JWKS-fetch origin. `acceptedIssuers` is what the token's `iss` is
// checked against — `origin` alone by default, or the multi-origin SET
// (origin ∪ the hub's other legitimate origins) when `allowedIssuers` is
// supplied. `jwksFetchOrigin` is where the keys are fetched — identical
// to `origin` unless the caller supplied `jwksOrigin`. All resolved per
// call so env changes propagate without a restart.
const origin = resolveOrigin(hubOrigin);
const acceptedIssuers = resolveAcceptedIssuers(origin);
const jwksFetchOrigin = resolveOrigin(jwksOriginInput);
const getter = pickGetter(jwksFetchOrigin);
const payload = await verifyWithRotationRetry(token, origin, getter);
const payload = await verifyWithRotationRetry(token, acceptedIssuers, getter);
if (typeof payload.sub !== "string" || payload.sub.length === 0) {

@@ -182,0 +234,0 @@ throw new HubJwtError("shape", "hub JWT missing required `sub` claim");

+1
-1
{
"name": "@openparachute/scope-guard",
"version": "0.4.1",
"version": "0.5.0",
"description": "Hub-issued JWT validation for Parachute resource servers (vault, scribe, parachute-agent, third-party modules).",

@@ -5,0 +5,0 @@ "license": "AGPL-3.0",