Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
c4a3b2f
test: reject spoofed forwarding IPs in rate limiter
seonghobae Aug 23, 2026
7e6daab
fix(security): anchor rate limiting to trusted network peers
seonghobae Aug 23, 2026
8a867cd
test: cover trusted proxy client-IP boundary
seonghobae Aug 23, 2026
4125cc1
test: keep route core in owned coverage
seonghobae Aug 23, 2026
7f893f9
docs: define trusted reverse-proxy rate-limit contract
seonghobae Aug 23, 2026
172ac1d
docs: trace trusted proxy client-IP boundary
seonghobae Aug 23, 2026
62e5406
test: keep SaaS serve-path assertion across security envelope
seonghobae Aug 23, 2026
dc2fe13
test(security): cover cross-client rate-limit poisoning
seonghobae Aug 23, 2026
fb98f87
fix(security): prevent duplicate proxy rate-limit buckets
seonghobae Aug 23, 2026
8b144b2
docs(security): trace proxy bucket-poisoning regression
seonghobae Aug 23, 2026
b4aa4a3
test(rate-limit): bound client bucket cardinality
seonghobae Aug 23, 2026
bf20414
fix(rate-limit): bound client bucket state
seonghobae Aug 23, 2026
2c6da6f
test(rate-limit): reject unsafe limiter config
seonghobae Aug 23, 2026
a86f845
fix(rate-limit): fail closed on unsafe config
seonghobae Aug 23, 2026
fb64472
docs(rate-limit): document bounded state and config
seonghobae Aug 23, 2026
19da673
test(security): cover IPv4-mapped trusted proxy peers
seonghobae Aug 23, 2026
dec4e7a
fix(security): canonicalize IPv4-mapped proxy peers
seonghobae Aug 23, 2026
a751633
docs(security): record mapped proxy peer normalization
seonghobae Aug 23, 2026
dcbabf8
docs(ops): explain dual-stack proxy address normalization
seonghobae Aug 23, 2026
ba7554e
test: reproduce unsigned Stripe webhook plan escalation
seonghobae Aug 23, 2026
042830d
fix: authenticate Stripe webhooks before entitlement handling
seonghobae Aug 23, 2026
cc0c6ed
test(billing): keep Stripe webhook behind abuse controls
seonghobae Aug 23, 2026
1f5c2b2
fix(billing): keep Stripe verification inside app controls
seonghobae Aug 23, 2026
4488ab9
test(ci): align public-route contract with Stripe repair
seonghobae Aug 23, 2026
dce2424
fix(billing): retire unsigned Stripe plan-upgrade handler
seonghobae Aug 23, 2026
c5e85d0
test(security): reject equivalent IPv6 proxy spelling mismatch
seonghobae Aug 23, 2026
f69ad0d
fix(security): canonicalize equivalent IPv6 proxy addresses
seonghobae Aug 23, 2026
5e30072
test(server): prove toast route is unique
seonghobae Aug 23, 2026
4ae65a6
test(a11y): exercise shipped toast asset route
seonghobae Aug 23, 2026
87d34bd
test(security): bind invites to intended identity
seonghobae Aug 23, 2026
eaab3f8
fix(security): bind invite redemption to email
seonghobae Aug 23, 2026
5dc72e7
test(ci): match secured route composition
seonghobae Aug 23, 2026
a1ad3bc
test(security): fail closed when OIDC is unconfigured
seonghobae Aug 23, 2026
7b19b86
fix(security): require explicit development for mock OIDC
seonghobae Aug 23, 2026
2d39860
ci: run owned-production coverage in server gate
seonghobae Aug 23, 2026
77406a9
test(security): require invite safety in protected route graph
seonghobae Aug 23, 2026
d611894
test(security): cover shared OIDC fail-closed boundary
seonghobae Aug 23, 2026
7c1f08c
fix(security): secure shared application boundary
seonghobae Aug 23, 2026
394e3cc
fix(security): align OIDC regression and trust docs
seonghobae Aug 23, 2026
7ef64d2
test(security): expose invite core bypass regression
seonghobae Aug 23, 2026
36c1037
test(security): bind invite guard to live sessions
seonghobae Aug 23, 2026
bc50cc9
fix(security): reject revoked sessions before invite binding
seonghobae Aug 23, 2026
40eb4d1
test(security): keep guard rejections behind abuse controls
seonghobae Aug 23, 2026
ab7c7fe
test(security): register guard abuse-control regression
seonghobae Aug 23, 2026
72f6acc
fix(security): account rejected guards in abuse controls
seonghobae Aug 23, 2026
de0a053
test(stripe): preserve literal signature timestamp
seonghobae Aug 23, 2026
131d79e
fix(stripe): verify literal signed timestamp
seonghobae Aug 23, 2026
cf67f2a
test(security): preserve guard rejection method evidence
seonghobae Aug 23, 2026
177996e
fix(observability): retain original guard request method
seonghobae Aug 23, 2026
04363c0
fix(observability): carry original guard method safely
seonghobae Aug 23, 2026
25b95ed
test(security): require core OIDC fail-closed
seonghobae Aug 23, 2026
5807225
test(security): require core invite fail-closed behavior
seonghobae Aug 23, 2026
03cbe9f
fix(security): harden internal invite and OIDC boundaries
seonghobae Aug 23, 2026
c7baef2
test(rate-limit): require metrics for blocked requests
seonghobae Aug 24, 2026
d3f8d4d
fix(rate-limit): retain blocked request observability
seonghobae Aug 24, 2026
e8ef4fd
merge security hotfix into trusted rate-limit lane
seonghobae Aug 25, 2026
3b3d4a3
docs(deploy): align fail-closed identity and AI configuration
seonghobae Aug 25, 2026
7d09220
test(security): expose shared-boundary limiter gaps
seonghobae Aug 25, 2026
8944945
test(security): run shared-boundary limiter regression
seonghobae Aug 25, 2026
2f3f8e8
refactor(security): share trusted rate-limit policy
seonghobae Aug 25, 2026
ad19c9d
fix(security): rate-limit the shared boundary before guards
seonghobae Aug 25, 2026
264bcbf
test(rate-limit): cover blocked Prometheus metrics
seonghobae Aug 25, 2026
d6463ec
test(rate-limit): register Prometheus observability regression
seonghobae Aug 25, 2026
d0f92c0
test(rate-limit): remove overflow timing dependency
seonghobae Aug 25, 2026
1fcb863
test(rate-limit): require one shared limiter implementation
seonghobae Aug 25, 2026
7f963b4
fix(rate-limit): share authoritative limiter implementation
seonghobae Aug 25, 2026
062fec8
test(coverage): require shared limiter instrumentation
seonghobae Aug 25, 2026
865f540
fix(coverage): instrument shared rate limiter
seonghobae Aug 25, 2026
f378e1d
test(ci): reject duplicate API execution
seonghobae Aug 25, 2026
0811a4f
ci: avoid duplicate API suite execution
seonghobae Aug 25, 2026
26ca25c
test(security): reproduce forged OIDC identity token acceptance
seonghobae Aug 25, 2026
a26ff22
fix(security): verify production OIDC identities before session minting
seonghobae Aug 25, 2026
c673acc
test(security): prove signed OIDC identity validation contract
seonghobae Aug 25, 2026
1ada93f
test(ci): require exact PR head checkout
seonghobae Aug 25, 2026
f9e87d0
ci: bind server tests to exact PR head
seonghobae Aug 25, 2026
1533e07
test(ci): require exact dependency review revisions
seonghobae Aug 25, 2026
204cbda
ci: bind dependency review to live exact revisions
seonghobae Aug 25, 2026
4c8b682
test(ci): preserve slashed dependency-review bases
seonghobae Aug 25, 2026
fe01284
fix(ci): support slashed live base refs
seonghobae Aug 25, 2026
5a7393e
test(security): bound abandoned OIDC authorization state
seonghobae Aug 25, 2026
55951ea
fix(security): bound transient auth state storage
seonghobae Aug 25, 2026
789d968
fix(security): cap production OIDC state
seonghobae Aug 25, 2026
fad52de
test(ci): restore coverage producer guards
seonghobae Aug 25, 2026
1f93371
fix: share trusted rate limiter with core routes
seonghobae Aug 28, 2026
886caf5
test: reproduce rate-limit import-order bypass
seonghobae Aug 28, 2026
75b7caf
fix: preserve shared limiter across import order
seonghobae Aug 28, 2026
dcf37e8
fix: redact bearer tokens from rate-limit logs
seonghobae Aug 28, 2026
74d41d1
test: fail closed on direct core production OIDC
seonghobae Aug 28, 2026
c2a3811
test: run core production OIDC fail-closed regression
seonghobae Aug 28, 2026
8e79805
fix: fail closed direct core production OIDC
seonghobae Aug 28, 2026
c53c87b
test: preserve moved core security contracts
seonghobae Aug 28, 2026
58695e4
fix(auth): preserve guard rejection observability
seonghobae Aug 28, 2026
4bc5fc3
test(auth): reuse mixed-case OIDC accounts
seonghobae Aug 28, 2026
efd9280
fix(auth): reuse case-variant OIDC accounts
seonghobae Aug 28, 2026
c542615
docs: clarify orchestrator token mapping
seonghobae Aug 28, 2026
50c74dd
test(ci): require dependency review merge-base scope
seonghobae Aug 28, 2026
67fd3fe
test(ci): run dependency merge-base regression
seonghobae Aug 28, 2026
199b811
fix(ci): scope dependency review to PR merge base
seonghobae Aug 28, 2026
e98cae8
test(rate-limit): preserve metrics response on fold failure
seonghobae Aug 28, 2026
f6bd5ea
test(rate-limit): run observability failure regression
seonghobae Aug 28, 2026
d3ea2b2
fix(rate-limit): isolate metrics folding failures
seonghobae Aug 29, 2026
0c38ee4
test(auth): define canonical email identity contract
seonghobae Aug 29, 2026
f4ceb08
test(auth): run canonical email identity contract
seonghobae Aug 29, 2026
8c1f666
fix(auth): migrate canonical email identities
seonghobae Aug 29, 2026
4a47e15
fix(auth): canonicalize password identities at shared boundary
seonghobae Aug 29, 2026
ede4d13
test(auth): compare legacy rows by value
seonghobae Aug 29, 2026
b251c6c
test(security): reproduce current identity and OIDC review findings
seonghobae Aug 29, 2026
81797d3
fix: close security boundary regressions
seonghobae Aug 29, 2026
a154cf1
fix: isolate shared signup metrics
seonghobae Aug 29, 2026
7430bb2
fix: count mock SSO signups
seonghobae Aug 29, 2026
f074b41
test: reject private OIDC metadata endpoints
seonghobae Aug 29, 2026
b5af769
feat: add pinned public HTTPS transport
seonghobae Aug 29, 2026
d46ac47
test: cover pinned public HTTPS transport
seonghobae Aug 29, 2026
474595a
fix: pin OIDC outbound HTTPS destinations
seonghobae Aug 29, 2026
f3c925e
test: inject bounded OIDC HTTPS seam
seonghobae Aug 29, 2026
b8283b5
test: inject OIDC HTTPS transport in production regression
seonghobae Aug 29, 2026
660d272
test: gate public HTTPS transport coverage
seonghobae Aug 29, 2026
d7575ce
test(runtime): reject unsupported Node 23.4
seonghobae Aug 29, 2026
70928aa
fix(runtime): exclude Node 23.4 from SQLite support
seonghobae Aug 29, 2026
08d328b
test(security): prove guarded invite rate-limit ordering
seonghobae Aug 29, 2026
f36eb24
fix(auth): unify Unicode identity and OIDC form boundaries
seonghobae Aug 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 65 additions & 14 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,24 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}

- name: Verify exact checkout revision
env:
EXPECTED_HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
shell: bash
run: |
set -euo pipefail
actual_head_sha="$(git rev-parse HEAD)"
printf 'expected_head_sha=%s\nactual_head_sha=%s\n' "$EXPECTED_HEAD_SHA" "$actual_head_sha"
test "$actual_head_sha" = "$EXPECTED_HEAD_SHA"

- name: Check dependency review support
id: dependency_review_support
env:
GH_TOKEN: ${{ github.token }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPOSITORY: ${{ github.repository }}
shell: bash
Expand All @@ -40,33 +52,72 @@ jobs:

api_url="${GITHUB_API_URL:-https://api.github.com}"
response_file="$(mktemp)"
status="$(
curl -fsS -o "$response_file" -w '%{http_code}' \
base_response_file="$(mktemp)"
compare_response_file="$(mktemp)"
trap 'rm -f "$response_file" "$base_response_file" "$compare_response_file"' EXIT

base_ref_encoded="$(python3 -c 'import os, urllib.parse; print(urllib.parse.quote(os.environ["BASE_REF"], safe="/"))')"
base_status="$(
curl -sS -o "$base_response_file" -w '%{http_code}' \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${api_url}/repos/${REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}" \
"${api_url}/repos/${REPOSITORY}/branches/${base_ref_encoded}" \
|| true
)"

if [ "$status" = "200" ]; then
echo "supported=true" >>"$GITHUB_OUTPUT"
exit 0
if [ "$base_status" != "200" ]; then
echo "::error::Unable to resolve live base branch ${BASE_REF}; GitHub API returned HTTP ${base_status}."
exit 1
fi

if [ "$status" = "403" ] || [ "$status" = "404" ]; then
echo "::warning::Dependency review is unavailable for ${REPOSITORY}; skipping dependency-review hard gate."
echo "supported=false" >>"$GITHUB_OUTPUT"
exit 0
LIVE_BASE_SHA="$(jq -er '.commit.sha | select(test("^[0-9a-f]{40}$"))' "$base_response_file")" || {
echo "::error::Live base branch response did not contain a valid commit SHA."
exit 1
}
printf 'live_base_sha=%s\n' "$LIVE_BASE_SHA"

compare_status="$(
curl -sS -o "$compare_response_file" -w '%{http_code}' \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${api_url}/repos/${REPOSITORY}/compare/${LIVE_BASE_SHA}...${HEAD_SHA}" \
|| true
)"

if [ "$compare_status" != "200" ]; then
echo "::error::Unable to resolve the pull-request merge base for ${LIVE_BASE_SHA}...${HEAD_SHA}; GitHub API returned HTTP ${compare_status}."
exit 1
fi

BASE_SHA="$(jq -er '.merge_base_commit.sha | select(test("^[0-9a-f]{40}$"))' "$compare_response_file")" || {
echo "::error::Compare response did not contain a valid merge-base SHA."
exit 1
}
echo "base_sha=$BASE_SHA" >>"$GITHUB_OUTPUT"

status="$(
curl -sS -o "$response_file" -w '%{http_code}' \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${api_url}/repos/${REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}" \
|| true
)"

if [ "$status" != "200" ]; then
echo "::error::Dependency review support check failed with HTTP ${status}."
exit 1
fi
Comment thread
seonghobae marked this conversation as resolved.

echo "::error::Dependency review support check failed with HTTP ${status}."
cat "$response_file"
exit 1
echo "supported=true" >>"$GITHUB_OUTPUT"

- name: Dependency review
if: steps.dependency_review_support.outputs.supported == 'true'
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
base-ref: ${{ steps.dependency_review_support.outputs.base_sha }}
head-ref: ${{ github.event.pull_request.head.sha }}
fail-on-severity: moderate
comment-summary-in-pr: on-failure
22 changes: 20 additions & 2 deletions .github/workflows/server-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Verify exact checkout revision
env:
EXPECTED_HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
run: |
actual_head_sha="$(git rev-parse HEAD)"
printf 'expected_head_sha=%s\nactual_head_sha=%s\n' "$EXPECTED_HEAD_SHA" "$actual_head_sha"
test "$actual_head_sha" = "$EXPECTED_HEAD_SHA"
- name: Setup Node 22.13
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
Expand All @@ -33,8 +42,8 @@ jobs:
run: npm ci
- name: Unit tests (EVM · CPM · baseline · workload)
run: npm run test:unit
- name: API tests (auth · tenancy · RBAC · billing · webhooks · rate limit)
run: npm run test:api
- name: Owned production coverage evidence (unit · API under c8)
run: npm run test:coverage
Comment thread
seonghobae marked this conversation as resolved.
- name: app.js stays eval-safe (no top-level import/export)
run: node -e "new Function(require('fs').readFileSync('app.js','utf8')); console.log('eval-safe OK')"

Expand All @@ -45,6 +54,15 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Verify exact checkout revision
env:
EXPECTED_HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
run: |
actual_head_sha="$(git rev-parse HEAD)"
printf 'expected_head_sha=%s\nactual_head_sha=%s\n' "$EXPECTED_HEAD_SHA" "$actual_head_sha"
test "$actual_head_sha" = "$EXPECTED_HEAD_SHA"
- name: Setup Node 22.13
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- Redacted invite and public-share bearer-token path segments in both ordinary
request logs and rate-limit rejection logs, including malformed trailing
paths, so access-log retention cannot become a credential disclosure channel.
- Unified server mailbox canonicalization across password, invitation, OIDC,
and legacy-migration boundaries with trim, NFC normalization, lowercase
conversion, and canonical unique-index enforcement.
- Made the public HTTPS transport serialize URLSearchParams as UTF-8 bytes so
production OIDC token exchanges reach the provider instead of failing at the
request boundary.
- Replaced the unsigned `POST /api/stripe/webhook` plan-upgrade stub with a
fail-closed raw-body HMAC-SHA-256 signature boundary. Signed deliveries are
acknowledged only; webhook JSON is not entitlement authority until durable
event reconciliation. Unsigned, stale, or body-mutated signatures fail closed,
and the public app copies protected logging and rate-limit middleware so abuse
controls still wrap the endpoint (Krawczyk et al., 1997; National Institute of
Standards and Technology, 2008).
- Made contextual-orchestrator briefing requests fail closed unless an authenticated endpoint is configured. Deterministic generated text is restricted to explicit `SCOPEWEAVE_DEV=1`, message/provider responses are bounded and validated, and non-loopback HTTP transport is rejected.
- Made `SCOPEWEAVE_JWT_SECRET` mandatory at startup and rejected weak or
unexpanded placeholder values so production deployments fail closed.
Expand Down
62 changes: 57 additions & 5 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,53 @@ persists the database in the `scopeweave-data` volume.
| `SCOPEWEAVE_JWT_SECRET` | **yes** | Signs session JWTs. Startup fails unless it contains at least 32 non-whitespace characters. |
| `PORT` | no (default 8787) | Listen port |
| `SCOPEWEAVE_DB` | no (default `/data/scopeweave.db`) | SQLite file path (on the volume) |
| `SCOPEWEAVE_DEV` | no | Must be `1` to enable the dev `activate-pro` endpoint. **Never set in production.** |
| `SCOPEWEAVE_DEV` | no | Must be `1` to enable development-only behavior: the `activate-pro` endpoint, the built-in OIDC mock when `OIDC_ISSUER` is unset, and the deterministic orchestrator mock when `ORCHESTRATOR_URL` is unset. **Never set in production.** |
| `STRIPE_SECRET_KEY`, `STRIPE_PRICE_ID`, `STRIPE_WEBHOOK_SECRET` | for live billing | Enables real Stripe Checkout (`npm i stripe` too). Without them, billing uses the mock path. |
| `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_REDIRECT_URI` | for real SSO | Points the OIDC login at your IdP. Unset → a built-in mock IdP (dev/test only). |
| `ORCHESTRATOR_URL` | for AI 브리핑 | contextual-orchestrator 주소. Unset → deterministic mock. |
| `ORCHESTRATOR_TOKEN` | with URL | orchestrator Bearer 토큰 (`CONTEXTUAL_ORCHESTRATOR_TOKEN`). |
| `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_REDIRECT_URI` | for real SSO | Points the OIDC login at your IdP. Outside explicit `SCOPEWEAVE_DEV=1`, a missing issuer fails closed with `404 sso not configured`; the built-in mock exists only when the issuer is unset **and** development mode is explicitly enabled. |
| `ORCHESTRATOR_URL` | for AI briefing | contextual-orchestrator origin. A missing URL fails closed outside explicit `SCOPEWEAVE_DEV=1`; the deterministic mock exists only in development mode. |
| `ORCHESTRATOR_TOKEN` | with URL | Required Bearer token for configured orchestrator requests. Set it to the same value as the orchestrator's `CONTEXTUAL_ORCHESTRATOR_TOKEN` setting. |
| `CLEARFOLIO_URL` | for 산출물 viewer | Clearfolio 문서 뷰어 백엔드 주소. Unset → built-in mock (dev/test). |
| `CLEARFOLIO_HMAC_SECRET` | optional | Signs tenant-claim headers (`clearfolio.tenant-claims.hmac-secret`와 동일 값). |
| `SCOPEWEAVE_ATTACHMENT_STATUS_CONCURRENCY` | no (default 8, maximum 32) | Maximum concurrent Clearfolio status lookups during one attachment-list request. Invalid values fall back to 8; values above 32 are clamped. |
| `SCOPEWEAVE_ATTACHMENT_STATUS_TIMEOUT_MS` | no (default 3000, maximum 30000) | Hard caller-side timeout for each Clearfolio status lookup. The AbortSignal is also forwarded downstream. |
| `SCOPEWEAVE_ATTACHMENT_STATUS_BUDGET_MS` | no (default 5000, maximum 60000) | Wall-clock budget for the entire best-effort refresh pass. Work not started before the deadline is deferred to a later list request. |
| `SCOPEWEAVE_RATE_LIMIT_MAX` (+ `SCOPEWEAVE_RATE_LIMIT_WINDOW_MS`) | recommended | Per-IP fixed-window rate limiting (429 + Retry-After). Off when unset. |
| `SCOPEWEAVE_RATE_LIMIT_MAX` | recommended | Per-client fixed-window request allowance. Unset or explicit `0` disables the limiter. Any other configured value must be a non-negative safe integer or startup fails. |
| `SCOPEWEAVE_RATE_LIMIT_WINDOW_MS` | no (default 60000) | Fixed-window duration in milliseconds. An explicit value must be a positive safe integer or startup fails. |
| `SCOPEWEAVE_RATE_LIMIT_BUCKETS_MAX` | no (default 10000) | Maximum number of live per-client limiter buckets held by one ScopeWeave process. An explicit value must be a positive safe integer or startup fails. Once capacity is reached, previously unseen identities share a fail-closed overflow bucket until expired regular buckets are reclaimed. |
| `SCOPEWEAVE_TRUSTED_PROXY_IPS` | only behind trusted reverse proxies | Comma-separated **immediate or chained proxy peer IPs** that ScopeWeave is allowed to trust when interpreting `X-Forwarded-For`. Configure the actual IP once; dotted IPv4-mapped Node spellings such as `::ffff:127.0.0.1` are normalized to their IPv4 address before trust comparison. Leave unset for direct deployments. |

### Rate-limit capacity and tuning

When the limiter is enabled, `429` responses include `Retry-After`. Client
identity is anchored to the actual network peer unless that peer is explicitly
trusted through `SCOPEWEAVE_TRUSTED_PROXY_IPS`. Valid dotted IPv4-mapped IPv6
peer and forwarded-hop spellings are canonicalized to their underlying IPv4
address before trust comparison and limiter-key selection. This prevents a
Node dual-stack listener from collapsing all proxied clients into one limiter
bucket merely because the socket exposed an IPv4 proxy as `::ffff:a.b.c.d`.
Invalid address text is never admitted as a trusted identity.

The regular in-memory bucket map is deliberately bounded by
`SCOPEWEAVE_RATE_LIMIT_BUCKETS_MAX`; attacker-driven high-cardinality source
identities therefore cannot create unbounded limiter state. At capacity, new
identities use one shared overflow bucket rather than allocating new map entries.
Expired regular buckets are reclaimed by a bounded sweep before admitting new
identities.

Size `SCOPEWEAVE_RATE_LIMIT_BUCKETS_MAX` for the maximum legitimate concurrent
client-identity population expected **per process**, with headroom for normal
bursts. Do not increase it merely to make overflow throttling disappear: first
confirm that trusted-proxy identity extraction is correct and that the observed
cardinality is legitimate. In horizontally scaled deployments, each replica has
its own limiter state; this fixed-window implementation is a process-local abuse
control, not a globally coordinated quota system. Use a shared rate-limit store
or edge control plane when a cross-replica/global quota is required.

Limiter numeric configuration is fail-closed. A malformed, infinite, negative,
or otherwise unsafe explicit value causes startup failure instead of silently
disabling protection or resetting windows on every request. Treat such a startup
failure as a configuration incident; correct the setting rather than removing
or bypassing the limiter gate.

## Attachment status refresh operations

Expand Down Expand Up @@ -133,6 +169,22 @@ Terminate TLS at a reverse proxy (nginx/Caddy/ALB) in front of the backend and
forward to `:8787`. The client and API share the origin, so no CORS config is
needed.

`X-Forwarded-For` is **ignored for the security rate-limit identity by default**.
If a reverse proxy is the only permitted ingress to ScopeWeave, list its actual
network peer address in `SCOPEWEAVE_TRUSTED_PROXY_IPS`. For multiple trusted
proxy hops, list every trusted hop. You do not need to duplicate an IPv4 proxy
as both `a.b.c.d` and Node's dotted IPv4-mapped `::ffff:a.b.c.d` representation;
ScopeWeave canonicalizes that mapped socket/hop spelling before the trust
comparison. ScopeWeave then walks `X-Forwarded-For` from right to left, skips
explicitly trusted proxy addresses, and chooses the first untrusted valid IP as
the client identity. Missing, malformed, or all-trusted forwarding evidence
falls back to the actual socket peer.

Do not configure this trust list while untrusted clients can connect directly to
the backend. The proxy must overwrite or append forwarding information according
to a controlled ingress policy; accepting a caller-selected forwarding header
without an authenticated/trusted peer would make rate limiting bypassable.

## Kubernetes

The existing `infra/k8s/` manifests deploy the **static-only** nginx image. For
Expand Down
Loading
Loading