Commit graph

8 commits

Author SHA1 Message Date
유창욱
3f2823a764 refactor: rename cloud/ to server/ (cafe24 is web hosting, not cloud)
The PHP app runs on Cafe24 shared web hosting -- calling it 'cloud'
was misleading. Renames the directory, the edge client module, and
every doc/script/identifier that used the term.

- cloud/ -> server/ (incl. server/deploy/build_cafe24_package.sh paths)
- edge/cloudclient.py -> edge/serverclient.py, CloudClient -> ServerClient
- cloud/docs/cloud-deploy.md -> server/docs/server-deploy.md
- CLOUD_* env/config keys -> SERVER_*
- tests renamed to match

Tests: PHP 109 pass (4 skipped), pytest 167 pass. dist/ regenerated.
2026-07-20 11:30:52 +09:00
유창욱
2ea0420de7 fix(delivery): resolve codex + workflow review findings across edge/cloud/deploy
Two independent adversarial reviews (codex CLI + a 6-dimension verification
workflow) of the delivery-hardening branch surfaced ~40 findings; this closes
the confirmed correctness/security/deploy blockers plus a codex re-review pass.

edge:
- geometry: track_displacement per-second threshold recalibrated (0.02->0.25,
  env-tunable via MOTION_THR, validated finite/positive); confidence-filter
  keypoints with a bbox-center fallback so hallucinated points don't fake motion
- fall: low-confidence frames no longer cancel an active FALLING/FALLEN incident
  (freeze + reset recovery timer so recovery needs a fresh confident streak);
  baseline height only seeded from trusted frames
- queue: PostClient.send() returns a tri-state SendResult; only definite 4xx
  count toward the poison drop cap, so a network outage (incl. 408/425/429/5xx)
  never deletes queued safety events; legacy rows persist their minted uuid
- config: reject placeholder / http / short DEVICE_SECRET at startup
- zones: warn+skip legacy pixel-space polygons that would silently match nothing

cloud:
- zone endpoints wired into P3-2b site-scope (cross-site read/write/delete
  closed) + role gate on list; catch narrowed so DB faults surface as 500
- snapshot purge keeps the DB row when unlink() fails (no untrackable PIPA orphan)
- ingest dedup path re-stores missing snapshots (+ short-write guard); UNIQUE
  (event_id, content_hash) makes recovery idempotent under retry/race
- event list cursor carries full-precision occurred_at (no float rounding at the
  page boundary); string keyset end-to-end
- SiteScope '' fails closed (only NULL is global); dashboard fetches full page cap
- runnable migration (site/event_uuid + backfill ordering + indexes); rate-limit
  reap fails open

deploy:
- offline bundle hashes EVERY file (fatal on missing/failed/injected; models in
  SBOM; arch in tarball name; REQUIRE_SIGNATURE option)
- installer verifies before any mutation, Hailo arch preflight before install,
  atomic src swap, backend reconcile preserves a working operator choice
- systemd DeviceAllow=char-dma_heap (the /dev/dma_heap/ dir form was inert)

Verified: edge pytest 165 passed / 2 skipped; cloud phpunit 109 passed / 4
skipped; deploy scripts bash -n clean. Reviewed by codex twice (findings +
fixes) with the second-pass findings folded in.
2026-07-17 19:24:42 +09:00
유창욱
930490d1b4 feat(cloud,edge): event dedup (event_uuid) + site-scoped access control (P1-9, P3-2b)
Two approved data-model changes.

P1-9 — ingest idempotency. A POST can succeed server-side but time out
client-side; the edge then re-queues and re-sends the same event, so the
cloud stored duplicate alerts. The edge Event now carries a stable
event_uuid (uuid4 hex) generated once at creation and preserved through
the offline queue and every retry. Cloud: events.event_uuid CHAR(32) NULL
UNIQUE; EventValidator accepts/validates it; IngestController returns the
already-stored event (200, dedup:true) on a repeat uuid, race-safe against
the UNIQUE key (catches the insert-race PDOException and returns the
winner).

P3-2b — site-scoped access control. site_scope was stored in the session
but never enforced, so any authenticated user could read every site's
worker snapshots (PIPA). Added devices.site; ingest denormalizes it onto
events.site (server-derived from the authenticated device_id, never
client-supplied). App\Security\SiteScope centralizes the rule (NULL scope
= global operator). The event list filters in SQL (page(...,$site)); event
detail + acknowledge and the snapshot proxy guard per-row and return 404
(not 403) cross-site so existence never leaks.

Schema migration ALTERs and per-device/user provisioning UPDATEs are
documented at the bottom of schema.sql.

Tests: edge queue round-trip keeps the uuid; repeat-uuid dedups to one
row; list scoped to caller site; detail cross-site 404; snapshot
cross-site 404 / same-site 200; event_uuid validation. phpunit 97 passing
(4 skipped: PDO), pytest 158 passing (2 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 12:58:27 +09:00
유창욱
99070f5e87 fix(cloud): don't swallow snapshot failures, clean orphan JPEGs (P4-7)
The ingest snapshot loop silently `continue`d on re-encode and file-write
failures (evidence lost with a 201), and a DB-index throw after the file
landed left an orphan JPEG on disk with no row — or bubbled into a 500
that discarded an already-stored event.

Extracts storeSnapshot(): re-encode → write file → index, in that order
so bytes exist before a row points at them. Every failure is error_log'd
instead of swallowed; a DB-index failure after the write @unlinks the
orphan file. A snapshot problem no longer fails the ingest — the event is
already persisted, so the 201 stands.

Test: testSnapshotIndexFailureCleansOrphanFileButKeepsEvent uses a
throwing snapshot repo and asserts 201, the event is stored, and no *.jpg
is left in the snapshot dir.

phpunit: 90 passing (4 skipped: PDO-backed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 10:08:28 +09:00
유창욱
1187df5628 feat(delivery-audit): PIPA retention + cloud hardening (P3-2, P4-1..4)
- P3-2 [H] snapshot retention (7 days): snapshot_retention_days config;
  SnapshotRepository::purgeOlderThan (interface + Pdo + double, files-then-rows,
  bounded batch); opportunistic purge in the ingest path (Cafe24 has no cron).
- P4-1 [H] index.php sets display_errors=0 + a global exception handler → generic
  {"error":"internal"} + error_log (no DSN/db_user leak); ships www/move_detect/.user.ini.
- P4-2 [H] ConfigController + HeartbeatController take an optional rate limiter +
  clientIp; pre-auth per-IP anti-flood → 429 (were unthrottled).
- P4-3 [M] PdoNonceRepository::remember + PdoRateLimiterRepository::hit opportunistically
  reap expired rows (Cafe24 has no cron; tables grew unbounded).
- P4-4 [M] Router::dispatch rawurldecodes captured params (Korean/space camera_id could
  save zones but never load them).

Tests: python green; phpunit 84 green. See docs/delivery-audit.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:28:16 +09:00
유창욱
7828f9c419 fix(cloud): move ingest per-device rate limit post-auth, add pre-auth IP limit (H1)
An unauthenticated attacker could guess a real device_id and 429-lock that
camera by hammering /api/events before HMAC verification ran, dropping its
accident events. Add a cheap pre-auth per-IP limiter (Config::ingestIpRateLimitPerMin,
default 300/min) that fires before any device-secret lookup, and move the
existing per-device limiter to run only after HMAC + timestamp + nonce checks
succeed, so only authenticated requests consume a device's own quota.
2026-07-12 09:09:31 +09:00
유창욱
450c88978f fix(cloud): session-id regeneration on login (fixation) + pristine test output 2026-07-12 00:35:41 +09:00
유창욱
57cfc64548 feat(cloud): ingest controller (HMAC+nonce+ratelimit+reencode+persist) 2026-07-12 00:01:05 +09:00