Skip to content

Commit dd02d00

Browse files
committed
Merge origin/main into claude/issue-20749-test-strings-s29
2 parents 65b42b7 + 345d3f3 commit dd02d00

80 files changed

Lines changed: 2919 additions & 261 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The form view `submitBehavior` property help no longer ends in a ruling date (#22093)
6+
7+
Clause-②: no
8+
9+
Studio, the reference docs and the published `objectstack-ui` skill show this describe as the property help. It still says that on the `redirect` arm `url` is relative-only, interpolates only declared record fields as `{{record.field_name}}` and is URL-escaped; the "(ruled 2026-08-11)" parenthetical is gone, and the ruling's date and tracker reference live in the code comment above the describe. Wording only: no key, type, accept/reject or export change. The `react-blocks.md` row and the `view` / `protocol` reference pages are regenerated from it.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/types': minor
3+
'@objectstack/rest': patch
4+
'@objectstack/runtime': patch
5+
'@objectstack/plugin-hono-server': patch
6+
'@objectstack/cloud-connection': patch
7+
---
8+
9+
fix(auth): a server-side session read no longer renews a browser session behind its cookie (#22258)
10+
11+
**What was wrong.** better-auth's `getSession` renews a session older than `session.updateAge`: it moves `sys_session.expires_at` to `now + expiresIn` and stages the renewed session cookie on that call's own response. Ten doors read the session in-process (`auth.api.getSession`) and answer with their own response, so the renewal landed in the database and its cookie was discarded. The browser kept its old cookie, later `GET /api/v1/auth/get-session` calls found a fresh row and re-issued nothing, and the cookie expired first: a dead cookie beside a live bearer, and every cookie-only path then saw a signed-out user. Measured on a fresh dev stack (better-auth 1.7.3, `expiresIn` 604800 s, `updateAge` 86400 s, a session aged to `now + expiresIn − updateAge − 60 s`): `GET /api/v1/data/:object`, `GET /api/v1/auth/me/permissions`, `GET /api/v1/meta/object`, `GET /api/v1/i18n/locales`, `GET /api/v1/packages`, `GET /api/v1/marketplace/install-local` and the MCP door each moved `expires_at` by +86460 s and set no session cookie.
12+
13+
**The rule now, decided by what the request carries.**
14+
15+
- **A session cookie** (a browser, including a console that sends its cookie beside its bearer): the in-process read passes `query.disableRefresh`. The session renews only through `GET /api/v1/auth/get-session`, which re-issues the cookie with `Max-Age = expiresIn`, so cookie and session expire together. Measured after the change: every door above leaves `expires_at` unchanged on a cookie request and sets no cookie.
16+
- **No session cookie** (a bearer-only client: `@objectstack/client` outside a browser, the `os` CLI): unchanged. A data read past `updateAge` still renews the session (+86460 s, measured on the same doors), so an active bearer client keeps sliding forward without calling `get-session`. No cookie is ever set on a response to a request that sent none.
17+
18+
**Upgrading.** Nothing to change. A browser session renews whenever the app calls `GET /api/v1/auth/get-session`; a tab that never calls it now signs out at the session's real expiry instead of keeping a live bearer beside a dead cookie.
19+
20+
`@objectstack/types` gains `inProcessSessionReadInput(headers)` (the `getSession` input for an in-process read: the request's own headers, plus `query: { disableRefresh: true }` when they carry a better-auth session cookie), `carriesSessionCookie(headers)` and the `InProcessSessionReadInput` type. A host that calls `auth.api.getSession` itself should read through `inProcessSessionReadInput` for the same reason.
21+
22+
Not changed here: the in-process readers in `@objectstack/plugin-auth`, `@objectstack/plugin-webhooks`, `@objectstack/plugin-sharing`, `@objectstack/service-storage`, `@objectstack/service-settings` and `@objectstack/service-datasource` still renew a cookie session without re-issuing its cookie.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/formula": minor
3+
---
4+
5+
feat(formula): `isoDate(t)` and `isoDatetime(t)`, the string form of a CEL timestamp
6+
7+
Clause-②: yes (widening)
8+
9+
- **What is new.** Two CEL stdlib functions that turn a timestamp into ISO text on the UTC calendar. `isoDate(t)` returns `YYYY-MM-DD` and `isoDatetime(t)` returns `YYYY-MM-DDTHH:mm:ss.sssZ`, always with three-digit milliseconds and a `Z`. Both are in `CEL_STDLIB_FUNCTIONS`, so `introspectScope` advertises them and the build check accepts them.
10+
- **The bytes the flow template dialect wrote.** `isoDate(today())` is the text `{TODAY()}` wrote, `isoDatetime(now())` is the text `{NOW()}` wrote, and `isoDate(daysFromNow(n))` / `isoDate(daysAgo(n))` are the text `{TODAY() + n}` / `{TODAY() - n}` wrote, in a flow value envelope. This holds at month, year, leap-day and DST-transition instants, whatever the host's zone.
11+
- **`isoDate(today())` is the reference-timezone day.** `today()` is that day at UTC midnight, and `isoDate` reads the UTC calendar. Under a non-UTC reference zone, `isoDate(now())` can be a different day: it is the UTC day of the instant.
12+
- **Only a timestamp is accepted.** Text, a number or `null` is refused, at build when the argument's type is known and at run otherwise. It is never coerced or rendered. For ISO text, parse it first: `isoDate(date(s))`. An invalid timestamp, or one outside 0001-01-01 to 9999-12-31, is refused at run.
13+
- **`string(timestamp)` is still refused.** CEL defines that conversion as RFC 3339 text that drops a zero fraction, which is not the template's `.000Z`, so each shape has one spelling.
14+
- **No write path changes.** An envelope that returns a timestamp (`today()`, `now()`, `daysFromNow(n)`, `addDays(…)`) still evaluates to a `Date` and writes what it wrote before.

‎.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: 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.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/plugin-security": major
3+
---
4+
5+
`sys_user_permission_set` refuses an assignment whose validity window ends at or before it starts (ADR-0091 D1/D2)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of one stored value combination on a data object; no metadata key, export or authorable surface moves, so `objectstack migrate meta` has nothing to rewrite -->
10+
11+
**BREAKING**: an accept-set narrowing on the data door, shipped as `major` on the v18 line (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`).
12+
13+
- **What is refused.** A write that leaves an assignment with both `valid_from` and `valid_until` set and `valid_until` at or before `valid_from` is refused with `400 VALIDATION_FAILED`, a `rule_violation` at `valid_until`. The window is half-open, `[valid_from, valid_until)`, so such an assignment can never grant anything: the resolver drops it at every evaluation. Until this release it was stored without a word, so an administrator who mistyped a date got an assignment that silently granted nothing.
14+
- **Which writes are judged.** Every insert, from every writer, system writers included, single rows and batches alike. An update is judged by the stored row overlaid with the patch, and only when it moves a bound: by id, and for each matched row of a multi-row update. The refusal is a declared validation rule on the object, `validity_window_order`, so the engine runs it on every one of those write paths.
15+
- **What still lands, unchanged.** A window that ends after it starts, one bound alone (either one), and no window at all. An update that moves neither bound is not judged, so an assignment stored before this release with an inverted window does not block an unrelated edit; the write that repairs it is the only one that has to state a valid window.
16+
- **To fix a refused write**, set `valid_until` later than `valid_from`, or leave one of the two empty for an open-ended window.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/platform-objects": minor
3+
---
4+
5+
The user page's "Permission Sets" list shows each assignment's validity window, `valid_from` and `valid_until` (ADR-0091 D1/D2)
6+
7+
The resolver drops a `sys_user_permission_set` assignment outside its half-open `[valid_from, valid_until)` window at every evaluation, so a list that showed only the set presented an expired or not-yet-active assignment as a live grant. The two bounds now sit beside the set. The list's Add affordance still writes the user and the set only; the window is set by editing the assignment row.

‎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/data-modeling/formulas.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ All functions are pure given a pinned `now`, which is what makes
178178
| `daysFromNow(n)` / `daysAgo(n)` | timestamp | `today() ± n` days (calendar-day, not wall-clock) |
179179
| `addDays(d, n)` / `addMonths(d, n)` | timestamp | Shift a *given* date; `addMonths` clamps to month end (Jan 31 + 1mo → Feb 28) |
180180
| `date(s)` / `datetime(s)` | timestamp | Parse an ISO date / date-time string (aliases) |
181+
| `isoDate(t)` / `isoDatetime(t)` | string | A timestamp as ISO text on the UTC calendar: `YYYY-MM-DD` / `YYYY-MM-DDTHH:mm:ss.sssZ`, the bytes a flow template's `{TODAY()}` / `{NOW()}` wrote. Refuses text, numbers and `null`: write `isoDate(date(s))` for ISO text |
181182
| `daysBetween(a, b)` | int | Whole days from `a` to `b` (negative when `b` is earlier) |
182183
| `isBlank(v)` | bool | True for `null`, `undefined`, `''`, `[]` |
183184
| `isEmpty(v)` | bool | True for `null` or zero-length string/list/map |

0 commit comments

Comments
 (0)