armasec-lite 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. armasec_lite-0.1.3/.github/workflows/ci.yml +40 -0
  2. armasec_lite-0.1.3/.github/workflows/deploy-docs.yml +140 -0
  3. armasec_lite-0.1.3/.github/workflows/release.yml +202 -0
  4. armasec_lite-0.1.3/.gitignore +14 -0
  5. armasec_lite-0.1.3/CLAUDE.md +229 -0
  6. armasec_lite-0.1.3/HARNESS-DESIGN.md +149 -0
  7. armasec_lite-0.1.3/LICENSE +21 -0
  8. armasec_lite-0.1.3/PKG-INFO +176 -0
  9. armasec_lite-0.1.3/README.md +150 -0
  10. armasec_lite-0.1.3/armasec_lite/__init__.py +33 -0
  11. armasec_lite-0.1.3/armasec_lite/armasec.py +196 -0
  12. armasec_lite-0.1.3/armasec_lite/exceptions.py +240 -0
  13. armasec_lite-0.1.3/armasec_lite/http.py +234 -0
  14. armasec_lite-0.1.3/armasec_lite/jwt.py +827 -0
  15. armasec_lite-0.1.3/armasec_lite/openid_config_loader.py +353 -0
  16. armasec_lite-0.1.3/armasec_lite/pluggable/__init__.py +309 -0
  17. armasec_lite-0.1.3/armasec_lite/pluggable/hookspecs.py +110 -0
  18. armasec_lite-0.1.3/armasec_lite/pytest_extension.py +534 -0
  19. armasec_lite-0.1.3/armasec_lite/schemas.py +331 -0
  20. armasec_lite-0.1.3/armasec_lite/token_decoder.py +348 -0
  21. armasec_lite-0.1.3/armasec_lite/token_manager.py +206 -0
  22. armasec_lite-0.1.3/armasec_lite/token_payload.py +98 -0
  23. armasec_lite-0.1.3/armasec_lite/token_security.py +645 -0
  24. armasec_lite-0.1.3/armasec_lite/utilities.py +84 -0
  25. armasec_lite-0.1.3/docs/superpowers/plans/2026-09-04-armasec-lite-library.md +6454 -0
  26. armasec_lite-0.1.3/docs/superpowers/specs/2026-09-04-armasec-lite-design.md +1277 -0
  27. armasec_lite-0.1.3/docusaurus/.gitignore +25 -0
  28. armasec_lite-0.1.3/docusaurus/docs/architecture/caching.md +49 -0
  29. armasec_lite-0.1.3/docusaurus/docs/architecture/index.md +50 -0
  30. armasec_lite-0.1.3/docusaurus/docs/architecture/request-lifecycle.md +39 -0
  31. armasec_lite-0.1.3/docusaurus/docs/architecture/threading-model.md +52 -0
  32. armasec_lite-0.1.3/docusaurus/docs/contributing.md +74 -0
  33. armasec_lite-0.1.3/docusaurus/docs/index.md +74 -0
  34. armasec_lite-0.1.3/docusaurus/docs/installation.md +40 -0
  35. armasec_lite-0.1.3/docusaurus/docs/migration.md +209 -0
  36. armasec_lite-0.1.3/docusaurus/docs/quickstart.md +112 -0
  37. armasec_lite-0.1.3/docusaurus/docs/security/index.md +41 -0
  38. armasec_lite-0.1.3/docusaurus/docs/security/jwt-verification.md +138 -0
  39. armasec_lite-0.1.3/docusaurus/docs/security/threat-model.md +63 -0
  40. armasec_lite-0.1.3/docusaurus/docusaurus.config.ts +190 -0
  41. armasec_lite-0.1.3/docusaurus/mmdcheck.mjs +41 -0
  42. armasec_lite-0.1.3/docusaurus/package-lock.json +20103 -0
  43. armasec_lite-0.1.3/docusaurus/package.json +58 -0
  44. armasec_lite-0.1.3/docusaurus/scripts/benchmark_charts.py +998 -0
  45. armasec_lite-0.1.3/docusaurus/scripts/generate_benchmarks.py +2918 -0
  46. armasec_lite-0.1.3/docusaurus/sidebars.ts +61 -0
  47. armasec_lite-0.1.3/docusaurus/src/components/PlotlyChart/Figure.tsx +195 -0
  48. armasec_lite-0.1.3/docusaurus/src/components/PlotlyChart/index.tsx +57 -0
  49. armasec_lite-0.1.3/docusaurus/src/components/PlotlyChart/styles.module.css +37 -0
  50. armasec_lite-0.1.3/docusaurus/src/css/custom.css +53 -0
  51. armasec_lite-0.1.3/docusaurus/static/.gitkeep +0 -0
  52. armasec_lite-0.1.3/docusaurus/tsconfig.json +17 -0
  53. armasec_lite-0.1.3/examples/basic.py +18 -0
  54. armasec_lite-0.1.3/examples/match_key_value_pairs.py +28 -0
  55. armasec_lite-0.1.3/examples/plugin.py +61 -0
  56. armasec_lite-0.1.3/examples/two_domains.py +28 -0
  57. armasec_lite-0.1.3/justfile +526 -0
  58. armasec_lite-0.1.3/legacy_comparison_compose/Dockerfile.app +51 -0
  59. armasec_lite-0.1.3/legacy_comparison_compose/Dockerfile.app.dockerignore +15 -0
  60. armasec_lite-0.1.3/legacy_comparison_compose/Dockerfile.bench +24 -0
  61. armasec_lite-0.1.3/legacy_comparison_compose/README.md +346 -0
  62. armasec_lite-0.1.3/legacy_comparison_compose/app/main.py +368 -0
  63. armasec_lite-0.1.3/legacy_comparison_compose/app/probe.py +434 -0
  64. armasec_lite-0.1.3/legacy_comparison_compose/bench/__init__.py +10 -0
  65. armasec_lite-0.1.3/legacy_comparison_compose/bench/cgroup.py +388 -0
  66. armasec_lite-0.1.3/legacy_comparison_compose/bench/report.py +611 -0
  67. armasec_lite-0.1.3/legacy_comparison_compose/bench/run.py +232 -0
  68. armasec_lite-0.1.3/legacy_comparison_compose/bench/runs.py +608 -0
  69. armasec_lite-0.1.3/legacy_comparison_compose/bench/scenarios.py +2401 -0
  70. armasec_lite-0.1.3/legacy_comparison_compose/docker-compose.yaml +218 -0
  71. armasec_lite-0.1.3/legacy_comparison_compose/proxy/main.py +364 -0
  72. armasec_lite-0.1.3/legacy_comparison_compose/realm/armasec-realm.json +143 -0
  73. armasec_lite-0.1.3/legacy_comparison_compose/results/.gitkeep +0 -0
  74. armasec_lite-0.1.3/legacy_comparison_compose/results/index.json +1284 -0
  75. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/call_graph.json +470 -0
  76. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/footprint.json +172 -0
  77. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/memory.json +452 -0
  78. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/profile_sampling.json +239 -0
  79. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/s1_cold_start.json +2656 -0
  80. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/s2_request_amplification.json +1199 -0
  81. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/s3_warm_flood.json +2413 -0
  82. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T00-33Z-raton-actions-00/s4_event_loop_blocking.json +16872 -0
  83. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T01-08Z-raton-actions-00/s8_failing_provider.json +261 -0
  84. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T01-55-12Z/s10_cpu_during_cold_load.json +26042 -0
  85. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T01-55-12Z/s11_idle_cpu.json +603 -0
  86. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T01-55-12Z/s9_cpu_per_request.json +10978 -0
  87. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/call_graph.json +478 -0
  88. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/footprint.json +180 -0
  89. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/memory.json +460 -0
  90. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/profile_sampling.json +259 -0
  91. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s10_cpu_during_cold_load.json +26090 -0
  92. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s11_idle_cpu.json +603 -0
  93. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s1_cold_start.json +2664 -0
  94. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s2_request_amplification.json +1207 -0
  95. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s3_warm_flood.json +2421 -0
  96. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s4_event_loop_blocking.json +16880 -0
  97. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s8_failing_provider.json +269 -0
  98. armasec_lite-0.1.3/legacy_comparison_compose/results/v0.1.0/2026-09-05T02-33-42Z/s9_cpu_per_request.json +10978 -0
  99. armasec_lite-0.1.3/legacy_comparison_compose/test_parity.py +368 -0
  100. armasec_lite-0.1.3/pyproject.toml +81 -0
  101. armasec_lite-0.1.3/tests/unit/__init__.py +0 -0
  102. armasec_lite-0.1.3/tests/unit/conftest.py +228 -0
  103. armasec_lite-0.1.3/tests/unit/test_armasec.py +79 -0
  104. armasec_lite-0.1.3/tests/unit/test_end_to_end.py +160 -0
  105. armasec_lite-0.1.3/tests/unit/test_examples.py +44 -0
  106. armasec_lite-0.1.3/tests/unit/test_exceptions.py +64 -0
  107. armasec_lite-0.1.3/tests/unit/test_http.py +133 -0
  108. armasec_lite-0.1.3/tests/unit/test_jwt_attacks.py +248 -0
  109. armasec_lite-0.1.3/tests/unit/test_jwt_claims.py +204 -0
  110. armasec_lite-0.1.3/tests/unit/test_jwt_crossvalidation.py +196 -0
  111. armasec_lite-0.1.3/tests/unit/test_jwt_parsing.py +82 -0
  112. armasec_lite-0.1.3/tests/unit/test_jwt_signatures.py +235 -0
  113. armasec_lite-0.1.3/tests/unit/test_openid_config_loader.py +193 -0
  114. armasec_lite-0.1.3/tests/unit/test_packaging.py +86 -0
  115. armasec_lite-0.1.3/tests/unit/test_pluggable.py +172 -0
  116. armasec_lite-0.1.3/tests/unit/test_pytest_extension.py +95 -0
  117. armasec_lite-0.1.3/tests/unit/test_schemas.py +208 -0
  118. armasec_lite-0.1.3/tests/unit/test_token_decoder.py +171 -0
  119. armasec_lite-0.1.3/tests/unit/test_token_manager.py +123 -0
  120. armasec_lite-0.1.3/tests/unit/test_token_payload.py +112 -0
  121. armasec_lite-0.1.3/tests/unit/test_token_security.py +483 -0
  122. armasec_lite-0.1.3/tests/unit/test_utilities.py +34 -0
  123. armasec_lite-0.1.3/uv.lock +918 -0
@@ -0,0 +1,40 @@
1
+ name: ci
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ env:
13
+ UV_VERSION: "0.10.3"
14
+
15
+ jobs:
16
+ check:
17
+ name: lint · test
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21
+
22
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
23
+ with:
24
+ version: ${{ env.UV_VERSION }}
25
+ python-version: '3.12'
26
+ enable-cache: true
27
+
28
+ # The runner image ships no `just`. The official installer needs no marketplace
29
+ # action or version pin beyond the script's own TLS pin.
30
+ - name: Install just
31
+ run: curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
32
+
33
+ - name: Install workspace
34
+ run: uv sync --all-groups
35
+
36
+ - name: Lint
37
+ run: just lint
38
+
39
+ - name: Test
40
+ run: just test
@@ -0,0 +1,140 @@
1
+ # Deploy the armasec-lite docs as a spoke site under /developer/armasec-lite/ on
2
+ # docs.vantagecompute.ai (S3 + CloudFront).
3
+ #
4
+ # Auth is handled at the edge by the Lambda@Edge Keycloak gateway on all of /developer/*,
5
+ # so there is no per-spoke auth configuration here and the site carries no login of its
6
+ # own.
7
+ #
8
+ # Required repo variables:
9
+ # DOCS_BUCKET S3 bucket name (shared with vantage-docs)
10
+ # DOCS_CF_DISTRIBUTION_ID CloudFront distribution ID
11
+ #
12
+ # Required repo secrets:
13
+ # DOCS_SPOKE_ROLE_ARN vantage-docs-spoke-armasec-lite, scoped to
14
+ # developer/armasec-lite/*. Created by the vantage-docs hub
15
+ # stack from its own spokes list, so there is no infrastructure
16
+ # to deploy from here -- adding this repo to that list is what
17
+ # brings the role into existence.
18
+
19
+ name: Deploy docs (spoke)
20
+
21
+ on:
22
+ push:
23
+ tags:
24
+ # Only a plain vX.Y.Z release publishes the docs. `v*` also matched a
25
+ # pre-release like v1.2.3rc1 and anything else beginning with v, so a release
26
+ # candidate silently replaced the published site. GitHub filter patterns are
27
+ # glob-like rather than regular expressions: `.` is literal and `+` means one
28
+ # or more of the preceding character, so this matches v1.2.3 and v10.20.30 and
29
+ # rejects v1.2.3rc1, v1.2 and vfoo.
30
+ - 'v[0-9]+.[0-9]+.[0-9]+'
31
+ workflow_dispatch:
32
+
33
+ permissions:
34
+ id-token: write
35
+ contents: read
36
+
37
+ concurrency:
38
+ group: docs-spoke
39
+ cancel-in-progress: false
40
+
41
+ jobs:
42
+ deploy:
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ # The pydoc plugin comes from npm, so there is no submodule to check out and no
46
+ # transport rewriting to do. It used to be vendored as a git submodule whose
47
+ # .gitmodules recorded an SSH URL that a runner has no key for.
48
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
49
+
50
+ # The API reference is generated from docstrings by the pydoc plugin, which parses
51
+ # the source with `ast` and never imports it. So this needs an interpreter able to
52
+ # parse the project's syntax, but deliberately not its dependencies or its
53
+ # virtualenv -- which is why there is no `uv sync` here. armasec-lite requires
54
+ # Python 3.12+, so 3.12 is enough.
55
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
56
+ with:
57
+ python-version: '3.12'
58
+
59
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
60
+ with:
61
+ node-version: 24
62
+ cache: npm
63
+ cache-dependency-path: docusaurus/package-lock.json
64
+
65
+ - name: Install docs dependencies
66
+ working-directory: docusaurus
67
+ run: npm ci
68
+
69
+ - name: Build docs
70
+ working-directory: docusaurus
71
+ run: npm run build
72
+
73
+ # The plugin fails the build when introspection fails outright, and when every
74
+ # requested module documents nothing. This catches the case neither covers: a partial
75
+ # generation, where some modules landed and others silently did not.
76
+ #
77
+ # Counts against the module list in the config rather than a list repeated here, so
78
+ # a module added to the site cannot be forgotten in the check. This site has a
79
+ # single plugin instance, so `instances` is always 1 and accounts for the one
80
+ # generated index.md that is not itself a module page.
81
+ - name: Verify the API reference is complete
82
+ working-directory: docusaurus
83
+ run: |
84
+ declared=$(grep -cE "^\s+\{module: " docusaurus.config.ts)
85
+ instances=$(grep -cE "^\s+id: '" docusaurus.config.ts)
86
+ generated=$(( $(find docs/api-reference -mindepth 1 -name '*.md' | wc -l) - instances ))
87
+
88
+ if [ "$generated" -ne "$declared" ]; then
89
+ echo "API reference is incomplete: $declared modules declared, $generated pages generated."
90
+ echo "Likely cause: a module was renamed or moved without updating the plugin's"
91
+ echo "module list in docusaurus/docusaurus.config.ts."
92
+ exit 1
93
+ fi
94
+
95
+ echo "API reference OK: $generated pages for $declared declared modules"
96
+
97
+ # The benchmark pages are generated by npm's prebuild hook from the committed result
98
+ # files, one page per run plus a summary, and are not committed. The generator raises
99
+ # on a missing or malformed result file, so a partial results tree already fails the
100
+ # build. This catches the remaining case: a run directory that produced no page.
101
+ #
102
+ # Counted against the results tree rather than a number repeated here, so committing
103
+ # a new run cannot be forgotten in the check.
104
+ - name: Verify the benchmark pages are complete
105
+ working-directory: docusaurus
106
+ run: |
107
+ committed=$(find ../legacy_comparison_compose/results -mindepth 2 -maxdepth 2 -type d | wc -l)
108
+ pages=$(find docs/benchmarks -name '*.mdx' | wc -l)
109
+
110
+ if [ "$pages" -ne "$(( committed + 1 ))" ]; then
111
+ echo "Benchmark pages are incomplete: $committed runs committed, $pages pages generated."
112
+ echo "Expected one page per committed run, plus one summary page."
113
+ exit 1
114
+ fi
115
+
116
+ echo "Benchmark pages OK: $pages pages for $committed committed runs"
117
+
118
+ - name: Configure AWS credentials
119
+ uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
120
+ with:
121
+ role-to-assume: ${{ secrets.DOCS_SPOKE_ROLE_ARN }}
122
+ aws-region: us-east-1
123
+
124
+ # --delete is safe here and only here: the hub excludes every spoke listed in
125
+ # .developer-subsites from its own sync, so this subtree is owned by this repo alone.
126
+ - name: Sync to S3
127
+ run: |
128
+ aws s3 sync docusaurus/build/ "s3://${{ vars.DOCS_BUCKET }}/developer/armasec-lite/" --delete
129
+
130
+ - name: Invalidate CloudFront
131
+ env:
132
+ DOCS_CF_DISTRIBUTION_ID: ${{ vars.DOCS_CF_DISTRIBUTION_ID }}
133
+ run: |
134
+ if [ -z "${DOCS_CF_DISTRIBUTION_ID}" ]; then
135
+ echo "DOCS_CF_DISTRIBUTION_ID is not set; skipping CloudFront invalidation."
136
+ exit 0
137
+ fi
138
+ aws cloudfront create-invalidation \
139
+ --distribution-id "${DOCS_CF_DISTRIBUTION_ID}" \
140
+ --paths "/developer/armasec-lite/*"
@@ -0,0 +1,202 @@
1
+ # Build and publish armasec-lite to PyPI.
2
+ #
3
+ # A tag publishes. A workflow_dispatch run builds, smoke tests and uploads the artifacts
4
+ # without publishing anything, so the packaging can be exercised without burning a version
5
+ # number: every publishing job is gated on the ref being a tag.
6
+ #
7
+ # Publishing uses PyPI trusted publishing (OIDC), so there is no API token to store or
8
+ # rotate. It requires a one-time setup on PyPI: add a trusted publisher for the project
9
+ # pointing at vantagecompute/armasec-lite, workflow `release.yml`, environment `pypi`.
10
+ #
11
+ # This is a pure Python package with no compiled extensions, so one universal
12
+ # `py3-none-any` wheel covers every platform. There is no build matrix and no
13
+ # architecture-specific retagging.
14
+
15
+ name: Build and Publish armasec-lite
16
+
17
+ on:
18
+ push:
19
+ tags:
20
+ - 'v*'
21
+ workflow_dispatch:
22
+
23
+ env:
24
+ PYTHON_VERSION: '3.12'
25
+
26
+ jobs:
27
+ build:
28
+ name: Build and smoke test
29
+ runs-on: ubuntu-24.04
30
+ outputs:
31
+ version: ${{ steps.version.outputs.version }}
32
+
33
+ steps:
34
+ - name: Checkout
35
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36
+
37
+ - name: Set up uv
38
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
39
+ with:
40
+ python-version: ${{ env.PYTHON_VERSION }}
41
+
42
+ - name: Determine version
43
+ id: version
44
+ run: |
45
+ if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
46
+ VERSION="${GITHUB_REF#refs/tags/v}"
47
+ else
48
+ VERSION="0.0.0.dev0+${GITHUB_SHA::8}"
49
+ fi
50
+
51
+ # Practical subset of the PEP 440 grammar, not the full spec: covers a plain
52
+ # release, one pre-release segment (a/b/c/rc, with a '.', '-', or no separator),
53
+ # an optional .postN, an optional .devN, and an optional +local segment. If a
54
+ # legitimate exotic version is ever rejected here, relax this regex rather than
55
+ # fight it.
56
+ PEP440_RE='^[0-9]+\.[0-9]+\.[0-9]+([.-]?(a|b|c|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?(\+[A-Za-z0-9]+)?$'
57
+ if [[ ! "${VERSION}" =~ ${PEP440_RE} ]]; then
58
+ echo "::error::Version '${VERSION}' is not a valid PEP 440 version. Push a tag shaped like 'vX.Y.Z' or 'vX.Y.Zrc1'." >&2
59
+ exit 1
60
+ fi
61
+
62
+ echo "version=${VERSION}" >> $GITHUB_OUTPUT
63
+ echo "Building version ${VERSION}"
64
+
65
+ - name: Set the package version
66
+ env:
67
+ VERSION: ${{ steps.version.outputs.version }}
68
+ run: |
69
+ # armasec_lite.__version__ reads importlib.metadata at import time, so the
70
+ # version lives in exactly one place and only pyproject.toml needs rewriting.
71
+ sed -i "0,/^version = \".*\"/s//version = \"${VERSION}\"/" pyproject.toml
72
+ grep '^version' pyproject.toml
73
+
74
+ - name: Run the test suite
75
+ run: |
76
+ uv sync --all-groups
77
+ uv run pytest tests/unit -q
78
+
79
+ - name: Lint and type check
80
+ run: |
81
+ uv run ruff check .
82
+ uv run ruff format --check .
83
+ uv run mypy armasec_lite
84
+
85
+ - name: Build sdist and wheel
86
+ run: uv build
87
+
88
+ - name: Smoke test the built wheel
89
+ env:
90
+ VERSION: ${{ steps.version.outputs.version }}
91
+ run: |
92
+ # Install the artifact itself into a clean environment, not the source tree.
93
+ # A package can pass its own test suite and still be unusable once built, if a
94
+ # module is missing from the wheel or an entry point does not resolve.
95
+ uv venv /tmp/smoke
96
+ VIRTUAL_ENV=/tmp/smoke uv pip install dist/*.whl
97
+ /tmp/smoke/bin/python - <<'PY'
98
+ import os
99
+ import armasec_lite
100
+ from armasec_lite import Armasec, TokenPayload, TokenSecurity
101
+
102
+ expected = os.environ["VERSION"]
103
+ assert armasec_lite.__version__ == expected, (
104
+ f"wheel reports {armasec_lite.__version__}, expected {expected}"
105
+ )
106
+ for name in armasec_lite.__all__:
107
+ assert hasattr(armasec_lite, name), f"{name} missing from the built wheel"
108
+ print(f"armasec-lite {armasec_lite.__version__} imports cleanly from the wheel")
109
+ PY
110
+
111
+ - name: Verify the pytest plugin entry point resolves
112
+ run: |
113
+ # The pytest11 entry point is resolved by pytest at startup, so a broken one
114
+ # breaks every consumer's test run rather than failing quietly.
115
+ VIRTUAL_ENV=/tmp/smoke uv pip install pytest
116
+ /tmp/smoke/bin/python -c "
117
+ from importlib.metadata import entry_points
118
+ names = [e.value for e in entry_points(group='pytest11')]
119
+ assert 'armasec_lite.pytest_extension' in names, names
120
+ print('pytest11 entry point resolves')
121
+ "
122
+
123
+ - name: Upload distributions
124
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
125
+ with:
126
+ name: dist
127
+ path: dist/*
128
+ retention-days: 30
129
+
130
+ publish-pypi:
131
+ name: Publish to PyPI
132
+ runs-on: ubuntu-24.04
133
+ needs: build
134
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
135
+ environment: pypi
136
+
137
+ permissions:
138
+ id-token: write
139
+
140
+ steps:
141
+ - name: Download distributions
142
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
143
+ with:
144
+ name: dist
145
+ path: dist
146
+
147
+ - name: List distributions
148
+ run: ls -lh dist/
149
+
150
+ - name: Publish
151
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
152
+ with:
153
+ packages-dir: dist/
154
+
155
+ create-release:
156
+ name: Create GitHub Release
157
+ runs-on: ubuntu-24.04
158
+ needs: [build, publish-pypi]
159
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
160
+
161
+ permissions:
162
+ contents: write
163
+
164
+ steps:
165
+ - name: Download distributions
166
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
167
+ with:
168
+ name: dist
169
+ path: dist
170
+
171
+ - name: Create Release
172
+ uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
173
+ with:
174
+ tag_name: v${{ needs.build.outputs.version }}
175
+ name: armasec-lite ${{ needs.build.outputs.version }}
176
+ body: |
177
+ ## armasec-lite ${{ needs.build.outputs.version }}
178
+
179
+ Injectable FastAPI authentication against OIDC providers, with three runtime
180
+ dependencies instead of upstream armasec's ten.
181
+
182
+ ### Install
183
+
184
+ ```bash
185
+ uv add armasec-lite==${{ needs.build.outputs.version }}
186
+ ```
187
+
188
+ ```bash
189
+ pip install armasec-lite==${{ needs.build.outputs.version }}
190
+ ```
191
+
192
+ The pytest fixtures ship in an extra, so they stay out of production installs:
193
+
194
+ ```bash
195
+ uv add "armasec-lite[test]==${{ needs.build.outputs.version }}"
196
+ ```
197
+
198
+ ### Documentation
199
+
200
+ https://docs.vantagecompute.ai/developer/armasec-lite/
201
+ files: dist/*
202
+ fail_on_unmatched_files: true
@@ -0,0 +1,14 @@
1
+ .worktrees/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ .coverage
12
+ coverage.xml
13
+ junit.xml
14
+
@@ -0,0 +1,229 @@
1
+ # armasec-lite
2
+
3
+ Injectable FastAPI authentication against OIDC providers. A dependency-minimal
4
+ reimplementation of [armasec](https://github.com/omnivector-solutions/armasec) 3.x with the
5
+ same public API.
6
+
7
+ Import package `armasec_lite`, distribution `armasec-lite`, Python 3.12+.
8
+
9
+ Runtime dependencies are exactly three: `fastapi`, `cryptography`, `pydantic`. Adding a
10
+ fourth is a design change, not a routine edit. `tests/unit/test_packaging.py` enforces both
11
+ the count and the absence of `jose`, `buzz`, `snick`, `auto_name_enum`, `pluggy`, `respx`
12
+ and `httpx` by walking the AST of every module.
13
+
14
+ `armasec_lite/jwt.py` is the only security-critical module. A defect there is an
15
+ authentication bypass, not a bug. Read its module docstring before changing it.
16
+
17
+ ## Writing docstrings
18
+
19
+ **The docstrings in `armasec_lite/` are the API reference.** There is no hand-written
20
+ reference page. `@vantagecompute/docusaurus-plugin-pydoc` parses each module with `ast`
21
+ at build time and generates the entire reference section from what it finds. A docstring
22
+ you skip is a page section that does not exist.
23
+
24
+ The generator never imports the code, so it sees only what is literally written in the
25
+ source: docstrings, signatures, annotations, decorators and class-level assignments.
26
+
27
+ ### What the generator renders, and what that forces
28
+
29
+ Read `docusaurus/node_modules/@vantagecompute/docusaurus-plugin-pydoc/src/renderer.js` if
30
+ you need the detail.
31
+ The four consequences that change how you write:
32
+
33
+ **1. The module docstring becomes the page's `## Overview`, and its FIRST LINE becomes the
34
+ Description column of the reference index, truncated to 80 characters.**
35
+
36
+ Write a first line that is a complete sentence, self-contained, and comfortably under 80
37
+ characters so the truncation never bites. It sits in a table beside every sibling module, so
38
+ it should distinguish this module from the others at a glance.
39
+
40
+ ```python
41
+ # Good: 56 characters, says what the module is
42
+ """A minimal JWS/JWT implementation, replacing python-jose."""
43
+
44
+ # Bad: truncated mid-word in the index table
45
+ """Exception types and the small assertion helpers armasec uses in place of py-buzz."""
46
+
47
+ # Bad: burns the budget before saying anything
48
+ """This module provides functionality for handling exceptions."""
49
+ ```
50
+
51
+ **2. Module-level constants render bare, as `NAME: annotation = value`, with no
52
+ description. `#:` comments above them are NOT captured.**
53
+
54
+ If a reader needs to understand a constant, explain it in the module docstring prose.
55
+ Nowhere else will reach the page. `JWKS_REFRESH_INTERVAL`, `MAX_BODY_BYTES`,
56
+ `DEFAULT_TIMEOUT`, `MAX_REDIRECTS` and `SUPPORTED_ALGORITHMS` all need this. Keep the `#:`
57
+ comments anyway; they serve someone reading the source.
58
+
59
+ **3. Class attributes render as a table with columns Attribute, Type and Default, and no
60
+ description column.**
61
+
62
+ The class docstring's `Attributes:` section is the only place a reader learns what a field
63
+ means. Every class with public attributes needs one, and it must cover every attribute.
64
+
65
+ **4. `__init__` and `__call__` are always rendered. Everything else beginning with an
66
+ underscore is private and never rendered.**
67
+
68
+ An earlier version of this file said these two render only when they carry a docstring.
69
+ That is wrong, and the difference matters: an undocumented `__init__` produces an **empty
70
+ section** on the page rather than being omitted. So a missing docstring there is visible to
71
+ readers as a gap, not hidden.
72
+
73
+ `TokenSecurity.__call__` is the most important method in the library. Methods like it earn
74
+ a substantial docstring, not a one-liner.
75
+
76
+ **5. Use `###` and below for any heading inside a docstring, never `##`.**
77
+
78
+ The generator emits docstrings verbatim into the page beneath its own `## Overview`
79
+ heading. An `##` in your prose escapes that section and collides with the generator's own
80
+ `## Constants` and `## Classes` headings, producing duplicate anchors and a broken table of
81
+ contents. This was a real defect found by building the site and reading the output.
82
+
83
+ ### Module docstrings
84
+
85
+ Two to five short paragraphs. Cover: what this module is for, where it sits in the request
86
+ path, its constants, and anything non-obvious a reader would otherwise have to reconstruct
87
+ from the source.
88
+
89
+ Non-obvious is the operative word. Prefer the thing that is surprising, deliberate, or
90
+ load-bearing over a restatement of the module's name:
91
+
92
+ - `http.py`: `urllib.request.build_opener()` installs `FileHandler`, so an opener will
93
+ happily read `file://` URLs. The scheme guard in `get_json` is therefore load-bearing and
94
+ not redundant with the URL validation in `schemas.py`.
95
+ - `openid_config_loader.py`: the lock is a `threading.Lock` and not an `asyncio.Lock`
96
+ because an `asyncio.Lock` binds to the loop that first awaits it and goes stale across
97
+ test loops.
98
+ - `schemas.py`: `OpenidConfig.issuer` is a plain `str` and deliberately not `AnyHttpUrl`,
99
+ because it is compared against a token's `iss` claim by exact string equality and
100
+ normalization would reject valid tokens from providers publishing a bare-host issuer.
101
+ - `exceptions.py`: `handle_errors` deliberately does not re-wrap `ArmasecError` subclasses,
102
+ unlike py-buzz, so a specific 401 is not flattened into a generic 500.
103
+
104
+ If you find yourself writing a comment in the source that explains why something is the way
105
+ it is, ask whether it belongs in the module docstring instead, where readers of the
106
+ reference will see it.
107
+
108
+ ### Class and function docstrings
109
+
110
+ Google style, which is what the codebase already uses.
111
+
112
+ Every public function and method needs `Args:` covering every parameter. Add `Returns:`
113
+ when the return value is not obvious from the name and annotation.
114
+
115
+ **`Raises:` matters more here than in most projects.** This is an authentication library:
116
+ the exception type a call raises determines the HTTP status a client sees. A caller needs to
117
+ know that `TokenDecoder.decode` can raise `ExpiredSignatureError` (401) or
118
+ `PayloadMappingError` (500), and what the difference means. Document it.
119
+
120
+ ```python
121
+ def decode(self, token: str, **claims: Any) -> TokenPayload:
122
+ """
123
+ Decode a JWT into a TokenPayload, checking signatures and claims.
124
+
125
+ Args:
126
+ token: The token to decode.
127
+ claims: Additional constraints, such as `audience` or `issuer`. May include an
128
+ `options` dict merged over `decode_options_override`.
129
+
130
+ Returns:
131
+ The verified payload, with `original_token` set to the input token.
132
+
133
+ Raises:
134
+ AuthenticationError: The token is malformed, its signature does not verify, or a
135
+ claim check failed. Subclasses carry the specific reason and all map to 401.
136
+ PayloadMappingError: The configured `permission_extractor` did not match a path in
137
+ the decoded token. Maps to 500, because that is a server misconfiguration
138
+ rather than a bad request.
139
+ """
140
+ ```
141
+
142
+ ### Style
143
+
144
+ Explain why, not what. The signature already says what. A docstring reading
145
+ "Initializes the TokenSecurity instance" is worse than no docstring, because it occupies the
146
+ space where the useful sentence would go.
147
+
148
+ Say when something is deliberate. Several choices in this codebase look wrong until you know
149
+ the reason, and a reader who does not know will "fix" them.
150
+
151
+ Do not pad to fill a template. If a function has no failure modes, omit `Raises:`.
152
+
153
+ **Never use the em-dash (U+2014) or en-dash (U+2013) character.** Use commas, colons,
154
+ parentheses, or separate sentences. Hyphens in compound words are fine. This applies to
155
+ every file in the repository, not only docstrings.
156
+
157
+ ### Adding a new module
158
+
159
+ The plugin's module list in `docusaurus/docusaurus.config.ts` is explicit. A new module
160
+ under `armasec_lite/` generates no reference page until you add it there, with a label.
161
+
162
+ `.github/workflows/deploy-docs.yml` fails the docs build when the number of generated pages
163
+ does not match the number of declared modules, so a module that is declared but fails to
164
+ introspect breaks the build rather than silently vanishing. It derives both counts
165
+ dynamically, so it needs no update when you add a module.
166
+
167
+ Do not add `armasec_lite/__init__.py`: it is re-exports, and its page would duplicate the
168
+ others.
169
+
170
+ ### Checking your work
171
+
172
+ ```bash
173
+ # Every module's first docstring line, with its length. Keep them under 80.
174
+ for f in armasec_lite/*.py armasec_lite/pluggable/*.py; do
175
+ python3 -c "
176
+ import ast
177
+ d = ast.get_docstring(ast.parse(open('$f').read()))
178
+ line = (d or '').strip().split(chr(10))[0] if d else '<NO MODULE DOCSTRING>'
179
+ print(f'{len(line):3d} $f {line}')
180
+ "
181
+ done
182
+
183
+ # Generate and read the reference. It is the actual deliverable, and building it is the
184
+ # only way to catch heading collisions and layout problems.
185
+ cd docusaurus && npm ci && npm run build && ls docs/api-reference/
186
+ ```
187
+
188
+ **Read the built output, do not just check that the build passed.** Two defects in this
189
+ repository's docs were found only by reading generated pages: three module descriptions
190
+ silently truncated in the index table, and `##` headings in docstrings colliding with the
191
+ generator's own section headings.
192
+
193
+ ### Renderer notes
194
+
195
+ `Args:`, `Returns:` and `Raises:` blocks render as single run-on paragraphs, because the
196
+ renderer emits docstring bodies verbatim into Markdown and Markdown collapses the newlines.
197
+ The content is correct; the formatting is poor, and it is worst on long `Raises:` sections.
198
+
199
+ This was fixed upstream in `@vantagecompute/docusaurus-plugin-pydoc` v0.1.1, which this
200
+ project depends on. Recognized Google-style sections now render as Markdown lists.
201
+
202
+ The general rule still holds: do not work around a rendering problem by reformatting
203
+ docstrings into Markdown. They would then read badly in an editor, in `help()`, and in every
204
+ IDE tooltip, which is where most people actually meet them. Fix the renderer in
205
+ `vantagecompute/docusaurus-plugin-pydoc` instead, which improves every Vantage project using
206
+ the plugin.
207
+
208
+ ## Testing
209
+
210
+ `just test` runs the unit suite. `just lint` runs `ruff check`, `ruff format --check` and
211
+ `mypy` in strict mode. Both must pass before a commit.
212
+
213
+ Test-driven: write the failing test, run it, watch it fail for the reason you expect, then
214
+ implement. For a security defense, go further and confirm the test fails when the defense is
215
+ removed. A passing test proves nothing if it would also pass against undefended code, and
216
+ that has already happened once in this codebase: the algorithm-confusion test passed for the
217
+ wrong reason until someone checked, because the fixture it used happened to lack the field
218
+ that would have made the forgery succeed.
219
+
220
+ `tests/unit/test_jwt_attacks.py` is the attack suite. Its test names are consumed by a
221
+ benchmark that publishes a security-posture chart, so a name there is a public claim about
222
+ what is defended. Do not rename one casually, and do not let a name outlive what its test
223
+ actually checks.
224
+
225
+ ## The justfile
226
+
227
+ `justfile` is a verbatim copy of `vantage-mcp-infra`'s, kept deliberately so it can be
228
+ pruned by hand later. Most of its recipes reference infrastructure this repository does not
229
+ have. `test`, `test-cov`, `lint`, `fmt` and the `docs-*` recipes work. Leave the rest alone.