Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .changeset/action-ctx-session-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
"@objectstack/spec": minor
"@objectstack/runtime": patch
---

feat(spec): declare the action-body `ctx.session` contract (#5697)

An action body reads `ctx.session` on every dispatch, and until now **nothing
declared it**. `actionContext` is a bare `any` at both dispatch sites
(`domains/actions.ts`, `action-execution.ts`), the sandbox seam types
`ScriptContext.session` as `unknown`, and the one spec-side mention was an
inline literal on `ActionHandlerContext` carrying a `[k: string]: unknown`
catch-all. Declared-nowhere, produced-anyway: no schema, no gate, no generated
reference page, and nothing the liveness ledger could reach.

That is how the surface drifted without anyone noticing. Its `roles` key carries
`ExecutionContext.positions` — the ADR-0090 D3 vocabulary handed to authors under
the one spelling that ADR forbids — while the hook side retired its own
`session.roles` at #5050. One platform, one key name, two opposite answers.

**`ActionSessionSchema` (`@objectstack/spec/ui`) declares that shape as built.**

```ts
{ userId?: string; organizationId?: string; roles?: string[] }
```

This release changes **nothing about what the runtime produces** — it is phase 1
of #5613's contract-first ruling, and declaring current reality is deliberately
not the same as endorsing it:

- `roles` is declared **deprecated** in its `.describe()` and its JSDoc. The
rename to `positions`, with a deprecation window and an ADR-0087 semantic
migration, is #5613 phase 2. There is deliberately **no `positions` key yet** —
minting one before the migration would ship two live spellings of one value.
- The schema is **not strict**, matching `HookContextSchema`: this is a runtime
shape the platform hands a body, never authored, and closing it would turn a
future engine-side enrichment into a parse failure for whoever parses a context
they were given.

Three facts the declaration now states, all of them previously discoverable only
by reading the builder:

- **Absent means the key is absent.** The builder uses conditional spreads, so
`'organizationId' in ctx.session` answers `false` — not "present and
`undefined`". The hook path's `input.id` on a bulk write is the opposite case
(#5668); an `in` test does not port between them.
- **No identity envelope yields no session at all** — `undefined`, never `{}`, so
a body can tell "no caller" from "an anonymous caller" (#3712). One consequence:
`roles` never appears on its own.
- **`organizationId` is the blessed name** for the caller's active org; the
v11-removed `session.tenantId` alias (#3280 / #3290) does not come back.

Type-only on the runtime side, no behaviour change: `buildActionSession()` now
declares `ActionSession | undefined` instead of `any | undefined`, and
`ActionHandlerContext.session` is the schema's inferred type rather than an
inline literal with a catch-all. A handler annotated with `ActionHandler` that
read an undeclared key off `ctx.session` now gets a compile error naming it —
that key was never produced. `ScriptContext.session` deliberately stays
`unknown`: it is one seam over both body kinds, and hook and action sessions are
different objects.

The declaration ships with the gate it needed —
`packages/runtime/src/action-session-shape-contract.test.ts` executes the real
producer and asserts a non-strict parse of the built object returns it
**unchanged**, so a key the builder starts producing without declaring here is
stripped and the pin goes red.
76 changes: 76 additions & 0 deletions content/docs/references/ui/action-params.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: Action Params
description: Action Params protocol schemas
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

The action DISPATCH contract: what the platform validates on the way in, and

what it hands the handler on the way out.

Two halves, one surface. **Inbound** — action-param VALUE validation

(ADR-0104 D2), below. **Outbound** — the runtime context an action body /

handler receives: `ActionSessionSchema` (the `ctx.session` contract,

#5697), `ActionEngineFacade`, `ActionHandlerContext` and

`ActionHandler`.

## Inbound — action-param VALUE validation (ADR-0104 D2)

An action's declared `params[]` is a complete value contract — `type`,

`required`, `multiple`, `options`, `reference` — but before this it only

informed the client dialog: the server passed `reqBody.params` straight to

the handler, unvalidated (`http-dispatcher.ts`). This module is the pure

contract that lets the REST and MCP dispatch paths enforce that declaration

BEFORE the handler runs, reusing the D1 field value-shape contract

(`valueSchemaFor`).

Purity: schema derivation only (Prime Directive #2). Field-backed params are

resolved to their effective value-shape inputs by the CALLER (the runtime,

which holds the object metadata registry); this module validates the already

resolved descriptors.

<Callout type="info">
**Source:** `packages/spec/src/ui/action-params.zod.ts`
</Callout>

## TypeScript Usage

```typescript
import { ActionSessionSchema } from '@objectstack/spec/ui';
import type { ActionSession } from '@objectstack/spec/ui';

// Validate data
const result = ActionSessionSchema.parse(data);
```

---

## ActionSession

Action-body `ctx.session` — the caller identity an action body reads (runtime shape, never authored)

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **userId** | `string` | optional | Invoking user id (absent when the call carries no user) |
| **organizationId** | `string` | optional | Active organization id (blessed developer-facing name; absent when the call is org-less) |
| **roles** | `string[]` | optional | DEPRECATED — the VALUE is the caller's ADR-0090 D3 `positions` (`ExecutionContext.positions`, "Formerly `roles`"), delivered at this boundary under the one spelling that vocabulary forbids. Declared here because `buildActionSession()` produces it today — declaring current reality is not endorsing the name: ADR-0090 D3 makes `role` a reserved-forbidden word, #4839 deleted the last two `roles.includes('admin')` readers, and #5050 retired the hook-side `HookContext.session.roles` outright, so a body author currently meets two different answers to one key name on one platform. The rename to `positions` — with its deprecation window, ADR-0087 semantic migration and the `buildActionSession()` comment correction — is #5613 phase 2. There is deliberately NO `positions` key on this shape yet: minting one before the migration would ship two live spellings of one value, which is the defect, not the fix. Never gate PRIVILEGE on this array — ask the security service, which evaluates capability grants, placements and the derived posture (ADR-0095), never a role-name string comparison. |


---

1 change: 1 addition & 0 deletions content/docs/references/ui/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This section contains all protocol schemas for the ui layer of ObjectStack.

<Cards>
<Card href="/docs/references/ui/action" title="Action" description="Source: packages/spec/src/ui/action.zod.ts" />
<Card href="/docs/references/ui/action-params" title="Action Params" description="Source: packages/spec/src/ui/action-params.zod.ts" />
<Card href="/docs/references/ui/app" title="App" description="Source: packages/spec/src/ui/app.zod.ts" />
<Card href="/docs/references/ui/bulk-action" title="Bulk Action" description="Source: packages/spec/src/ui/bulk-action.zod.ts" />
<Card href="/docs/references/ui/chart" title="Chart" description="Source: packages/spec/src/ui/chart.zod.ts" />
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/ui/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"pages": [
"---Apps & Navigation---",
"action",
"action-params",
"app",
"page",
"view",
Expand Down
22 changes: 12 additions & 10 deletions docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ regenerate.
| Measure | Value |
|---|---|
| Triaged directories | 5 |
| Object sites in them | 454 |
| Still-open (strip) sites | 195 |
| Files carrying at least one | 29 |
| Object sites in them | 455 |
| Still-open (strip) sites | 196 |
| Files carrying at least one | 30 |

Remaining strip sites by class:

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 13 |
| unresolved — needs a per-schema verdict | 33 |
| wire / open — out of forced scope | 106 |
| wire / open — out of forced scope | 107 |
| no door — no carrier, ADR-0049 territory | 14 |
| no gate — carrier live, no parse | 29 |

Expand All @@ -43,12 +43,12 @@ The `strict` column is the one the campaign schedules against; it counts both th

| Dir | Sites | strict | passthrough | catchall | strip |
|---|---|---|---|---|---|
| `ui/` | 170 | 116 | 5 | 0 | 49 |
| `ui/` | 171 | 116 | 5 | 0 | 50 |
| `data/` | 162 | 54 | 1 | 0 | 107 |
| `automation/` | 75 | 49 | 0 | 0 | 26 |
| `security/` | 20 | 7 | 0 | 0 | 13 |
| `studio/` | 27 | 27 | 0 | 0 | 0 |
| **total** | **454** | **253** | **6** | **0** | **195** |
| **total** | **455** | **253** | **6** | **0** | **196** |

## File-level triage — site counts

Expand All @@ -60,6 +60,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit

| File | Sites |
|---|---|
| `action-params.zod.ts` | 1 |
| `action.zod.ts` | 8 |
| `app.zod.ts` | 18 |
| `bulk-action.zod.ts` | 3 |
Expand All @@ -75,7 +76,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit
| `theme.zod.ts` | 6 |
| `view.zod.ts` | 53 |
| `widget.zod.ts` | 9 |
| **total** | **170** |
| **total** | **171** |

### `data/` — sites

Expand Down Expand Up @@ -155,23 +156,24 @@ over it is here.

### `ui/` — open

**49 strip of 170**, in 6 file(s).
**50 strip of 171**, in 7 file(s).

| File | Strip | Sites |
|---|---|---|
| `action-params.zod.ts` | 1 | 1 |
| `app.zod.ts` | 1 | 18 |
| `chart.zod.ts` | 2 | 8 |
| `component.zod.ts` | 29 | 29 |
| `i18n.zod.ts` | 5 | 6 |
| `view.zod.ts` | 3 | 53 |
| `widget.zod.ts` | 9 | 9 |
| **total** | **49** | **170** |
| **total** | **50** | **171** |

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 4 |
| unresolved — needs a per-schema verdict | 0 |
| wire / open — out of forced scope | 2 |
| wire / open — out of forced scope | 3 |
| no door — no carrier, ADR-0049 territory | 14 |
| no gate — carrier live, no parse | 29 |

Expand Down
Loading
Loading