Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c761c52
test(clearfolio): specify bounded provider transport
seonghobae Aug 14, 2026
f38a3d0
fix(clearfolio): bound provider transport and JSON responses
seonghobae Aug 14, 2026
71a8aaf
test(clearfolio): run adapter regressions through real response streams
seonghobae Aug 14, 2026
312466d
test(clearfolio): exercise configuration contracts with streamed resp…
seonghobae Aug 14, 2026
b9c18e6
test(clearfolio): register bounded provider regressions
seonghobae Aug 14, 2026
099cf2f
docs(clearfolio): record bounded provider response boundary
seonghobae Aug 14, 2026
892484b
docs(deploy): document Clearfolio transport limits
seonghobae Aug 14, 2026
a733a2f
docs(changelog): record bounded Clearfolio provider transport
seonghobae Aug 14, 2026
a70ac7d
test(clearfolio): preserve IPv6 loopback parent regression
seonghobae Aug 15, 2026
ad6ca38
fix(clearfolio): carry forward IPv6 loopback parent fix
seonghobae Aug 15, 2026
28cff4a
merge: refresh Clearfolio provider stack on current parent
seonghobae Aug 15, 2026
3adca52
test(clearfolio): require cancellation of rejected provider bodies
seonghobae Aug 15, 2026
88fa42e
test(clearfolio): cover bodyless and failed cancellation paths
seonghobae Aug 15, 2026
4f01714
fix(clearfolio): cancel rejected provider response bodies
seonghobae Aug 15, 2026
11de7b8
merge(clearfolio): reconcile provider boundary with parent hardening
seonghobae Aug 16, 2026
b0e2df8
test(clearfolio): retain parent cross-origin token rejection
seonghobae Aug 16, 2026
c7f0ddd
fix(clearfolio): reject cross-origin artifact tokens
seonghobae Aug 16, 2026
b362077
merge(clearfolio): reconcile current production configuration parent
seonghobae Aug 16, 2026
6d7774d
docs(deploy): inherit orchestrator fail-closed contract
seonghobae Aug 16, 2026
9f4b2b8
merge(clearfolio): reconcile latest production configuration parent
seonghobae Aug 16, 2026
036f916
fix(clearfolio): preserve current parent tree during stack reconcilia…
seonghobae Aug 16, 2026
d4b09cd
fix(changelog): preserve parent XML import entry
seonghobae Aug 16, 2026
d6af6bb
merge: reconcile Clearfolio response boundary with current parent
seonghobae Aug 16, 2026
af0af30
test(clearfolio): lock artifact redirect authority
seonghobae Aug 16, 2026
e6353be
fix(clearfolio): preserve artifact redirect authority
seonghobae Aug 16, 2026
35dc370
merge(clearfolio): reconcile provider boundary with parent
seonghobae Aug 16, 2026
9bdfe5c
docs(clearfolio): preserve parent production authority
seonghobae Aug 17, 2026
ae0ffb5
test(clearfolio): preserve parent artifact-origin contract
seonghobae Aug 17, 2026
a5ef31b
chore(clearfolio): reconcile provider boundary with current parent
seonghobae Aug 17, 2026
5fa7952
fix(clearfolio): preserve current parent tree during stack reconcilia…
seonghobae Aug 17, 2026
c7d0dc7
merge(stack): reconcile provider boundary with current Clearfolio parent
seonghobae Aug 19, 2026
a3f1f49
fix(stack): inherit current Clearfolio configuration parent
seonghobae Aug 20, 2026
dca5264
chore(stack): reconcile Clearfolio provider boundary with current parent
seonghobae Aug 20, 2026
0e9da6e
test(clearfolio): require provider timeout disposal
seonghobae Aug 25, 2026
c14ff6e
fix(clearfolio): dispose provider request timers
seonghobae Aug 25, 2026
8faa7c2
test(clearfolio): reproduce provider timeout metric classification
seonghobae Aug 26, 2026
6b58548
fix(clearfolio): preserve sanitized timeout identity
seonghobae Aug 26, 2026
35c8157
fix(attachments): classify downstream timeouts accurately
seonghobae Aug 26, 2026
d60e281
test(clearfolio): execute refresh timeout regression
seonghobae Aug 26, 2026
b24db31
fix(clearfolio): preserve status timeout classification
seonghobae Aug 28, 2026
10ee53c
test(clearfolio): preserve persistence failure category
seonghobae Aug 28, 2026
9408588
fix(attachments): preserve persistence timeout category
seonghobae Aug 28, 2026
8d0ff9d
test(clearfolio): preserve late caller abort reason
seonghobae Aug 28, 2026
d4ca96a
fix(clearfolio): forward caller abort reason
seonghobae Aug 28, 2026
d7fae63
test(clearfolio): exercise streamed response byte ceiling
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
and rejected cross-origin, credential-bearing, or fragmented artifact links
until an explicit reviewed artifact-origin allowlist is configured by a later
slice.
- 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
before allocation, persistence, or URL construction.
- Preserved Clearfolio provider timeouts that occur while streaming a status
response so attachment-refresh timeout metrics remain accurate.
- Made `SCOPEWEAVE_JWT_SECRET` mandatory at startup and rejected weak or
unexpanded placeholder values so production deployments fail closed.
- Neutralized audit-log CSV formulas even when executable prefixes are hidden
Expand Down
15 changes: 15 additions & 0 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ 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.

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
`application/json`, and both declared and streamed response bytes are capped at
256 KiB before JSON parsing. Provider job identifiers are limited to 256
characters, and upload metadata/bytes are validated before Blob/FormData
allocation. The adapter's document ceiling is 10 MiB, matching the current
ScopeWeave attachment API limit.

These limits are code constants rather than operator-tunable environment
settings. A deployment that needs larger provider responses, longer requests, or
larger documents requires a reviewed application change with corresponding
resource, latency, and security evidence; do not work around these bounds at the
proxy layer.

## Attachment status refresh operations

The attachment-list API reads `job_id` in its initial project-scoped query and
Expand Down
83 changes: 83 additions & 0 deletions docs/doctoring/clearfolio-provider-response-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Clearfolio provider response and request boundary

## Decision

ScopeWeave treats the Clearfolio service as an untrusted external API even after its root origin and tenant HMAC configuration have passed the production configuration boundary. Every hosted submit, status, and artifact-link call therefore uses the same fail-closed transport and response rules before provider data can affect ScopeWeave state or browser-visible behavior.

This record is intentionally narrower than the full Clearfolio production-readiness issue. It extends the configuration boundary introduced by the preceding production-configuration slice and does not claim that arbitrary cross-origin artifact delivery, retry/idempotency policy, or the complete provider lifecycle is finished.

## Request contract

Hosted provider requests:

1. use the configuration-validated provider origin and adapter-owned endpoint path;
2. send tenant claims only to that direct origin request;
3. use Fetch `redirect: "error"` so a redirect is a transport failure rather than a credential-forwarding opportunity;
4. carry a hard 15,000 ms total-request `AbortSignal`;
5. compose a caller cancellation signal with that hard budget for status refreshes; and
6. collapse network, redirect, timeout, and cancellation details into fixed operation-level errors before they can reach browser or diagnostic payloads; and
7. preserve the timeout category when the hard budget aborts an in-progress status response body, so refresh metrics distinguish timeouts from malformed responses.

ScopeWeave does not retry provider calls in this slice. Retry eligibility, idempotency keys, backoff, cancellation recovery, and persisted operation lifecycle remain explicit follow-up work rather than being guessed at the transport layer.

## Response contract

Successful provider responses are accepted only when the media type essence is `application/json`. If `Content-Length` is present it must be an exact non-negative decimal integer no greater than 256 KiB. The body stream is independently counted to the same 256 KiB ceiling, so missing or dishonest length metadata cannot bypass the resource limit. Empty bodies, malformed streams, invalid UTF-8, malformed JSON, and incompatible JSON shapes fail closed with fixed operation-specific errors.

The adapter never uses `response.json()` directly for successful hosted provider responses. This prevents an otherwise successful response from being buffered without an application-level byte ceiling before validation.

Provider conversion states remain the exact `PENDING`, `RUNNING`, `SUCCEEDED`, and `FAILED` set. Provider job identifiers are trimmed and limited to 256 characters without control characters before persistence or URL construction.

## Document boundary

Document metadata and bytes are validated before `Blob` or `FormData` construction. The provider adapter accepts only:

- a non-empty document name of at most 512 characters without control characters;
- a MIME string of at most 255 characters without control characters; an empty value retains the existing `application/octet-stream` fallback;
- `Uint8Array`-compatible bytes no larger than 10 MiB.

The 10 MiB limit matches the current ScopeWeave attachment API ceiling, so the downstream adapter cannot accept a document larger than the application path that feeds it.

## Artifact boundary and remaining work

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.

## Verification contract

Regression evidence covers:

- `redirect: "error"` on submit, status, and artifact-link calls;
- hard request-budget signals and caller-signal composition;
- non-JSON media rejection;
- declared and streamed response-size overflow;
- cancellation-detail sanitization;
- document metadata/byte rejection before provider transport;
- oversized provider job identifiers before URL construction;
- valid streamed JSON compatibility for submission, status, HMAC, loopback-development, and artifact-link behavior;
- the predecessor configuration, HMAC, artifact-token-origin, sanitized-error, status-enum, and attachment-refresh contracts under the same normal unit/coverage paths.

`server/clearfolio.mjs` remains an owned c8 production target. The new provider-boundary regression is registered in both `test:unit` and `test:coverage:cases`; exact statement, branch, function, and line evidence remains a merge gate rather than a documentation claim.

## Security rationale

OWASP API10:2023 identifies unsafe consumption of third-party APIs when applications trust integrated-service data, blindly follow redirects, fail to validate returned data, omit timeouts, or fail to limit resources used to process third-party responses. OWASP API4:2023 separately highlights unbounded memory, bandwidth, and execution-time consumption. The transport, timeout, media-type, streaming-byte, identifier, and document limits in this slice apply those controls at the provider boundary rather than relying on Clearfolio to behave correctly.

The WHATWG Fetch Standard explicitly supports `redirect: "error"` to reject redirect responses. ScopeWeave uses that mode because tenant HMAC claims are provider-origin credentials and there is no reviewed redirect allowlist in the current protocol.

Node.js 22 provides `AbortSignal.timeout()` and `AbortSignal.any()`, allowing the adapter to impose its own total request budget while preserving upstream cancellation without maintaining a second timer/cancellation protocol.

## Rollback

Rollback reverts the provider-boundary implementation, the new and adapted unit regressions, test registrations, deployment guidance, this doctoring record, and the CHANGELOG entry together. The slice adds no database schema or migration. Existing persisted attachment state remains readable by the predecessor implementation.

## References

Node.js contributors. (2026). *Global objects: AbortSignal*. Node.js documentation. Retrieved August 15, 2026, from https://nodejs.org/download/release/v22.18.0/docs/api/globals.html

OWASP Foundation. (2023a). *API4:2023 unrestricted resource consumption*. OWASP API Security Top 10. https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/

OWASP Foundation. (2023b). *API10:2023 unsafe consumption of APIs*. OWASP API Security Top 10. https://owasp.org/API-Security/editions/2023/en/0xaa-unsafe-consumption-of-apis/

WHATWG. (2026). *Fetch Standard* (Living Standard, updated May 8, 2026). https://fetch.spec.whatwg.org/
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 && node tests/api/orchestrator-attribution.test.mjs",
"test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && 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/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.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 && node tests/unit/toast-accessibility.test.mjs",
"test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && 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/orchestrator-attribution.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-refresh-timeout.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.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/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.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-refresh-timeout.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/orchestrator-attribution.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 tests/e2e/toast-accessibility.spec.js",
Expand Down
7 changes: 4 additions & 3 deletions server/attachment_status.mjs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Malformed stored job IDs counted as downstream lookup failures

validateJobId rejects a persisted job ID over 256 chars or with control characters before any request. The refresh worker only screens empty IDs as skipped, so such rows fall into the catch as a plain error and increment downstream_lookup instead of a data-quality category.

(Refers to this code)

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,10 @@ export async function refreshAttachmentStatuses(rows, options) {
}
} catch (error) {
counts.failed += 1;
const category = error?.name === ATTACHMENT_STATUS_TIMEOUT_ERROR
? 'timeout'
: failureCategory;
const category = (
error?.name === ATTACHMENT_STATUS_TIMEOUT_ERROR
|| (failureCategory === 'downstream_lookup' && error?.name === 'TimeoutError')
) ? 'timeout' : failureCategory;
Comment on lines +284 to +287

@devin-ai-integration devin-ai-integration Bot Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Timeout guard scoped to lookup phase

The timeout mapping in worker triggers only while failureCategory is downstream_lookup. A standard TimeoutError thrown during persistence or status validation keeps its original category. This matches the adapter, which can only emit its 15s TimeoutError during the fetch phase, and each failure still increments exactly one category.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

failureCounts[category] += 1;
reportRefreshFailure(options.onError, category);
}
Expand Down
Loading