Skip to content
Closed
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
38b67fc
⚡ 병렬 외부 네트워크 호출을 위한 Promise.all 적용
seonghobae Aug 4, 2026
5a9dfaa
⚡ 병렬 외부 네트워크 호출을 위한 Promise.all 적용 및 CVE 패치
seonghobae Aug 4, 2026
a3c76d5
fix(review): bound job-status refresh concurrency to chunks of 5
seonghobae Aug 4, 2026
99ff6dd
ci: run one-shot attachment refresh hardening
seonghobae Aug 4, 2026
f5ecb72
ci: remove inactive one-shot repair workflow
seonghobae Aug 4, 2026
63a8f18
ci: validate and apply attachment refresh hardening
seonghobae Aug 4, 2026
bcdb27f
perf(attachments): add bounded status refresh engine
seonghobae Aug 4, 2026
d705f23
test(attachments): cover bounded refresh engine
seonghobae Aug 4, 2026
f8077e0
test(clearfolio): cover status cancellation signal
seonghobae Aug 4, 2026
f455f99
test(api): cover attachment status refresh contract
seonghobae Aug 4, 2026
0d6b439
feat(clearfolio): make status lookups abortable
seonghobae Aug 4, 2026
ff15fca
test(attachments): wire refresh coverage and API regressions
seonghobae Aug 4, 2026
0cc3809
docs(changelog): record bounded attachment refresh
seonghobae Aug 4, 2026
ee4930b
ci: make attachment repair marker-based
seonghobae Aug 4, 2026
62da5d0
Restore scorecard-analysis.yml for branch protection rule
seonghobae Aug 4, 2026
8ebd8cd
docs(attachments): document refresh worker
seonghobae Aug 4, 2026
9f37f79
ci: make attachment repair atomic and marker-based
seonghobae Aug 4, 2026
8fdffb1
ci: patch attachment repair browser setup
seonghobae Aug 4, 2026
a0e53d3
test(e2e): install Chromium for cloud validation
seonghobae Aug 4, 2026
0caf4eb
ci: remove browser bootstrap workflow
seonghobae Aug 4, 2026
0e3cd25
perf(attachments): bound status refresh and remove N+1 queries
github-actions[bot] Aug 4, 2026
4f6565f
chore: remove unrelated scorecard workflow
seonghobae Aug 4, 2026
dddfd60
chore: keep attachment change focused
seonghobae Aug 4, 2026
bb45a51
fix(attachments): enforce request-wide refresh budget
seonghobae Aug 4, 2026
f46c36a
fix(clearfolio): preserve status on HTTP failures
seonghobae Aug 4, 2026
d0a1b44
test(attachments): prove deadline and failure categories
seonghobae Aug 4, 2026
4f1dfe1
test(clearfolio): enforce status endpoint HTTP contract
seonghobae Aug 4, 2026
609c78c
test(attachments): use behavior-only list assertions
seonghobae Aug 4, 2026
73603cd
test(coverage): include Clearfolio status paths
seonghobae Aug 4, 2026
e883548
ci: repair exact-head coverage evidence for PR 419
seonghobae Aug 4, 2026
e34b138
ci: stage PR 419 review repair script
seonghobae Aug 4, 2026
230bafa
ci: arm ready-event repair for PR 419
seonghobae Aug 4, 2026
afeb3c0
fix(ci): make test coverage produce Istanbul evidence
seonghobae Aug 4, 2026
8da3b0e
test(ci): lock coverage-producing script contract
seonghobae Aug 4, 2026
64b7ed5
fix(ci): make coverage evidence deterministic
github-actions[bot] Aug 4, 2026
140bf95
test(ci): document coverage evidence contract
seonghobae Aug 4, 2026
ba5294d
test: reject malformed Clearfolio status responses
seonghobae Aug 4, 2026
a4dd487
fix: preserve attachment status on malformed Clearfolio payloads
seonghobae Aug 4, 2026
13f2402
docs: record malformed status response isolation
seonghobae Aug 4, 2026
02c8d84
fix(security): enforce strict revocable sessions on every JWT transport
seonghobae Aug 4, 2026
285efef
test(security): reject excessive session lifetimes
seonghobae Aug 4, 2026
94f819b
fix(security): cap general session lifetime at seven days
seonghobae Aug 4, 2026
ae63ea1
docs(doctoring): record session revocation evidence
seonghobae Aug 4, 2026
88f59ac
docs: record bounded general session lifetime
seonghobae Aug 4, 2026
c2c3ad5
docs(doctoring): remove superseded session verification note
seonghobae Aug 4, 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
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added regression coverage that prevents array-valued passwords from being
coerced into valid credentials.
- Updated Hono runtime dependencies to patched supported releases.
- Centralized session JWT verification and database-backed `token_version`
revocation across bearer middleware, calendar feeds, server-sent events, and
attachment-view URL transports.
- Made session-token minting fail closed unless the subject, token version, and
lifetime are bounded safe integers, and capped general session lifetime at
seven days so internal callers cannot mint excessive or numerically unsafe
credentials.
- Rejected signed session JWTs with a non-HS256/JWT header, non-object claims,
missing or invalid subject/expiry, or a missing, Boolean, fractional,
negative, unsafe, or otherwise invalid token-version claim before user lookup.
- Added cross-device regression coverage proving that `logout-all` rejects stale
tokens on bearer, calendar, SSE, and attachment-view transports while the
replacement token continues through the same authentication boundary.

### Changed

- Attachment-list status refresh now removes the per-row database lookup,
uses a configurable bounded worker pool with abortable downstream timeouts,
preserves stale status after downstream, timeout, malformed-response, and
persistence failures, excludes internal conversion identifiers from
responses, and reports attempted, changed, failed, and deferred counters.
- 프로젝트 이름 입력 필드에 입력 예시(placeholder)를 추가하여 사용자 편의성을 개선했습니다.
- 데이터 테이블의 반복되는 액션 버튼에 컨텍스트 정보(작업명)를 포함한 명시적인 ARIA 레이블을 추가하고, 유효성 검사 에러를 폼 필드에 연결하여 접근성을 개선했습니다.
- `createGanttBarElement`, `renderGantt`, `buildWeekdayTimeline`에서 반복적으로 호출되던 `compareDateStrings`를 직접적인 문자열 비교 연산(`>=`, `<=`)으로 교체하여 O(N*D) 복잡도의 캐시 스레싱과 정규식 검사를 방지했습니다.
Expand All @@ -61,4 +79,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.0.1] - 2026-06-25
### 성능 개선 (Performance)
- 드래그 앤 드롭 동작 중 `dragover` 이벤트에서 발생하는 O(N) 작업 리스트 검색 성능 병목 문제를, O(1) 해시맵(Map) 기반의 캐싱 조회 로직으로 개선하여 큰 크기의 WBS 리스트에서의 버벅임 현상을 해결했습니다.
- 드래그 앤 드롭 동작 중 `dragover` 이벤트에서 발생하는 O(N) 작업 리스트 검색 성능 병목 문제를, O(1) 해시맵(Map) 기반의 캐싱 조회 로직으로 개선하여 큰 크기의 WBS 리스트에서의 버벅임 현상을 해결했습니다.
82 changes: 82 additions & 0 deletions docs/doctoring/session-revocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Session JWT revocation: evidence and design record

## Decision

Every ScopeWeave transport that accepts a general session JWT uses one
fail-closed verifier. Bearer middleware, calendar feeds, server-sent events, and
attachment-view routes therefore share signature, header, claim, subject, expiry,
and database-backed revocation checks.

The implementation:

1. pins the compact token to an authenticated `HS256` signature and signed `JWT`
type;
2. authenticates the compact representation before interpreting the JOSE header
or claim set;
3. requires a non-array claims object, positive safe-integer subject, future
safe-integer expiry, and non-negative safe-integer token version;
4. requires the subject to exist and compares the signed token version exactly
with the current persisted version;
5. rejects malformed, forged, expired, missing-user, and stale sessions before
tenant or resource lookup;
6. caps general session minting at seven days and rejects fractional, unsafe,
non-positive, or longer lifetimes; and
7. reserves narrower and shorter authority for the opaque access-grant design in
issue #413 rather than overloading the general session JWT.

## Standards rationale

RFC 7519 defines a JWT claims set as a JSON object and defines `sub` and `exp` as
registered claims. ScopeWeave narrows those flexible JSON representations to
safe integers because its database identifiers and token-version comparisons are
integer security boundaries.

RFC 8725 requires callers to perform algorithm verification, validate every
cryptographic operation, use explicit typing for new JWT uses, and apply mutually
exclusive validation rules where different token kinds coexist. ScopeWeave pins
one algorithm and one type for general sessions and does not reuse this JWT
contract for the scoped URL grants planned in issue #413.

RFC 6750 explains that any holder of a bearer token can exercise its authority,
recommends short-lived and audience-scoped credentials, and warns against page
URL transport because browser history and server logs can expose tokens. RFC
9700 updates OAuth security best current practice and prohibits clients from
passing access tokens in URI query parameters. This pull request does not claim
to remove the existing URL transport; it makes revocation and validation
consistent until issue #413 replaces those general credentials with narrowly
scoped opaque grants and separately revocable calendar subscription secrets.

## Verification contract

Regression tests must prove:

- the signer rejects invalid subject, token version, fractional lifetime,
numerically unsafe lifetime, and any general-session lifetime over seven days;
- malformed compact tokens, signatures, JOSE headers, claim-set shapes, subjects,
expiries, and token-version values fail across every transport;
- a correctly signed token for a nonexistent subject fails before resource
lookup;
- two independently minted device sessions work before revocation;
- `logout-all` invalidates both stale sessions on bearer, calendar, SSE, and
attachment-view paths; and
- the replacement session continues through the same authentication boundary.

All changed production helpers require complete JSDoc and 100% statement,
branch, function, and line coverage before the pull request can leave Draft.

## References

Jones, M., Bradley, J., & Sakimura, N. (2015). *JSON Web Token (JWT)* (RFC
7519). Internet Engineering Task Force. https://doi.org/10.17487/RFC7519

Jones, M. B., & Hardt, D. (2012). *The OAuth 2.0 authorization framework:
Bearer token usage* (RFC 6750). Internet Engineering Task Force.
https://doi.org/10.17487/RFC6750

Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best current
practice for OAuth 2.0 security* (BCP 240; RFC 9700). Internet Engineering Task
Force. https://doi.org/10.17487/RFC9700

Sheffer, Y., Hardt, D., & Jones, M. (2020). *JSON Web Token best current
practices* (BCP 225; RFC 8725). Internet Engineering Task Force.
https://doi.org/10.17487/RFC8725
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@
},
"scripts": {
"check:python-docstrings": "node scripts/ci/static_coverage_evidence.mjs docstrings",
"coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/app.mjs --include=server/auth.mjs --reporter=json --reporter=json-summary npm run test:coverage",
"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",
"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/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",
"test:coverage": "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: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/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 --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/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 test tests/e2e/cloud.spec.js",
"test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js",
"test:fuzz": "playwright install chromium && playwright test tests/e2e/csv_formula_fuzz.spec.js",
"fuzz": "node --test tests/fuzz/*.mjs"
},
"dependencies": {
"@hono/node-server": "^2.0.12",
"hono": "^4.12.32"
"hono": "^4.13.0"
},
"devDependencies": {
"@playwright/test": "1.61.1",
Expand Down
79 changes: 57 additions & 22 deletions server/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { db, rowid } from './db.mjs';
import { hashPassword, verifyPassword, signToken, verifyToken, generateApiToken, hashApiToken } from './auth.mjs';
import { PLANS, planOf, orgUsage, wouldExceed, createCheckout } from './billing.mjs';
import { clearfolioMock, mockArtifact, submitJob, jobStatus, artifactUrl } from './clearfolio.mjs';
import { normalizeAttachmentStatusBudgetMs, normalizeAttachmentStatusConcurrency, normalizeAttachmentStatusTimeoutMs, refreshAttachmentStatuses } from './attachment_status.mjs';
import { chat as orchestratorChat } from './orchestrator.mjs';
import { computeEvm } from '../analytics.js'; // pure math, shared with the client

Expand Down Expand Up @@ -73,7 +74,20 @@ function projectAccess(userId, projectId) {
}

// --- observability: in-process counters + structured request log.
const metrics = { startedAt: new Date().toISOString(), requests: 0, s2xx: 0, s4xx: 0, s5xx: 0, signups: 0, projectsCreated: 0, webhookDeliveries: 0 };
const metrics = {
startedAt: new Date().toISOString(),
requests: 0,
s2xx: 0,
s4xx: 0,
s5xx: 0,
signups: 0,
projectsCreated: 0,
webhookDeliveries: 0,
attachmentStatusRefreshAttempted: 0,
attachmentStatusRefreshChanged: 0,
attachmentStatusRefreshFailed: 0,
attachmentStatusRefreshDeferred: 0,
};

// Outbound webhooks: POST signed JSON to each active hook subscribed to `event`.
// Fire-and-forget with a timeout, one retry on failure, and a recorded outcome
Expand Down Expand Up @@ -993,6 +1007,31 @@ app.post('/api/projects/:id/ai/brief', requireAuth, async (c) => {
// 갱신), 서명 아티팩트 열람(302), 삭제. 테넌트 = 조직, 브라우저에는 Clearfolio
// 자격이 절대 노출되지 않음.
const ATTACH_MAX_BYTES = 10 * 1024 * 1024;

const ATTACH_STATUS_CONCURRENCY = normalizeAttachmentStatusConcurrency(
process.env.SCOPEWEAVE_ATTACHMENT_STATUS_CONCURRENCY,
);
const ATTACH_STATUS_TIMEOUT_MS = normalizeAttachmentStatusTimeoutMs(
process.env.SCOPEWEAVE_ATTACHMENT_STATUS_TIMEOUT_MS,
);
const ATTACH_STATUS_BUDGET_MS = normalizeAttachmentStatusBudgetMs(
process.env.SCOPEWEAVE_ATTACHMENT_STATUS_BUDGET_MS,
);
const ATTACHMENT_LIST_COLUMNS = `a.id, a.task_id AS taskId, a.name, a.mime, a.size,
a.job_id AS jobId, a.status, a.created_at AS createdAt, u.email AS uploadedBy`;
const ATTACHMENT_LIST_FROM =
'FROM attachments a LEFT JOIN users u ON u.id = a.created_by';
const listAttachmentsStatement = db.prepare(
`SELECT ${ATTACHMENT_LIST_COLUMNS} ${ATTACHMENT_LIST_FROM}
WHERE a.project_id = ? ORDER BY a.id DESC`,
);
const listTaskAttachmentsStatement = db.prepare(
`SELECT ${ATTACHMENT_LIST_COLUMNS} ${ATTACHMENT_LIST_FROM}
WHERE a.project_id = ? AND a.task_id = ? ORDER BY a.id DESC`,
);
const updateAttachmentStatusStatement = db.prepare(
'UPDATE attachments SET status = ? WHERE id = ?',
);
app.post('/api/projects/:id/attachments', requireAuth, async (c) => {
const uid = c.get('user').sub;
const p = projectAccess(uid, c.req.param('id'));
Expand Down Expand Up @@ -1022,28 +1061,24 @@ app.get('/api/projects/:id/attachments', requireAuth, async (c) => {
const uid = c.get('user').sub;
const p = projectAccess(uid, c.req.param('id'));
if (!p) return c.json({ error: 'not found' }, 404);

const taskId = c.req.query('taskId');
const rows = (taskId
? db.prepare(`SELECT a.id, a.task_id AS taskId, a.name, a.mime, a.size, a.status, a.created_at AS createdAt, u.email AS uploadedBy
FROM attachments a LEFT JOIN users u ON u.id = a.created_by
WHERE a.project_id = ? AND a.task_id = ? ORDER BY a.id DESC`).all(p.id, taskId)
: db.prepare(`SELECT a.id, a.task_id AS taskId, a.name, a.mime, a.size, a.status, a.created_at AS createdAt, u.email AS uploadedBy
FROM attachments a LEFT JOIN users u ON u.id = a.created_by
WHERE a.project_id = ? ORDER BY a.id DESC`).all(p.id));
// PENDING 잡 상태 갱신(최선 노력)
for (const r of rows) {
if (r.status === 'PENDING' || r.status === 'RUNNING') {
try {
const jid = db.prepare('SELECT job_id FROM attachments WHERE id = ?').get(r.id).job_id;
const st = await jobStatus(p.org_id, uid, jid);
if (st !== r.status) {
db.prepare('UPDATE attachments SET status = ? WHERE id = ?').run(st, r.id);
r.status = st;
}
} catch { /* keep stale status */ }
}
}
return c.json({ attachments: rows });
const rows = taskId
? listTaskAttachmentsStatement.all(p.id, taskId)
: listAttachmentsStatement.all(p.id);
await refreshAttachmentStatuses(rows, {
orgId: p.org_id,
userId: uid,
jobStatus,
updateStatus: (status, attachmentId) =>
updateAttachmentStatusStatement.run(status, attachmentId),
concurrency: ATTACH_STATUS_CONCURRENCY,
timeoutMs: ATTACH_STATUS_TIMEOUT_MS,
budgetMs: ATTACH_STATUS_BUDGET_MS,
metrics,
});
const attachments = rows.map(({ jobId: _internalJobId, ...publicRow }) => publicRow);
return c.json({ attachments });
});

// 열람: 서명 아티팩트 URL로 302. 새 탭 열기용으로 ?token=도 허용(ics/stream 패턴).
Expand Down
Loading
Loading