Skip to content

Commit 6154011

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/analytics-objectql-read-scope-3597
2 parents 26db346 + 3d5f726 commit 6154011

105 files changed

Lines changed: 2933 additions & 1949 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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@objectstack/client": patch
3+
"@objectstack/runtime": patch
4+
---
5+
6+
fix(client,runtime): analytics.meta/explain now call routes that actually exist (#3584)
7+
8+
The route audit (#3563) ledgered four dispatcher↔client shape mismatches.
9+
Re-verification showed the two analytics shapes the client spoke —
10+
`GET /analytics/meta/:cube` and `POST /analytics/explain` — were served by
11+
**nothing**: not the dispatcher, not `@objectstack/rest`, not
12+
`service-analytics`. Both methods 404ed against every deployment.
13+
14+
- `analytics.meta(cube?)` — FROM `GET /analytics/meta/:cube` TO
15+
`GET /analytics/meta[?cube=<name>]`. The cube argument is now optional; when
16+
given, the dispatcher threads it into `AnalyticsService.getMeta(cubeName?)`,
17+
which always supported the filter. Responses now use the dispatcher envelope
18+
(`{ success, data }`).
19+
- `analytics.explain(payload)` — FROM `POST /analytics/explain` TO
20+
`POST /analytics/sql` (the dispatcher's SQL dry-run route, backed by
21+
`generateSql`). Method name unchanged.
22+
23+
No migration is expected in practice: a method that unconditionally 404ed can
24+
have no working callers (none exist in objectstack or objectui). Anyone who
25+
had hand-rolled fetches against the imaginary shapes should switch to the
26+
routes above.
27+
28+
The two storage rows from the same audit are deliberately NOT reshaped: the
29+
presigned/chunked protocol the SDK speaks is registered autonomously by
30+
`service-storage` on any http-server and stays canonical; the dispatcher's
31+
bare `POST /storage/upload` / `GET /storage/file/:id` are reclassified in the
32+
route ledger as a `server-only` low-level compat surface.

.changeset/apimethod-enum-shrink.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66

77
feat(spec)!: shrink the `ApiMethod` enum to the six primitives — legacy values are stripped at parse, never honored (#3543, P2 of #3391)
88

9-
**BREAKING** (ships as `minor` per the lockstep launch-window policy — every
10-
package versions together, so a `major` here would promote the entire
11-
monorepo; the `!` marker and this changeset are the breaking-change record):
9+
**BREAKING** (the `!` marker and this changeset are the breaking-change
10+
record; the train ships as the v17 major — see the `v17-rc-anchor` changeset):
1211
the authored `enable.apiMethods` enum is now exactly the six
1312
primitives (`get`, `list`, `create`, `update`, `delete`, `bulk`). The eight
1413
legacy values (`upsert`, `aggregate`, `history`, `search`, `restore`, `purge`,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/plugin-approvals": patch
4+
---
5+
6+
fix(approvals): return decision attachments as file values, not "[object Object]" (#3504)
7+
8+
`sys_approval_action.attachments` is a `Field.file`, so the column **stores an
9+
opaque `sys_file` id** (ADR-0104 D3 — the stored form of every media field). The
10+
ObjectQL read path resolves that id into its expanded
11+
`{ id, name, size, mimeType, url }` form on the way out. But `rowFromAction`
12+
mapped the column with `.map(String)`, collapsing each expanded value to the
13+
literal string `"[object Object]"`. Every `listActions` consumer (the approval
14+
inbox timeline) then received garbage: the attachment chip had no filename and
15+
its id was `"[object Object]"`, so opening it 404'd.
16+
17+
- `ApprovalActionRow.attachments` is now `ApprovalActionAttachment[]` — the
18+
expanded file value plus its id, so a consumer can label and open an
19+
attachment without needing read access to the system `sys_file` object (which
20+
regular approvers do not have).
21+
- Three read forms are accepted: the expanded value (the normal case), a bare id
22+
(nothing to expand it into — storage service absent, file not committed), and
23+
a legacy inline blob written before file-as-reference (`file_id` /
24+
`mime_type`), until the backfill converts it. The id test reuses the
25+
platform's `isFileIdToken`, so this and the engine's read resolver cannot
26+
disagree about what counts as an id.
27+
- The decision *input* (`ApprovalDecisionInput.attachments`) is unchanged — it
28+
still takes fileId strings, which is also exactly what the column stores. Only
29+
the read shape changed.

.changeset/file-access-delegate.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-storage": patch
4+
"@objectstack/plugin-approvals": patch
5+
---
6+
7+
fix(storage): let an object delegate file-read authorization to its service
8+
9+
Fixes a regression from the governed-download change (ADR-0104 D3 wave 2): a
10+
**legitimate approver could see a decision attachment's filename but got 403
11+
opening it**, found by driving app-showcase in a browser as a real non-admin
12+
approver.
13+
14+
Cause: a field-owned file's download was authorized by testing whether the
15+
caller can READ the owning row. For an ordinary business object that is right —
16+
row readability *is* the access rule. For `sys_approval_action` it is the wrong
17+
authority: the audit table is deliberately closed to ordinary approver
18+
positions (`operation 'find' … is not permitted for positions [auditor,
19+
everyone]`), so the test denied the very approver the attachment was filed for.
20+
The approvals *service* has always had the real rule, which is why the timeline
21+
listing the attachment returned 200 while the bytes returned 403.
22+
23+
An object may now name a service to answer the question instead:
24+
25+
- `ObjectSchema.fileAccessDelegate` — a kernel service that authorizes
26+
downloads of files owned by that object's media fields.
27+
- `IFileAccessDelegate.authorizeFileRead(recordId, context)` — the contract.
28+
- `sys_approval_action` declares `'approvals'`; `ApprovalService.authorizeFileRead`
29+
reuses the *same* gate `listActions` applies (visibility of the parent
30+
request) rather than inventing a second, looser rule for the bytes.
31+
32+
**Fails closed**: a declared delegate that is missing or does not implement the
33+
method denies, rather than silently reverting to the raw read it was declared to
34+
replace. Objects without the declaration are unchanged.
35+
36+
Verified in the browser against app-showcase, both sides of the gate: the
37+
approver now downloads the real PDF (200), and an anonymous request is still
38+
refused (401) — the anonymous capability URL the original change closed stays
39+
closed. A decision attachment ends up exactly as readable as the decision it
40+
hangs off: never more, and no longer less.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/lint": patch
4+
"@objectstack/cli": patch
5+
---
6+
7+
feat(spec,lint): freeze the `{current_user_id}` filter vocabulary and fail the build on unresolvable placeholders (#3574)
8+
9+
A dashboard widget filtered on `{current_user}` rendered `0`. Not an error — a
10+
zero, indistinguishable from a metric that is legitimately empty, with nothing
11+
in the console or the server log. `service_dashboard.my_open_cases_by_priority`
12+
in the HotCRM template had shipped broken this way since the day it was
13+
written.
14+
15+
The token had never been part of the contract. Date macros were frozen in
16+
`date-macros.zod.ts` with a spec vocabulary, a lint-usable predicate, and a
17+
single client resolver; `{current_user_id}` had only prose in an `app.zod.ts`
18+
JSDoc and three ad-hoc client implementations that each handled one surface's
19+
filter shape. Nothing could tell an author their token was wrong.
20+
21+
- **`@objectstack/spec`** — new `data/context-tokens.zod.ts` freezing
22+
`CONTEXT_TOKENS` (`current_user_id`, `current_org_id`) as the sibling of
23+
`DATE_MACRO_TOKENS`, with `isContextToken` / `isKnownFilterToken` /
24+
`classifyFilterToken` and a `CONTEXT_TOKEN_SUGGESTIONS` near-miss table. The
25+
module documents what the tokens are *not*: presentation scope, never an
26+
access boundary — that is RLS, which uses the unrelated `current_user.id`
27+
expression root.
28+
- **`@objectstack/lint`** — new `validateFilterTokens` (rule
29+
`filter-token-unknown`, severity `error`). It walks `filter` / `filters` /
30+
`runtimeFilter` subtrees across dashboards, objects, views, reports,
31+
datasets, pages and apps, and reports any placeholder that resolves in
32+
neither vocabulary. It scans for filter *keys* rather than enumerating known
33+
surfaces, so a new surface following the convention is covered the day it
34+
ships — enumerating surfaces is how the dashboard was missed in the first
35+
place. Navigation `recordId` / `params` are deliberately out of scope: they
36+
resolve `AppContextSelector` ids, which are meaningless in a filter.
37+
- **`@objectstack/cli`** — the gate runs in `os validate` and `os compile`.
38+
39+
It is an error rather than a warning because of who authors this metadata. An
40+
AI reads a query returning `0` as a correct answer and builds on it; its
41+
correction loop is author → validate → fix, so a diagnostic only reaches it if
42+
it can fail the build. The three spellings the suggestion table covers —
43+
`{current_user}`, `{user_id}`, `{organization_id}` — are each correct
44+
*somewhere else* in the platform, which is exactly why authors reach for them.
45+
46+
Also fixes a `ViewSchema` JSDoc example that documented `{user_id}`, a token
47+
that resolves nowhere.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/rest": patch
3+
"@objectstack/plugin-auth": patch
4+
---
5+
6+
fix(import): sanitize row errors — never leak raw SQL, map constraint failures to human wording (#3566)
7+
8+
A failing import row surfaced the driver's raw error verbatim. When a write hit
9+
a DB constraint (e.g. `sys_user.phone_number` is `unique`), the query builder
10+
embeds the entire failing statement in `err.message`, and `toFailedResult`
11+
handed that straight back — so the importer saw ``insert into `sys_user`
12+
(...) values (...) - UNIQUE constraint failed: sys_user.phone_number``. That is
13+
both unreadable and an information disclosure of the schema.
14+
15+
- `sanitizeRowError()` (import-runner) maps the common constraint failures —
16+
SQLite / MySQL / Postgres `UNIQUE` and `NOT NULL` — to human wording
17+
("A record with this `<column>` already exists.", "`<column>` is required.")
18+
and, as a backstop, never lets a message that still reads as a SQL statement
19+
reach the client (it salvages the driver's trailing reason, or falls back to
20+
a generic message). Already-friendly messages (e.g. better-auth's "User
21+
already exists") pass through unchanged. Applies to every import path.
22+
- `isLikelyEmail` now rejects non-ASCII addresses, so an address like
23+
`x@柴仟.com` fails the import **dry-run** pre-check instead of passing client
24+
and dry-run validation only to be rejected by better-auth's strict ASCII
25+
validator at real-import time.

.changeset/pre.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"mode": "pre",
3+
"tag": "rc",
4+
"initialVersions": {
5+
"@objectstack/docs": "4.2.1",
6+
"@objectstack/example-crm": "4.0.91",
7+
"@objectstack/example-showcase": "0.3.13",
8+
"@objectstack/example-todo": "4.0.91",
9+
"@objectstack/example-embed-objectql": "0.0.31",
10+
"@objectstack/hono": "16.1.0",
11+
"@objectstack/account": "16.1.0",
12+
"@objectstack/setup": "16.1.0",
13+
"@objectstack/studio": "16.1.0",
14+
"@objectstack/cli": "16.1.0",
15+
"@objectstack/client": "16.1.0",
16+
"@objectstack/client-react": "16.1.0",
17+
"@objectstack/cloud-connection": "16.1.0",
18+
"@objectstack/connector-mcp": "16.1.0",
19+
"@objectstack/connector-openapi": "16.1.0",
20+
"@objectstack/connector-rest": "16.1.0",
21+
"@objectstack/connector-slack": "16.1.0",
22+
"@objectstack/console": "16.1.0",
23+
"@objectstack/core": "16.1.0",
24+
"create-objectstack": "16.1.0",
25+
"@objectstack/formula": "16.1.0",
26+
"@objectstack/lint": "16.1.0",
27+
"@objectstack/mcp": "16.1.0",
28+
"@objectstack/metadata": "16.1.0",
29+
"@objectstack/metadata-core": "16.1.0",
30+
"@objectstack/metadata-fs": "16.1.0",
31+
"@objectstack/metadata-protocol": "16.1.0",
32+
"@objectstack/objectql": "16.1.0",
33+
"@objectstack/observability": "16.1.0",
34+
"@objectstack/platform-objects": "16.1.0",
35+
"@objectstack/driver-memory": "16.1.0",
36+
"@objectstack/driver-mongodb": "16.1.0",
37+
"@objectstack/driver-sql": "16.1.0",
38+
"@objectstack/driver-sqlite-wasm": "16.1.0",
39+
"@objectstack/embedder-openai": "16.1.0",
40+
"@objectstack/knowledge-memory": "16.1.0",
41+
"@objectstack/knowledge-ragflow": "16.1.0",
42+
"@objectstack/plugin-approvals": "16.1.0",
43+
"@objectstack/plugin-audit": "16.1.0",
44+
"@objectstack/plugin-auth": "16.1.0",
45+
"@objectstack/plugin-dev": "16.1.0",
46+
"@objectstack/plugin-email": "16.1.0",
47+
"@objectstack/plugin-hono-server": "16.1.0",
48+
"@objectstack/plugin-pinyin-search": "16.1.0",
49+
"@objectstack/plugin-reports": "16.1.0",
50+
"@objectstack/plugin-security": "16.1.0",
51+
"@objectstack/plugin-sharing": "16.1.0",
52+
"@objectstack/plugin-webhooks": "16.1.0",
53+
"@objectstack/dogfood": "0.0.39",
54+
"@objectstack/downstream-contract": "0.0.37",
55+
"@objectstack/http-conformance": "0.0.5",
56+
"@objectstack/rest": "16.1.0",
57+
"@objectstack/runtime": "16.1.0",
58+
"@objectstack/sdui-parser": "16.1.0",
59+
"@objectstack/service-analytics": "16.1.0",
60+
"@objectstack/service-automation": "16.1.0",
61+
"@objectstack/service-cache": "16.1.0",
62+
"@objectstack/service-cluster": "16.1.0",
63+
"@objectstack/service-cluster-redis": "16.1.0",
64+
"@objectstack/service-datasource": "16.1.0",
65+
"@objectstack/service-i18n": "16.1.0",
66+
"@objectstack/service-job": "16.1.0",
67+
"@objectstack/service-knowledge": "16.1.0",
68+
"@objectstack/service-messaging": "16.1.0",
69+
"@objectstack/service-package": "16.1.0",
70+
"@objectstack/service-queue": "16.1.0",
71+
"@objectstack/service-realtime": "16.1.0",
72+
"@objectstack/service-settings": "16.1.0",
73+
"@objectstack/service-sms": "16.1.0",
74+
"@objectstack/service-storage": "16.1.0",
75+
"@objectstack/spec": "16.1.0",
76+
"@objectstack/trigger-api": "16.1.0",
77+
"@objectstack/trigger-record-change": "16.1.0",
78+
"@objectstack/trigger-schedule": "16.1.0",
79+
"@objectstack/types": "16.1.0",
80+
"@objectstack/verify": "16.1.0",
81+
"objectstack-vscode": "16.1.0"
82+
},
83+
"changesets": []
84+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: remove the dead `AuditConfig` cluster from `@objectstack/spec/system` (#1878 recheck loose-end)
6+
7+
The entire `system/audit.zod.ts` module — `AuditConfigSchema`,
8+
`AuditStorageConfigSchema`, `AuditRetentionPolicySchema`,
9+
`AuditEventFilterSchema`, `SuspiciousActivityRuleSchema`,
10+
`DEFAULT_SUSPICIOUS_ACTIVITY_RULES`, and the `AuditEvent` /
11+
`AuditEventActor` / `AuditEventTarget` / `AuditEventChange` /
12+
`AuditEventType` / `AuditEventSeverity` shape schemas (plus all their
13+
type exports) — is removed. Verified zero consumers repo-wide: the live
14+
audit path (`plugin-audit`) imports none of it, defines its own
15+
`sys_audit_log` row shape, and captures **unconditionally** via engine
16+
hooks, so `AuditConfigSchema.enabled: false` advertised a semantic
17+
(turning the compliance ledger off) the platform deliberately rejects.
18+
Same ADR-0056 D8 family as the earlier `compliance.zod` / `masking.zod` /
19+
`RLSAuditConfig` / `PolicySchema` removals: security/compliance-shaped
20+
config must never merely look live.
21+
22+
**Migration — every dead knob maps to a live surface (or is deliberately
23+
not configurable):**
24+
25+
| Removed (never enforced) | Live replacement |
26+
| --- | --- |
27+
| `AuditConfigSchema.enabled` | none — audit capture is **always on** (compliance ledger; `object.zod` `trackHistory` contract) |
28+
| `eventTypes` / `excludeEventTypes` / `minimumSeverity` / `AuditEventFilterSchema` | none today — if event filtering ships it lands as an `audit` **settings** namespace (ADR-0069 pattern), not app metadata |
29+
| which fields/objects are summarized + History tab UI | object-level + field-level **`trackHistory`** (live, enforced by plugin-audit) |
30+
| `AuditRetentionPolicySchema` / `storage` | object **`lifecycle`** `audit` category (retain → archive → delete) + per-org settings overrides (ADR-0057) |
31+
| `SuspiciousActivityRuleSchema` / `DEFAULT_SUSPICIOUS_ACTIVITY_RULES` | none — no detection engine exists; security monitoring is org-operations tooling, not app-package metadata |
32+
| `AuditEvent*` shape schemas | the `sys_audit_log` object definition in `plugin-audit` is the row-shape source of truth |
33+
34+
No first-party, example, or downstream-contract code imported any of
35+
these symbols; `defineStack` never accepted an `audit` key, so no stack
36+
config changes. Docs page `references/system/audit.mdx` is removed by
37+
regeneration; the security-context module doc now marks audit alongside
38+
the previously removed compliance/masking subsystems.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/rest": patch
3+
"@objectstack/client": patch
4+
"@objectstack/runtime": patch
5+
---
6+
7+
feat(rest): route audit tranche 2 — the REST surface gets its own ledger +
8+
conformance guard (#3587, follow-up to #3563)
9+
10+
The dispatcher tranche closed its 27 gaps and guards them (#3569#3579), but
11+
`@objectstack/rest` mounts a second, larger surface the client also reaches —
12+
89 routes, never audited. `rest-route-ledger.ts` now records a reviewed
13+
disposition for every one of them (38 sdk, 43 gap, 3 server-only, 3 public,
14+
2 mismatch), and the guard is real enumeration on both sources: RouteManager
15+
routes via the `getRoutes()` introspection seam, and the two
16+
RouteManager-bypassing registrars (`package-routes.ts`,
17+
`external-datasource-routes.ts`) via captured mock-server registrations — no
18+
pinned-by-hand list. The client half
19+
(`rest-route-ledger-coverage.test.ts`) verifies every claimed method exists;
20+
a 43-gap ratchet is wired into CI. Every guard direction was negative-tested.
21+
22+
Notable dispositions the audit surfaced: `POST /api/v1/packages` is a
23+
publish/install shape collision between REST and the dispatcher (REST
24+
registers first and wins) — ledgered `mismatch`; the REST
25+
`GET /ui/view/:object/:type` path dialect is unreachable by the SDK's
26+
query-param dialect — ledgered `mismatch`; `service-storage` /
27+
`service-i18n` mount a third route surface outside `@objectstack/rest`,
28+
explicitly out of scope here and tracked under #3587.
29+
30+
No behavior change — data + tests only, plus a scope-note refresh in the
31+
runtime ledger pointing at the new REST ledger.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: delete `DEFAULT_DISPATCHER_ROUTES` — the dead route table that
6+
underwrote a false compliance verdict (#3586, #3563 follow-up)
7+
8+
The const was consumed by nothing in the runtime — only its own tests and
9+
`api-surface.json`. It listed dispatcher branches that never existed
10+
(`/workflow`, `/realtime`) while omitting eight real prefixes (`/keys`,
11+
`/mcp`, `/mcp/skill`, `/actions`, `/security`, `/share-links`, `/ready`,
12+
`/openapi.json`), and `CLIENT_SPEC_COMPLIANCE.md` anchored a "FULLY
13+
COMPLIANT" verdict on it while 27 real routes had no SDK expression.
14+
15+
The audited, guard-enforced source of truth for the dispatcher's route
16+
surface is `packages/runtime/src/route-ledger.ts` (#3569): the conformance
17+
suite fails when the registry and the ledger drift, which the dead table
18+
never could.
19+
20+
Also swept the last GraphQL fixture debris that #3562's surface removal
21+
left behind: registry test fixtures renamed to honest OData naming, the
22+
tautological `config.graphql` assertions dropped, and the stale
23+
`"type": "graphql"` JSDoc example in `registry.zod.ts` corrected.
24+
25+
Breaking for anyone importing `DEFAULT_DISPATCHER_ROUTES` (a repo-wide and
26+
objectui-wide grep shows zero consumers); shipped as minor per the
27+
launch-window convention, cf. #3562/#3581.

0 commit comments

Comments
 (0)