Skip to content

Commit 5e725b8

Browse files
feat(service-storage): the file-attachments service, a server-side verb that stores bytes and attaches them to a record (#23386)
Fixes #23306 Clause-②: yes A server-side producer (inbound mail first, then imports and generated output) can now store bytes and attach them to a record. `StorageServicePlugin` registers the kernel service `file-attachments` (`FILE_ATTACHMENT_SERVICE`), declared in `@objectstack/spec/contracts` as `IFileAttachmentService`. Its one verb, `attachFile({ parentObject, parentId, organizationId, filename, mimeType, data })`, writes the bytes, one `sys_file` row and one `sys_attachment` row linking it to the parent, and answers `{ fileId, attachmentId, size }`. The widening is additive: four new exports from `@objectstack/spec/contracts` (`IFileAttachmentService`, `AttachFileInput`, `AttachedFile`, `FILE_ATTACHMENT_SERVICE`) and one more service in `StorageServicePlugin.providesServices`. Nothing is removed or renamed. ## One writer: each check, and where it comes from The triage direction is "one writer, no second copy". Each check the verb runs is the code every other attachment of the parent already meets. Measured on a real `ObjectQL` over sqlite `:memory:` with the plugin's own hooks and `plugin-audit`'s real writers installed: | check | where it comes from | under the verb's system context | | --- | --- | --- | | upload size limit | `uploadSizeRefusal` in `storage-limits.ts`: the verdict the REST upload doors ask, against the same `resolveStorageLimits` over the `storage` service's Limits snapshot | runs, `413 PAYLOAD_TOO_LARGE`, before anything is stored | | the `sys_file` row and its organization stamp | `StorageMetadataStore.createFile` / `updateFile` / `deleteFile`, the upload doors' writer, through the same `StorageWriteContext` channel | runs | | `enable.files` opt-in | `plugin-audit`'s `enforceFilesCapability`, a `beforeInsert` hook on `sys_attachment` | runs: `403 FILES_DISABLED` (measured, and pinned with the real gate) | | parent EDIT (`installAttachmentAccessHooks`) | the `sys_attachment` `beforeInsert` hook in this package | returns at its system-context line by its own declared design; `checkEdit` was called 0 times (measured). A server-side producer has no caller whose edit access could be asked. | | attachment lifecycle | `installAttachmentLifecycleHooks` `afterInsert` | runs (a pending file is not a tombstone, so it changes nothing) | | the parent's organization | the parent read by id as the system; its organization from `createWallOrganizationResolver`, the package's one answer to "which column is this record walled by" | a parent walled into another organization than `organizationId` is `404 RECORD_NOT_FOUND`, the same answer as a missing parent | Both rows are written in the parent's organization when it has one, otherwise in the caller's. `organizationId` is required and nullable: acting in no organization is stated, never a left-out field. ### Mechanism hypotheses, measured - **H1 holds, with one correction.** The upload door's "attach" is a data-API insert of `sys_attachment` judged by `beforeInsert` hooks, so the one writer is an engine insert, not a function in `storage-routes.ts`. Under the system context the parent-EDIT gate does not run (above). The three refusals the card names do run. - **H2 holds, and "quota" is the per-upload limit.** No per-organization aggregate storage quota exists in open code (`git grep -i` for a storage quota concept: 0 hits; the only `quota` code is SMS, lifecycle row counts and AI). The byte-acceptance limit is `max_upload_mb`. It was enforced by `requireWithinUploadLimit`, a closure inside `registerStorageFamily` (`storage-routes.ts`) with the refusal constants beside it. **The extraction is a move:** the status, the code, the sentence and the comparison now live in `storage-limits.ts` (`uploadSizeRefusal`, `UPLOAD_TOO_LARGE_STATUS`, `UPLOAD_TOO_LARGE_CODE`), beside the limit; the doors call it, and so does the verb. The doors answer byte-for-byte as before (their 18 limit pins stay green). `main` was merged before this PR opened (PR #23346 landed in the same file; its hunks and these are disjoint). - **H3: a sibling contract, not a member of `IStorageService`.** `IStorageService` is the blob store an adapter implements (`LocalStorageAdapter`, `S3StorageAdapter`, third parties), and the `storage` slot's occupant can be a bare adapter with no data engine. A row-writing verb there would be an optional member most implementors could never honour. The sibling follows the existing non-core slot convention (`SEED_SETTLEMENT_SERVICE`, `SHARE_LINK_SERVICE`): an interface plus a slot constant, in `storage-service.ts` beside `IFileAccessDelegate`. - **H4: no refusal leaves a committed `sys_file`, by write order.** The order is the presigned door's: the `sys_file` row first, `pending`, then the bytes, so the row is always the pointer the pending-upload retention and the reap guard reclaim the bytes by, even if the process dies mid-call. The row turns `committed` only in the same `engine.transaction` as the `sys_attachment` insert. A refused call then removes the bytes and the pending row before it rethrows. On an engine with no transaction, or inside a caller's transaction the verb joined, the commit never runs after a refused insert, and the verb removes an attachment row it did insert. `attachment-refused-attach-tombstone.test.ts` was read first: its tombstoner is for a user's committed upload; this verb never leaves one, so it does not need it. The verb is armed at `kernel:bootstrapped`, after every plugin's `kernel:ready` handler has installed its gates (`AuditPlugin` installs the `enable.files` gate from its own `kernel:ready`). Before that, or with no data engine, it refuses `503 SERVICE_UNAVAILABLE` rather than write a row a gate has not judged. ## Tests `packages/services/service-storage/src/file-attachment-service.test.ts`, 17 tests, through the plugin's own wiring: `init`, `start`, `kernel:ready`, `kernel:bootstrapped`, the service resolved by `FILE_ATTACHMENT_SERVICE`, a settings namespace carrying `max_upload_mb`, the plugin's local adapter in a temp directory, and `plugin-audit`'s real gate. `@objectstack/plugin-audit` is a new devDependency, aliased to source in `vitest.config.ts` like `core` and `driver-sql`. - Success: one committed `sys_file` (attachments scope, private, key `attachments/` plus the file id plus the extension, bytes behind it) and one `sys_attachment`, both in the parent's organization. - The three refusals the card lists, each asserting `code` and `status` and that NOTHING is left (no `sys_file` committed or pending, no `sys_attachment`, no stored key), each beside its control (the same call without the refused condition succeeds): - over the limit: `413 PAYLOAD_TOO_LARGE`; control: exactly at the limit; - `enable.files` off: `403 FILES_DISABLED`; control: an opted-in object; - parent in another organization: `404 RECORD_NOT_FOUND`; control: the same parent from its own organization. A missing parent answers the same. - Organization stamping: `null` takes the parent's; a global parent takes the caller's. - A refused commit after the insert rolls the attachment back; on an engine with no transaction the verb removes the row itself; an upload failure removes the pending row and propagates unchanged. - `503` before `kernel:bootstrapped` and with no engine; `400 VALIDATION_ERROR` for seven malformed inputs. `packages/spec/src/contracts/storage-service.test.ts`: the slot name and the contract's shape. ### Ablations (one-off, each through `scripts/ablation-replace.mjs` in WRAP mode) Every leg: the anchor hit once and moved 1 to 0, the blob changed on disk, and the restore was proven (blob equals HEAD, `git diff HEAD` empty). The suite imports the mutated modules from source (relative imports, and `plugin-audit` through its source alias), so no rebuild sits between mutation and measurement. Run at `00e7f3217`; the five mutated files are byte-identical at the final head `ac4fcfdaf` (`git diff` empty across the merge). | leg | mutation | prediction | observed | | --- | --- | --- | --- | | A1 | the verb ignores the size verdict | 413 pin red | 1 red (413 pin) | | A2 | a refused call cleans nothing up | the leave-nothing pins red | 4 red (FILES_DISABLED, commit rollback, no-transaction, upload failure) | | A2b | the row is written `committed` up front | green: the cleanup still removes it | 17 green, as predicted. The crash-window half of pending-first (process death mid-call) is not pinned. | | A3 | no organization wall | 404 pin red | 1 red | | A4 | no transaction, claimed owned | rollback pins red | 2 red | | A5 | no compensation outside an owned transaction | no-transaction pin red | 1 red | | A6 | armed at `kernel:ready` | the 503-until-bootstrapped pin red | 1 red | | A7 | `organizationId` not validated | validation pin red | 1 red | | A8 | the shared `uploadSizeRefusal` admits everything | the verb's pin AND the doors' pins red | verb 1 red; `storage-limits.test.ts` 12 red: one verdict for both | | A10 | `plugin-audit`'s `enable.files` gate never refuses | FILES_DISABLED pin red | 1 red: the verb does not bypass the gate | The first attempts at A2 and A6 were refused by the tool as no-ops (the replacement contained the anchor, so the anchor count did not drop) and were re-anchored; the observed rows above are the re-anchored runs. ### Readings (final head `ac4fcfdaf`, after merging `main`) - `pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2`: 55 files, 956 tests passed. - `pnpm --filter @objectstack/service-storage typecheck`: exit 0; the test program compiles all 55 test files, 0 debt. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/contracts`: 48 files, 453 tests passed. `pnpm --filter @objectstack/spec typecheck`: exit 0, test-typecheck ledger unchanged. - `pnpm --filter @objectstack/spec check:generated`: 14 of 14 artifacts current, built from this tree. The `contracts` shards of `api-surface/` and `export-origins/` gained exactly the four exports. - Narrowing, declared: the full `@objectstack/spec` local suite ran past the 10-minute foreground cap and was not measured locally; `src/contracts` (the changed directory) was. `turbo ls --affected` names 82 packages (the package graph only: it is blind to tests that read another package's source from disk, and root-level suites are not on it). The spec change is additive (0 removed exports), no package re-exports `@objectstack/spec/contracts` wholesale, and the four names have 0 other declarations in the tree, so no downstream verdict can move through it. CI runs the farm. - ESLint, narrowed and measured: `eslint --no-inline-config --format json` over the 8 changed `.ts` files gives 8 files, 0 errors, 0 warnings, 0 fatal. Population: `eslint.config.mjs` `files: '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'`; the rest of the diff is md, mdx, json and yaml. Invariance: the config enables no type-aware linting (no `parserOptions.project`), so no untouched file's verdict can move. - Built output: `require` of `dist/index.cjs` and `import` of `dist/index.js` both load and list the new service. - Gates: `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derives 124 families at `ac4fcfdaf`; 120 ran with exit 0, and `--ran` reconciles 124 of 124 with 0 UNRUN. NOT MEASURED, 4: `check:skill-examples`, `check:dual-build-cjs-loads` and `check:i18n` refused with their own PREREQUISITE NOT MET (exit 3: packages this diff does not touch are not built here; the changed package's built CJS and ESM entries were probed directly, above); `check:type-check-debt` was not run (its `--re-measure` is a repo-wide tsc sweep; this diff adds no package and moves no ledger entry). `check:tenant-audit-census` is green on the re-derived census. ## Paths beyond the claimed surface (each forced by a named gate or by the tests) - `content/docs/permissions/tenant-audit-census.mdx` and `docs/audits/2026-08-tenant-audit-write-call-sites.counts.md`: `check:tenant-audit-census` re-derives the write-call-site census; the verb adds two `sys_attachment` writes, both carrying a tenant context. Regenerated with `node scripts/tenant-audit-census.mjs --write`, with the page's enforced prose figures moved with it (on the merged tree: 226 to 228). - `packages/services/service-storage/package.json`, `vitest.config.ts` and `pnpm-lock.yaml`: `@objectstack/plugin-audit` as a test-only dependency. The lockfile change is the three-line importer entry, accepted by `pnpm install --frozen-lockfile`; a plain `pnpm install` also re-resolved an unrelated vitest peer variant, which is not in this diff. ## Acceptance notes - No README or docs page names the new service yet. The contract docblock is its documentation; the consumer card #23307 reads it. - A system call with `organizationId: null` against a global parent on a walled deployment is refused by the engine's own system-insert organization rule, surfaced as the store's `StorageMetadataStoreError` (whose `Cause:` names it), not as a `4xx`. That is the upload doors' behaviour for a session with no active organization too. --- _Generated by [Claude Code](https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent eeb95f7 commit 5e725b8

15 files changed

Lines changed: 1053 additions & 66 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-storage': minor
4+
---
5+
6+
A server-side producer can store a file and attach it to a record: `IFileAttachmentService.attachFile`, registered by `StorageServicePlugin` as the kernel service `file-attachments` (`FILE_ATTACHMENT_SERVICE`), writes the bytes, one `sys_file` row and one `sys_attachment` row linking it to the parent, under the checks every other attachment of that parent meets
7+
8+
Clause-②: yes (widening: `@objectstack/spec/contracts` gains `IFileAttachmentService`, `AttachFileInput`, `AttachedFile` and `FILE_ATTACHMENT_SERVICE`, and `StorageServicePlugin` registers and declares one more service)
9+
10+
- **The call.** `attachFile({ parentObject, parentId, organizationId, filename, mimeType, data })` answers `{ fileId, attachmentId, size }`. `organizationId` is required: the organization the caller acts in, or `null` for none. It acts as the system, for a producer with no browser upload in front of it (inbound mail first). Reach it with `ctx.getService(FILE_ATTACHMENT_SERVICE)` once the kernel has bootstrapped.
11+
- **What it runs.** Each check is the one the rest of the platform already runs, not a copy:
12+
- The `max_upload_mb` storage setting, through the same verdict the REST upload doors ask. Over it, the call is refused `413 PAYLOAD_TOO_LARGE` before anything is stored.
13+
- The parent's organization. A parent walled into another organization than `organizationId` is refused `404 RECORD_NOT_FOUND`, as a missing parent is. Both rows are written in the parent's organization, or in the caller's for a global parent.
14+
- Every gate on the `sys_attachment` insert, among them the `enable.files` opt-in: a parent object without it is refused `403 FILES_DISABLED`.
15+
- Before the kernel has bootstrapped, or with no data engine, the call is refused `503 SERVICE_UNAVAILABLE`. A malformed input is refused `400 VALIDATION_ERROR`.
16+
- **No refusal leaves a committed `sys_file`.** The row is written `pending` before the bytes, and turns `committed` only in the same transaction as the `sys_attachment` insert. A refused call removes the bytes and the row before it throws. If the process dies mid-call, the pending-upload retention reclaims both.
17+
- `@objectstack/service-storage`: the upload doors' size refusal moved from the route module into `storage-limits.ts`, beside the limit it enforces, so the doors and the verb read one status, one code and one sentence. The doors answer exactly as before.

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ what moved this page's population from 225 to 227; nothing about the two sites
8383
changed, only whether this instrument could see them.
8484

8585
**The expensive failure direction is a keyword.** Sites whose receiver the author
86-
typed `any` have no type to read, and there are 40 of them — just under a fifth
86+
typed `any` have no type to read, and there are 41 of them — just under a fifth
8787
of the population, concentrated in exactly the seed and bootstrap paths this
8888
control exists for. Scoring an unreadable receiver as "not an engine" would have
8989
dropped every one of them silently, with a clean exit and a smaller number that
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **53 of the 226 sites are spelled that way**. A
125+
forwarding shim cannot, and **53 of the 228 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187187

188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190-
| 175 write call sites | quoted in the merged changeset | **226** |
190+
| 175 write call sites | quoted in the merged changeset | **228** |
191191
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **32** more whose options argument is unreadable |
192-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **153 of 226** decidable, **73** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 121 decidably elevated, 0 decidably not, 97 undecidable |
192+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **155 of 228** decidable, **73** undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 121 decidably elevated, 0 decidably not, 99 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -200,18 +200,18 @@ be stated is what this instrument counts, which is written above and re-runnable
200200
at any commit.
201201

202202
Two structural facts do plausibly widen this reading against any hand or regex
203-
one, and both are counted in the generated tables below: the 40 sites reached
203+
one, and both are counted in the generated tables below: the 41 sites reached
204204
through an erased (`any`) receiver, and the 50 that name their object through a
205205
`const` rather than inline. An instrument that read either the way a person does
206206
would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
121 of 226 (54%) as decidably elevated, with 97 more whose elevation is a
210+
121 of 228 (53%) as decidably elevated, with 99 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `2 / 226`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `2 / 228`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217217
without tenant context" — **32 further sites** have an options argument this
@@ -223,28 +223,28 @@ cannot read, and they are neither in nor out.
223223

224224
| what | count |
225225
| :--- | ---: |
226-
| write call sites on the application surface | **226** |
227-
| …whose object name is statically decidable | 153 |
226+
| write call sites on the application surface | **228** |
227+
| …whose object name is statically decidable | 155 |
228228
| …whose object name is chosen at run time | 73 |
229-
| …against an object with tenancy ENABLED | 151 |
229+
| …against an object with tenancy ENABLED | 153 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 165 |
231+
| threading a tenant context | 167 |
232232
| PROVABLY carrying none (options read, no context key) | **8** |
233233
| …of those, against a decidably tenancy-enabled object | **2** |
234234
| options argument UNREADABLE — may or may not carry one | 53 |
235235
| …of those, against a decidably tenancy-enabled object | 32 |
236236
| threading a decidably ELEVATED (`isSystem`) context | 121 |
237237
| threading a context that is decidably NOT elevated | 0 |
238-
| threading a context whose elevation is a run-time fact | 97 |
238+
| threading a context whose elevation is a run-time fact | 99 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242-
| receiver carried a readable engine type | 186 |
243-
| receiver erased, placed by the object NAME | 27 |
242+
| receiver carried a readable engine type | 187 |
243+
| receiver erased, placed by the object NAME | 28 |
244244
| receiver erased, placed by an `object: string` PARAMETER | 7 |
245245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 6 |
246246

247-
| object name spelled inline | 103 |
247+
| object name spelled inline | 105 |
248248
| object name spelled through a `const` | 50 |
249249
| object name is an `object: string` parameter | 9 |
250250
| object name is some other run-time expression | 64 |
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-13 at `c33c6dc67`.
300+
Measured on 2026-10-14 at `5d4205c6b`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 650 |
305-
| engine-shaped types recognised | 73 |
304+
| tracked non-test sources scanned | 652 |
305+
| engine-shaped types recognised | 75 |
306306
| declared objects in the registry | 116 |
307-
| same-named calls subtracted as non-engine | 162 |
307+
| same-named calls subtracted as non-engine | 163 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3333

3434
| Measure | Value |
3535
|---|---:|
36-
| Write call sites | 226 |
37-
| Object name statically decidable | 153 |
36+
| Write call sites | 228 |
37+
| Object name statically decidable | 155 |
3838
| Object name chosen at run time | 73 |
39-
| Against a tenancy-enabled object | 151 |
39+
| Against a tenancy-enabled object | 153 |
4040
| Against an object declaring tenancy off | 1 |
41-
| Threading a tenant context | 165 |
41+
| Threading a tenant context | 167 |
4242
| Provably carrying none | 8 |
4343
| …and decidably tenancy-enabled | 2 |
4444
| Options argument unreadable | 53 |
4545
| …and decidably tenancy-enabled | 32 |
4646
| Threading a decidably elevated context | 121 |
4747
| Threading a decidably non-elevated context | 0 |
48-
| Threading a context of undecidable elevation | 97 |
48+
| Threading a context of undecidable elevation | 99 |
4949

5050
## Subtractions the census could NOT defend — enforced
5151

@@ -90,14 +90,14 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-13 at `c33c6dc67`.
93+
Measured on 2026-10-14 at `5d4205c6b`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 650 |
98-
| engine-shaped types recognised | 73 |
97+
| tracked non-test sources scanned | 652 |
98+
| engine-shaped types recognised | 75 |
9999
| declared objects in the registry | 116 |
100-
| same-named calls subtracted as non-engine | 162 |
100+
| same-named calls subtracted as non-engine | 163 |
101101

102102
## Every site
103103

@@ -238,6 +238,8 @@ Measured on 2026-10-13 at `c33c6dc67`.
238238
| `packages/services/service-storage/src/backfill-file-references.ts` | `insert` | `sys_file` | enabled | context, elevation undecidable | 1 |
239239
| `packages/services/service-storage/src/backfill-sys-file-organizations.ts` | `update` | `sys_file` | enabled | context, elevation undecidable | 1 |
240240
| `packages/services/service-storage/src/backfill-sys-file-public-scope.ts` | `update` | `sys_file` | enabled | context, elevation undecidable | 1 |
241+
| `packages/services/service-storage/src/file-attachment-service.ts` | `delete` | `sys_attachment` | enabled | context, elevation undecidable | 1 |
242+
| `packages/services/service-storage/src/file-attachment-service.ts` | `insert` | `sys_attachment` | enabled | context, elevation undecidable | 1 |
241243
| `packages/services/service-storage/src/file-reference-lifecycle.ts` | `insert` | `sys_file` | enabled | context, elevation undecidable | 1 |
242244
| `packages/services/service-storage/src/file-reference-lifecycle.ts` | `update` | `sys_file` | enabled | elevated | 2 |
243245
| `packages/services/service-storage/src/metadata-store.ts` | `delete` | `sys_file` | enabled | options unreadable | 1 |

‎packages/services/service-storage/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"devDependencies": {
4747
"@objectstack/objectql": "workspace:*",
4848
"@objectstack/driver-sql": "workspace:*",
49+
"@objectstack/plugin-audit": "workspace:*",
4950
"@types/node": "^26.6.3",
5051
"tsx": "^4.23.15",
5152
"typescript": "^6.0.3",

0 commit comments

Comments
 (0)