Skip to content

Commit b7fdd50

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22343-try-catch-retry-strict
2 parents a77cb02 + 6a53564 commit b7fdd50

39 files changed

Lines changed: 669 additions & 136 deletions

File tree

‎.changeset/22283-storage-limits-honoured.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Clause-②: no (narrowing)
1212

1313
Setup's File Storage page has always rendered a "Limits" group — "Max upload size (MB)", "Presigned URL TTL (seconds)" and "Upload session TTL (seconds)" — and saved it, but nothing read it. `StorageServicePlugin` now reads the group on every settings pass and its upload doors honour it, on the plugin's own mount and on a host's `mountStorageRoutes` mount alike.
1414

15-
**What stops being accepted.** Where the plugin is bound to the `storage` settings namespace (its default, `bindToSettings: true`, with a settings service present), an upload larger than the resolved `max_upload_mb` is refused with `413` and the standard code `VALIDATION_ERROR`, in the usual error envelope, with a message naming the limit and the setting. Nothing is stored: no `sys_file` or `sys_upload_session` row is written and no byte reaches the adapter. The doors that judge it:
15+
**What stops being accepted.** Where the plugin is bound to the `storage` settings namespace (its default, `bindToSettings: true`, with a settings service present), an upload larger than the resolved `max_upload_mb` is refused with `413` and the registered code `PAYLOAD_TOO_LARGE`, in the usual error envelope, with a message naming the limit and the setting. Nothing is stored: no `sys_file` or `sys_upload_session` row is written and no byte reaches the adapter. The doors that judge it:
1616

1717
- `POST /storage/upload/presigned` — the declared `size`;
1818
- `POST /storage/upload/chunked` — the declared `totalSize`;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): the error-code ledger lists `PAYLOAD_TOO_LARGE` under `@objectstack/service-storage`
6+
7+
Clause-②: yes (widening: a new owner provenance row in the published error-code ledger)
8+
9+
`ERROR_CODE_LEDGER['@objectstack/service-storage']` now lists `PAYLOAD_TOO_LARGE` (`413`), the code `@objectstack/rest` already registers for its import row ceilings. The storage upload doors answer it for an upload over the File Storage settings' `max_upload_mb`: the presigned upload's declared `size`, the chunked upload's declared `totalSize`, the local raw PUT body and each chunk's running total.
10+
11+
No code is added. `ErrorCode`, `RegisteredErrorCode` and `REGISTERED_ERROR_CODES` are unchanged, so `ApiErrorSchema` accepts exactly what it accepted before. What widens is the per-package list a reader consults to learn which packages answer a code: a code emitted by several packages is listed under each of them.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix(service-automation): a `get_record` whose `fields` leave out `id` answers the row's id as its `id` output on the SQL drivers
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** On its single-row branch (no `limit` above 1) a `get_record` node answers `output: { record, id, object }`, and a later node reads that `id` as `{read.id}` (for a node whose id is `read`). The node handed the author's `fields` to the engine unchanged, and the SQL drivers return exactly the columns a projection names. So a `get_record` with `fields: ['title']` read a row with no `id` column, and `{read.id}` was empty with no error. A later `update_record` filtering on it was refused for a filter condition that resolved to nothing, and any other reference read nothing.
10+
- **What it does now.** That branch names `id` in the projection it hands the engine, so `{read.id}` is the row's id whatever `fields` names, on every driver. The record the node serves, and binds to its `outputVariable`, carries that `id` beside the columns the author named, so the variable's `id` reads the same value. The stored-metadata tables (`sys_metadata`, `sys_metadata_history`) are read the same way: the served row is the named columns plus `id`.
11+
- **Unchanged.** A `get_record` with no `fields`, or an empty list, still reads the whole row. A projection that already names `id` is handed over as written. The multi-row branch (`limit` above 1) answers `records` only and is untouched.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
'@objectstack/plugin-security': patch
4+
'@objectstack/runtime': patch
5+
'@objectstack/service-analytics': patch
6+
'@objectstack/platform-objects': patch
7+
---
8+
9+
fix(service-automation)!: retire the `RunProvenanceContext` type; `RunDataContext` names only what `resolveRunDataContext` returns
10+
11+
Clause-②: no (narrowing)
12+
13+
<!-- adr-0087: not-required (runtime-interface-only packages/services/service-automation/src/runtime-identity.ts#RunDataContext) Two TypeScript-only declarations in one runtime file move, and neither is metadata: `RunDataContext` loses its `RunProvenanceContext` arm, and the `RunProvenanceContext` interface declared beside it in the same file is removed together with its type export. No spec key, authorable spelling, Zod schema or stored shape moves, and no stored row is read or rewritten, so `objectstack migrate meta` has nothing to rewrite. The channel that reaches an affected consumer is the compiler, at the consumer's own import. -->
14+
15+
**BREAKING** for TypeScript code that imports from `@objectstack/service-automation`: one type export is removed and one type narrows. It ships as `minor` under the launch-window convention for breaking changes. No runtime behaviour changes in any package.
16+
17+
**What is removed.** The `RunProvenanceContext` type export, the `{ flowRunId }`-only "provenance" envelope, and its arm of `RunDataContext`. No code in this repository builds or reads one. Since ADR-0096 D5, the data engine's security middleware refuses a context that carries no principal and is not a system context (`403 PERMISSION_DENIED`, for every verb). So the type described a context the engine refuses, while its docstring said such a context was "indistinguishable from passing no context at all" and was let through.
18+
19+
**What `RunDataContext` is now.** Exactly what `resolveRunDataContext` returns: `isSystem: true` for a `runAs: 'system'` run, and the triggering user's principal for a `runAs: 'user'` run. A run that resolves no principal is refused before any data operation (`AUTOMATION_UNSCOPED_RUN_DATA_ACCESS`), as before.
20+
21+
**If your code named the type.** There is no replacement, because the context it described is refused. A custom data node builds its context with `resolveRunDataContext(context)`, typed `RunDataContext`.
22+
23+
**`@objectstack/plugin-security`, `@objectstack/runtime`, `@objectstack/service-analytics`, `@objectstack/platform-objects`:** comment and docstring text only. Shipped comments that said the security middleware hands through, skips or falls open for a context with no principal now say that it used to, and that ADR-0096 D5 refuses such a context. Nothing these packages do changes.

‎content/docs/api/error-catalog.mdx‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ navTitle: Error Code Catalog
44
description: Complete reference for all ObjectStack error codes with causes, fixes, and retry strategies
55
---
66

7-
ObjectStack uses a structured error system with **9 error categories** and **51 error codes reachable on the wire**. Every error includes a machine-readable code, HTTP status mapping, and retry guidance.
7+
ObjectStack uses a structured error system with **9 error categories** and **52 error codes reachable on the wire**. Every error includes a machine-readable code, HTTP status mapping, and retry guidance.
88

99
This catalog documents the **wire face** — the codes a client can actually receive. That is not quite the
1010
`StandardErrorCode` enum: the enum also carries in-process spellings the REST door translates at the
@@ -62,16 +62,10 @@ entry's cross-reference sentence so the in-process one stays findable.
6262
**Fix:** Check the `fields` array for specific field-level issues.
6363
**Retry:** `no_retry`
6464

65-
<Callout type="warn">
66-
**`VALIDATION_ERROR` is `400` with one exception: the storage upload doors answer `413` for
67-
an upload over the File Storage settings' `max_upload_mb`.**
68-
69-
The presigned-upload door (the declared `size`), the chunked-upload start (the declared
70-
`totalSize`), the local adapter's raw upload (the body) and the chunk door (the bytes the
71-
upload already holds plus the chunk) refuse it before anything is stored. The message names
72-
the size, the limit and the setting. Raise the setting or send a smaller file; resending the
73-
same upload cannot succeed. The protocol page carries the same exception:
74-
[`VALIDATION_ERROR`](/docs/protocol/kernel/error-handling#validation_error).
65+
<Callout type="info">
66+
**A request too large for its door is not this code.** An upload over the File Storage
67+
settings' `max_upload_mb`, and an import over its route's row ceiling, answer `413`
68+
[`PAYLOAD_TOO_LARGE`](#payload_too_large).
7569
</Callout>
7670

7771
```json
@@ -510,6 +504,31 @@ an environment scope (no `X-Environment-Id` header and no hostname mapping).
510504

511505
---
512506

507+
## Size Limit Errors (413)
508+
509+
`PAYLOAD_TOO_LARGE` is an [error-code ledger](/docs/references/api/error-code-ledger) code, listed
510+
under each package that answers it — `@objectstack/rest` and `@objectstack/service-storage` — not a
511+
standard-catalog member, so its status is published here rather than inherited from a category.
512+
The standard catalog names no `413` member.
513+
514+
### `PAYLOAD_TOO_LARGE`
515+
516+
**HTTP Status:** 413
517+
**Cause:** The request carries more than its door accepts:
518+
519+
- **An upload over the File Storage settings' `max_upload_mb`.** The presigned-upload door (the
520+
declared `size`), the chunked-upload start (the declared `totalSize`), the local adapter's raw
521+
upload (the body) and the chunk door (the bytes the upload already holds plus the chunk) refuse
522+
it before anything is stored. The message names the size, the limit and the setting.
523+
- **An import over its route's row ceiling** — 5,000 rows on the synchronous route, 50,000 on the
524+
job route ([size limits](/docs/data-modeling/import-mappings#size-limits)).
525+
526+
**Fix:** Raise the setting or send a smaller file; split the import across requests. Resending
527+
the same request cannot succeed.
528+
**Retry:** `no_retry`
529+
530+
---
531+
513532
## Rate Limit Errors (429)
514533

515534
### `RATE_LIMIT_EXCEEDED`
@@ -917,7 +936,7 @@ async function handleApiCall() {
917936
| 403 | `authorization` | `PERMISSION_DENIED`, `FIELD_NOT_ACCESSIBLE`, `LICENSE_REQUIRED` |
918937
| 404 | `not_found` | `RECORD_NOT_FOUND`, `OBJECT_NOT_FOUND`, `ENDPOINT_NOT_FOUND` |
919938
| 409 | `conflict` | `CONCURRENT_MODIFICATION`, `UNIQUE_VIOLATION`, `DELETE_RESTRICTED` |
920-
| 413 | `validation` | `VALIDATION_ERROR` on an upload over the storage settings' `max_upload_mb` (see [above](#validation_error)) — this row is an exception to the 400 row, not a second home for the code |
939+
| 413 | `validation` | `PAYLOAD_TOO_LARGE` — an upload over the storage settings' `max_upload_mb`, or an import over its row ceiling (see [above](#payload_too_large)) |
921940
| 422 | `validation` | `MISSING_REQUIRED_FIELD` on an absent `controlled_by_parent` master reference (see [above](#missing_required_field)) — this row is an exception to the 400 row, not a second home for the code |
922941
| 429 | `rate_limit` | `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED` |
923942
| 500 | `server` | `INTERNAL_ERROR`, `DATABASE_ERROR` |

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ ObjectStack uses standard HTTP status codes:
214214
| **403** | Forbidden | Authenticated but insufficient permissions |
215215
| **404** | Not Found | Resource doesn't exist |
216216
| **409** | Conflict | Resource already exists or version mismatch |
217-
| **413** | Content Too Large | An upload over the storage settings' `max_upload_mb` — answered as [`VALIDATION_ERROR`](#validation_error), its one exception to 400 |
217+
| **413** | Content Too Large | An upload over the storage settings' `max_upload_mb`, or an import over its row ceiling — answered as [`PAYLOAD_TOO_LARGE`](/docs/api/error-catalog#payload_too_large) |
218218
| **422** | Unprocessable Entity | Semantic validation failed (e.g. metadata spec validation); also an absent `controlled_by_parent` master reference — see [`MISSING_REQUIRED_FIELD`](#missing_required_field) |
219219
| **429** | Too Many Requests | Rate limit exceeded |
220220
| **500** | Internal Server Error | Server-side error |
@@ -402,7 +402,7 @@ if (body.code === 'VALIDATION_FAILED') {
402402
```
403403
404404
#### `VALIDATION_ERROR`
405-
**HTTP Status:** 400 — with one documented exception, which answers **413** (see below)
405+
**HTTP Status:** 400
406406
**Meaning:** The request is malformed — the generic "you sent something wrong" code
407407
**Envelope:** **both**, and one emitter writes neither (see the warning below)
408408
@@ -418,12 +418,10 @@ Where it comes from:
418418
specific.
419419
- **Query-parameter refusals.** `refuseUnknownQueryParams` and `refuseRepeatedQueryParams`
420420
answer 400 `VALIDATION_ERROR` for an unrecognised or repeated query parameter.
421-
- **Upload size refusals — the one 413.** The storage upload doors answer **413**
422-
`VALIDATION_ERROR` for an upload over the File Storage settings' `max_upload_mb`: the
423-
presigned-upload door (the declared `size`), the chunked-upload start (the declared
424-
`totalSize`), the local adapter's raw upload (the body) and the chunk door (the bytes the
425-
upload already holds plus the chunk), each before anything is stored. The message names the
426-
size, the limit and the setting.
421+
422+
Not from a request too large for its door: an upload over the File Storage settings'
423+
`max_upload_mb` answers **413** with the registered code
424+
[`PAYLOAD_TOO_LARGE`](/docs/api/error-catalog#payload_too_large), not this one.
427425
428426
**Nested** (the dispatcher door, status-derived — `GET /api/v1/packages/:id?version=1&version=2`):
429427
```json

‎examples/app-showcase/src/automation/flows/index.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,9 +1581,11 @@ export const InboundTaskWebhookFlow = defineFlow({
15811581
type: 'api',
15821582
status: 'active',
15831583
// An inbound webhook has no authenticated user, so the create must run as the
1584-
// system principal (#1888 runAs is now enforced). Without this it relies on the
1585-
// "no identity → security-skipped" fall-through, which breaks the moment the
1586-
// target object carries row-level security.
1584+
// system principal (#1888 runAs is now enforced). Without this the run is
1585+
// user-less under the default `runAs:'user'`, so its create is refused
1586+
// (`AUTOMATION_UNSCOPED_RUN_DATA_ACCESS`); the "no identity → security-skipped"
1587+
// fall-through it once relied on is gone (ADR-0096 D5 refuses a principal-less
1588+
// context in the security middleware too).
15871589
runAs: 'system',
15881590
nodes: [
15891591
{

‎packages/lint/src/build-access-matrix.test.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,18 @@ describe('buildAccessMatrix CRUD columns — the spec fold, asked (#18785)', ()
167167
permissions: entries.map((e, i) => ({ name: `ps_${String(i).padStart(4, '0')}`, objects: { o: e } })),
168168
});
169169
expect(m.entries.length).toBe(entries.length);
170+
// Index the rows by permission set ONCE. A `find` per entry is quadratic —
171+
// 3^8 entries make ~21.5M callbacks, each rebuilding the name — which ran
172+
// this case past vitest's 5 s default on a loaded CI shard. First match
173+
// wins, so every lookup returns the row `find` returned.
174+
const rowByPermissionSet = new Map<string, (typeof m.entries)[number]>();
175+
for (const e of m.entries) {
176+
if (!rowByPermissionSet.has(e.permissionSet)) rowByPermissionSet.set(e.permissionSet, e);
177+
}
170178
const mismatches: string[] = [];
171179
let cells = 0;
172180
for (let i = 0; i < entries.length; i++) {
173-
const row = m.entries.find((e) => e.permissionSet === `ps_${String(i).padStart(4, '0')}`)!;
181+
const row = rowByPermissionSet.get(`ps_${String(i).padStart(4, '0')}`)!;
174182
for (const verb of ['create', 'read', 'edit', 'delete'] as const) {
175183
cells++;
176184
if (row[verb] !== referenceFold(entries[i], verb)) {

‎packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
* [#21911, ADR-0096] The metadata protocol's platform-store calls carry the
55
* EXPLICIT system opt-in, never a principal-less context.
66
*
7-
* plugin-security hands an engine operation whose context carries no user, no
8-
* position, no permission set and no `isSystem` straight to `next()` (the
9-
* ADR-0096 E1 hand-off). The protocol's own reads and writes of the
7+
* plugin-security used to hand an engine operation whose context carries no
8+
* user, no position, no permission set and no `isSystem` straight to `next()`
9+
* (the ADR-0096 E1 hand-off); since ADR-0096 D5 it refuses one. The protocol's
10+
* own reads and writes of the
1011
* `sys_metadata` family reached the engine exactly that way — a `{ where }`
1112
* with no `context`, or a repository transaction context with only its
1213
* handle — so they worked only because the hand-off let them through. They

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11230,11 +11230,13 @@ export class ObjectStackProtocolImplementation implements
1123011230
// were eliminated by measurement, not by reading:
1123111231
// - the driver's tenant wall never engages — `buildDriverOptions`
1123211232
// sets `DriverOptions.tenantId` only from `execCtx.tenantId`
11233-
// (`objectql/engine.ts`), and this read passes no context;
11234-
// - plugin-security's Layer 0 never engages — the middleware takes
11235-
// its principal-less `return next()` thousands of lines before the
11236-
// `objectFields.has('organization_id')` gate that would have
11237-
// carried it;
11233+
// (`objectql/engine.ts`), and this read's context carries no
11234+
// `tenantId` (only the system opt-in below);
11235+
// - plugin-security's Layer 0 never engages — the middleware
11236+
// short-circuits on that `isSystem` opt-in (it used to take its
11237+
// principal-less `return next()`, refused since ADR-0096 D5)
11238+
// thousands of lines before the `objectFields.has('organization_id')`
11239+
// gate that would have carried it;
1123811240
// - no posture would save it anyway: `computeTenantLayer0Filter`
1123911241
// yields `null` under `single` and the deny sentinel under
1124011242
// `isolated` with no tenantId.

0 commit comments

Comments
 (0)