You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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.
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:
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).
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.
Problem
The spec's attachment-delete contract (spec §Attachments, "Delete"):
The second half of that promise is unimplementable: a fileId stored in record content is just a
stringfield. The schema vocabulary hasrecord-reffor typed record references (spec §Types) but nofile-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 theattachmentFileIdfilter — association references only.Consequences:
Decided direction
Add
file-refas a scalar field kind, mirroring the existingrecord-refprecedent:FieldDefgainskind: '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).record-ref, and upload-before-associate flows make strictness hostile).file-refvalues, and theattachmentFileIdquery filter matches records that reference the file either by attachment association or by anyfile-reffield. 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);MemoryAdapterimplements it for real per Core code treats one query() page as the complete result set (grants, attachment metadata, uploader checks) #50's fidelity work.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 throughattachmentFileIdsemantics.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) getsfile-refas exact-match only.stringfields keep working but get no reference semantics (no delete protection, no access conveyance, no GC protection) —file-refis what makes a reference a reference.Work items
FieldDef/ScalarFieldKind+ validation (packages/core/src/types.ts,validate.ts)attachmentFileIdcovers both)MemoryAdapter(Core code treats one query() page as the complete result set (grants, attachment metadata, uploader checks) #50)isCompatible()table entry (isCompatible() is too weak to be the official cross-app mechanism #47 makes it (optional-field hole, text vs string) #54)file-ref; access conveyed by readable record withfile-ref; plain-string fileId conveys nothingRefs #51 (access predicate), #46 (indexing home), #50 (MemoryAdapter fidelity), #54 (kind table), #57 (additive in-place policy). Blocks the blob-GC issue (filed next).