Skip to content

feat: proxy skill package downloads through ornn-api for chrono-bucket (#1196) - #1197

Merged
chronoai-shining merged 5 commits into
developfrom
feature/1196-chrono-bucket-download-proxy
Jul 7, 2026
Merged

feat: proxy skill package downloads through ornn-api for chrono-bucket (#1196)#1197
chronoai-shining merged 5 commits into
developfrom
feature/1196-chrono-bucket-download-proxy

Conversation

@chronoai-shining

Copy link
Copy Markdown
Collaborator

Summary

Adapts ornn to chrono-bucket (the new object-storage backend replacing chrono-storage). chrono-bucket is a NyxID resource server that removed the presigned-URL and object-copy endpoints and now streams downloads via GET /api/buckets/:bucket/objects/download?key= behind the NyxID proxy. This PR swaps the storage-read primitive, adds a proxied download route through ornn-api, and stops leaking a direct storage URL to clients.

What changed

Backend (ornn-api)

  • StorageClient: add streaming downloadObject(); remove the now-dead getPresignedUrl() and copy() (copy had no callers).
  • SkillService: internal byte reads (diff / /json / rescan) go through downloadObject; add a gated getPackageBytes() that funnels version→storageKey resolution + the object-level visibility gate (shared with getSkillJson); buildDetailResponse stops minting a presigned URL and drops SkillDetailResponse.presignedPackageUrl — no direct-to-storage URL reaches a client, and the hot detail-read path loses a per-read storage round-trip.
  • New route GET /skills/:idOrName/versions/:version/download — streams the ZIP through ornn-api (application/zip, attachment; RFC 7807 on error). This is the exact route the TS SDK's downloadPackage() already targeted but which was never registered. optionalAuth; the visibility gate lives in getPackageBytes (private skills 404 without leaking existence; public skills download anonymously). No analytics pull is recorded — the endpoint also backs the web file-tree viewer, so counting UI views would inflate the pull metric.
  • AuditService: fetch package bytes via skillService.getPackageBytes(guid, SYSTEM_ACTOR) instead of the skill doc's presigned URL — removing the wasted presigned round-trip and cast-riddled fallback ([Misc] buildAuditContext makes a wasted presigned-URL round-trip and keeps dead, cast-riddled code #995).
  • OpenAPI schema + spec updated (field removed, download path documented).

Frontend (ornn-web)

  • apiClient: extract rawFetchWithRetry (auth + refresh + 401-retry in one place) and add apiGetBinary(), an authenticated GET returning raw bytes.
  • useSkillPackage(guid, version) fetches the proxied route via apiGetBinary + JSZip instead of fetch(presignedUrl); callers (useSkillDetail, usePlaygroundSession, SkillsetMemberViewer) repointed at skill.guid/skill.version; presignedPackageUrl dropped from the SkillDetail type. Every live download surface (file-tree viewer + hero "Download package") now routes through ornn-api.

Testing

  • ornn-api full suite green (incl. new download-route tests + a real-service regression guard on the getPackageBytes visibility gate); ornn-web (Vitest) and TS SDK suites green; typecheck (api/web/sdk) + ESLint clean.
  • A multi-agent adversarial review of the diff surfaced one test-coverage gap on the new anonymous-reachable gate, now closed by the added guard.

Note

The runtime cutover is separate: NyxID's proxy node must be repointed to chrono-bucket and ornn granted chrono-bucket-user + ornn-bucket ownership before downloads function end-to-end. This PR is the code adaptation only.

Closes #1196
Closes #995

…#1196)

chrono-bucket (replacing chrono-storage) removed the presigned-URL and
object-copy endpoints and now serves downloads from a streaming
GET /api/buckets/:bucket/objects/download?key=. Adapt ornn-api:

- StorageClient: add downloadObject() (raw-bytes streaming read); remove
  the now-dead getPresignedUrl() and copy() (copy had no callers).
- SkillService: route the internal byte reads (diff, /json, rescan)
  through downloadObject; add a gated getPackageBytes() that funnels
  version->storageKey resolution + the visibility gate (shared with
  getSkillJson), and stop minting a client-facing presigned URL in
  buildDetailResponse. SkillDetailResponse.presignedPackageUrl is dropped
  so no direct-to-storage URL ever reaches a client.
- Register GET /skills/:idOrName/versions/:version/download — the route
  the TS SDK's downloadPackage() already targets — streaming the ZIP
  through ornn-api (application/zip, attachment). No analytics pull is
  recorded: the endpoint also backs the web file-tree viewer, so counting
  UI views would inflate the pull metric (programmatic reads use /json).
- AuditService: fetch package bytes via skillService.getPackageBytes
  (SYSTEM_ACTOR) instead of the skill doc's presigned URL, deleting the
  wasted presigned round-trip and cast-riddled fallback (#995).
- Drop presignedPackageUrl from the OpenAPI schema + spec; document the
  new download path.

Part of #1196. Closes #995.
The web viewer used to fetch the skill ZIP straight from chrono-bucket /
MinIO via the presigned URL on SkillDetail. chrono-bucket removed
presigned URLs and now sits behind the NyxID proxy, so route the
download through ornn-api instead:

- apiClient: extract rawFetchWithRetry (auth + refresh + 401-retry in one
  place) and add apiGetBinary(), an authenticated GET returning raw bytes.
- useSkillPackage(guid, version): fetch GET /skills/:idOrName/versions/
  :version/download via apiGetBinary + JSZip, replacing fetch(presignedUrl).
  A non-2xx surfaces as ApiClientError -> the same translated
  "download failed" payload the direct-fetch path produced.
- Point the three callers (useSkillDetail, usePlaygroundSession,
  SkillsetMemberViewer) at (skill.guid, skill.version) and drop
  presignedPackageUrl from the SkillDetail type + test fixtures.

Part of #1196.
#1196)

The new GET /skills/:idOrName/versions/:version/download route is
optionalAuth and anonymous-reachable, so its object-level (BOLA) gate
lives entirely in SkillService.getPackageBytes. That gate is a distinct
copy from getSkillJson's, and the route test mocks getPackageBytes — so
nothing exercised the real gate. Deleting the canReadSkill check would
have shipped green while streaming private-skill ZIPs to anonymous
callers. Add real-service tests (private+stranger -> skill_not_found
before any download; public+stranger -> bytes; private+owner/SYSTEM_ACTOR
-> bytes) so that regression can never pass silently.

Part of #1196.
useSkillPackage called apiGetBinary("/skills/:guid/versions/:version/
download") without the /api/v1 prefix that every other apiClient path
carries, so the browser's request 404'd at the NyxID proxy and the skill
detail page showed "failed to load package contents". The in-repo route
and the SDK both work; only the web path was wrong. Component tests mock
useSkillPackage wholesale, so nothing caught it — add a focused hook test
asserting the exact proxied path (/api/v1/skills/:guid/versions/:version/
download) alongside the fix.

Part of #1196.
@chronoai-shining
chronoai-shining merged commit 7a9b743 into develop Jul 7, 2026
17 checks passed
@chronoai-shining
chronoai-shining deleted the feature/1196-chrono-bucket-download-proxy branch July 7, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant