Skip to content

file-ref field kind: content-held file references are invisible to the reference checks the spec says exist #63

Description

@cuibonobo

Problem

The spec's attachment-delete contract (spec §Attachments, "Delete"):

Returns 409 Conflict if any record in the stack still references the file (i.e. has it in an attachment association or its content references the fileId).

The second half of that promise is unimplementable: a fileId stored in record content is just a string field. The schema vocabulary has record-ref for typed record references (spec §Types) but no file-ref, so neither core nor any adapter can distinguish "content that references a file" from "content that happens to contain 64 hex characters." Stack.deleteAttachment()'s reference check (packages/core/src/stack.ts:625) queries the attachmentFileId filter — association references only.

Consequences:

  1. Dangling references on delete. A record holding a fileId in content doesn't count at delete time → bytes removed, content now points at nothing, and the spec's 409 promise silently didn't apply.
  2. False denials in the access predicate. Attachment access is governed by referencing records (spec: "if any Record referencing a fileId is accessible to the requester, the attachment is accessible" — and ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51 builds its shared predicate on the same rule). Content-held references are invisible there too, so a file referenced only from content is unreachable by non-owners who can read the referencing record.
  3. Blob GC inherits the blind spot. The garbage collector (companion issue, filed next) decides liveness by "is there a referencing record" — built before this lands, it would delete content-referenced files.

Decided direction

Add file-ref as a scalar field kind, mirroring the existing record-ref precedent:

  1. FieldDef gains kind: 'file-ref' (core types + spec §Types). Additive vocabulary change — in place, no version churn, per the standing policy (_grant records store the grantee in entityId, the authorship field — move it into GrantContent #57).
  2. Validation: values must be well-formed fileIds (SHA-256 hex). Referential existence is not validated at write time (same stance as record-ref, and upload-before-associate flows make strictness hostile).
  3. Adapters index file-ref values, and the attachmentFileId query filter matches records that reference the file either by attachment association or by any file-ref field. This lands naturally in Split SQLite support: native record-adapter-sqlite for Node, browser-only record-adapter-sqljs, and a StackTokenStore interface in core #46's shared SQL layer (where-builder + an index table); MemoryAdapter implements it for real per Core code treats one query() page as the complete result set (grants, attachment metadata, uploader checks) #50's fidelity work.
  4. deleteAttachment's reference check and ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51's access predicate then get content-reference coverage for free — both already go through attachmentFileId semantics.
  5. isCompatible()'s kind table (isCompatible() is too weak to be the official cross-app mechanism #47 makes it (optional-field hole, text vs string) #54) gets file-ref as exact-match only.
  6. Documentation note: apps storing fileIds in plain string fields keep working but get no reference semantics (no delete protection, no access conveyance, no GC protection) — file-ref is what makes a reference a reference.

Work items

Refs #51 (access predicate), #46 (indexing home), #50 (MemoryAdapter fidelity), #54 (kind table), #57 (additive in-place policy). Blocks the blob-GC issue (filed next).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions