{"id":50305500,"url":"https://github.com/altinn/apim-rate-limit-compiler","last_synced_at":"2026-05-28T16:01:14.442Z","repository":{"id":359402748,"uuid":"1245786841","full_name":"Altinn/apim-rate-limit-compiler","owner":"Altinn","description":"Tool to compile JSON-based rate limit policies into APIM fragments","archived":false,"fork":false,"pushed_at":"2026-05-28T13:21:54.000Z","size":166,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-28T14:12:51.156Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Altinn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-21T14:54:57.000Z","updated_at":"2026-05-28T13:21:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Altinn/apim-rate-limit-compiler","commit_stats":null,"previous_names":["altinn/altinn-apim-policy-compiler","altinn/apim-policy-compiler"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/Altinn/apim-rate-limit-compiler","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Altinn%2Fapim-rate-limit-compiler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Altinn%2Fapim-rate-limit-compiler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Altinn%2Fapim-rate-limit-compiler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Altinn%2Fapim-rate-limit-compiler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Altinn","download_url":"https://codeload.github.com/Altinn/apim-rate-limit-compiler/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Altinn%2Fapim-rate-limit-compiler/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33615490,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-28T16:00:30.744Z","updated_at":"2026-05-28T16:01:14.425Z","avatar_url":"https://github.com/Altinn.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# APIM Rate Limit Compiler\n\nDeterministic .NET 10 CLI for compiling rate-limit JSON into Azure API Management policy fragment XML.\n\nThe compiler is intended for CI pipelines that keep rate-limit configuration as reviewed JSON and publish generated APIM `policyFragments` as build artifacts or deployment inputs.\n\n## Projects\n\n- `src/ApimRateLimitCompiler.Core`: JSON model, validation, diagnostics, hashing, and XML generation.\n- `src/ApimRateLimitCompiler.Cli`: AOT-friendly command-line entrypoint.\n- `tests/ApimRateLimitCompiler.Tests`: snapshot-focused tests for valid XML and invalid diagnostics.\n\n## Requirements\n\n- .NET SDK 10.0.100 or newer feature band.\n- The repo includes `global.json` with `rollForward` set to `latestFeature`.\n\n## Usage\n\nCompile a rate-limit file to an APIM fragment:\n\n```bash\ndotnet run --project src/ApimRateLimitCompiler.Cli -- \\\n  rate-limit \\\n  --input rate-limits/dialogporten.json \\\n  --output generated/rate-limit-dialogporten.fragment.xml\n```\n\nThe published native binary uses the same command shape:\n\n```bash\napim-rate-limit-compiler rate-limit \\\n  --input rate-limits/dialogporten.json \\\n  --output generated/rate-limit-dialogporten.fragment.xml\n```\n\nOptions:\n\n- `--input \u003cfile\u003e`: required JSON input.\n- `--output \u003cfile\u003e`: write generated fragment XML.\n- `--stdout`: write generated fragment XML to stdout.\n- `--write-hash \u003cfile\u003e`: write SHA-256 hash of the generated XML.\n- `--fail-on-warning`: return exit code `1` when validation warnings are produced.\n- `--warnings-as-json`: write diagnostics as JSON to stderr.\n- `--client-id-variable-name \u003cname\u003e`: override the APIM context variable used for resolved client IDs. Defaults to `oauthClientId`.\n- `--emit-rate-limit-headers`: emit `X-RateLimit-Remaining-*` and `X-RateLimit-Limit-*` headers.\n- `--source-ref \u003cvalue\u003e`: emit an operational source reference comment, typically a commit-pinned repository URL to the input JSON.\n- `--source-revision \u003cvalue\u003e`: emit an operational source revision comment, typically the Git commit SHA used to generate the fragment.\n\nAt least one of `--output` or `--stdout` is required.\n\nExit codes:\n\n- `0`: success.\n- `1`: validation, compilation, or file IO failure.\n- `2`: invalid CLI usage.\n\n## Rate-Limit JSON v1\n\nTop-level shape:\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/Altinn/apim-rate-limit-compiler/main/schemas/rate-limit-v1.schema.json\",\n  \"version\": 1,\n  \"name\": \"dialogporten\",\n  \"enabled\": true,\n  \"rules\": []\n}\n```\n\nRule shape:\n\n```json\n{\n  \"id\": \"default\",\n  \"enabled\": true,\n  \"action\": \"limit\",\n  \"match\": {\n    \"methods\": [\"GET\", \"POST\"],\n    \"pathMode\": \"prefix\",\n    \"path\": \"/dialogporten\",\n    \"caller\": {\n      \"clientIds\": [\"client-a\"],\n      \"scopes\": [\"dialogporten:read\"]\n    }\n  },\n  \"keyMode\": \"client-id\",\n  \"calls\": 120,\n  \"renewalPeriod\": 60\n}\n```\n\nSupported values:\n\n- `action`: `limit` or `exclude`. Defaults to `limit` when omitted.\n- `match.methods`: `[\"*\"]` or explicit methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`, `TRACE`.\n- `match.pathMode`: `any`, `exact`, `prefix`.\n- `match.caller.clientIds`: optional client IDs that the rule applies to.\n- `match.caller.scopes`: optional OAuth scopes that the rule applies to.\n- `keyMode`: `client-id`, `client-id-ip`.\n\n`keyMode`, `calls`, and `renewalPeriod` are required for `limit` rules.\n\nIf both `match.caller.clientIds` and `match.caller.scopes` are present, both must match. Scope matching uses a padded string match against the bearer token's `scope` claim.\n\n`exclude` rules are evaluated before all `limit` rules. If any enabled exclude rule matches, the generated fragment skips all rate limiting for that request:\n\n```json\n{\n  \"id\": \"health-exempt\",\n  \"enabled\": true,\n  \"action\": \"exclude\",\n  \"match\": {\n    \"methods\": [\"GET\"],\n    \"pathMode\": \"exact\",\n    \"path\": \"/dialogporten/health\",\n    \"caller\": {\n      \"scopes\": [\"monitoring:read\"]\n    }\n  }\n}\n```\n\nAn exclude rule can also exempt a specific caller from all rate limiting:\n\n```json\n{\n  \"id\": \"foobar-exempt\",\n  \"enabled\": true,\n  \"action\": \"exclude\",\n  \"match\": {\n    \"methods\": [\"*\"],\n    \"pathMode\": \"any\",\n    \"caller\": {\n      \"clientIds\": [\"foobar\"]\n    }\n  }\n}\n```\n\nThe canonical JSON Schema for v1 is published at:\n\n```text\nhttps://raw.githubusercontent.com/Altinn/apim-rate-limit-compiler/main/schemas/rate-limit-v1.schema.json\n```\n\nProduct repositories can reference that URL in the top-level `$schema` property to get editor and CI validation while keeping the file directly consumable by the compiler.\n\nIf top-level `enabled` is `false`, the compiler emits:\n\n```xml\n\u003cfragment /\u003e\n```\n\nDisabled rules are ignored.\n\n## Generated Policy Behavior\n\nThe output is APIM fragment XML rooted at `\u003cfragment\u003e`.\n\nGenerated fragments start with deterministic metadata comments:\n\n- A warning that the fragment is compiler-generated and must not be edited manually.\n- `Source-SHA256`, the SHA-256 hash of the JSON source content as compiled.\n- `Compiler`, the compiler name and version.\n- `Source`, when `--source-ref` is supplied.\n- `Source-Revision`, when `--source-revision` is supplied.\n\nThe compiler does not emit timestamps. Prefer source revision and source hash for operational traceability without breaking deterministic output.\n\nGenerated fragments use `context.Variables[\"oauthClientId\"]` as the client ID source by default. This variable name can be changed with `--client-id-variable-name`.\n\nThe fragment starts with a deterministic preamble that:\n\n1. Leaves the configured client ID variable unchanged if it is already set and non-empty.\n2. Reads the `Authorization` header otherwise.\n3. Extracts the JWT payload from non-empty `Bearer` tokens.\n4. Sets the configured client ID variable from the first `client_id` claim found in the payload.\n5. Sets the configured client ID variable to an empty string when no client ID can be resolved.\n\nThe generated claim extractor is deliberately narrow and optimized for the expected token shape. It scans the decoded payload bytes for string-valued low-ASCII `client_id` and `scope` claims, but it does not validate the token and does not perform general JSON parsing.\n\nWhen scope matching is used, the fragment decodes and scans the token once into an internal packed variable, then derives the configured client ID variable and `oauthScopes` from that value.\n\nRate limiting is skipped when the configured client ID variable is empty.\n\nGenerated rules use static `choose`/`when` blocks and `rate-limit-by-key` statements. Multiple matching rules emit multiple `rate-limit-by-key` statements, so burst and sustained limits can both apply.\n\nGenerated headers are stable. `Retry-After` is always configured. `X-RateLimit-*` headers are emitted only when `--emit-rate-limit-headers` is set:\n\n- `Retry-After`\n- `X-RateLimit-Remaining-{Name}-{RuleId}`\n- `X-RateLimit-Limit-{Name}-{RuleId}`\n\nOutput is byte-for-byte deterministic for the same input and compiler version.\n\n## Validation\n\nErrors fail compilation:\n\n- Invalid JSON.\n- Unknown `version`.\n- Unknown JSON properties.\n- Duplicate rule IDs.\n- Unsafe `name` or `id` characters. Only ASCII letters, digits, `-`, and `_` are allowed.\n- Missing or invalid `calls`, `renewalPeriod`, `match.methods`, `match.pathMode`, or `keyMode` for `limit` rules.\n- Missing or invalid `match.methods` or `match.pathMode` for `exclude` rules.\n- `calls \u003c= 0`.\n- `renewalPeriod \u003c= 0` or `renewalPeriod \u003e 300`.\n- `exact` or `prefix` path modes without `path`.\n- Unsupported actions, methods, path modes, or key modes.\n- Generated XML that cannot be parsed as XML.\n\nWarnings do not fail compilation unless `--fail-on-warning` is set:\n\n- More than 50 enabled rules in one configuration.\n- Very high call limits.\n\n## Development\n\nRun tests:\n\n```bash\ndotnet test tests/ApimRateLimitCompiler.Tests/ApimRateLimitCompiler.Tests.csproj --no-restore -v minimal -nr:false\n```\n\nRun the local client ID extractor benchmark:\n\n```bash\ndotnet run -c Release --project benchmarks/ClientIdExtractorBench/ClientIdExtractorBench.csproj -- --iterations 3000000\n```\n\nPublish a Native AOT binary for a specific runtime identifier:\n\n```bash\ndotnet publish src/ApimRateLimitCompiler.Cli/ApimRateLimitCompiler.Cli.csproj \\\n  -c Release \\\n  -r linux-x64 \\\n  -p:PublishAot=true \\\n  -p:Version=1.2.3 \\\n  -p:InformationalVersion=1.2.3+local \\\n  -v minimal \\\n  -nr:false\n```\n\nFor local test builds, use the helper script for the current platform:\n\n```bash\n./publish.sh\n```\n\nTo stamp a local build with the same compiler version metadata shape used by release builds, pass a SemVer-like version:\n\n```bash\n./publish.sh 1.2.3\n```\n\nOn Windows:\n\n```bat\npublish.bat 1.2.3\n```\n\nThe scripts also accept `PUBLISH_VERSION=1.2.3` from the environment. If no version is supplied, the SDK default assembly version is used.\n\nThe published binary is written to:\n\n```text\nsrc/ApimRateLimitCompiler.Cli/bin/Release/net10.0/linux-x64/publish/\n```\n\nThe release workflow currently builds `linux-x64`, `osx-arm64`, and `win-x64`.\n\n## Release Workflow\n\nCreating and publishing a GitHub Release runs `.github/workflows/release.yml`. The same workflow can also be run manually with `workflow_dispatch` by providing an existing release tag name.\n\nThe workflow:\n\n- Derives the compiler version from the GitHub Release tag, accepting tags like `v1.2.3` or `1.2.3`.\n- Restores the CLI for each release runtime identifier.\n- Runs the test suite.\n- Publishes the CLI as a Native AOT binary stamped with the release version.\n- Prints `dotnet --info`, `file`, and `ldd` output to make release-run failures diagnosable.\n- Smoke-tests the published binary against a fixture.\n- Uploads release archives for `linux-x64`, `osx-arm64`, and `win-x64`.\n\nFragments generated by release binaries include the release version in the compiler metadata comment, for example:\n\n```xml\n\u003c!-- Compiler: apim-rate-limit-compiler 1.2.3 --\u003e\n```\n\nFor downstream product repository usage, see [GitHub Actions with Bicep](examples/github-actions-bicep/README.md). That example downloads a pinned release asset, compiles reviewed rate-limit JSON, publishes XML/hash artifacts, and deploys an APIM `policyFragments` resource.\n\n## Snapshot Tests\n\nValid fixtures live in:\n\n```text\ntests/ApimRateLimitCompiler.Tests/Fixtures/valid\n```\n\nEach valid `*.json` file has a matching `*.fragment.xml.snap`.\n\nInvalid fixtures live in:\n\n```text\ntests/ApimRateLimitCompiler.Tests/Fixtures/invalid\n```\n\nEach invalid `*.json` file has a matching `*.diagnostics.snap`.\n\nSnapshots are committed intentionally and should be reviewed like generated APIM artifacts.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faltinn%2Fapim-rate-limit-compiler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faltinn%2Fapim-rate-limit-compiler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faltinn%2Fapim-rate-limit-compiler/lists"}