Skip to content

Commit e5899a6

Browse files
docs(spec): fileAccessDelegate and the refused file marker name the record-read verdict beside the download (#22698) (#22713)
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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
`fileAccessDelegate` and the refused file marker now describe what the delegate decides on a record read, beside the download
7+
8+
Clause-②: no
9+
10+
Wording only: no key, type, schema shape or runtime behaviour changes. A record read now follows the download door's field-owned verdict for a file the record owns, so the texts an author reads understated what a declared delegate decides.
11+
12+
- **`fileAccessDelegate`.** This covers the object schema's description and TSDoc, the object form's help text in `en`, `zh-CN`, `ja-JP` and `es-ES`, and the generated reference pages. The named service authorizes downloads of files owned by the object's media fields. It also decides whether a reader who may not read `sys_file` sees those files' name, size and type in a record read of the object. It is asked once per owning record on each such read, so an implementation runs on reads, not only on downloads. A reader who may read `sys_file` never reaches it on a record read. It fails closed on both paths.
13+
- **`FileRefusedValueSchema` (`{ id, metadataRefused: true }`).** The TSDoc and the `metadataRefused` description now give two cases for a reader who may not read `sys_file`: the record being read does not own the file, or the download verdict on the owning record did not allow it. A file the record owns and the verdict allows is served as the full file object.
14+
- **`IFileAccessDelegate`.** The interface's TSDoc and its `authorizeFileRead` doc now name both questions one verdict answers: 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 warning now covers metadata too: a permissive implementation leaks the metadata as well as the bytes.
15+
16+
**For authors.** Nothing to change in metadata. An existing delegate now also decides a refused reader's file metadata on every read. It should keep applying the same rule its service uses to read the record.

‎content/docs/references/api/metadata.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -955,7 +955,7 @@ Metadata query with filtering, sorting, and pagination
955955
| **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). |
956956
| **requiredPermissions** | `string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }` | optional | [ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. |
957957
| **lifecycle** | `{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }` | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. |
958-
| **fileAccessDelegate** | `string` | optional | Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. |
958+
| **fileAccessDelegate** | `string` | optional | Kernel service that 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 whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. |
959959
| **validations** | `any[]` | optional | Object-level validation rules |
960960
| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
961961
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |

‎content/docs/references/data/field-value.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Type: `string`
143143
| Property | Type | Required | Description |
144144
| :--- | :--- | :--- | :--- |
145145
| **id** | `string` | ✅ | The sys_file id the record holds |
146-
| **metadataRefused** | `true` | ✅ | The reader was refused the file's metadata (no read on sys_file): name, size and type are withheld. Distinct from an empty field, which holds no file at all. |
146+
| **metadataRefused** | `true` | ✅ | The reader was refused the file's metadata (no read on sys_file, and the record being read does not own the file or its download verdict did not allow it): name, size and type are withheld. Distinct from an empty field, which holds no file at all. |
147147

148148

149149
---

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ const result = ApiMethod.parse(data);
159159
| **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). |
160160
| **requiredPermissions** | `string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }` | optional | [ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. |
161161
| **lifecycle** | `{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }` | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. |
162-
| **fileAccessDelegate** | `string` | optional | Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. |
162+
| **fileAccessDelegate** | `string` | optional | Kernel service that 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 whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. |
163163
| **validations** | `any[]` | optional | Object-level validation rules |
164164
| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
165165
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |

‎content/docs/references/system/migration.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ Create a new object
339339
| **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). |
340340
| **requiredPermissions** | `string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }` | optional | [ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. |
341341
| **lifecycle** | `{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }` | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. |
342-
| **fileAccessDelegate** | `string` | optional | Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. |
342+
| **fileAccessDelegate** | `string` | optional | Kernel service that 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 whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. |
343343
| **validations** | `any[]` | optional | Object-level validation rules |
344344
| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
345345
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
@@ -630,7 +630,7 @@ Create a new object
630630
| **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). |
631631
| **requiredPermissions** | `string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }` | optional | [ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. |
632632
| **lifecycle** | `{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }` | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. |
633-
| **fileAccessDelegate** | `string` | optional | Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. |
633+
| **fileAccessDelegate** | `string` | optional | Kernel service that 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 whether the caller can read the owning row. For objects whose access is mediated by a service (e.g. sys_approval_action → approvals). Fails closed. |
634634
| **validations** | `any[]` | optional | Object-level validation rules |
635635
| **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
636636
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
423423
},
424424
fileAccessDelegate: {
425425
label: "File Access Delegate",
426-
helpText: "Kernel service that authorizes downloads of files owned by this object's media fields, instead of testing whether the caller can read the owning row. For objects whose access is mediated by a service. Fails closed."
426+
helpText: "Kernel service that 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 whether the caller can read the owning row. For objects whose access is mediated by a service. Fails closed."
427427
},
428428
lifecycle: {
429429
label: "Lifecycle",

‎packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
423423
},
424424
fileAccessDelegate: {
425425
label: "Delegado de acceso a archivos",
426-
helpText: "Servicio del kernel que autoriza la descarga de archivos pertenecientes a los campos multimedia de este objeto, en lugar de comprobar si quien llama puede leer la fila propietaria. Para objetos cuyo acceso media un servicio. Falla cerrado."
426+
helpText: "Servicio del kernel que autoriza la descarga de archivos pertenecientes a los campos multimedia de este objeto y decide si un lector que no puede leer sys_file ve su nombre, tamaño y tipo al leer un registro de este objeto (se consulta una vez por registro propietario en cada lectura de ese tipo), en lugar de comprobar si quien llama puede leer la fila propietaria. Para objetos cuyo acceso media un servicio. Falla cerrado."
427427
},
428428
lifecycle: {
429429
label: "Ciclo de vida de los datos",

‎packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
423423
},
424424
fileAccessDelegate: {
425425
label: "ファイルアクセス委譲サービス",
426-
helpText: "このオブジェクトのメディア項目が持つファイルのダウンロードを認可するカーネルサービス。指定すると「呼び出し元がそのレコードを読めるか」では判定しなくなります。アクセスがサービスによって仲介されるオブジェクト向けで、判定できない場合は拒否します。"
426+
helpText: "このオブジェクトのメディア項目が持つファイルのダウンロードを認可し、sys_file を読めない読者がこのオブジェクトのレコードを読んだときにそのファイルの名前・サイズ・種類を見られるかも判定するカーネルサービス(そうした読み取りのたびに、ファイルを持つレコードごとに一度問い合わせます)。指定すると「呼び出し元がそのレコードを読めるか」では判定しなくなります。アクセスがサービスによって仲介されるオブジェクト向けで、判定できない場合は拒否します。"
427427
},
428428
lifecycle: {
429429
label: "データライフサイクル",

‎packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
423423
},
424424
fileAccessDelegate: {
425425
label: "文件访问委托服务",
426-
helpText: "授权下载该对象媒体字段所属文件的内核服务;有了它就不再以“调用方能否读取宿主记录”来判断。适用于访问权由某个服务裁决的对象。失败即拒绝。"
426+
helpText: "授权下载该对象媒体字段所属文件的内核服务,并决定无权读取 sys_file 的读者在读取该对象记录时能否看到这些文件的名称、大小和类型(每次此类读取,对每条所属记录询问一次);有了它就不再以“调用方能否读取宿主记录”来判断。适用于访问权由某个服务裁决的对象。失败即拒绝。"
427427
},
428428
lifecycle: {
429429
label: "数据生命周期",

‎packages/spec/src/contracts/storage-service.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,15 +407,30 @@ export interface IStorageService {
407407
* The approvals service already knows who may see a request's history, so it
408408
* answers instead.
409409
*
410+
* One verdict answers two questions about a file owned by a record of that
411+
* object (the file's `ref_object` / `ref_id`):
412+
*
413+
* - **Download.** May this caller download the file? The download door asks
414+
* for the owning record, unless the caller uploaded the file.
415+
* - **Metadata in a record read.** When a reader's own `sys_file` read is
416+
* refused, may they see the file's name, size and type where a record read
417+
* of that object returns it? Asked once per owning record on every such
418+
* read, so an implementation runs on reads, not only on downloads. A denial
419+
* leaves the field as the refused marker `{ id, metadataRefused: true }`.
420+
*
410421
* Implementations should apply the SAME rule that governs reading the record
411422
* through their own API — not a looser one. The delegate widens who can reach
412-
* the bytes, so a permissive implementation is a data leak.
423+
* the bytes and the file's metadata, so a permissive implementation is a data
424+
* leak of both.
413425
*/
414426
export interface IFileAccessDelegate {
415427
/**
416-
* May this caller download a file owned by `recordId` on the delegating
417-
* object? Return `false` (never throw) to deny; a throw is treated as a
418-
* denial too, since authorization must fail closed.
428+
* May this caller read the files owned by `recordId` on the delegating
429+
* object — download them, and, when the caller's own `sys_file` read is
430+
* refused, see their name, size and type in a record read? Called by the
431+
* download door, and once per owning record on each such record read, with
432+
* the caller's execution context. Return `false` (never throw) to deny; a
433+
* throw is treated as a denial too, since authorization must fail closed.
419434
*/
420435
authorizeFileRead(recordId: string, context: unknown): Promise<boolean>;
421436
}

0 commit comments

Comments
 (0)