Skip to content

Commit 1eb7378

Browse files
committed
Merge origin/main (286eb9e) into claude/issue-23648-sqlite-wasm-sql-driver
PR #23647 has landed on main (c2bb302, from the same head 107a7a9 this branch merged), so after this merge the branch's diff against main is this card's alone. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
2 parents 9c9568a + 286eb9e commit 1eb7378

7 files changed

Lines changed: 456 additions & 65 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/driver-sqlite-wasm': patch
3+
---
4+
5+
fix(driver-sqlite-wasm): a database-file write that fails in the background is reported once, at `error` with the file named, instead of reaching the process as an unhandled rejection
6+
7+
`WasmSqliteConnection` writes the in-memory image to disk without anyone awaiting it in three places: after each write under `persist: 'on-write'`, when the `debounced:N` timer fires, and when a transaction closes with a flush deferred. A write that failed there (the disk full, the data directory removed under a live driver) had no caller to throw to, so its rejection reached the host process as an `unhandledRejection`: a failed run under Vitest, and a terminated process under Node's default.
8+
9+
Those three flushes now go through one helper. A failed one is logged once per failure episode, at `error` where the logger has one and at `warn` where it does not, naming the database file and saying that writes since the last good flush are not on disk. The file stays marked dirty, so the next write's flush (or `close()`) retries it and lands once the cause is gone. Further failures stay quiet until a flush has succeeded.
10+
11+
An awaited `flush()` and `close()` are unchanged and still throw to their caller. The `logger` option's type is unchanged; a logger that only has `warn` still hears the report, at `warn`.

‎docs/qa/platform-checklist/areas/access-security.json‎

Lines changed: 49 additions & 49 deletions
Large diffs are not rendered by default.

‎docs/qa/platform-checklist/areas/attachments-storage.json‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,15 +347,15 @@
347347
"title": "Attaching requires EDIT on the parent record; deleting requires uploader-or-parent-editor; unscoped multi-delete is refused outright",
348348
"since": "v15.1",
349349
"status": "active",
350-
"revision": 5,
350+
"revision": 6,
351351
"priority": "P1",
352352
"surface": "api",
353353
"personas": ["member with EDIT on the parent", "member with READ but not EDIT on the parent", "member who uploaded the attachment", "member who is neither uploader nor parent editor", "admin (isolates clause 5's FILES_DISABLED refusal from ATTACHMENT_PARENT_ACCESS by holding EDIT on both the original and target parent)"],
354354
"fixtures": {
355355
"app": "showcase",
356356
"requires": [
357357
"a files-enabled parent readable-but-not-editable by one persona (the dogfood matrix builds this with its own fixture; on stock showcase, projects are public_read_write so any member can edit — the read-not-edit split needs the fixture or a sharing tweak)",
358-
"plugin-sharing present (canEdit is the authority; without it the gate degrades to parent READ visibility by design)"
358+
"plugin-sharing present (its tri-state checkEdit verdict is the authority: allow admits and deny refuses, and an abstention, which means record sharing does not enforce on the parent, is judged by the security service's master-detail write check, whose allow and not_applicable alone admit; without plugin-sharing the gate degrades to parent READ visibility by design)"
359359
],
360360
"provisioning": {
361361
"use": "qa-scratch-authz",
@@ -378,7 +378,7 @@
378378
],
379379
"acceptance": [
380380
{
381-
"clause": "attach without parent EDIT is 403 ATTACHMENT_PARENT_ACCESS — Salesforce parity (#2970 item 3): canEdit on the parent, not mere read",
381+
"clause": "attach without parent EDIT is 403 ATTACHMENT_PARENT_ACCESS — Salesforce parity (#2970 item 3): EDIT on the parent, not mere read",
382382
"oracle": "api",
383383
"verify": "the read-only member's insert is refused with that code, while the same member's LIST of the parent's attachments succeeds",
384384
"evidence": "the 403 + the successful list"
@@ -423,7 +423,7 @@
423423
"automated": { "kind": "api", "ref": "packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts (clauses 0, 1, 2 and 4: attach without parent EDIT is 403 ATTACHMENT_PARENT_ACCESS while the same member's LIST still succeeds, uploaded_by is server-stamped over a spoofed value, delete splits uploader-vs-outsider, and FILES_DISABLED) + packages/qa/dogfood/test/attachments-unscoped-delete-gate.dogfood.test.ts, which now carries TWO blocks and pins CLAUSE 3 end to end. Block 1 (split fixture — the caller owns one row and may not touch the other): the predicate-less `{ multi: true }` delete is refused with ATTACHMENT_DELETE_DENIED / 403 / the 'Refusing an unscoped multi-delete of attachments' message and removes NOTHING, not even the row it WAS entitled to, so the refusal is authoritative and cannot land as a partial delete; a match-all `where: {}` reaches the same verdict by a DIFFERENT rule and asserts the PER-ROW message instead, which is what keeps the #9797 boundary honest (the whole-operation dispatch is deliberately scoped to a delete with no `where` at all, so `where: {}` stays a real query); and a predicate that MATCHES NOTHING is allowed through, the live half of the #4757 reasoning. Block 2 (the discriminating fixture — the caller uploaded EVERY row, so the per-row gate has nothing to refuse): the predicate-less delete is still refused, which is the only shape that proves 'refused OUTRIGHT' rather than 'every row happened to be denied'; the refusal also fires on an EMPTY table (the zero-match limb — the per-row dispatch is gated on matched rows, so a handler-only fix could never reach it); and both sides are held by the same caller sweeping the same rows with a SCOPED predicate successfully, plus a scoped `where: {}` over the empty table resolving. In both blocks the member is granted the sys_attachment delete bit first and the grant is asserted, because otherwise RBAC refuses ahead of the attachment hook and the file would be green for a reason it is not about. ⭐ Block 2 exists because block 1 provably cannot see this clause: ablating `dispatchUnscopedMultiWrite` from both registrations (rebuilt, dist-preflighted) leaves block 1 5/5 GREEN, while block 2 goes red BY RESOLVING — 'promise resolved \"2\"', which is #9719's measured wipe reproduced. Unit-level companion, also through a wired engine rather than a hand-built context: packages/services/service-storage/src/attachment-access-hooks.test.ts, describe 'unscoped multi-delete (no id, no where) — #4757 through the wired engine (#9719)'. CLAUSE 5 (the UPDATE-verb FILES_DISABLED re-point, new) has a HANDLER/ENGINE-level pin, NOT the wired dogfood/REST lane above: packages/plugins/plugin-audit/src/capability-gate-update-verb.test.ts, describe '[#10170] enable.files is asked on the UPDATE verb too' — runs a real ObjectQL (stub driver) rather than the hand-rolled fake in audit-writers.test.ts (whose registerHook ignores the { object } scope, so it cannot observe a registration-scope regression), and pins BOTH dispatch shapes (by-id AND predicate/per-row) refusing 403 FILES_DISABLED on re-point while leaving the row's parent_object unchanged, plus the positive (a re-point onto a files-ENABLED parent still succeeds) and the negative-control (an update that never names parent_object is not re-checked at all). This engine-level pin is why the checklist step above can isolate the refusal to FILES_DISABLED with confidence; it is NOT itself evidence about the wired REST PATCH lane, which is what clause 5's own oracle drives by hand." },
424424
"source": [
425425
"docs/plans/release-15.1-test-plan.md §A12 (attach 需 parent EDIT)",
426-
"packages/services/service-storage/src/attachment-access-hooks.ts#canEdit (beforeInsert canEdit gate + uploaded_by stamping; beforeDelete/beforeUpdate uploader-or-editor + #4757 unscoped refusal reached via the `dispatchUnscopedMultiWrite` declaration (#9719/#9974), MULTI_WRITE_AUTH_LIMIT fail-closed; authorizes an update-verb re-point onto the NEW parent, the access half clause 5 relies on)",
426+
"packages/services/service-storage/src/attachment-access-hooks.ts#mayEditParent (the one parent-EDIT question the beforeInsert attach gate, the beforeDelete/beforeUpdate parent-editor limb and an update-verb re-point all ask: plugin-sharing's tri-state checkEdit, never its two-state canEdit projection, with an abstention judged by the master-detail write check, #22513) + uploaded_by stamping; beforeDelete/beforeUpdate uploader-or-editor + #4757 unscoped refusal reached via the `dispatchUnscopedMultiWrite` declaration (#9719/#9974), MULTI_WRITE_AUTH_LIMIT fail-closed; authorizes an update-verb re-point onto the NEW parent, the access half clause 5 relies on)",
427427
"packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts#FILES_DISABLED (items 3, a, f; FILES_DISABLED)",
428428
"packages/plugins/plugin-audit/src/audit-writers.ts#enforceFilesCapability (enforceFilesCapability — the #2727 enable.files gate; [#10170] registered it on beforeUpdate as well as beforeInsert because the flag gates the TARGET object a write makes an attachment point at, not the create/update verb) — the FILES_DISABLED source for both clause 4 (create) and the new clause 5 (update/re-point)",
429429
"packages/spec/src/data/object.zod.ts#create (enable.files describe string, post-#10733: 'otherwise any write that makes an attachment target it is rejected (403 FILES_DISABLED) — a create and an update that re-points an existing attachment alike')",
@@ -449,6 +449,12 @@
449449
"date": "2026-08-25",
450450
"change": "added clause 5, the UPDATE half of the #10170 FILES_DISABLED gate, which this item was missing entirely: a re-point of an existing sys_attachment's parent_object onto the qa_nofiles fixture (already provisioned by qa-scratch-authz, #7670 — one throwaway record is inserted into it, no new fixture) now has its own step, persona note, negative entry and acceptance clause, isolated from ATTACHMENT_PARENT_ACCESS by running as admin (who can edit both the original and target parents). Filed because #10336/PR #10733 rewrote the enable.files describe string to name the update case explicitly ('a create and an update that re-points an existing attachment alike') while this item's create-only probe (clause 4) kept passing regardless of whether the update-verb enforcement #10170 added still held — the same gap the fixtures.qa-scratch-authz step-4 source re-quote (this file, above) makes visible. source/knownGaps/negative/automated updated to match, including the engine-level #10170 pin (capability-gate-update-verb.test.ts) that grounds the isolation claim.",
451451
"ref": "#10735"
452+
},
453+
{
454+
"revision": 6,
455+
"date": "2026-10-15",
456+
"change": "the parent-EDIT authority re-cited to the gate on main, and the source anchor re-pointed. #22513 (ce3d0ad41) removed the sharing shape's two-state `canEdit` member from attachment-access-hooks.ts: the attach rule, the parent-editor limb of update and delete, and an update-verb re-point now ask one composition, mayEditParent, which reads plugin-sharing's tri-state checkEdit and sends an abstention to the master-detail write check, so a controlled_by_parent parent is judged through its master. The source anchor on that file named canEdit and no longer resolved (check:platform-checklist ABSENT SYMBOL, and the area's anchor floor read 27 of 28); it now names mayEditParent, which holds that composition. The fixtures requirement and clause 0 stop naming canEdit as the authority. What each clause asserts is unchanged — the read-but-not-edit persona is still refused 403 ATTACHMENT_PARENT_ACCESS — and the steps, personas and provisioning are unchanged",
457+
"ref": "#23593"
452458
}
453459
]
454460
},

0 commit comments

Comments
 (0)