Skip to content

docs(file-storage): split into control plane + signed-URL sidecar (ADR-0003)#4097

Merged
Artifizer merged 8 commits into
constructorfabric:mainfrom
ffedoroff:file-storage-3
Jun 28, 2026
Merged

docs(file-storage): split into control plane + signed-URL sidecar (ADR-0003)#4097
Artifizer merged 8 commits into
constructorfabric:mainfrom
ffedoroff:file-storage-3

Conversation

@ffedoroff

@ffedoroff ffedoroff commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Overview

Reworks the FileStorage design to a two-plane architecture (new ADR-0003, supersedes ADR-0001).

  • Control plane = FileStorage API / SDK. Owns metadata, authorization, versioning, and conditional-request semantics. Its HTTP REST surface never accepts or returns file content — it issues short-lived signed URLs instead. The only path where control-side code touches bytes is the in-process SDK proxy mode, which streams inside the consumer gear's process.
  • Data-plane sidecar = the only component that moves user bytes. Own domain/URL, connected to the storage backends, verifies signed-URL signatures and platform auth tokens, and reaches the control plane through the FS SDK (direct-DB or REST).

The presigned URL always points at our sidecar, never at the raw backend — so backend opacity, centralized per-byte metering, and uniform audit/policy coverage are all preserved, while the bandwidth-bound data plane scales independently of the control plane. Every content operation is therefore at least two requests: a control request to mint a signed URL, then one or more data requests against the sidecar.


Identity & version types

A file's identity is separated from its content. Content is an immutable blob per version; the file is a small mutable record holding a pointer to the current version.

Field Type Lives on Meaning / lifecycle
file_id uuid files (PK) The stable logical identity of a file. Never changes for the life of the file. Everything else hangs off it.
version_id uuid file_versions Identity of one immutable content blob. Assigned by the control plane (not the backend) at pre-register. The backend object lives at /{file_id}/{version_id} and is written once, never mutated.
content_id uuid (nullable) files The content pointer: the version_id currently bound as the file's live content. NULL until the first bind. Changing content is a pointer swap (content_id := new version_id), not an in-place mutation.
meta_version bigint files Monotonic counter, bumped only on metadata-only writes (custom-metadata PATCH). Independent of content.

Derived, not stored:

  • Full snapshot = (file_id, content_id, meta_version) — uniquely pins the complete observable state (content axis × metadata axis, orthogonal).
  • ETag (content-only) = opaque, derived from (file_id, content_id):
    etag_payload = file_id_bytes (16) || content_id_bytes (16)   # both UUIDs, 32 bytes
    etag_header  = '"' || base64url(etag_payload) || '"'
    
    It changes exactly when content is (re)bound; a metadata-only write does not touch it (so a CDN does not invalidate cached bytes on a metadata change). It is never equal to the content hash (a separate concern, ADR-0002). Restoring a prior version re-binds its content_id, so the ETag returns to that version's value (content-addressed).

Write semantics. A content write is: upload a new immutable version → bind it. Bind is an optimistic CAS on content_id guarded by If-Match. On conflict the bind returns 412 and the version_id; the client re-reads the current ETag and replays the bind with the fresh If-Matchno byte re-upload, because the version already exists durably. meta_version gives metadata its own validator via the optional If-Match-Metadata: <u64> header (absent → last-write-wins).

content_id replaces the old content_revision counter: it is the pointer (no separate counter to maintain), at the cost of monotonicity (use created_at for ordering).


Signed URLs & token validation

Signed URLs are how the control plane delegates a single content operation to the sidecar. They are stateless (à-la S3 presigned: verifiable with no DB lookup).

Signing (Ed25519, control-minted)

  • Asymmetric Ed25519. The control plane signs with the private key and is the sole minter; the sidecar verifies with the public key and can never forge a URL. P1 ships one static keypair distributed by config (private in control, public in sidecar) — no rotation, no per-URL revocation; key rotation + keyset is P2. Distributing the public key (and the backend registry) is the only control→sidecar "sync".
  • Parameters (query, X-FS-*): X-FS-Algorithm=Ed25519, X-FS-KeyId, X-FS-Expires=<unix>, X-FS-Op=GET|PUT|part, resource (X-FS-FileId, X-FS-ContentId/X-FS-VersionId, X-FS-BackendId), constraints (X-FS-Ip, X-FS-Tok-<claim>), baked response headers (X-FS-Rh-<name>), and finally X-FS-Signature.
  • Canonical string to sign = method + host + path + all X-FS-* params except X-FS-Signature, sorted by key and percent-encoded (RFC 3986), joined k=v&….
    • host is signed → a URL cannot be replayed against a different sidecar.
    • Signing all X-FS-* (minus the signature) means any added, removed, or altered parameter breaks verification — so there is no separate SignedParams list, and a client cannot strip or weaken a constraint.
  • Deliberately NOT signed: the Range header (so one signed download URL serves many ranges — random access), conditional headers, and the PUT body (byte integrity is verified by the hash at bind, not the URL). Consequence: a signed PUT URL can be replayed with different bytes until exp → that only creates an orphan version/blob (swept by the P2 cleanup engine).

Constraints (AND-combined)

A signed URL carries any number of constraints, all of which must hold:

  • exp — expiry (required).
  • ip / CIDR — optional client-address restriction.
  • Token-claim predicates — optional X-FS-Tok-<claim>=<value> (e.g. typ=user, sub=<id>, tenant_id=<id>, any claim).

"Available to everyone for 5 minutes" = only exp (no token needed). "Only a user token with sub=123…" = exp + X-FS-Tok-typ=user + X-FS-Tok-sub=123….

Verification (at the sidecar)

On each request the sidecar:

  1. Recomputes the signature over the canonical string using the public key named by X-FS-KeyId; rejects (403) on mismatch.
  2. Checks exp; checks ip/CIDR if present.
  3. Token validation — if (and only if) the URL carries one or more X-FS-Tok-<claim> predicates, the request must present a valid platform JWT. The sidecar validates that token the standard platform way and then checks each predicate (claim == value), all AND-combined. Any missing/invalid token or unmatched claim → 403. A URL with no token predicate is served on signature + exp/ip alone.
  4. Echoes the baked X-FS-Rh-* response headers verbatim (e.g. Content-Disposition, Content-Type override, Cache-Control) — no control round-trip.

This keeps authorization decisions on the control plane (made at presign, encoded into the URL) and the sidecar a pure enforcer of what the URL + token assert.


Flows

Upload (single-shot, P1): POST /files (authz) → signed PUT url → client PUTs to the sidecar with If-Match → sidecar pre-registers a pending version via SDK (checks If-Match as a free early-fail, before any bytes) → streams to the backend computing size + SHA-256 → auto-binds under app-token + on-behalf-of user. 412 → sidecar returns version_id → client rebinds, no re-upload.

Download (P1): GET /files/{id}/download-url (authz) → signed GET url pinning the current content_id → client fetches from the sidecar (Range/conditional handled there).

Multipart (P2): server-authoritative parts plan (control returns exact part sizes/offsets + a signed URL per part); native backend multipart or offset-writes into the new-version object; per-part BLAKE3 subtree hashes persisted in multipart_upload_parts.part_hash and combined into the root at complete.


Versioning & cleanup

  • Versioning is P1, FileStorage-level and backend-agnostic (each version is a distinct immutable object + the content_id pointer) — works on any backend; the versioning_native capability is removed.
  • No automatic cleanup in P1 (versions accumulate — the indefinite-retention default). The cleanup engine (layered version retention ≤ X / age T + orphan reconciliation) is P2, a single internal control-plane background process.

DB schema

  • files slimmed to identity + pointer: file_id, tenant_id, owner_kind, owner_id, name, gts_file_type, content_id, meta_version, timestamps (no bytes, no per-content fields).
  • New P1 file_versions: (file_id, version_id, mime_type, size, hash_algorithm, hash_value, status pending|available, is_current, backend_id, backend_path, created_at).
  • P2 hash-allow-list widening and P3 encryption columns now target file_versions.

Files & validation

  • ADR/0003-…sidecar-data-plane.md (new); ADR/0001 → superseded; ADR/0002 references repointed to the sidecar.
  • PRD.md, DESIGN.md, api.md, migration.sql reworked.
  • cpt validate: 0 errors, code coverage 104/104.

Draft — pending review.

Summary by CodeRabbit

  • New Features
    • Introduced a two-plane FileStorage flow: a control plane issues short-lived signed URLs, and a sidecar exclusively handles upload/download bytes, including sidecar-served Range behavior.
    • Added opaque PASETO (v4.public) signed URL tokens delivered via query (fs-token) or X-FS-Token, with strict expiry and claim validation.
  • Documentation
    • Rewrote PRD/API docs to reflect signed-URL semantics, conditional headers, and pointer-based immutable versioning.
    • Added ADRs for the sidecar data plane and signed-URL transport; removed the deprecated proxy-traffic ADR.
  • Refactor
    • Updated database migration to support immutable, versioned content and moved encryption metadata to per-version storage.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updates FileStorage documentation and migration notes to describe a control-plane/sidecar split, opaque signed URL transport, immutable versioned content storage, revised conditional and range semantics, and schema changes that move content state from files to file_versions.

Changes

FileStorage sidecar architecture transition

Layer / File(s) Summary
Sidecar data-plane ADR baseline and traceability
gears/file-storage/docs/ADR/0002-...md, gears/file-storage/docs/ADR/0003-...md
Introduces ADR-0003 for the signed-URL sidecar data plane and updates ADR-0002 references so hashing traceability points to the sidecar streaming path.
Signed token format and transport ADR
gears/file-storage/docs/ADR/0004-...md
Adds ADR-0004 defining opaque PASETO v4.public token encoding, query/header transport envelopes, opacity rules, option tradeoffs, confirmation scope, and traceability links.
PRD overview, glossary, scope, and core content flows
gears/file-storage/docs/PRD.md
Rewrites the PRD overview, glossary, scope, upload/download/delete requirements, content-type validation, and delegated authorization around the control-plane and sidecar split.
PRD lifecycle, conditional behavior, API, and acceptance updates
gears/file-storage/docs/PRD.md
Updates orphan reconciliation, versioning, capabilities, latency and bandwidth limits, control-plane REST and signed URL contracts, conditional semantics, use cases, acceptance criteria, and assumptions for the two-plane model.
API reference rewrite for control-plane and sidecar behavior
gears/file-storage/docs/api.md
Replaces the API framing with the two-plane model, defines sidecar token verification, updates multipart and CAS conflict handling, revises token specification, conditionals, range handling, response headers, and status codes.
Migration SQL documentation reshaped for immutable file versions
gears/file-storage/docs/migration.sql
Reworks migration documentation so files hold logical identity plus content pointer and meta counter, file_versions holds immutable content state and indexes, hash-policy widening targets file_versions, and encryption and capability notes align with the new model.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Artifizer
  • MikeFalcon77

A rabbit hopped through docs so neat,
With signed URLs and sidecar beat.
The bytes now travel a cleaner way,
And versions rest where pointers stay.
Hoppity! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: splitting FileStorage into a control plane and signed-URL sidecar.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread gears/file-storage/docs/DESIGN.md
Comment thread gears/file-storage/docs/ADR/0004-cpt-cf-file-storage-adr-signed-url-transport.md Outdated
* Good, because it is the **canonical S3 SigV4 scheme** — ~20 years in production, universally understood, well-tooled
* Good, because discrete fields are **debuggable** (visible/greppable in logs) and verified without an extra decode step
* Bad, because re-presigning changes the URL → CDN cache-key churn unless normalized
* Bad, because the signature appears in URLs (logs/Referer/history) — accepted, same as S3

@Artifizer Artifizer Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bad, because browser/frontend/proxy may log everything (maybe it's OK for some cases, but not OK for others), imagine some of the fields need to be hiddent (e.g. signature expiration)
Bad, because if I have a batch request I'll need to pass and parse parameters for every file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the reworked ADR-0004. Fields like exp aren't secrets — the only secret is the signature, which is in the URL regardless, so hiding the other fields buys no security. If the credential must stay out of URLs/logs, that's the new header envelope (for programmatic/SDK/batch), which also removes the per-file query-param parsing for batch. The query envelope stays only for bare, embeddable URLs (browser/<img>).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved by the token model. The credential is now an opaque token; to keep it out of URLs/logs entirely, programmatic/batch callers use the X-FS-Token header (the fs-token query form is only for bare embeddable URLs). One opaque token per file also removes the per-file param parsing in batch. (Note: PASETO v4.public is signed-not-encrypted, so exp is technically decodable — the opacity is an interface/evolvability boundary, not secrecy; exp isn't a secret anyway.)

### B. Single opaque query token

* Good, because it is compact and hides internal structure
* Bad, because it must be base64-decoded and parsed before verification — extra ceremony, easy to get wrong

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does it mean 'query' parameters will not be in base64 and will not parse it?
I think we need to have base64-decode, parsing and verification regardless of the URL vs header vs other options.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct — parsing + Ed25519 verification always happen, in any envelope. I dropped the misleading 'verify without a decode step' pro. The point is only that with discrete fields there is no opaque token to base64-decode (named params + one composite signature); decode/parse isn't an argument for either side. We kept discrete fields.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and that is exactly the model now: the sidecar base64-decodes + PASETO-verifies the token in whichever carrier it arrives (query or header). The earlier 'verify without a decode step' framing is gone.

### D. Single opaque HTTP-header token

* Good, because URL is stable and the secret is out of the URL
* Bad, because it combines the worst of B and C: opaque blob (no debuggability, decode ceremony, no benefit) **and**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see contradiction of what is Bad and what is Good:

Option A says:
Bad, because the signature appears in URLs (logs/Referer/history) <<< I think is correct 'bad'

Option D says:
Bad, because ... no debuggability

What is our debuggability pattern?

I think it should be like - whatever logging is intentional:

  • no information leak on rpoxy / routers / browser / etc
  • whatever logging is intentional and sanitized (e.g. log tenant ID, but do not log token expiration)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — the contradictory 'debuggability via visible URL' pro is removed. Debuggability is now defined as intentional, sanitized server-side structured logging (log tenant/file ids + outcome, never the signature or raw exp), independent of the envelope. URL visibility is treated only as the leak it is, not as a feature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Debuggability is defined as sanitized server-side structured logging by control/sidecar (tenant/file ids + outcome; never the token or exp), independent of carrier. With the opaque token the edge intentionally cannot read fields — observability is server-side only. The contradictory 'debuggability via visible URL' pro is removed.

Comment thread gears/file-storage/docs/ADR/0004-cpt-cf-file-storage-adr-signed-url-transport.md Outdated
Comment thread gears/file-storage/docs/ADR/0001-cpt-cf-file-storage-adr-proxy-content-traffic.md Outdated
@pr0tey

pr0tey commented Jun 19, 2026

Copy link
Copy Markdown

Quota enforcement: the data plane should enforce the cap directly at upload, with the allowance carried in the signed URL

Context. DESIGN §4.5 ("No policy or quota in the data plane", lines 1427–1434) and PRD cpt-cf-file-storage-fr-storage-quota currently place all quota logic in the control plane, evaluated only at presign: if an
upload would violate a limit, the control plane either refuses to mint the URL or "bakes a tighter constraint, e.g. MaxSize, into it." The sidecar holds none of this state and makes no tenant/user/backend decision.

We discussed this and believe presign-only enforcement leaves two gaps that the control/data-plane split (ADR-0003) introduces and that the monolith (ADR-0001) did not have, because in the monolith the quota check
and the byte-write were a single act:

  1. Presign cannot know the real size. The control plane gates the quota before any bytes exist. Unless the client declares X-FS-ExactSize, the presign-time check operates on an unknown quantity. Today nothing
    requires ExactSize/MaxSize on an upload URL when a quota is active, so the check can be vacuous.
  2. TOCTOU across concurrent presigns. Actual consumption is known only at bind (the sidecar reports the size), and usage reporting is asynchronous. With a presign→use window of up to ~max_url_ttl(recommended 7 days), N concurrent presigns can each pass the quota check yet collectively blow past the owner's limit. There is no reservation or re-check onbind.

Proposed direction (what we want the P2 design to adopt). Make the data plane enforce the quota cap directly during the upload stream, and carry the maximum allowance inside the signed URL / token — not just a
per-file MaxSize, but an explicit quota allowance for that operation.

Concretely:

  • At presign, the control plane computes the owner's remaining quota and bakes the allowed byte ceiling into the signed fields (e.g. an X-FS-MaxSize-style quota allowance). The limit travels with the credential,
    tamper-evident under the same composite signature.
  • The sidecar already streams the body and counts bytes (it enforces MaxSize/ExactSize and computes the hash). It therefore aborts mid-stream the moment the signed allowance is exceeded (413), reusing the exact
    machinery that already exists — no new state, no cross-request coordination. The cap is per-URL, so the data plane stays stateless: it enforces what the signature asserts, nothing more.
  • This makes the upload-time limit authoritative at the point where the bytes actually arrive, closing gap (1): the cap no longer depends on the client truthfully declaring its size — the sidecar terminates the
    transfer at the signed ceiling regardless.

Reconciling with the "policy-free data plane" principle. This does not ask the sidecar to read tenant/owner quota state or make a policy decision — the decision stays in the control plane at presign; the sidecar
only enforces a per-URL number it was handed, exactly as it already does for MaxSize. So §4.5's statelessness invariant holds.

What remains a control-plane responsibility is the aggregate owner quota across many outstanding URLs (gap 2). The clean complement to the above is a reserve-at-presign / commit-at-bind model: the control plane
reserves the baked allowance against the owner's quota when it mints the URL, the sidecar reports the actual bytes at bind, and the control plane commits the real size and releases the unused reservation (and
reclaims reservations from URLs that expire unused). The per-URL ceiling enforced by the sidecar is what makes the reservation bounded and safe.

Asks for the P2 FEATURE (cpt-cf-file-storage-fr-storage-quota):

  • Make a signed quota allowance (or ExactSize/MaxSize) mandatory on upload URLs when a quota is active; specify the sidecar's mid-stream 413 on breach.
  • Specify the control-plane reserve/commit/release lifecycle keyed to presign → bind → expiry, so concurrent presigns cannot collectively exceed the owner's quota.
  • Note explicitly in DESIGN §4.5 that the per-URL quota allowance is the one quota-related value the data plane enforces directly, and why that does not violate the stateless/policy-free invariant.

@ffedoroff
ffedoroff marked this pull request as ready for review June 22, 2026 09:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
gears/file-storage/docs/ADR/0003-cpt-cf-file-storage-adr-sidecar-data-plane.md (2)

158-158: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Minor style: remove redundant "of".

Line 158 reads "all of the prior proxy-all design's properties" — the "of" is redundant here. Suggested fix: "all the prior proxy-all design's properties".

✏️ Proposed fix
-* Good, because all of the prior proxy-all design's properties hold trivially (everything is in one place)
+* Good, because all the prior proxy-all design's properties hold trivially (everything is in one place)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@gears/file-storage/docs/ADR/0003-cpt-cf-file-storage-adr-sidecar-data-plane.md`
at line 158, The phrase "all of the prior proxy-all design's properties"
contains a redundant "of" that should be removed for better style and
readability. Locate this text in the bullet point describing why the design is
good (around line 158) and change it to "all the prior proxy-all design's
properties" by removing the word "of" between "all" and "the".

99-107: 🧹 Nitpick | 🔵 Trivial

Clarify owner-level quota enforcement responsibility.

Per the PR objectives and substantive feedback, FileStorage must address two quota gaps: (1) presign cannot know real size unless the client declares ExactSize, and (2) concurrent presigns create a TOCTOU race. The proposed P2 solution bakes a per-URL quota allowance (computed from the owner's remaining quota at presign time) into the signed URL and has the sidecar enforce it mid-stream.

ADR-0003 mentions per-upload size constraints (max_size/exact_size in lines 102–103) but does not address the owner-level reserve/commit/release lifecycle that synchronizes presign → bind → expiry. DESIGN §4.5 clarifies that owner-level quota (storage quota, per-owner caps) is a control-plane concern evaluated at presign (P2); the sidecar enforces only per-URL constraints and holds no tenant/user/backend policy state.

Add a brief note to §4 explaining that per-upload size constraints (max_size/exact_size) are sidecar-enforced, while owner-level quota reserve/commit/release is a control-plane responsibility (documented in DESIGN §4.5).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@gears/file-storage/docs/ADR/0003-cpt-cf-file-storage-adr-sidecar-data-plane.md`
around lines 99 - 107, The ADR currently mentions per-upload size constraints
(max_size and exact_size) in the constraints section but does not clearly
delineate responsibility boundaries between the sidecar and control plane for
quota enforcement. Add a clarifying note to section §4 explaining that max_size
and exact_size are sidecar-enforced per-URL constraints applied during upload,
while the owner-level quota reserve/commit/release lifecycle (managing storage
quota and per-owner caps) remains a control-plane responsibility evaluated at
presign time and synchronized across presign, bind, and expiry events, noting
that DESIGN section 4.5 provides further details on this control-plane quota
mechanism.
gears/file-storage/docs/ADR/0004-cpt-cf-file-storage-adr-signed-url-transport.md (1)

114-132: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Token Opacity Contract is correctly framed as an encapsulation/evolvability boundary.

The contract explicitly distinguishes between secrecy (not provided; PASETO v4.public is signed-not-encrypted, so payload is base64-decodable) and opacity (enforced by convention: only control+sidecar parse; everyone else treats it as opaque bytes and format can evolve freely). This is the correct mental model and resolves the prior design tension: opacity is not about hiding field values — it is about decoupling the external world from internal format changes.

The rationale (lines 129–132) is sound: exp was never a secret; the only secret is the signing key (held solely by control). Field values like exp are acceptable-to-expose; the real value of opacity is format encapsulation — control and sidecar deploy together, so they can change claims and crypto at will without coordinating with browsers, CDNs, proxies, or consuming apps that merely forward the token.

This closes the earlier design debate (past comments from Artifizer / ffedoroff about what is debuggable, what leaks, what the security model is) and provides a clean, logically consistent model: encapsulation, not secrecy.

Recommend: ensure observability guidance in DESIGN/ops docs emphasizes that logging/debugging must be done at the control/sidecar level (they know the format), never by decoding the token at the edge. This prevents intermediaries from mistakenly trying to parse the token and breaking on format changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@gears/file-storage/docs/ADR/0004-cpt-cf-file-storage-adr-signed-url-transport.md`
around lines 114 - 132, Update the observability and operations documentation to
ensure it clearly emphasizes that all logging and debugging related to token
validation and format must be performed exclusively at the control plane and
sidecar level where the token format is known, and that intermediaries
(browsers, CDNs, proxies, API gateways, logging systems) must never attempt to
parse, decode, or inspect tokens as this practice would break when the internal
format evolves. Make explicit that any needed debugging insights should come
from sanitized structured logs emitted by the control/sidecar components, not
from token introspection at the edge.
gears/file-storage/docs/api.md (3)

122-144: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Minor: Clarify which signed URL claims are mandatory in P1 vs optional.

The claims table (lines 122–133) is well-formatted and shows the phase and violation for each claim. However, the "Req." column uses "yes" for exp and op and "no" for all others, which implies that in P1 only exp and op are mandatory. The narrative below (lines 134–144) reinforces this.

For absolute clarity to implementers, consider adding a brief note: "In P1, only exp and op are required on every signed URL. All other claims are optional; if not present, they impose no constraint." This makes the read path explicit: if max_size is absent, the sidecar does not enforce a size limit for that URL.

Alternatively, the table header could be expanded to clarify: "Req. (in Phase X)" to remove ambiguity about what "yes" means across phases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gears/file-storage/docs/api.md` around lines 122 - 144, The documentation
about signed URL claims needs clearer explanation of mandatory vs optional
claims in P1. Add an explicit note after the claims table stating that in P1,
only the `exp` and `op` claims are required on every signed URL, while all other
claims are optional and impose no constraint if absent (e.g., if `max_size` is
not present, the sidecar will not enforce a size limit). Alternatively, modify
the table header's "Req." column to read "Req. (in Phase X)" to make it
unambiguous that the yes/no values apply specifically to the P1 phase. This will
clarify the read path for implementers so they understand the default behavior
when optional claims are omitted.

173-193: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Verify that response headers X-FS- header set is complete and matches implementation expectations.*

The response headers section (lines 173–193) lists a comprehensive set of headers, including X-FS-File-Id, X-FS-Version-Id, X-FS-GTS-File-Type, X-FS-Hash-Algorithm, X-FS-Hash-Value, X-FS-Metadata-Revision, X-FS-Owner-Kind, X-FS-Owner-Id, X-FS-Created-At, baked response headers, and per-metadata-key headers (X-FS-Meta-<key>).

The list aligns with the PRD's description of versioning, metadata, and hash tracking. However, a few details should be verified:

  1. Are all X-FS- headers always present, or optional?*

    • For instance, if a file has no custom metadata, should X-FS-Meta-* headers be absent?
    • Should all headers be present on every response?
  2. Custom metadata headers (X-FS-Meta-):

    • Custom metadata is updated via PATCH and optionally passed on download presign.
    • Should these headers be included on sidecar GET/HEAD responses, or only on control GET/HEAD?
    • If included on sidecar responses, how does the sidecar know which custom metadata keys to return (the signed URL could carry them, but this adds overhead)?
  3. Baked response headers:

    • The section notes "baked response headers echoed verbatim from the token's response-header claims".
    • These are useful for setting Content-Disposition, Cache-Control, etc. without a control-plane round-trip.
    • Should a list of allowed/safe baked headers be documented to prevent header injection?

For clarity, the response headers section should note which headers are always present, which are conditional, and how custom metadata headers are conveyed to the sidecar (if at all).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gears/file-storage/docs/api.md` around lines 173 - 193, The response headers
section (lines 173–193) needs clarification on header presence and handling. Add
documentation notes to specify which X-FS-* headers are always present versus
optional/conditional (for example, clarify whether X-FS-Meta-<key> headers
appear only when custom metadata exists). Additionally, clarify whether custom
metadata headers are included on sidecar GET/HEAD responses and explain how the
sidecar determines which metadata keys to return. Finally, document any
restrictions or an allowed list for baked response headers to prevent header
injection vulnerabilities, ensuring the section clearly distinguishes between
headers always present, conditionally present, and those controlled by token
claims.

74-90: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Clarify multipart behavior for backends without native multipart support and the BLAKE3 subtree-hash mechanics.

The multipart section (lines 74–90) states:

"For a multipart_native backend the sidecar drives the backend multipart API; otherwise it offset-writes each part into the single new-version object. Per-part BLAKE3 subtree hashes are persisted in multipart_upload_parts.part_hash and combined into the root at complete."

Several details need clarification:

  1. How does the sidecar know whether a backend is multipart_native?

    • Is this advertised via the backend capabilities discovery?
    • Or inferred at presign/initiate time?
  2. When a backend is NOT multipart-native, does the sidecar offset-write parts sequentially or in parallel?

    • If sequential, the order is deterministic.
    • If parallel, how are offsets coordinated to avoid overwrites?
  3. BLAKE3 subtree-hash combination:

    • The PRD mentions multipart validation (line 326–329: "first part validation for content-type checking").
    • The api.md references BLAKE3 subtree hashes but does not explain the hash-combination algorithm.
    • Is the root hash = BLAKE3_combine(part_hash_0, part_hash_1, ...)?
    • Or is it computed incrementally as parts arrive?
  4. Is P2 multipart meant to be server-authoritative only, or can clients also request specific part boundaries?

    • Line 76 says "server-authoritative" (client suggests params, control plane returns exact plan).
    • This is good for avoiding off-by-one errors, but limits client flexibility (e.g., resuming a partial upload with different part boundaries).

For P1 clarity and P2 design, the multipart specification should address these details or explicitly defer them to the P2 FEATURE. If they are in DESIGN.md, this api.md section should cross-reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gears/file-storage/docs/api.md` around lines 74 - 90, The P2 multipart upload
section lacks clarity on four critical implementation details that must be
addressed: (1) how the sidecar discovers whether a backend is multipart_native
(via capabilities discovery or runtime inference), (2) whether non-native
backends perform offset-writes sequentially or in parallel and how offset
coordination prevents overwrites, (3) the exact BLAKE3 subtree-hash combination
algorithm (simple combine vs. incremental computation), and (4) whether
server-authoritative behavior is absolute or if clients can negotiate specific
part boundaries. Expand the documentation after the endpoint definitions (P2-1
through P2-5) to explicitly answer each point, or add cross-references to
DESIGN.md if these details are defined elsewhere. Include information about how
the multipart_native capability is advertised and how the sidecar behaves
differently for native versus non-native backends.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gears/file-storage/docs/api.md`:
- Around line 91-107: The "Upload, bind, and the conflict retry" section
describes the happy path but does not address the edge case behavior when a
client receives a 412 conflict with a version_id but then decides to re-presign
(call POST /files/{id}/versions again) instead of rebinding the returned
version_id. Add a clarification note to this section explaining whether
re-presigning after a 412 is idempotent (returns the same version_id) or creates
a new sibling version_id, and recommend that clients should prefer rebinding the
returned version_id without re-presigning to avoid orphan accumulation. You can
document this as either: "On 412, the client should rebind the returned
version_id; if re-presigning occurs, the control plane returns the same
version_id (idempotent)" or "Presigning for a file with a pending version
returns the existing pending version_id and its upload URL, making re-presign a
no-op."

In `@gears/file-storage/docs/PRD.md`:
- Around line 214-224: In the section describing the bind conflict behavior
(where 412 is returned), clarify the signed URL retry behavior by explicitly
documenting whether the original signed URL remains valid for the retry or if
the client must re-presign with the control plane. Add guidance that if the
original signed URL has expired by the time the 412 is received, the client
should request a fresh signed URL before retrying the bind, and that the control
plane provides a fresh If-Match value for the retry attempt. This clarification
should address the operational concern about using potentially stale URLs across
multiple retries while maintaining the core requirement that bytes do not need
to be re-uploaded.

---

Nitpick comments:
In
`@gears/file-storage/docs/ADR/0003-cpt-cf-file-storage-adr-sidecar-data-plane.md`:
- Line 158: The phrase "all of the prior proxy-all design's properties" contains
a redundant "of" that should be removed for better style and readability. Locate
this text in the bullet point describing why the design is good (around line
158) and change it to "all the prior proxy-all design's properties" by removing
the word "of" between "all" and "the".
- Around line 99-107: The ADR currently mentions per-upload size constraints
(max_size and exact_size) in the constraints section but does not clearly
delineate responsibility boundaries between the sidecar and control plane for
quota enforcement. Add a clarifying note to section §4 explaining that max_size
and exact_size are sidecar-enforced per-URL constraints applied during upload,
while the owner-level quota reserve/commit/release lifecycle (managing storage
quota and per-owner caps) remains a control-plane responsibility evaluated at
presign time and synchronized across presign, bind, and expiry events, noting
that DESIGN section 4.5 provides further details on this control-plane quota
mechanism.

In
`@gears/file-storage/docs/ADR/0004-cpt-cf-file-storage-adr-signed-url-transport.md`:
- Around line 114-132: Update the observability and operations documentation to
ensure it clearly emphasizes that all logging and debugging related to token
validation and format must be performed exclusively at the control plane and
sidecar level where the token format is known, and that intermediaries
(browsers, CDNs, proxies, API gateways, logging systems) must never attempt to
parse, decode, or inspect tokens as this practice would break when the internal
format evolves. Make explicit that any needed debugging insights should come
from sanitized structured logs emitted by the control/sidecar components, not
from token introspection at the edge.

In `@gears/file-storage/docs/api.md`:
- Around line 122-144: The documentation about signed URL claims needs clearer
explanation of mandatory vs optional claims in P1. Add an explicit note after
the claims table stating that in P1, only the `exp` and `op` claims are required
on every signed URL, while all other claims are optional and impose no
constraint if absent (e.g., if `max_size` is not present, the sidecar will not
enforce a size limit). Alternatively, modify the table header's "Req." column to
read "Req. (in Phase X)" to make it unambiguous that the yes/no values apply
specifically to the P1 phase. This will clarify the read path for implementers
so they understand the default behavior when optional claims are omitted.
- Around line 173-193: The response headers section (lines 173–193) needs
clarification on header presence and handling. Add documentation notes to
specify which X-FS-* headers are always present versus optional/conditional (for
example, clarify whether X-FS-Meta-<key> headers appear only when custom
metadata exists). Additionally, clarify whether custom metadata headers are
included on sidecar GET/HEAD responses and explain how the sidecar determines
which metadata keys to return. Finally, document any restrictions or an allowed
list for baked response headers to prevent header injection vulnerabilities,
ensuring the section clearly distinguishes between headers always present,
conditionally present, and those controlled by token claims.
- Around line 74-90: The P2 multipart upload section lacks clarity on four
critical implementation details that must be addressed: (1) how the sidecar
discovers whether a backend is multipart_native (via capabilities discovery or
runtime inference), (2) whether non-native backends perform offset-writes
sequentially or in parallel and how offset coordination prevents overwrites, (3)
the exact BLAKE3 subtree-hash combination algorithm (simple combine vs.
incremental computation), and (4) whether server-authoritative behavior is
absolute or if clients can negotiate specific part boundaries. Expand the
documentation after the endpoint definitions (P2-1 through P2-5) to explicitly
answer each point, or add cross-references to DESIGN.md if these details are
defined elsewhere. Include information about how the multipart_native capability
is advertised and how the sidecar behaves differently for native versus
non-native backends.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61acd372-4f6f-4a4f-a49f-8713f038b660

📥 Commits

Reviewing files that changed from the base of the PR and between 443d63f and dc0489f.

📒 Files selected for processing (8)
  • gears/file-storage/docs/ADR/0001-cpt-cf-file-storage-adr-proxy-content-traffic.md
  • gears/file-storage/docs/ADR/0002-cpt-cf-file-storage-adr-content-hash-selection.md
  • gears/file-storage/docs/ADR/0003-cpt-cf-file-storage-adr-sidecar-data-plane.md
  • gears/file-storage/docs/ADR/0004-cpt-cf-file-storage-adr-signed-url-transport.md
  • gears/file-storage/docs/DESIGN.md
  • gears/file-storage/docs/PRD.md
  • gears/file-storage/docs/api.md
  • gears/file-storage/docs/migration.sql
💤 Files with no reviewable changes (1)
  • gears/file-storage/docs/ADR/0001-cpt-cf-file-storage-adr-proxy-content-traffic.md

Comment thread gears/file-storage/docs/api.md
Comment thread gears/file-storage/docs/PRD.md Outdated
@ffedoroff

Copy link
Copy Markdown
Contributor Author

@pr0tey good idea — and it already fits the model: the sidecar enforces a per-URL size cap carried in the token (the max_size claim) and aborts mid-stream with 413, which is exactly the "data plane enforces the signed allowance at upload" you describe — no new state, no cross-request coordination.

One contract decision, though: max_size stays optional, not mandatory-when-quota-active. If the claim is present the data plane MUST enforce it; if it is absent, only the backend's own default size limits apply (no FS-imposed per-URL cap). So quota policy decides whether to bake a max_size into the token; absent that, backend defaults govern.

On the aggregate-owner-quota / TOCTOU gap: the reserve-at-presign / commit-at-bind / release-on-expiry lifecycle is the right direction and we've recorded it as a P2 control-plane concern in the cpt-cf-file-storage-fr-storage-quota FEATURE. We are not making the per-URL allowance mandatory, but that reservation model (control-side) is what bounds concurrent presigns.

Captured in DESIGN §4.5 (the size claim is optional; when present the sidecar MUST enforce it; the per-URL max_size is the one quota-derived value the data plane enforces; reserve/commit/release deferred to the P2 quota FEATURE).

@ffedoroff

Copy link
Copy Markdown
Contributor Author

Pushed 08c52ba0 addressing the latest review:

  • CodeRabbit (api.md / PRD §5.1): clarified the 412 retry — the client re-binds the returned version_id via POST /files/{id}/bind, a control-plane call independent of the signed upload URL (its exp is irrelevant; no re-presign, no re-upload). The 412 carries the current content ETag for the fresh If-Match. Re-presigning is not idempotent (creates a sibling version, swept by the P2 cleanup engine).
  • @pr0tey (quota): the token's max_size is optional — present ⇒ the data plane MUST enforce it (mid-stream 413); absent ⇒ only the backend's default limits apply. The per-URL max_size baked from a quota decision is the one quota-derived value the data plane enforces (a signed number, not a policy lookup). Reserve-at-presign / commit-at-bind / release-on-expiry recorded as a P2 control-plane concern in cpt-cf-file-storage-fr-storage-quota.

(Inline replies left on each thread.)

@MikeFalcon77 MikeFalcon77 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Inline architecture notes.

Comment thread gears/file-storage/docs/PRD.md
Comment thread gears/file-storage/docs/DESIGN.md
Comment thread gears/file-storage/docs/DESIGN.md
Comment thread gears/file-storage/docs/DESIGN.md
Comment thread gears/file-storage/docs/migration.sql
Comment thread gears/file-storage/docs/DESIGN.md
Comment thread gears/file-storage/docs/DESIGN.md
discrete `X-FS-*` parameters are replaced by a **single token** carried as `?fs-token=<token>` (query) or an
`X-FS-Token` header. SigV4-style canonical-string signing is replaced by **PASETO mint (control) / verify
(sidecar)**; all claims live inside the token.
* **New dependency:** a PASETO v4 library — control plane signs (`v4.public`), sidecar verifies. Ed25519 keys as before

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Before we lock in PASETO, can we confirm the FIPS story? Ed25519/PASETO may be fine cryptographically, but if this must run in FIPS mode we need a compliant dependency/provider plan, or a fallback, before implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed in ADR-0004: documented the FIPS posture — Ed25519/PASETO is the choice, with a noted fallback plan if a FIPS-mode provider is mandated (tracked for P3).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What I mean that we shouldn't bring new rust crates which are using non-FIPS algorithms and we will unable to replace thse algs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Understood — the constraint is on dependency selection, not a deferred fallback. Reworked ADR-0004 "FIPS posture" (cc63f02f) accordingly:

  • MUST NOT add any crate that hard-wires a non-FIPS algorithm or a self-contained crypto impl we cannot swap.
  • Sign/verify sits behind a thin in-house SignatureProvider abstraction (sign/verify); that abstraction — not the crate — binds to the concrete algorithm + module.
  • FIPS deployments back the provider with a validated module (rustls-corecrypto-provider); otherwise swap to a FIPS-approved alternative (ECDSA P-256) with no codec/claim-set change (opaque token).
  • The candidate PASETO crate is evaluated for this replaceability before adoption; one that statically links a non-replaceable non-FIPS impl is rejected.

DESIGN.md and api.md carry the same rule. The replaceability requirement gates dependency selection and is no longer deferrable.

@MikeFalcon77 MikeFalcon77 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved with note: please be sure that we don't use non-FIPS compatible crates

ffedoroff and others added 7 commits June 27, 2026 18:06
…R-0003)

Supersede ADR-0001 (proxy-all monolith) with ADR-0003: a control plane that
issues short-lived Ed25519 signed URLs and a data-plane sidecar that moves the
bytes. The signed URL points at the sidecar (never the backend), so backend
opacity, per-byte metering, and uniform audit/policy coverage are preserved
while the byte-moving data plane scales independently.

- Content is an immutable blob per version at /{file_id}/{version_id}; a file's
  live content is the content_id pointer, swapped under optimistic CAS
  (If-Match). A new version is a new object + a pointer swap; backend content is
  never mutated. A bind 412 retries the rebind without re-uploading bytes.
- Three identifiers: content_id (current version pointer; content-only ETag),
  meta_version (metadata CAS), derived snapshot (file_id, content_id, meta_version).
- Versioning is now P1 and FileStorage-level (distinct objects + pointer),
  backend-agnostic; versioning_native capability removed. Cleanup engine
  (version retention + orphan reconciliation) stays P2.
- Multipart (P2): server-authoritative parts plan with per-part BLAKE3 subtree
  hashes persisted in the shared DB and combined at complete.
- Signed URLs: Ed25519, stateless, AND-combined constraints (exp, ip, token-claim
  predicates) + baked response headers; single static keypair in P1.
- DB: files slimmed to identity + content_id + meta_version; new P1 file_versions
  table (immutable versions); P2 hash widening + P3 encryption move to file_versions.

PRD/DESIGN/api.md/migration.sql updated; ADR-0002 references repointed to the
sidecar. cpt validate: 0 errors, code coverage 104/104.

Signed-off-by: Roland From <rfedorov@linkentools.com>
…d examples

Builds on the sidecar redesign:

- ADR-0004 (signed-URL field & signature transport): discrete X-FS-* query
  params chosen (bare shareable URL, S3 SigV4 query form); headers = deferred
  opt-in; token blobs rejected. Includes an option-comparison matrix.
- Signed-URL constraint model: exp mandatory + capped by max_url_ttl (~7d,
  enforced at signing); optional ip, token-claim predicates, and upload-only
  MaxSize/ExactSize (mutually exclusive) + ExpectedHash; MaxRate/MaxConns
  declared P2 (per-(file_id,op), cross-instance coordination deferred). op is
  signed and method-checked.
- Trimmed redundant URL params: file_id is the path; backend_id resolved from
  the version row; no iat (cap enforced at signing); no key id in P1 (single
  static key; X-FS-KeyId returns in P2 with rotation).
- Data plane holds no backend/tenant/user quota or policy — all such limits
  live in the control plane (P2) and are applied at presign; the sidecar only
  enforces per-URL signed constraints (+ per-URL conn/rate in P2).
- Worked examples (DESIGN 4.6/4.7): LMS image upload+display end-to-end with
  example URLs and a per-field breakdown; multipart upload + resume after
  logout (non-contiguous missing parts; session expires_at vs per-part URL exp;
  reaped-session branch). Sidecar domain placement (cookies vs short-lived
  URLs) and signing locality/cost notes added.
- Fix: removed ';' from mermaid sequence message/note text (mermaid treats it
  as a statement separator).

cpt validate: 0 errors, code coverage 104/104.

Signed-off-by: Roland From <rfedorov@linkentools.com>
…(query + header)

Address @Artifizer review on signed-URL transport:

- Keep discrete SigV4-style X-FS-* fields with one composite Ed25519 signature;
  reject the opaque token (PASETO). Opacity gives no security (fields aren't
  secrets; the signature is in the URL regardless), loses edge observability
  (CDN/WAF/logs can't read exp/scope without decoding), is non-standard, and the
  'single signature' it advertises is already provided by composite signing.
- Adopt a dual envelope chosen by access intent: query params (bare, embeddable
  URL) + the same fields/signature carried as request headers (programmatic /
  SDK / batch: credential out of the URL, stable cacheable URL). Supersedes the
  earlier 'query-only, header deferred'.
- Remove the contradictory 'debuggability via visible URL' pro; debuggability is
  sanitized server-side structured logging (never signature/exp).
- S3 SigV4 cited only as evidence the shape (discrete fields, one signature,
  dual envelope) is sound; NOT S3 wire-compatible.
- Reworked options (A-D), pros/cons, and the option-comparison table.

Branch rebased onto latest main.

Signed-off-by: Roland From <rfedorov@linkentools.com>
Team decision (reverses the discrete-fields ADR-0004): the signed-URL
credential is a single opaque PASETO v4.public token (Ed25519, asymmetric, not
JWT), carried in the query param 'fs-token' (?fs-token=<token>, embeddable) or
the 'X-FS-Token' header (programmatic/batch). The token is NEVER carried in
Authorization, which always holds the platform JWT.

Rationale: we are not S3-wire-compatible, so discrete-field readability buys
nothing externally and only couples intermediaries to our layout; an atomic,
opaque token is simpler and lets the format evolve freely.

Token Opacity Contract: the token's claim-set and crypto are private to control
plane + sidecar; every other participant (browser, CDN, proxy, app, logs, SDK)
MUST treat it as opaque bytes and never parse it — the format can and will
change. kid lives in the PASETO footer (P2 rotation).

- ADR-0004 fully reworked (opaque token; dual envelope; option matrix).
- DESIGN: §4.5 token spec + claims, §4.6/§4.7 examples (?fs-token=<token>),
  domain SignedUrl, components, principle, driver table, mermaid.
- PRD: fr-signed-urls, glossary, sidecar interface, acceptance criteria.
- api.md: Signed URLs section, status codes.
- migration.sql unchanged (signing is stateless; nothing in the DB).

Rebased onto latest main (origin/main).

Signed-off-by: Roland From <rfedorov@linkentools.com>
Per PR review (@Artifizer): superseded ADRs add confusion for humans/LLMs and
the history is available via git log. ADR-0001 (proxy-all monolith) is superseded
by ADR-0003; deleted the file and scrubbed all references — ADR-0003/0002/DESIGN
no longer link it, and the dangling cpt-id is removed. ADR-0003 now reads
self-contained (the prior proxy-all design is described in-place).

Signed-off-by: Roland From <rfedorov@linkentools.com>
Address PR review:

- CodeRabbit (api.md, PRD): on a 412 bind conflict the client re-binds the
  returned version_id via the control endpoint POST /files/{id}/bind, which is
  independent of the signed upload URL (its exp is irrelevant; no re-presign, no
  re-upload). The 412 carries the current content ETag for the fresh If-Match.
  Re-presigning is not idempotent (creates a new sibling version, swept by the
  P2 cleanup engine).
- @pr0tey (quota): record that the token's max_size is OPTIONAL — if present the
  data plane MUST enforce it (mid-stream 413), if absent only the backend's own
  default limits apply. The per-URL max_size baked from a quota decision is the
  one quota-derived value the data plane enforces (still a signed number, not a
  policy lookup). Reserve-at-presign/commit-at-bind/release-on-expiry deferred to
  the P2 fr-storage-quota FEATURE.

Signed-off-by: Roland From <rfedorov@linkentools.com>
Bring the review-resolved documentation onto the PR branch:
- DESIGN.md: auth model (delegated authz), sidecar contract, short-default
  + ceiling URL TTL, P1 implementation notes
- api.md: sidecar platform exception, Ed25519 codec + FIPS note,
  default/max URL TTL, stale-permission behavior
- migration.sql: flat column names, version_id PK, atomic-bind invariant
- ADR-0003/0004, PRD.md: aligned with the above
- PLAN-P1.md: P1 coding plan

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Roland From <rfedorov@linkentools.com>
…ncy constraint

Address review: the concern is not a deferred fallback but dependency
selection — do not pull in any crate that hard-wires a non-FIPS algorithm
we cannot swap.

- ADR-0004 'FIPS posture': reworked into a binding rule — sign/verify sits
  behind an in-house SignatureProvider abstraction; MUST NOT add a crate
  that statically links a non-replaceable non-FIPS implementation; FIPS
  deployments back the provider with a validated module
  (rustls-corecrypto-provider) or swap to ECDSA P-256 without codec changes;
  candidate PASETO crate evaluated for replaceability before adoption.
- DESIGN.md + api.md: synchronized short form of the same rule.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Roland From <rfedorov@linkentools.com>
@Artifizer
Artifizer merged commit f89d22d into constructorfabric:main Jun 28, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants