Documentation · Security and Assurance

Open-Source Licence Policy SDK

Open-source licence policy as code. Turn your licence policy into an enforceable build rule: validate SPDX licence evidence locally and fail CI when a component breaks your policy.

Package @wardenkit/open-source-licence-policy · v0.1.2 · Node.js 22.x · Node.js 24.x · Product overview

Quickstart

For Node.js, CI, DevSecOps, security and platform teams who already have an approved open-source licence policy in a spreadsheet or wiki and want it enforced as a repeatable policy-as-code check. The SDK evaluates a policy you author over component records you supply. It performs no dependency discovery, no licence detection and no legal analysis: you provide one SPDX licence expression per component, and the SDK validates it, normalizes it, applies your policy and returns a machine-readable result. Free local evaluation needs no account, email, card, licence key or hosted evaluator; evaluation runs entirely in your process and makes no network call. Production use adds the maintained signed Licence Reference Pack and managed Production authorization.

Installbash
npm install @wardenkit/open-source-licence-policy

Step 1 — author a policy. The policy is a JSON document you own and commit beside your code. It states your default outcome, how complex expressions and deprecated identifiers are treated, which outcomes fail CI, and your explicit per-licence rules. The licence identifiers below are an example of a customer-authored policy, not a WardenKit recommendation about which licences you should allow or deny.

licence-policy.jsonjson
{
  "policySchemaVersion": 1,
  "policyId": "acme-platform-licence-policy",
  "policyVersion": "2026.09.1",
  "defaultOutcome": "review-required",
  "complexExpressionTreatment": "review-required",
  "deprecatedIdTreatment": "review-required",
  "ciFailureOutcomes": ["policy-denied", "invalid-unsupported"],
  "rules": [
    { "ruleId": "allow-mit", "match": { "type": "exact-license-id", "licenseId": "MIT" }, "outcome": "policy-allowed" },
    { "ruleId": "allow-apache", "match": { "type": "exact-license-id", "licenseId": "Apache-2.0" }, "outcome": "policy-allowed" },
    { "ruleId": "deny-agpl", "match": { "type": "exact-license-id", "licenseId": "AGPL-3.0-only" }, "outcome": "policy-denied" },
    { "ruleId": "allow-dual", "match": { "type": "exact-normalized-expression", "expression": "Apache-2.0 OR MIT" }, "outcome": "policy-allowed" },
    { "ruleId": "internal-ref", "match": { "type": "exact-license-ref", "licenseRef": "LicenseRef-acme-internal" }, "outcome": "policy-allowed" }
  ]
}

Step 2 — supply component records. Each record carries a component identifier and exactly one SPDX licence expression, with an optional version and an optional free-text source label. Unknown fields are rejected and duplicate component identifiers are reported.

components.jsonjson
[
  { "id": "left-pad", "version": "1.3.0", "licenseExpression": "MIT", "source": "internal inventory" },
  { "id": "acme-http", "version": "4.2.1", "licenseExpression": "Apache-2.0 OR MIT" },
  { "id": "acme-internal-utils", "version": "0.9.0", "licenseExpression": "LicenseRef-Acme-Internal" },
  { "id": "copyleft-tool", "version": "2.0.0", "licenseExpression": "AGPL-3.0-only" },
  { "id": "legacy-lib", "version": "1.0.0", "licenseExpression": "LGPL-2.1" },
  { "id": "mystery-lib", "version": "0.1.0", "licenseExpression": "Totally-Made-Up-1.0" }
]

Step 3 — evaluate. Validate the policy first, then run the evaluation.

Evaluate locallybash
npx wardenkit-licence-policy validate-policy --policy licence-policy.json --json
npx wardenkit-licence-policy evaluate \
  --components components.json \
  --policy licence-policy.json \
  --json

With the example policy and component records above, three components are policy-allowed, one is policy-denied, one is review-required because LGPL-2.1 is a deprecated identifier handled by deprecatedIdTreatment, and one is invalid-unsupported because its identifier is not in the pinned SPDX License List. The aggregate outcome is CI-blocking and the CLI exits 1.

Using the SDK

createEvaluator() returns an evaluator bound to the bundled SPDX reference snapshot. evaluate() returns a discriminated union: check ok before reading result, exactly as every WardenKit SDK does.

check-licences.tstypescript
import { readFile } from "node:fs/promises";
import { createEvaluator, validatePolicy } from "@wardenkit/open-source-licence-policy";

const policy = JSON.parse(await readFile("licence-policy.json", "utf8"));
const components = JSON.parse(await readFile("components.json", "utf8"));

const policyCheck = validatePolicy(policy);
if (!policyCheck.valid) {
  console.error(policyCheck.diagnostics);
  process.exit(2);
}

const evaluator = createEvaluator();
const outcome = evaluator.evaluate({ components, policy });

if (!outcome.ok) {
  // INPUT_DOCUMENT_INVALID, POLICY_INVALID or a commercial state.
  console.error(outcome.error.code, outcome.error.message, outcome.error.diagnostics);
  process.exit(2);
}

const { aggregate, components: results, provenance } = outcome.result;
console.log(aggregate.outcome, aggregate.counts);
console.log("policy", provenance.policyHash, "reference", provenance.reference.referenceVersion);

for (const component of results) {
  if (component.outcome !== "policy-allowed") {
    console.log(component.id, component.outcome, component.decidedBy, component.normalizedExpression);
  }
}

if (aggregate.ciBlocking) process.exitCode = 1;

normalizeExpression() validates and canonicalizes a single expression without running a policy, which is useful when you are cleaning up licence metadata before you evaluate it.

Normalize one expressiontypescript
const evaluator = createEvaluator();

evaluator.normalizeExpression("apache-2.0 or mit");
// { valid: true, normalizedExpression: "Apache-2.0 OR MIT", shape: "complex", diagnostics: [] }

evaluator.normalizeExpression("MIT And Apache-2.0");
// { valid: false, normalizedExpression: null, shape: null,
//   diagnostics: [{ code: "EXPR_SYNTAX", message: 'Operator "And" must be spelled "AND" or "and".' }] }
ExportPurpose
createEvaluator(options?)Creates a local evaluator synchronously; zero-network. No options uses the bundled SPDX snapshot; referencePackPath uses a signed Licence Reference Pack and requires a valid entitlement.
createManagedProductionEvaluator(options?)Returns a Promise of a Managed Production evaluator. Before each evaluate() it may perform a bounded, activity-driven authorization refresh against the WardenKit perimeter (HTTPS only), then runs the identical local evaluator. Evaluation inputs and results are never transmitted. status() reports AUTHORIZATION_ACTIVE, AUTHORIZATION_REFRESH_DUE, AUTHORIZATION_REFRESH_RETRY_SCHEDULED, REVALIDATION_REQUIRED, PAYMENT_RECOVERY or COMMERCIAL_RIGHTS_ENDED; refresh() is a recovery/diagnostic action.
validatePolicy(policy)Validates a policy document and returns its stable policyHash.
getEntitlementStatus(options?)Reports the local entitlement state. Reads local files only.
LicencePolicyErrorThrown for option, trust-state and reference-pack failures.
PRODUCT_VERSIONVersion string recorded in every result's provenance.

CLI reference

The CLI has exactly six commands. Machine-readable JSON goes to stdout and diagnostics go to stderr, so a pipeline can capture the result and still surface a readable summary in the job log. Of the CLI commands, only activate and refresh touch the network; evaluate, validate-policy, reference and status never do. In the SDK, createManagedProductionEvaluator is the only other network-capable path, and it transmits authorization data only, never evaluation inputs or results.

CommandFlagsWhat it does
evaluate--components, --policy, --reference-pack, --outputEvaluates the policy over the component records and writes the JSON result.
validate-policy--policyValidates a policy document and reports its policy hash.
reference--reference-packPrints the pinned SPDX reference metadata in use.
status—Prints the local entitlement state.
activate--activation-fileAsks for the one-time Activation Code without echoing it and exchanges it for a signed entitlement and the current pack. For non-interactive use, set WARDENKIT_ACTIVATION_KEY or pass --activation-file <file> (mode 0600); the code never goes on the command line.
refresh—Re-fetches the signed entitlement and pack when online. A recovery and diagnostic action: the managed Production evaluator refreshes on evaluate activity, each signed authorization lasts at most 72 hours and only that signed deadline is binding.

Global flags: --json for machine-readable output, and --state-dir to relocate the local artefact cache.

Exit codeMeaning
0Success, and for evaluate an aggregate outcome that is not CI-blocking.
1Evaluation completed and the aggregate outcome is CI-blocking under your policy.
2Usage error, malformed input document or invalid policy.
3Production authorization is required, not yet valid, or expired.
4Local trust state or reference pack was rejected.
5Network request to the WardenKit service failed.
6The WardenKit perimeter rejected the request.
Pinned SPDX reference in usebash
npx wardenkit-licence-policy reference --json
Outputjson
{
  "referenceKind": "bundled-snapshot",
  "referenceVersion": "spdx-license-list-3.28.0/snapshot-1",
  "spdxLicenseListVersion": "3.28.0",
  "spdxLicenseListReleaseDate": "2026-02-20",
  "spdxSpecVersion": "3.0.1",
  "spdxExpressionGrammar": "annex-b"
}

Reading the result

A result has four parts: the aggregate, one entry per component, document-level diagnostics and a provenance block. The four per-component outcomes describe the result of your configured policy. They are not legal permissions, prohibitions or compatibility opinions.

OutcomeMeaning
policy-allowedYour policy allows this component's normalized expression.
policy-deniedYour policy denies it, by an explicit rule or by your defaultOutcome.
review-requiredYour policy routes it to human review, including complex expressions and deprecated identifiers where you configured that treatment.
invalid-unsupportedThe expression is malformed, unknown, or uses a construct this version does not support. It never silently passes.
Result (abridged)json
{
  "resultSchemaVersion": 1,
  "aggregate": {
    "outcome": "invalid-unsupported",
    "componentCount": 6,
    "ciBlocking": true,
    "counts": {
      "policy-allowed": 3,
      "policy-denied": 1,
      "review-required": 1,
      "invalid-unsupported": 1
    }
  },
  "components": [
    { "id": "left-pad", "normalizedExpression": "MIT", "shape": "listed-id", "outcome": "policy-allowed", "decidedBy": "rule", "matchedRuleId": "allow-mit", "diagnostics": [] },
    { "id": "mystery-lib", "normalizedExpression": null, "shape": null, "outcome": "invalid-unsupported", "decidedBy": "grammar", "diagnostics": [{ "code": "EXPR_UNKNOWN_ID", "severity": "error", "message": "Unknown SPDX licence identifier \"Totally-Made-Up-1.0\"." }] }
  ],
  "provenance": {
    "productVersion": "0.1.2",
    "policyId": "acme-platform-licence-policy",
    "policyVersion": "2026.09.1",
    "policyHash": "…",
    "componentsHash": "…",
    "reference": {
      "referenceKind": "bundled-snapshot",
      "referenceVersion": "spdx-license-list-3.28.0/snapshot-1",
      "spdxLicenseListVersion": "3.28.0",
      "spdxSpecVersion": "3.0.1",
      "spdxExpressionGrammar": "annex-b"
    }
  }
}

decidedBy states which part of your policy produced the outcome: rule, defaultOutcome, complexExpressionTreatment, deprecatedIdTreatment or grammar. aggregate.ciBlocking is derived from your own ciFailureOutcomes list, and the CLI exit code mirrors it.

The provenance block makes a result reproducible: it records the product version, the schema versions, your policyId, policyVersion and policyHash, a hash of the component input, and the exact SPDX reference version used. Identical inputs, identical policy, identical product version and identical reference version produce byte-identical output.

SPDX expressions, normalization and diagnostics

Expressions are parsed against the SPDX Specification 3.0.1 Annex B grammar and resolved against a pinned SPDX License List 3.28.0 baseline. Operators are matched case-sensitively as AND, OR and WITH (or their lowercase forms); identifiers are matched case-insensitively and re-emitted in canonical List case. Because AND and OR are commutative, operands are ordered canonically so that two teams writing the same expression differently produce the same normalized form.

ExpressionNormalizedShape
mitMITlisted-id
apache-2.0 or mitApache-2.0 OR MITcomplex
MIT AND (Apache-2.0 OR BSD-3-Clause)(Apache-2.0 OR BSD-3-Clause) AND MITcomplex
gpl-3.0-only WITH Classpath-exception-2.0GPL-3.0-only WITH Classpath-exception-2.0complex
GPL-2.0-or-laterGPL-2.0-or-laterlisted-id
LicenseRef-Acme-InternalLicenseRef-acme-internallicense-ref

Deprecated identifiers are preserved exactly as pinned and reported with a deterministic diagnostic; no successor identifier is ever substituted for you. LicenseRef- identities and AdditionRef- additions are supported as opaque customer-defined references, carry no legal meaning, and are matched by your policy only where you name them. DocumentRef-qualified references are not supported in this version, because it ingests no SPDX document-reference context.

Diagnostic codeCause
EXPR_SYNTAXThe expression does not parse under the Annex B grammar, for example a mis-cased operator.
EXPR_UNKNOWN_IDThe licence identifier is not in the pinned SPDX License List.
EXPR_UNKNOWN_EXCEPTION_IDThe exception identifier after WITH is not in the pinned list.
EXPR_DEPRECATED_IDThe identifier is present but flagged deprecated in the pinned list.
EXPR_DOCUMENTREF_UNSUPPORTEDA DocumentRef-qualified reference was used.
INPUT_UNKNOWN_FIELDA component record carried a field outside the accepted schema.
POLICY_EXPRESSION_NOT_CANONICALA policy rule matched on an expression that is not in canonical normalized form.

SPDX is referenced as a technical standard family. WardenKit is not affiliated with, endorsed by or operated by SPDX or the Linux Foundation, and infers no legal meaning from any SPDX metadata field.

Running in CI

The CLI is the CI surface: one command, JSON on stdout, a deterministic exit code, and no hosted service in the path. Commit both the policy and the component records, then fail the job on a CI-blocking outcome.

GitHub Actionsyaml
name: licence-policy
on: [push, pull_request]

jobs:
  licence-policy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - name: Validate the policy document
        run: npx wardenkit-licence-policy validate-policy --policy licence-policy.json --json
      - name: Evaluate the licence policy
        run: |
          npx wardenkit-licence-policy evaluate \
            --components components.json \
            --policy licence-policy.json \
            --json \
            --output licence-policy-result.json
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: licence-policy-result
          path: licence-policy-result.json

Any CI system that can run Node.js works the same way, because the contract is the exit code and the JSON file. There is no WardenKit GitHub Action, and none is required.

Generic CI shell stepbash
set -euo pipefail

npx wardenkit-licence-policy validate-policy --policy licence-policy.json --json > /dev/null

set +e
npx wardenkit-licence-policy evaluate \
  --components components.json \
  --policy licence-policy.json \
  --json \
  --output licence-policy-result.json
status=$?
set -e

case "$status" in
  0) echo "licence policy: not blocking" ;;
  1) echo "licence policy: blocking outcome"; exit 1 ;;
  2) echo "licence policy: invalid input or policy"; exit 1 ;;
  *) echo "licence policy: failed with exit $status"; exit "$status" ;;
esac

Keep the evaluation step separate from the artefact upload, and upload the result file even when the job fails, so the JSON evidence for a blocked release is still retrievable.

Entitlement, activation and error states

Evaluation against the bundled SPDX snapshot needs no entitlement, no account and no licence key. Production use runs against a signed WardenKit Licence Reference Pack, which requires a signed local entitlement; verification happens locally in your process and createEvaluator itself makes no network call. The CLI activate and refresh commands, and the managed Production evaluator's activity-driven authorization refresh, are the only paths that contact the WardenKit service; none of them is required for a local evaluation to run, and none transmits components, policies, SPDX expressions or results.

How activation works after a purchase: completing a checkout in the browser does not itself grant rights. The WardenKit commercial service treats the payment provider's signature-verified webhook as the only authority for a subscription. The product page never polls that service; when you press “Activate WardenKit” it sends exactly one request carrying only the provider's transaction reference, and receives either a pending answer (the webhook has not arrived yet — press again later; this never creates another purchase), a one-time Activation Code (HTTP 201, shown once and never stored by the page), or an already-redeemed answer (HTTP 409, terminal). The credential is exchanged locally by activate for a signed entitlement. Requests for one transaction are never applied to another transaction, and the page keeps an issued Activation Code in memory only until you confirm you have activated WardenKit.

Checkouts reached through a provider transaction link (the provider's default payment link with its transaction parameter, also used for payment-method updates) are created by the payment provider, not by the product page, so they carry no wardenkit_legal_version metadata. The page asks for acceptance of the Terms & Conditions before it loads the provider's checkout for such a link; acceptance for those transactions is therefore recorded only as the buyer's action in the browser, not as transaction metadata.

Local entitlement statebash
npx wardenkit-licence-policy status --json
StateMeaning
evaluationNo production entitlement is installed. Evaluation-rights use only.
not-yet-validThe signed entitlement's validFrom is in the future.
production-activeA valid signed production entitlement is installed and the current time is within its authorization window: at most 72 hours from the signed issuedAt, and never past the signed validUntil (which the service caps at the verified paid-through date or any known cancellation boundary). The status output reports the deadline as authorizedUntil.
production-expiredPast authorizedUntil. There is no additional grace period. Production invocations fail closed until `refresh` installs a newly signed entitlement; nothing installed is deleted or disabled.

Managed Production automates refresh on activity only: a refresh becomes due 24 hours after the signed issuedAt and is attempted on the next evaluate(); there is no timer or keepalive, so a dormant process sends nothing and a long-running process refreshes on its next activity. Each request has a 5-second timeout. Transient failures are retried on later activity along a fixed ladder (5 min, 15 min, 1 h, 4 h, 8 h cap; at most 30 min when 6 h or less remain and 5 min when 1 h or less remain); retry metadata is persisted and survives restarts, and editing or deleting it can only make a request earlier, never extend signed authority. Missing or expired authorization gets exactly two bounded recovery attempts (immediately, then after 1 s); if both fail transiently the status is REVALIDATION_REQUIRED, a technical state that is not a revocation of your rights, and that evaluate() fails closed with ENTITLEMENT_EXPIRED. A later distinct evaluate() on the same evaluator begins a fresh bounded recovery cycle — no recreation, manual refresh(), repurchase or support step is needed — so a dormant or expired eligible installation revalidates automatically on its next managed Production use; refresh() remains available for immediate recovery. An authoritative denial is never retried.

No routine daily manual refresh is required: managed Production refresh is activity-driven (due 24 hours after the signed issuedAt), the CLI refresh command is an explicit recovery and diagnostic action, and the only binding limit is the signed deadline of each authorization (at most 72 hours). Each refresh re-verifies your current subscription with the payment provider on the server before a new 72-hour authorization is signed; a failed or unavailable verification is reported as a retryable error and never silently extends authorization. Cancellation and expiry are non-destructive: no remote kill, no background daemon, no background network requirement, and cached historical evidence is retained. A production-mode invocation after the signed deadline never returns a production-authorized success — it returns a deterministic entitlement error state and the CI outcome fails closed.

Commercial lifecycle, commercial rights and cached technical authorization are separate. A subscription whose renewal payment fails enters PAYMENT_RECOVERY: rights continue while the provider currently reports the subscription as past due, even after the historical paid-through date and the billing period have passed, and each new authorization stays bounded to at most 72 hours and re-verified against the provider; a successful payment returns it to ACTIVE with paid-through advancing only from verified payment, and rights end only on the provider's authoritative terminal or paused state or a scheduled cancellation/pause boundary, never by local arithmetic. A provider or network outage never changes your subscription, and an expired local authorization is not a revoked right. Commercial state follows the published Refund Policy. Ordinary cancellation keeps paid access through the verified paid-through boundary and the signed validUntil never exceeds that boundary. A refund that is requested or pending approval changes nothing; an approved refund of the current paid period or a confirmed chargeback revokes the activation credential and stops issuance, while a refund of an earlier billing period or a tax-only adjustment leaves current rights unchanged, and a partial refund or proration affects only the rights that correspond to the refunded consideration (the remaining balance of a partly refunded charge stays eligible under the Refund Policy). A revoked credential is never revived: after a dispute is reversed or payment is restored, `activate` with a fresh credential issued against the current subscription is required. During a commercial-service or provider outage the installed authorization stays valid until its own deadline, is not extended, and `evaluate`, `validate-policy`, `reference` and `status` keep working on your files.

Data boundary

  • Component records, policies, dependency names, licence evidence and results never leave your process.
  • The SDK contains no telemetry and reports no usage, repository names or dependency names.
  • evaluate, validate-policy, reference and status make no network request at all; createEvaluator is synchronous and zero-network.
  • activate and refresh, and the managed Production evaluator's bounded authorization refresh, exchange only the product identity, entitlement identity, tier, an organization subject identifier, an activation credential, timestamps and the product version — over HTTPS to the WardenKit perimeter only. Evaluation computation stays local.
  • The only files written are inside the local artefact cache, and only by the CLI.

What this product does not do

  • It does not discover your dependencies. You supply the component records.
  • It does not detect or classify licences from source code or licence texts.
  • It does not scan for vulnerabilities or perform general software composition analysis.
  • It does not generate an SBOM, and this version reads no SPDX document or CycloneDX file.
  • It does not give legal advice, determine legal permissions or obligations, opine on licence compatibility, or certify compliance.
  • It does not decide which licences your organization should allow, deny or review. That decision is yours, expressed in your policy.

Task guides

Deterministic tooling, professional judgment

This SDK returns deterministic, inspectable technical results evaluated against the profile version and inputs you select. WardenKit provides developer tooling for evaluating and implementing compliance logic; it does not replace professional legal, tax, or accounting judgment.

Product overview and pricing · Terms · Refund Policy · Privacy · Support

Documentation questions and licensing scope: support@wardenkit.com.