You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit dd02d00
Browse filesBrowse the repository at this point in the historyBrowse files
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.
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.
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.
Copy file name to clipboardExpand all lines: .changeset/22283-storage-limits-honoured.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,7 +12,7 @@ Clause-②: no (narrowing)
12
12
13
13
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.
14
14
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:
16
16
17
17
-`POST /storage/upload/presigned` — the declared `size`;
18
18
-`POST /storage/upload/chunked` — the declared `totalSize`;
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.
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.
`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.
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.
Copy file name to clipboardExpand all lines: content/docs/api/error-catalog.mdx
+31-12Lines changed: 31 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ navTitle: Error Code Catalog
4
4
description: Complete reference for all ObjectStack error codes with causes, fixes, and retry strategies
5
5
---
6
6
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.
8
8
9
9
This catalog documents the **wire face** — the codes a client can actually receive. That is not quite the
10
10
`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.
62
62
**Fix:** Check the `fields` array for specific field-level issues.
63
63
**Retry:**`no_retry`
64
64
65
-
<Callouttype="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:
| 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))|
921
940
| 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 |
Copy file name to clipboardExpand all lines: content/docs/data-modeling/formulas.mdx
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -178,6 +178,7 @@ All functions are pure given a pinned `now`, which is what makes
178
178
|`daysFromNow(n)` / `daysAgo(n)`| timestamp |`today() ± n` days (calendar-day, not wall-clock) |
179
179
|`addDays(d, n)` / `addMonths(d, n)`| timestamp | Shift a *given* date; `addMonths` clamps to month end (Jan 31 + 1mo → Feb 28) |
180
180
|`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 |
181
182
|`daysBetween(a, b)`| int | Whole days from `a` to `b` (negative when `b` is earlier) |
182
183
|`isBlank(v)`| bool | True for `null`, `undefined`, `''`, `[]`|
183
184
|`isEmpty(v)`| bool | True for `null` or zero-length string/list/map |
0 commit comments