Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9f7de29
test(clearfolio): require explicit artifact origin trust
seonghobae Aug 15, 2026
ce4e7c5
fix(clearfolio): enforce artifact origin trust
seonghobae Aug 15, 2026
462f659
docs(changelog): record artifact origin trust
seonghobae Aug 15, 2026
865fec5
docs(clearfolio): record artifact origin trust boundary
seonghobae Aug 15, 2026
e4d2f5f
test(clearfolio): align legacy artifact origin contract
seonghobae Aug 15, 2026
1595046
merge: refresh artifact origin policy on provider boundary
seonghobae Aug 15, 2026
82ff679
merge(clearfolio): reconcile artifact origin trust with provider boun…
seonghobae Aug 16, 2026
6657767
merge(clearfolio): reconcile artifact origin policy with current prov…
seonghobae Aug 16, 2026
2954f1a
merge(clearfolio): reconcile artifact-origin policy with current prov…
seonghobae Aug 16, 2026
0c193aa
fix(clearfolio): require canonical artifact-origin allowlist entries
cursoragent Aug 16, 2026
4ab5911
chore(clearfolio): reconcile artifact-origin policy to current parent
seonghobae Aug 16, 2026
7b4b9fd
merge(clearfolio): reconcile artifact-origin policy with current prov…
seonghobae Aug 17, 2026
cac0efc
test(clearfolio): preserve parent default-deny artifact cases
seonghobae Aug 17, 2026
243fe12
docs(clearfolio): preserve parent redirect and default-deny contract
seonghobae Aug 17, 2026
2717e75
Merge current Clearfolio provider-response parent into artifact-origi…
seonghobae Aug 17, 2026
3d98ed2
merge(clearfolio): reconcile artifact-origin child with current provi…
seonghobae Aug 17, 2026
bbc9de2
merge(stack): reconcile artifact-origin child with current provider p…
seonghobae Aug 20, 2026
d8c58a8
chore(stack): reconcile artifact-origin policy with current provider …
seonghobae Aug 20, 2026
ae5daff
test(clearfolio): retain parent timeout-disposal contract
seonghobae Aug 26, 2026
6b47857
fix(clearfolio): reconcile provider timer disposal
seonghobae Aug 26, 2026
6ad5a8f
test(clearfolio): preserve provider timeout classification
seonghobae Aug 26, 2026
84afc14
chore(stack): reconcile artifact origins with live provider parent
seonghobae Aug 28, 2026
ceaa982
fix(stack): preserve parent timeout classification
seonghobae Aug 28, 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
4 changes: 4 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@
contain.
- Kubernetes/IaC security coverage remains a follow-up design lane for
any future `infra/` or container packaging surface.
- Clearfolio artifact redirects trust only the configured provider origin
unless `CLEARFOLIO_ARTIFACT_ORIGINS` lists additional exact HTTPS origins.
Cross-origin `artifactToken` values stay on the returned origin and are
never transplanted into the Clearfolio viewer.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
required a canonical signed production origin, rejected ambiguous provider
URL components, and prevented cross-origin artifact tokens from being
transplanted into the trusted Clearfolio viewer URL.
- Restricted hosted Clearfolio artifact redirects to the provider origin by
default; optional `CLEARFOLIO_ARTIFACT_ORIGINS` entries must be exact HTTPS
origins (`URL.origin` or that origin plus `/`), so scheme/host/port changes,
credentials, fragments, protocol-relative lookalikes, and unapproved
cross-origin links fail closed while approved cross-origin tokens remain bound
to the origin that issued them.
- Bounded hosted Clearfolio calls to non-redirecting 15-second requests and
256 KiB streamed JSON responses, composed caller cancellation with the
provider budget, and validated document metadata/bytes and provider job IDs
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ Docker: set a **persistent** `SCOPEWEAVE_JWT_SECRET` first, then run `docker com
| `OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/REDIRECT_URI` | Real SSO IdP (mock when unset) |
| `STRIPE_SECRET_KEY` | Real checkout (mock URL when unset) |
| `SCOPEWEAVE_RATE_LIMIT_MAX` (+`_WINDOW_MS`) | Opt-in per-IP rate limiting |
| `SCOPEWEAVE_DEV=1` | Dev-only endpoints (activate-pro) |
| `SCOPEWEAVE_DEV=1` | Dev-only endpoints (activate-pro, loopback Clearfolio HTTP, in-memory Clearfolio adapter when no provider URL exists). Never set in production. |
| `CLEARFOLIO_URL` | Production 산출물 viewer origin (HTTPS). Unset fails closed unless `SCOPEWEAVE_DEV=1`. |
| `CLEARFOLIO_HMAC_SECRET` | Tenant-claim HMAC secret (at least 32 non-whitespace characters) when a provider URL is set. |
| `CLEARFOLIO_ARTIFACT_ORIGINS` | Optional comma-separated exact HTTPS artifact origins. Unset trusts only `CLEARFOLIO_URL`. Empty or malformed values fail closed. |

## Verification

Expand Down
6 changes: 4 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ credentials never reach the browser. HWP/HWPX are rejected (Clearfolio policy).
| `GET` | `/api/projects/:id/attachments/:aid/view` | 302 → signed artifact URL (`?token=` for new-tab opens) |
| `DELETE` | `/api/projects/:id/attachments/:aid` | Uploader or manage |

Env: `CLEARFOLIO_URL` (+ optional `CLEARFOLIO_HMAC_SECRET` for gateway-signed
tenant claims). Unset → a built-in mock converter (dev/test only).
Env: `CLEARFOLIO_URL` plus `CLEARFOLIO_HMAC_SECRET` for production conversion.
Optional `CLEARFOLIO_ARTIFACT_ORIGINS` adds reviewed HTTPS CDN/object-store
origins; unset trusts only the Clearfolio origin. An unset URL is not a
successful converter: the in-memory mock exists only with `SCOPEWEAVE_DEV=1`.

## Comments (코멘트)

Expand Down
9 changes: 9 additions & 0 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ persists the database in the `scopeweave-data` volume.
| `ORCHESTRATOR_TOKEN` | with URL | Required bearer token for the configured contextual-orchestrator service (`CONTEXTUAL_ORCHESTRATOR_TOKEN`). |
| `CLEARFOLIO_URL` | for production 산출물 viewer | Root Clearfolio service origin. Production requires HTTPS and rejects credentials, paths, query strings, and fragments. When absent in production, document conversion/viewing is unavailable rather than simulated. |
| `CLEARFOLIO_HMAC_SECRET` | with URL | Required tenant-claim HMAC secret; must contain at least 32 non-whitespace characters and match Clearfolio's configured verifier secret. |
| `CLEARFOLIO_ARTIFACT_ORIGINS` | optional with URL | Comma-separated exact HTTPS artifact origins (scheme, host, optional non-default port). Leave unset to trust only `CLEARFOLIO_URL`. Empty, whitespace-only, HTTP, credentialed, path, query, fragment, or non-canonical entries fail closed before any provider call. |
| `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. |
Expand All @@ -61,6 +62,14 @@ Keep credentials in the dedicated HMAC secret setting rather than URL userinfo,
and do not configure a path, query string, or fragment. The adapter constructs
its own versioned API paths from the validated origin.

Artifact links returned by Clearfolio are untrusted until they match the
provider origin or an origin listed in `CLEARFOLIO_ARTIFACT_ORIGINS`. If
Clearfolio serves files from a reviewed CDN or object store, add only that
origin, for example `https://artifacts.example.com`. Do not put signed paths,
object keys, or tokens in the setting. Unset keeps the least-privilege default:
only the Clearfolio origin is trusted, and a cross-origin `artifactToken` is
never copied into the viewer URL.

Every hosted Clearfolio request is non-redirecting and has a hard 15-second
adapter budget; attachment status lookups compose that budget with the caller's
own cancellation signal. Successful provider responses must be
Expand Down
58 changes: 58 additions & 0 deletions docs/doctoring/clearfolio-artifact-origin-trust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Clearfolio artifact-origin trust boundary

## Decision

ScopeWeave treats every artifact link returned by Clearfolio as untrusted provider data. The configured `CLEARFOLIO_URL` origin is the default artifact trust boundary. A production operator may add reviewed CDN or object-storage origins through `CLEARFOLIO_ARTIFACT_ORIGINS`, but each entry must be an origin only: HTTPS scheme, host, and optional non-default port, with no credentials, path, query, fragment, or empty comma-separated entry.

This slice is deliberately narrower than the complete Clearfolio production-adapter program in issue #489. It does not claim that provider DNS/IP authorization, artifact content validation, retention, or all operational acceptance work is complete. It closes the redirect-origin and token-confusion boundary on top of the provider-response controls owned by the parent PR.

## Why exact origins

RFC 6454 defines an origin around scheme, host, and port. Comparing canonical URL origins therefore keeps `https://cdn.example`, `https://cdn.example:8443`, and HTTP variants in distinct trust domains instead of relying on string-prefix matching. The WHATWG URL Standard supplies the parser and serialization semantics used by Node's `URL` implementation, including explicit username/password and fragment components.

The adapter uses a positive allowlist rather than accepting any syntactically valid HTTPS URL. OWASP's SSRF guidance recommends allowlisting known destinations and disabling or tightly validating redirects when the intended service set is known. Although ScopeWeave is redirecting a browser to a provider-selected artifact rather than issuing a second server-side fetch, the same positive-trust principle prevents an untrusted provider response from turning the application into an arbitrary external redirector.

## Runtime contract

1. `CLEARFOLIO_ARTIFACT_ORIGINS` is optional. When absent, only the validated Clearfolio provider origin is trusted.
2. When present, the value is a comma-separated list of canonical HTTPS origins. Whitespace around entries is ignored. Empty values, whitespace-only values, and empty comma-separated entries are rejected. After parsing, each trimmed entry must equal `URL.origin` or that origin plus a single trailing `/`, so default-port `:443`, empty fragments, empty userinfo, and path-normalized lookalikes cannot sneak in.
3. Any malformed entry, HTTP entry, URL credential, path, query, fragment, or non-canonical origin produces `ClearfolioConfigurationError` with stable code `clearfolio_artifact_origins_invalid` before the artifact-link provider request is sent.
4. Provider-returned artifact URLs must still satisfy the existing HTTP/HTTPS and downgrade rules, must contain no credentials or fragment, and must resolve to the provider origin or an explicitly configured artifact origin. Protocol-relative links, backslash-normalized protocol-relative paths, and userinfo-as-host URLs inherit a foreign origin and are rejected unless that origin is explicitly allowlisted.
5. A same-origin `artifactToken` may be translated into the trusted Clearfolio viewer route. A token on an approved cross-origin artifact URL remains on that returned URL; ScopeWeave never transplants it into the provider-origin viewer.
6. Exact origin comparison includes the effective port. Approving `https://cdn.example:8443` does not approve `https://cdn.example`.

## Operator action

If Clearfolio returns artifacts from a separate reviewed CDN or object-storage service, configure only that service origin, for example:

```text
CLEARFOLIO_ARTIFACT_ORIGINS=https://artifacts.example.com,https://archive.example.com:8443
```

Do not place signed paths, object keys, tokens, credentials, query strings, or fragments in this setting. If no cross-origin artifact service is required, leave the variable unset; the provider origin remains the least-privilege default.

## Verification contract

`tests/unit/clearfolio-artifact-origin.test.mjs` exercises the production adapter with real `URL` parsing and a bounded mocked provider response. It proves:

- cross-origin HTTPS artifacts fail by default;
- protocol-relative, backslash-normalized, and userinfo-as-host links do not inherit the provider origin;
- same-origin relative artifacts continue to resolve against the provider;
- an explicitly approved origin succeeds only for the same scheme/host/port identity;
- approved cross-origin `artifactToken` values remain on the approved origin;
- credentials and fragments are rejected on provider and approved origins;
- malformed, HTTP, credentialed, path-, query-, fragment-bearing, non-canonical, empty, and whitespace-only configuration fails before any provider transport.

The test is registered in both the normal unit suite and the production coverage cases so changes to this boundary cannot silently bypass repository coverage evidence.

## Failure and rollback

Configuration failure is fail-closed and limited to the artifact-link capability; it does not broaden trust or silently fall back to arbitrary URLs. Rollback removes the additional-origin feature and returns to provider-origin-only artifact redirects. Do not roll back by permitting arbitrary HTTPS destinations or by moving cross-origin tokens into a trusted same-origin viewer URL.

## References

Barth, A. (2011). *The web origin concept* (RFC 6454). Internet Engineering Task Force. https://doi.org/10.17487/RFC6454

Open Worldwide Application Security Project. (n.d.). *Server side request forgery prevention cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 15, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html

WHATWG. (2026). *URL standard*. https://url.spec.whatwg.org/
4 changes: 2 additions & 2 deletions docs/doctoring/clearfolio-production-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ This boundary prevents configuration text from becoming an arbitrary downstream

## Artifact-token origin rule

If Clearfolio returns an `artifactToken`, ScopeWeave rewrites it into the trusted Clearfolio viewer route only when the returned URL has the same origin as the configured Clearfolio service. A token-bearing link from another origin is rejected rather than transplanted into the trusted viewer or returned directly to an unreviewed host. This closes the token-confusion boundary without claiming that arbitrary cross-origin artifact hosts are approved.
If Clearfolio returns an `artifactToken`, ScopeWeave rewrites it into the trusted Clearfolio viewer route only when the returned URL has the same origin as the configured Clearfolio service. A token-bearing link from another origin is never transplanted into the trusted viewer. Reviewed CDN or object-storage hosts are added only through the later `CLEARFOLIO_ARTIFACT_ORIGINS` allowlist recorded in `docs/doctoring/clearfolio-artifact-origin-trust.md`; without that setting, a cross-origin token remains rejected.

Issue #489 remains open after this slice. A subsequent bounded change must still implement the explicit reviewed artifact-origin allowlist, redirect policy, streaming response-size/media-type limits, provider-wide request budget, and the remaining resource/lifecycle acceptance criteria before the Clearfolio adapter can be described as fully production-complete.
Issue #489 remains open after this slice. Streaming response-size/media-type limits and the provider-wide request budget are owned by the provider-response-boundary record. Remaining work is capability readiness, persistence/lifecycle controls, incident and recovery evidence, and destination DNS/IP authorization so an allowlisted origin cannot become an arbitrary in-origin redirector.

## Executable evidence

Expand Down
2 changes: 1 addition & 1 deletion docs/doctoring/clearfolio-provider-response-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The 10 MiB limit matches the current ScopeWeave attachment API ceiling, so the d

The preceding slice already prevents a cross-origin `artifactToken` from being transplanted into the trusted Clearfolio viewer origin. This slice bounds and media-validates the artifact-link response itself and disables redirects on the request.

It **does not yet approve arbitrary cross-origin artifact URLs**. Issue #489 still owns the reviewed artifact-origin allowlist and the remaining URL rules for returned links, including credential and fragment rejection. Until that later slice integrates, cross-origin artifact URLs retain the narrower predecessor behavior and must not be represented as a fully qualified production CDN/object-storage policy.
Reviewed cross-origin artifact hosts are not implied here. They are owned by the later `CLEARFOLIO_ARTIFACT_ORIGINS` allowlist in `docs/doctoring/clearfolio-artifact-origin-trust.md`, which also rejects credentials and fragments on returned links. Until that allowlist is configured, only the provider origin is trusted. Issue #489 still owns destination DNS/IP authorization, signed-artifact URL shape, and the remaining lifecycle/acceptance work.

## Verification contract

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"coverage": "npm run test:coverage",
"server": "node server/server.mjs",
"test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs",
"test:unit": "node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/clearfolio-provider-boundary.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs",
"test:unit": "node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/clearfolio-provider-boundary.test.mjs && node tests/unit/clearfolio-artifact-origin.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs",
"test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases",
"test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/clearfolio-provider-boundary.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api",
"test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/clearfolio-provider-boundary.test.mjs && node tests/unit/clearfolio-artifact-origin.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js",
Expand Down
Loading