Guard JMAP Request

JMAP request-envelope validator (RFC 8620 JMAP Core). Validates the shape of an HTTP request body posted to /jmap/api and refuses requests that exceed operator caps, omit required capability declarations, or contain malformed back-references.

## Request shape (RFC 8620 §3.3)

{
  "using":  ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
  "methodCalls": [
    ["Mailbox/get",  { "accountId": "A1", "ids": null }, "c0"],
    ["Email/query",  { "filter": { "inMailbox": "#c0/list/0" } }, "c1"]
  ],
  "createdIds": null
}

using is the set of capability URIs the request invokes; the server's urn:ietf:params:jmap:core is implicit. methodCalls is an array of 3-tuples [methodName, args, clientId] where clientId echoes back on the response for client-side correlation.

## Back-reference resolution (RFC 8620 §3.7)

Subsequent methodCalls reference earlier results via { "resultOf": , "name": , "path": } placeholders inside the args object. The validator detects back-references and caps the chain depth so a pathological chain doesn't degrade into a O(2^N) blowup.

## Caps

A refusal carries one of the four request-level types RFC 8620 §3.6.1 defines: urn:ietf:params:jmap:error:unknownCapability, :notJSON, :notRequest, or :limit with a limit property naming the cap that was reached.

b.guardJmap.MAX_KEYS_PER_OBJECT #

stable0.20.32

How many members one object of a JMAP request body may carry. RFC 8620 bounds a request by its size and by its call count, not by the width of any one object, so this sits above what maxSizeRequest admits at the smallest member a JMAP name can be. validate applies it to both input forms, and a transport that reads the body before handing it on reads it under the same number, so one request is answered the same way whichever way it arrived.

b.guardJmap.MAX_KEYS_PER_OBJECT;
// → 250000

b.guardJmap.limitsFor(opts?) #

stable0.20.32
{
  profile:  "strict" | "balanced" | "permissive",
  posture:  "hipaa" | "pci-dss" | "gdpr" | "soc2",
}

Resolve the caps a profile and posture select, as { maxCallsInRequest, maxObjectsInGet, maxObjectsInSet, maxSizeRequest, maxBackRefDepth, maxUsingCapabilities }. validate applies these, and b.mail.server.jmap publishes them in the session's urn:ietf:params:jmap:core capability, so a client reads the numbers the server enforces. The object is frozen, so a caller cannot raise a cap the guard applies. Throws GuardJmapError with code guard-jmap/bad-profile when the profile is not one of the three, and guard-jmap/bad-posture when the posture is not one of the four, and guard-jmap/bad-opt for an option this guard does not read, including compliancePosture, whose regime belongs in posture; validate and b.mail.server.jmap.create refuse the same way.

var limits = b.guardJmap.limitsFor({ profile: "balanced" });
// → { maxCallsInRequest: 128, maxObjectsInGet: 1000, ... }

b.guardJmap.validate(rawBody, opts?) #

stable0.9.50
{
  profile:               "strict" | "balanced" | "permissive",
  posture:               "hipaa" | "pci-dss" | "gdpr" | "soc2",
  serverCapabilities:    { "urn:ietf:params:jmap:mail": true, ... },
                         // capability URIs the server has wired; `using`
                         //   entries not in this set are refused with
                         //   urn:ietf:params:jmap:error:unknownCapability
}

Validate a JMAP request envelope. Accepts either a raw JSON string (bytes) or a pre-parsed object, the form b.middleware.bodyParser leaves on req.body; maxSizeRequest bounds both. Returns { using, methodCalls, createdIds } on success; throws GuardJmapError with the matching RFC 8620 §3.6.1 request-level type on refusal. A limit refusal also carries a limit property naming the cap.

var parsed = b.guardJmap.validate(rawBody, {
  serverCapabilities: { "urn:ietf:params:jmap:mail": true },
});
// → { using: [...], methodCalls: [[methodName, args, clientId], ...] }

b.guardJmap.compliancePosture(name) #

stable0.9.50hipaapci-dssgdprsoc2

Return the effective profile NAME for a compliance posture, or null for a name this parser does not map. Unlike the content-guard variant this returns the resolved profile string (every line-protocol parser composes gateContract.ALL_STRICT_POSTURES, so "hipaa" / "pci-dss" / "gdpr" / "soc2" all resolve to "strict") and never throws — the parser shape carries no overlay-clone, no buildProfile, and no loadRulePack. Wired by gateContract.defineParser.

b.guardJmap.compliancePosture("hipaa");                   // → "strict"
b.guardJmap.compliancePosture("not-a-regime");            // → null

Last updated 2026-10-12T14:21:56.879Z by seeder.