Skip to content

Commit 234b282

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15302-provenance-hook-residue
2 parents a192423 + 1c00b01 commit 234b282

20 files changed

Lines changed: 1218 additions & 52 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
`HistoryCleanupManager` computes its retention cutoff on one calendar, not two.
6+
7+
Both call sites — the age-based delete in `runCleanup()` and the preview count in `getCleanupStats()` — built the cutoff with `cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays)` and then rendered it with `toISOString()`. `setDate`/`getDate` read and write the **local** calendar; `toISOString()` renders **UTC**. They now use `setUTCDate`/`getUTCDate`, so the arithmetic and the rendering agree.
8+
9+
`setDate` preserves wall-clock time, so shifting the local calendar back `n` days moves the *instant* by exactly `n × 24h` only while every local day in the window is 24 hours long. When the window straddles a DST transition it is 23 hours (spring-forward) or 25 (fall-back), and the cutoff instant that goes into the `recorded_at: { $lt: … }` **delete** filter is off by the size of that transition — one hour in most zones, thirty minutes on Lord Howe Island. History rows within that slip of the retention boundary were deleted early, or retained too long.
10+
11+
The exposure is not limited to the two transition days: the window only has to *straddle* a transition, so it grows with `maxAgeDays`. Measured over a 12-zone × 366-day × 48-half-hour sweep of 2026, in `America/New_York` the old spelling produced a wrong cutoff for 0.6% of instants at `maxAgeDays: 1`, 16.4% at 30, 49.7% at 90 and 69.4% at 180. In zones that do not observe DST (`UTC`, `Asia/Shanghai`, `Asia/Kolkata`, `Australia/Perth`) the rate is 0.0% at every `maxAgeDays` — which is why no test had ever gone red on this.
12+
13+
This does **not** make retention timezone-aware, and does not change what `maxAgeDays` means. The cutoff was already intended to be `now − maxAgeDays × 24h`; it is now that in every zone rather than only in zones without DST. Nothing else in either filter moved: the `organization_id` scoping, the ADR-0009 `executionPinned` exclusion and the `maxVersions` path are untouched.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/hono': patch
3+
---
4+
5+
The Hono adapter's `/auth/*` mount yields only a 404 that disclaims ownership
6+
7+
`createHonoApp`'s `${prefix}/auth/*` mount forwards every request under it to the
8+
kernel's `auth` service and, since #4117, hands the request on to the rest of the
9+
chain when that service answers 404 — which is what keeps `/auth/me/permissions`
10+
and `/auth/me/localization` reachable through the gated `dispatch()`. The yield
11+
had only the status to go on, so it could not tell "I do not serve this path"
12+
from "I serve it and the answer is 404".
13+
14+
Measured on a real boot through this adapter (a real kernel with `AuthPlugin`,
15+
`prefix: '/api/v1'`), `GET /api/v1/auth/delete-user/callback?token=…&callbackURL=…`
16+
answered `404 {"message":"Not found","code":"NOT_FOUND"}` from better-auth and
17+
`200 {}` on the wire. `plugin-auth`'s route ledger carries that route under its
18+
`disabled` disposition precisely because it is published and answers 404, so the
19+
ledger's recorded answer was true of the auth service and false on this adapter's
20+
wire. Nothing had to be composed in for that: the `${prefix}/*` dispatcher
21+
catch-all this same function registers is terminal and answers `200 {}` for paths
22+
under `/auth/`.
23+
24+
The mount now asks the auth service whether its own router serves the path, via
25+
an optional `ownsRoute(request)` — the seam `AuthManager` grew in the plugin-side
26+
fix for the same defect — and yields only when it does not. Every answer that is
27+
not a literal `true` (no such method, a throw, anything else) means yield, so a
28+
service predating the method behaves exactly as before and a failure to decide
29+
can never cost the ordering-independent surface.
30+
31+
⛔ The mount is unchanged and still claims `${prefix}/auth/*`; 401/403 were never
32+
yielded and still are not. What narrowed is only which 404 may be handed on.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
`os lint --eval --json` now carries the ADR-0112 error carriers on its generator-load failure, instead of a bare `{error}`.
6+
7+
Eval mode's `--generator` load failure was the one exit on that mode with a machine face, and it was off-envelope: the `catch` built its human message and discarded the error object, so `code` and `httpStatus` could never reach the payload. A consumer that reads `code` to branch got a real code from the same command's project-lint catch-all and `undefined` from eval mode — the case a consumer is most likely to be caught by, because the face is present and looks answerable.
8+
9+
The exit now spreads `errorCodeFields(error)`, the same helper the project-lint catch-all spreads, so both failure faces of `os lint` are built from one source rather than two hand-written shapes.
10+
11+
Nothing is minted. `errorCodeFields` passes a producer's code through and returns nothing otherwise — ADR-0112's ledger stays the authority on who may mint a code — so the exit is polymorphic in exactly the way its sibling already is. Measured on the command's own output, across the reachable load-failure classes:
12+
13+
- a generator whose top-level evaluation throws a coded failure (an SDK refusal as the module builds its client at import) now answers `{"error": …, "code": "FORBIDDEN", "httpStatus": 403}`; both keys were being dropped;
14+
- a file the generator reads at import that is missing now answers `code: "ENOENT"`, the errno vocabulary already documented for this command, and no invented HTTP status;
15+
- an unresolvable path or a syntax error — esbuild's own build failure, which carries neither key — still answers a bare `{error}`, as does the hand-thrown "module must default-export a function".
16+
17+
The human (non-`--json`) path, the eval report exit, and offline eval are unchanged.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os migrate meta` no longer prints the protocol version under the word "runtime", where it read as the installed package version.
6+
7+
The chain line used to end `(runtime 17.0.0)`. That number is `PROTOCOL_VERSION` — the protocol major padded to a semver — and it is not, and never tracks, the version of the installed `@objectstack/cli` or `@objectstack/spec`. On a 17.3.0 install the line appeared beside the real package versions of the same upgrade session (`npm view`, the changelog), so it read as "your runtime is 17.0.0": an apparent downgrade or a stale install, neither of which was true.
8+
9+
The value was never wrong — the label and the semver form were. The line now states the fact in the protocol's own units:
10+
11+
```
12+
Chain: protocol 17 → 17 (this runtime implements protocol 17)
13+
```
14+
15+
The parenthetical is relabelled rather than dropped, because it carries a fact nothing else on screen does: when `--to` stops below this build's major, it is the only place the operator is told where the runtime actually stands (`Chain: protocol 16 → 16 (this runtime implements protocol 17)`).
16+
17+
The `--json` payload is deliberately untouched: its `runtime` key still carries the same padded protocol semver. Renaming a machine-readable key is a contract change owing a reader census and a deprecation window of its own, and it is tracked separately — an e2e pin now asserts the key's current value so that move cannot happen silently.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/plugin-sharing": patch
4+
"@objectstack/runtime": patch
5+
---
6+
7+
`publicSharing.enabled` now has one canonical predicate, exported from the package that declares the key.
8+
9+
`isPublicSharingEnabled(schema)` is a new export of `@objectstack/spec/data`, declared in `src/data/object.zod.ts` beside the `publicSharing` block itself — the same shape as the neighbouring `isTenancyDisabled`. It is additive: nothing was removed or narrowed from the spec's public API.
10+
11+
Until now the same policy read existed in two spellings. `@objectstack/plugin-sharing` defined it (for the share-link service's redemption gate and the route probe above it), and `@objectstack/runtime` carried a documented private mirror for its `/share-links` dispatcher domain — copied rather than imported because the plugin is only a **dev** dependency of the runtime. That reasoning was true of that one home and not of the question: both packages already depend on `@objectstack/spec`, so a shared home existed all along and the de-duplication adds no dependency edge. Both surfaces now consume the exported predicate and the runtime copy is deleted.
12+
13+
Behaviour is unchanged, fail-closed included: an absent `publicSharing` block, an absent schema, and an engine that cannot answer `getSchema` at all remain **one** answer, `false`, and only the boolean `true` enables. The two pins that held the copies equal — `share-link-eligibility.test.ts` in the plugin and `share-links-enforcement-context.test.ts` in the runtime, which assert the same observable answer on both surfaces rather than trusting the copy — are unchanged and still green; they are what proves the merge did not move behaviour. The predicate's own contract, which those tests can only observe indirectly, is now pinned directly in `packages/spec/src/data/object.test.ts`.

0 commit comments

Comments
 (0)