Skip to content

Commit 7e9b721

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-bix98b
2 parents ff6720f + 21c4bdd commit 7e9b721

45 files changed

Lines changed: 1475 additions & 100 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
---
4+
5+
fix(service-storage): `sys_file`'s storage-owned columns (`key`, `mime_type`, `size`, `status`, `etag`) can no longer be set or changed by a write that does not come from the storage service
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) A data-door write to five storage-owned sys_file columns is no longer applied: no key of any metadata schema is removed, renamed or re-shaped, no export, type or error code is removed or renamed, and no write is refused (an update keeps the stored values, a create starts pending). There is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
11+
12+
A file field refuses a file whose upload never completed by reading `sys_file.status`, and judges its `accept` / `maxSize` by `mime_type` / `size`. Those columns were plain writable fields, so a caller allowed to write `sys_file` rows (an administrator, on a stock boot) could `PATCH /api/v1/data/sys_file/ID` a presigned upload that never received its bytes to `committed`, and a record's file field then claimed it; or rewrite `size` / `mime_type` after commit so the field's constraints judged values that do not describe the stored bytes.
13+
14+
- **Update.** The five columns are now `readonly` on `sys_file`. A non-system update, by id or by predicate, keeps the values the storage service wrote and lands the rest of the payload; the write answers as before and the engine logs the dropped column at `warn`, as it does for every read-only column. A caller that passes `strictReadonlyWrites` is refused with `ERR_READONLY_FIELD_REJECTED`, as for every read-only column.
15+
- **Create.** A non-system create of a `sys_file` row keeps the caller's other columns, drops any value it gives these five, and starts the row `pending`, with one `warn` naming the dropped columns. A file field refuses such a file, and the lifecycle's `pending` retention collects it. The create is not refused.
16+
- **Unchanged.** The storage service's own writes all run under the system context and still move every column: presign, PUT and `/upload/complete`, the chunked upload, a file field's claim, release and copy, and the attachment tombstone. Every other `sys_file` column (`name`, `acl`, `scope`, ownership) is written through the data door as before.
17+
18+
To change what a file is, upload it again through the storage upload doors (`POST /api/v1/storage/upload/presigned`, then `/upload/complete`).
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the API key Revoke and Restore toasts name the key
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `revoke_api_key` and `restore_api_key` row actions on `sys_api_key` now say which key they acted on, for example `API key "QA key" revoked`, instead of the bare `API key revoked` / `API key restored`. The zh-CN, ja-JP and es-ES translations name the key too. Nothing to do.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/types': patch
4+
'@objectstack/rest': patch
5+
---
6+
7+
fix(rest): a hook that aborts with free text answers the data door with `HOOK_ABORTED` instead of no `code`
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
- **Before.** A sandboxed hook body that aborted a write with free text (`throw new Error('…')`, no `code`, no `refusals` catalogue) answered `/api/v1/data` with `400 {"error":"…","object":"…"}` and no `code`, the one refusal on that door a client could not branch on.
13+
- **Now.** The same abort answers `400 {"error":"…","code":"HOOK_ABORTED","object":"…"}`; a body that declared a 4xx `status` (or `statusCode`) keeps that status and gains the same code. `HOOK_ABORTED` is registered in the ADR-0112 ledger under `@objectstack/types` (`@objectstack/spec`), stamped by `mapDataError`'s sandbox unwrap door and its wrapper fallback (`@objectstack/types`, exported as `UNDECLARED_HOOK_REFUSAL_CODE`) and by the route door's declared-status passthrough (`@objectstack/rest`).
14+
- **Moved.** The same undeclared abort also changes value on doors that already carried a code, from the status-derived `VALIDATION_ERROR` to `HOOK_ABORTED`: the nested refusal envelopes of the sharing rules routes (`POST /api/v1/sharing/rules`, `DELETE /api/v1/sharing/rules/:idOrName`, `POST /api/v1/sharing/rules/:idOrName/evaluate`), the security suggested-bindings routes (`POST /api/v1/security/suggested-bindings/:id/confirm`, `…/dismiss`, `…/reopen`), the approvals request routes (`POST /api/v1/approvals/requests/:id/approve`, `…/reject`, `…/recall`, `…/reassign`, `…/comment`, `…/remind`, `…/request-info`, `…/resubmit`, `…/revise`), and each row of the `failures` list that `POST /api/v1/data/import/jobs/:jobId/undo` returns.
15+
- **Unchanged.** A refusal that declared a code keeps it: a `refusals` catalogue entry still answers `declaredCode` beside the status-derived `code`, and a registered code a body throws is still its own wire `code`. A non-hook producer that declared no code still gets none, and a crashed body is still the sanitised `500 INTERNAL_ERROR`. The server log line for the abort is not changed here.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/plugin-sharing': patch
3+
---
4+
5+
fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A federated object that maps a remote column onto `owner_id` through `external.columnMap`, served by a plugin driver that does not apply the map, returns rows with no `owner_id`, so every member edit or delete is refused. The refusal is unchanged. Since the mapped class stopped counting as a phantom owner anchor, the one-line server warning that tells the operator why had stopped firing. It fires again, at most once per object, as the new `UNSERVED_OWNER_WRITE_DENY_NOTICE`, which names the driver returning no value for the mapped or declared owner column. A driver that serves the column, including a row whose owner is NULL, stays silent.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
fix(service-datasource)!: a datasource create may not name a stored credential reference; it stores only the one its own secret is bound under (#27722)
6+
7+
Clause-②: no (narrowing) — the datasource admin door's create refuses a draft that names `external.credentialsRef`, where it stored the value as given before. Nothing is widened.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: a create draft value the admin door used to store as given is refused. The fix for a caller is to send the credential as `secret` and leave the key out, which the refusal says; there is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing on the datasource admin door's create (`POST /api/v1/datasources`, `IDatasourceAdminService.createDatasource`), the same rule #27582 set for `secret` fields: a write may only seal a new value or keep its own record's stored reference.
13+
14+
- **Before.** A create draft's `external.credentialsRef` was stored as given, beside or instead of the reference a supplied `secret` is bound under.
15+
- **Now.** A create has no stored reference of its own, so a draft that names `external.credentialsRef` is refused with `400` before anything is written: no record, no secret, no pool. The message names the key and never the value, and the answer is the same whatever the value names. The only reference a create stores is the one its own `secret` is bound under.
16+
- **Unchanged.** An update keeps the record's own reference unless a supplied `secret` rewraps it, as before. A create with a `secret` and no `external.credentialsRef` binds it as before. The Console's datasource form never sends the key, so it is unaffected.
17+
- **Who sees it.** A caller of the admin door's create that sends `external.credentialsRef`.
18+
19+
**The remedy.** Send the credential as `secret` and leave `external.credentialsRef` out; the door binds it and stores the reference.

‎content/docs/permissions/system-context.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ One of the largest consumers of the flag;
184184
| 47d | The print service REFUSES the system context: `printRecordToFile` admits a human user acting for themselves alone | service-print | Get: nothing — a system caller is refused (`PERMISSION_DENIED`) before any page is read, session minted or browser started. Lose: nothing a user held — the PDF is rendered as the requesting user and never as the system, so a record is archived only by someone who could read it | `refuseUnlessHumanCaller` |
185185
| 47e | The caller-scoped attach verbs REFUSE the system context: `attachFileAs` and `assertMayAttach` admit a signed-in user alone | service-storage | Get: nothing — a system caller is refused (`PERMISSION_DENIED`); the system attaches through `attachFile`, whose rows name no user. Lose: nothing a user held — the caller-scoped verbs insert the attachment under the caller's context so every insert gate judges that user and both rows name them | `requireUserContext` |
186186
| 47f | The file-reference claim path asks no read verdict of a system write: a record write claims or copies a referenced file only as its caller may read it (the download door's verdict), and a system write carries no caller to ask | service-storage | Get: a system write claims or copies any file the organization-scoped lookup reaches, as before #25692; a deliberate server-side copy between records runs this way. Lose: nothing a user held — a user write is judged by the download door's verdict, and a context-less programmatic write is not asked either | `referencingCallerOf` |
187+
| 47g | The `sys_file` storage-owned columns guard returns early on create: a system insert keeps the `key`, `mime_type`, `size`, `status` and `etag` it writes | service-storage | Get: the storage service's own inserts (the upload doors, the field-file copy and backfill) write those five columns. Lose: nothing a user held — a non-system create has them dropped and starts `pending`, and on update the same columns are `readonly`, so the UPDATE `readonly` strip keeps them for every non-system caller | `installSysFileStorageOwnedColumnsGuard` |
187188

188189
### 5. Actions, metadata plane, provenance, the organization wall
189190

@@ -659,6 +660,7 @@ and CI refuses one that differs from what the tree produces.
659660
| `packages/services/service-storage/src/attachment-access-hooks.ts#installAttachmentReadVisibility` | 1 | 44 |
660661
| `packages/services/service-storage/src/file-attachment-service.ts#requireUserContext` | 1 | 47e |
661662
| `packages/services/service-storage/src/file-reference-lifecycle.ts#referencingCallerOf` | 1 | 47f |
663+
| `packages/services/service-storage/src/sys-file-storage-owned-columns.ts#installSysFileStorageOwnedColumnsGuard` | 1 | 47g |
662664

663665
{/* END GENERATED: system-context-census */}
664666

‎content/docs/protocol/kernel/error-handling.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ nested envelope would nest under `details` rides as **top-level siblings** here.
164164
- `error` — the human-readable message, **a string**.
165165
- `code` — the machine-readable code. **Optional**: ADR-0112 invents nothing for the half
166166
a producer did not name, so a refusal that declared a status but no code arrives with
167-
`error` alone. Branch on `code` only after checking it is there.
167+
`error` alone. Branch on `code` only after checking it is there. One producer is named
168+
instead: a sandboxed hook body that aborts with free text and no code answers
169+
`HOOK_ABORTED`, at the 4xx status it declared, else `400`.
168170
- `object` — the object the refusal is about, when the door knows it.
169171
- `declaredCode` / `userMessage` — the same two optional channels as above, as top-level
170172
siblings.

‎content/docs/references/api/automation-api.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ const result = AutomationApiErrorCode.parse(data);
347347

348348
| Property | Type | Required | Description |
349349
| :--- | :--- | :--- | :--- |
350-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +351 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
350+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +352 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
351351
| **message** | `string` | ✅ | The inner refusal's own diagnostic text, verbatim, without the node and operation prefixes the run's failure text adds |
352352

353353
### Allowed Values: `FlowInnerRefusal.code`
@@ -520,6 +520,7 @@ const result = AutomationApiErrorCode.parse(data);
520520
* `FORBIDDEN`
521521
* `FORM_NOT_FOUND`
522522
* `FORM_RESOLVE_FAILED`
523+
* `HOOK_ABORTED`
523524
* `HOOK_UNSCOPED_DATA_ACCESS`
524525
* `IMPORT_JOB_CREATE_FAILED`
525526
* `IMPORT_ROW_FAILED`

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const result = ApiErrorSchema.parse(data);
2828

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
31-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +351 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
31+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +352 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3232
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
3333
| **message** | `string` | ✅ | Readable error message |
3434
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -208,6 +208,7 @@ const result = ApiErrorSchema.parse(data);
208208
* `FORBIDDEN`
209209
* `FORM_NOT_FOUND`
210210
* `FORM_RESOLVE_FAILED`
211+
* `HOOK_ABORTED`
211212
* `HOOK_UNSCOPED_DATA_ACCESS`
212213
* `IMPORT_JOB_CREATE_FAILED`
213214
* `IMPORT_ROW_FAILED`

‎content/docs/references/api/error-code-ledger.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ const result = ErrorCode.parse(data);
375375
* `FORBIDDEN`
376376
* `FORM_NOT_FOUND`
377377
* `FORM_RESOLVE_FAILED`
378+
* `HOOK_ABORTED`
378379
* `HOOK_UNSCOPED_DATA_ACCESS`
379380
* `IMPORT_JOB_CREATE_FAILED`
380381
* `IMPORT_ROW_FAILED`

0 commit comments

Comments
 (0)