Repository navigation
Commit e5899a6
Fixes #22698
Clause-②: no
Wording only. No key, type, schema shape or runtime code changes.
Dispatched by the PM loop (round 1, `domain:spec` seat 1), session
`session_01S3aAf11JjbW1mSGL1EhfFj`, branch
`claude/issue-22698-file-delegate-metadata-text`.
## What changes
PR #22697 (#22637) made a record read follow the download door's
field-owned verdict when the reader's own `sys_file` read is refused.
The texts an author reads about `fileAccessDelegate` and the refused
marker still described the download door only. This PR names the
record-read verdict beside the download verdict:
| Text | Before | After |
| :--- | :--- | :--- |
| `fileAccessDelegate` `.describe()` (`object.zod.ts`), which feeds the
JSON Schema and three reference pages | "authorizes downloads of files
owned by this object's media fields, instead of testing ..." |
"authorizes downloads of files owned by this object's media fields,
**and decides whether a reader who may not read sys_file sees their
name, size and type in a record read of this object (asked once per
owning record on each such read)**, instead of testing ..." |
| `fileAccessDelegate` form `helpText` (`object.form.ts`), plus its `en`
/ `zh-CN` / `ja-JP` / `es-ES` bundle values | same sentence, no example
| the same addition |
| `fileAccessDelegate` TSDoc (`object.zod.ts`) | "may this caller
download ...", "denies the download" | the two paths that ask the
delegate: download and record read. On each path it states when the
delegate is asked and what a refused reader receives. Fails closed on
both. |
| `FileRefusedValueSchema` TSDoc (`field-value.zod.ts`) | "Nothing else
is served, because nothing else was read" | names the owned-file case:
the system-context row read and the field-owned verdict. For a refused
reader the marker means the record does not own the file, the verdict
refused the owning record, or the verdict could not be asked. |
| `metadataRefused` `.describe()` (`field-value.zod.ts`), which feeds
`references/data/field-value.mdx` | "(no read on sys_file)" | "(no read
on sys_file, and the record being read does not own the file or its
download verdict did not allow it)" |
| `IFileAccessDelegate` docblock and `authorizeFileRead` doc
(`packages/spec/src/contracts/storage-service.ts`), the implementer's
text (patch round 1) | "May this caller download a file owned by
`recordId`", "widens who can reach the bytes" | one verdict answers two
questions: the download, and whether a reader refused `sys_file` sees
the file's name, size and type in a record read (asked once per owning
record per such read); the data-leak warning covers the metadata too;
the fail-closed sentence is kept |
## Each sentence, read off the code at `d8830c2805`
- **When the delegate is asked on a record read.**
`ObjectQL.resolveFileReferences` (`packages/objectql/src/engine.ts`)
calls `readParentDerivedFiles` only in the `isReadRefusal(error)` arm,
which is the caller's own `sys_file` read answering `PERMISSION_DENIED`.
A caller whose `sys_file` read succeeds hydrates from that read and
never reaches the verdict. That is the source of "a reader who may read
`sys_file` never reaches the delegate on this path".
- **What is put to it, and how often.** `readParentDerivedFiles` reads
`sys_file` under `{ ...caller, isSystem: true }`, where `id $in` is the
ids the result holds, `ref_object` is this object and `ref_id $in` is
the result's record ids. It then calls the registered authorizer once
with the distinct owner ids. The authorizer is `readableFieldOwners`
(`packages/services/service-storage/src/storage-service-plugin.ts`).
When the owner object declares `fileAccessDelegate`, it calls
`delegate.authorizeFileRead(ownerId, authz)` once for each owner id.
That gives "once per owning record on each such read".
- **What a refused reader receives.** An allowed row goes through
`servableFileRows` and `toFileValue`, the same renderer the caller's own
read uses, so the reader gets `{ id, name, size, mimeType, url }`, or
the bare id for an unservable row, exactly as a `sys_file` reader would.
Every other id gets `{ id, metadataRefused: true }`: one not owned by
the record it sits on, one whose owner the verdict refused, and every id
when the verdict is unwired or fails.
- **Download path.** In `buildFileReadAuthorizer`, the uploader is
allowed before the field-owned arm. The field-owned arm then calls the
same `readableFieldOwners` with the one `ref_id`. That gives "unless the
caller uploaded the file".
- **Fails closed.** In `readableFieldOwners`, a `getService` throw, a
missing delegate, a delegate with no `authorizeFileRead`, a falsy answer
and a throw all leave that owner out of the readable set. The door then
answers `deny`, and the record read keeps the marker.
## Regenerated artifacts (generator output only, no hand edits)
- `pnpm --filter @objectstack/spec build` (runs `gen:schema`), then
`pnpm --filter @objectstack/spec check:generated` named exactly one
stale artifact, `content/docs/references/**`. `pnpm --filter
@objectstack/spec gen:docs` moved four pages: `api/metadata.mdx`,
`data/object.mdx`, `system/migration.mdx` (two rows) and
`data/field-value.mdx`.
- `node scripts/check-i18n-bundles.mjs --write
--filter=platform-objects` rewrote `en.metadata-forms.generated.ts`, one
line.
## A producer outside `packages/spec`: the three translated help texts
Studio renders the `fileAccessDelegate` help text in the author's
locale. The `zh-CN`, `ja-JP` and `es-ES` values are hand-written
translations, and `--write` keeps them in merge mode. Without an edit,
those three locales would still say "downloads" only. No gate catches
this: none of the three leaves has a source-hash record, so it counts as
legacy-trusted. So the three values are updated by hand to say the same
as `en`, and the changeset also carries `@objectstack/platform-objects:
patch`, because the bundles ship in its `dist`. `check:i18n` and
`check:i18n-stale-fill` are green after the edit.
## Verification, at HEAD `c5513532f2` (round 1) and `ed75b6fea1` (patch
round 1)
- **Patch round 1, at `ed75b6fea1`** (adds `storage-service.ts` and one
changeset bullet; TSDoc only): `pnpm --filter @objectstack/spec run
typecheck` exit 0; the spec `local` test project `Test Files 642 passed
(642)`, `Tests 19164 passed | 1 todo (19165)`; `dispatch-gates --ran`
"106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN"; `check:generated` "All
15 generated artifacts are up to date". The bullets below are round 1's,
at `c5513532f2`.
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2` (under `os-verify-lock`): `Test Files 642 passed (642)`,
`Tests 19164 passed | 1 todo (19165)`.
- `pnpm --filter @objectstack/platform-objects exec vitest run
--maxWorkers=2`: `Test Files 69 passed (69)`, `Tests 1082 passed
(1082)`.
- `pnpm --filter @objectstack/spec run typecheck` exit 0. `pnpm --filter
@objectstack/platform-objects run typecheck` exit 0.
- `pnpm --filter @objectstack/spec check:generated`: "All 15 generated
artifacts are up to date". `pnpm check:i18n`: "OK (9 package(s) — all
bundles in sync, no undeclared authoring keys)". `pnpm check:nul-bytes`:
OK.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, derived from this diff, gives 105 commands.
All 105 ran, and the exit codes were recorded before reading. `--ran`
reconciliation: "105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN". Two
gates first answered exit 3, PREREQUISITE NOT MET, because packages were
unbuilt in this worktree: `check:skill-examples` needed `client-react`'s
dist and `check:dual-build-cjs-loads` needed nine dists. After building
those packages, both re-ran with exit 0.
- Lint, narrowed and proven: ESLint ran on the 7 changed TypeScript
files with `--no-inline-config --format json`: 7 files, 0 errors, 0
warnings. ① Population: ESLint's own `isPathIgnored` reads `false` for
all 7. ② Count: from the JSON output. ③ Invariance: `eslint.config.mjs`
enables no type-aware linting (no `parserOptions.project`), so this diff
cannot change a verdict on an untouched file. The repo-wide `pnpm lint`
is CI's.
- Not applicable: reverse verification and ablation. No type, schema or
behaviour changed, so there is no assertion to flip.
## Acceptance notes
- **`IFileAccessDelegate` TSDoc**
(`packages/spec/src/contracts/storage-service.ts`): reported to the seat
in round 1, and resolved in this PR by patch round 1 (seat order on
#22698), since this card is the family's close-out.
- **Liveness ledger row** `packages/spec/liveness/object.json`
`fileAccessDelegate`: the `note` names the download question only, and
`evidence` cites only `buildFileReadAuthorizer`. Status `live` is still
right. This is an internal ledger, not author-facing.
- `origin/main` moved to `9f159397fb` while this ran. `git diff
--name-only d8830c2 origin/main` touches none of these 12 paths, so
`main` was not merged in. The queue rebuilds on the merge ref.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 762db99 commit e5899a6
13 files changed
Lines changed: 83 additions & 25 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- system
- packages
- platform-objects/src/apps/translations
- spec/src
- contracts
- data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
955 | 955 | | |
956 | 956 | | |
957 | 957 | | |
958 | | - | |
| 958 | + | |
959 | 959 | | |
960 | 960 | | |
961 | 961 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
146 | | - | |
| 146 | + | |
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
| 162 | + | |
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
339 | 339 | | |
340 | 340 | | |
341 | 341 | | |
342 | | - | |
| 342 | + | |
343 | 343 | | |
344 | 344 | | |
345 | 345 | | |
| |||
630 | 630 | | |
631 | 631 | | |
632 | 632 | | |
633 | | - | |
| 633 | + | |
634 | 634 | | |
635 | 635 | | |
636 | 636 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
423 | 423 | | |
424 | 424 | | |
425 | 425 | | |
426 | | - | |
| 426 | + | |
427 | 427 | | |
428 | 428 | | |
429 | 429 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
423 | 423 | | |
424 | 424 | | |
425 | 425 | | |
426 | | - | |
| 426 | + | |
427 | 427 | | |
428 | 428 | | |
429 | 429 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
423 | 423 | | |
424 | 424 | | |
425 | 425 | | |
426 | | - | |
| 426 | + | |
427 | 427 | | |
428 | 428 | | |
429 | 429 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
423 | 423 | | |
424 | 424 | | |
425 | 425 | | |
426 | | - | |
| 426 | + | |
427 | 427 | | |
428 | 428 | | |
429 | 429 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
407 | 407 | | |
408 | 408 | | |
409 | 409 | | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
410 | 421 | | |
411 | 422 | | |
412 | | - | |
| 423 | + | |
| 424 | + | |
413 | 425 | | |
414 | 426 | | |
415 | 427 | | |
416 | | - | |
417 | | - | |
418 | | - | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
419 | 434 | | |
420 | 435 | | |
421 | 436 | | |
0 commit comments