Guard Graphql

GraphQL request-shape safety guard — validates user-supplied request bundles against the canonical query-shape DoS catalog BEFORE the framework hands the query to a schema-aware executor. KIND is graphql-request; the gate consumes ctx.graphqlRequest (or ctx.gql) shape { query, operationName?, variables?, extensions? }. Pair downstream with the operator's schema-aware parser — this layer is the shape / depth / breadth contract that runs before any schema-resolution work.

Query depth caps: deeply-nested selection sets multiply exponentially against schema depth, bypassing per-field rate limits. The gate's _measureQueryShape walker counts brace-depth without a full lex/parse (the operator's executor handles full parsing); strict caps at 8, balanced 12, permissive 24. The cap fires as graphql.depth-exceeded — the canonical N²-amplification DoS class.

Alias-amplification caps: the same field repeated under different aliases (a:friend b:friend c:friend ...) bypasses per-field limits because each alias is a separate selection. Strict caps at 8 aliases per selection-set, balanced 16, permissive 32. Fires as graphql.alias-bomb — breadth-amplification DoS class.

Fragment-cycle defense: operator's executor handles cyclic fragment refs at parse time; the guard's contribution is the total-bytes cap (maxBytes) and per-query cap (maxQueryBytes), which bound the worst-case parser-DoS shape regardless of cycle structure.

Introspection toggle: __schema / __type queries leak schema details and tooling expects them in development but not production. Strict refuses (production posture); balanced audits; permissive allows. Detection is substring-match on the query string — fast and impossible to evade with whitespace tricks.

Persisted-query allowlist: when the operator opts in via persistedQueryPolicy: "require", the request must carry extensions.persistedQuery.sha256Hash. Free-form queries are refused as graphql.persisted-query-missing — eliminates ad-hoc query attack surface entirely (operator pre-approves the catalog of permitted queries by hash).

Operation-name allowlist: when opts.allowedOperations is set, the request operationName must be in the list. Complements the persisted-query approach for operators that keep free-form queries on but want a denylist for ad-hoc shapes.

Variable shape validation: when opts.variableShapes declares { varName: "string"|"number"|"boolean"|"object" }, the gate refuses any variables entry whose typeof doesn't match. Catches type-confusion exploits where executors silently coerce (string-for-ID-expecting-Int).

Batch defense: operators supporting [{},{}] batch arrays get N requests for one HTTP hit. Strict refuses batches outright; balanced caps at 10; permissive 50. Each batch entry is validated with the same threat catalog applied recursively.

Profiles: strict / balanced / permissive. Compliance postures: hipaa / pci-dss / gdpr / soc2. BIDI / null / control / zero-width universal-refuse applies on the query string at every profile so trojan-source codepoints can't ride inside a query identifier.

b.guardGraphql.validate(input, opts?) #

stable0.7.49hipaapci-dssgdprsoc2
{
  profile:                 "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  introspectionPolicy:     "reject"|"audit"|"allow",
  persistedQueryPolicy:    "require"|"audit"|"allow",
  operationNamePolicy:     "reject"|"audit"|"allow",
  batchPolicy:             "reject"|"audit"|"allow",
  aliasBombPolicy:         "reject"|"audit"|"allow",
  depthPolicy:             "reject"|"audit"|"allow",
  variableShapePolicy:     "reject"|"audit"|"allow",
  allowedOperations:       string[],
  variableShapes:          { [name: string]: "string"|"number"|"boolean"|"object" },
  maxDepth:                number,
  maxAliasesPerSelection:  number,
  maxBatchSize:            number,
  maxQueryBytes:           number,
  maxVariableBytes:        number,
  maxBytes:                number,
}

Apply the full guard-graphql threat catalog to a request bundle (or batch array). Returns { ok, issues } per gateContract.aggregateIssues. Detected classes include query-missing, query-cap, variables-cap, request-cap, batch-size, introspection, persisted-query-missing, operation-not-allowed, depth-exceeded, alias-bomb, variable-type-confusion, plus codepoint-class issues on the query string. Operator-supplied opts are bounds-checked; bad opts throw GuardGraphqlError("graphql.bad-opt").

var hostile = {
  query: "query Inspect { __schema { types { name } } }",
  operationName: "Inspect",
};
var rv = b.guardGraphql.validate(hostile, { profile: "strict" });
rv.ok;                                              // → false
rv.issues[0].ruleId;                                // → "graphql.introspection"

var benign = {
  query: "query GetMe { me { id name } }",
  operationName: "GetMe",
};
var ok = b.guardGraphql.validate(benign, { profile: "strict" });
ok.ok;                                              // → true

b.guardGraphql.sanitize(input, opts?) #

stable0.7.49
{
  profile:    "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  ...:        every guardGraphql.validate opt is honored,
}

Pass-through-or-throw form of validate. GraphQL request bundles can't be partially repaired — depth bombs, alias amplification, and introspection leaks are refuse-class outcomes, not something the guard can patch up safely. Returns the input unchanged when the issue list contains no critical / high entries; throws GuardGraphqlError carrying the offending ruleId otherwise.

try {
  b.guardGraphql.sanitize({
    query: "query Inspect { __schema { types { name } } }",
    operationName: "Inspect",
  }, { profile: "strict" });
} catch (e) {
  e.code;                                           // → "graphql.introspection"
}

b.guardGraphql.gate(opts?) #

stable0.7.49hipaapci-dssgdprsoc2
{
  profile:    "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  name:       string,            // gate label for audit trails
  ...:        every guardGraphql.validate opt is honored,
}

Build a gateContract.buildGuardGate-shaped gate that pulls ctx.graphqlRequest (or ctx.gql) and dispatches to validate. Returns { ok: true, action: "serve" } when the issue list is empty, { ok: true, action: "audit-only", issues } when only low-severity issues fire, and { ok: false, action: "refuse", issues } on any critical / high issue. Compose into the GraphQL request handler before any schema-resolution work — refusal short-circuits hostile depth / alias / batch shapes before they reach the executor.

var gqlGate = b.guardGraphql.gate({ profile: "strict" });
var rv = await gqlGate.check({
  graphqlRequest: {
    query: "{ a:me { id } b:me { id } c:me { id } d:me { id } " +
           "e:me { id } f:me { id } g:me { id } h:me { id } " +
           "i:me { id } }",
  },
});
rv.action;                                          // → "refuse"
rv.issues[0].ruleId;                                // → "graphql.alias-bomb"

b.guardGraphql.compliancePosture(name) #

stable0.7.49hipaapci-dssgdprsoc2

Look up a compliance-posture overlay by name (one of "hipaa" / "pci-dss" / "gdpr" / "soc2"). Returns a fresh clone of the posture overlay so the caller may mutate it freely without disturbing the shared table. Throws GuardGraphqlError with code "graphql.bad-posture" when the name is not one this guard maps. Wired by gateContract.defineGuard through gateContract.lookupCompliancePosture, so the clone semantics and error code are identical across every guard in the family.

var posture = b.guardGraphql.compliancePosture("hipaa");
posture;                                             // → overlay clone (mutable)

try {
  b.guardGraphql.compliancePosture("not-a-regime");
} catch (e) {
  e.code;                                            // → "graphql.bad-posture"
}

b.guardGraphql.buildProfile(opts) #

stable0.7.49
{
  extends:   string|string[],   // base profile name(s) to compose
  ...:       any guard key,      // inline override of resolved keys
}

Compose a derived profile from one or more named bases plus inline overrides, resolving names through this guard's own PROFILES table. opts.extends is a base profile name ("strict" / "balanced" / "permissive") or an array of names — later entries shadow earlier ones, and inline opts keys win last. Wired by gateContract.defineGuard through gateContract.makeProfileBuilder, so operator-defined profiles stay traceable to a baseline instead of a hand-typed dictionary.

var custom = b.guardGraphql.buildProfile({ extends: "strict" });
custom;                                              // → composed profile object

b.guardGraphql.loadRulePack(pack) #

stable0.7.49

Register an operator-supplied rule pack with this guard's rule-pack registry. The pack is identified by pack.id (a non-empty string) and stored for later dispatch by gates that opt in via opts.rulePackId. Returns the pack unchanged on success; throws GuardGraphqlError with code "graphql.bad-opt" when pack is missing or pack.id is not a non-empty string. Wired by gateContract.defineGuard through gateContract.makeRulePackLoader, so storage shape and validation are identical across the family.

var pack = b.guardGraphql.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id;                                             // → "tenant-policy"

b.guardGraphql.resolveOpts(opts?) #

stable0.7.49
{
  profile:           string,    // one of PROFILES; default this guard's default
  compliancePosture: string,    // overlay one of hipaa/pci-dss/gdpr/soc2
}

Resolve caller opts against this guard's PROFILES + compliance-posture overlays into the fully-defaulted option set the guard runs on — the same resolution validate / sanitize / gate apply internally. Wired by gateContract.defineGuard from the guard's binding config (profiles / postures / defaults / error class), so a guard's bespoke gate calls resolveOpts instead of re-declaring the per-guard resolver wrapper. Throws GuardGraphqlError with code "graphql.bad-opt" / "graphql.bad-posture" on an unknown profile or posture name.

var resolved = b.guardGraphql.resolveOpts({ profile: "strict" });
resolved.profile;                                    // → "strict"

Last updated 2026-08-08T16:39:49.652Z by seeder.