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
8 changes: 8 additions & 0 deletions .changeset/v17-docs-sweep-run-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
---

Docs-only: v17 docs sweep run 4 (rc.1 catch-up; closes the missing-run-2
coverage gap over `a641d10..0f9faa2`). Fixes the retired `api.requireAuth`
opt-out still being taught in permissions/forms docs (#3963) and the
half-updated kernel services checklist (16 → 15 services after the `workflow`
slot retirement, #4451). Releases nothing.
43 changes: 21 additions & 22 deletions content/docs/kernel/services-checklist.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ the code wins. See [Plugins & Packages](/docs/plugins/packages) for the full
package catalog.
</Callout>

The ObjectStack protocol defines **16 kernel services** registered via the `CoreServiceName` enum (the never-implemented `graphql` entry was removed in v17). Each service maps to a set of protocol methods governed by its per-domain contract (`DataProtocol`, `MetadataProtocol`, ...) — the transitional `ObjectStackProtocol` composition alias was dissolved in v17 (ADR-0076 D9); capability availability comes from the runtime discovery `services` registry.
The ObjectStack protocol defines **15 kernel services** registered via the `CoreServiceName` enum (v17 removed the never-implemented `graphql` entry and retired the never-filled `workflow` slot, #4451). Each service maps to a set of protocol methods governed by its per-domain contract (`DataProtocol`, `MetadataProtocol`, ...) — the transitional `ObjectStackProtocol` composition alias was dissolved in v17 (ADR-0076 D9); capability availability comes from the runtime discovery `services` registry.

**Key architecture principle**: the kernel guarantees only **data** and **metadata**, and even those are filled by packages (`@objectstack/objectql`, `@objectstack/metadata`) rather than baked in — the kernel's own contribution is an in-memory fallback for the `core` slots that have one (`metadata`, `cache`, `queue`, `job`, `i18n` — **not** `auth`). Everything else — including **auth** and **automation** — is delivered by plugins. `@objectstack/objectql` is an example kernel implementation to get the basic API running; production kernels will be rebuilt as separate plugins.

Expand Down Expand Up @@ -50,7 +50,7 @@ The ObjectStack protocol defines **16 kernel services** registered via the `Core
├─────────────────────────────────────────────────────────┤
│ Plugin Layer │
│ All other services: analytics, auth, automation, │
workflow, ui, realtime, notification, ai, i18n, │
│ ui, realtime, notification, ai, i18n,
│ search, file-storage, cache, queue, job │
│ │
│ Discovery API reports availability per service │
Expand All @@ -70,17 +70,16 @@ The ObjectStack protocol defines **16 kernel services** registered via the `Core
| 3 | **analytics** | `optional` | 2 | ❌ Plugin Required | `@objectstack/service-analytics` |
| 4 | **auth** | `core` | — | ✅ Implemented | `@objectstack/plugin-auth` |
| 5 | **ui** | `optional` | 5 | ❌ Nothing fills this slot | `@objectstack/metadata-protocol` — `/ui/view` is served by its `protocol` service, not by a `ui` service |
| 6 | **workflow** | `optional` | 3 | ❌ Nothing ships | — |
| 7 | **automation** | `optional` | 1 | ❌ Plugin Required | `@objectstack/service-automation` |
| 8 | **realtime** | `optional` | 6 | ❌ Plugin Required (in-process only — no HTTP/WS route is mounted) | `@objectstack/service-realtime` |
| 9 | **notification** | `optional` | 7 | ❌ Plugin Required | `@objectstack/service-messaging` |
| 10 | **ai** | `optional` | — | ❌ Nothing ships in this repo | `@objectstack/service-ai` (Cloud/EE — not installable, so the table entry is `null`) |
| 11 | **i18n** | `core` | 3 | ✅ Built-in (in-memory fallback) | `@objectstack/service-i18n` |
| 12 | **file-storage** | `optional` | — | ❌ Plugin Required | `@objectstack/service-storage` |
| 13 | **search** | `optional` | — | ❌ Nothing ships | — |
| 14 | **cache** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-cache` |
| 15 | **queue** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-queue` |
| 16 | **job** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-job` |
| 6 | **automation** | `optional` | 1 | ❌ Plugin Required | `@objectstack/service-automation` |
| 7 | **realtime** | `optional` | 6 | ❌ Plugin Required (in-process only — no HTTP/WS route is mounted) | `@objectstack/service-realtime` |
| 8 | **notification** | `optional` | 7 | ❌ Plugin Required | `@objectstack/service-messaging` |
| 9 | **ai** | `optional` | — | ❌ Nothing ships in this repo | `@objectstack/service-ai` (Cloud/EE — not installable, so the table entry is `null`) |
| 10 | **i18n** | `core` | 3 | ✅ Built-in (in-memory fallback) | `@objectstack/service-i18n` |
| 11 | **file-storage** | `optional` | — | ❌ Plugin Required | `@objectstack/service-storage` |
| 12 | **search** | `optional` | — | ❌ Nothing ships | — |
| 13 | **cache** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-cache` |
| 14 | **queue** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-queue` |
| 15 | **job** | `core` | — | ✅ Built-in (in-memory fallback) | `@objectstack/service-job` |

<Callout type="info">
The Provider column mirrors `CORE_SERVICE_PROVIDER` in
Expand Down Expand Up @@ -319,7 +318,7 @@ registered under their own names (`security.permissions`, `security.rls`,

---

## 5–7. Business Services
## 5–6. Business Services

### 5. ui Service — 5 declared methods, none routed ❌
`listViews`, `getView`, `createView`, `updateView`, `deleteView`
Expand All @@ -337,7 +336,7 @@ the domain answers **501** with that remedy spelled out, not a generic "install
plugin".
</Callout>

### 6. workflow Service — retired in v17
### Retired in v17: the `workflow` slot
The slot, its `IWorkflowService` contract and the three `WorkflowProtocol`
methods (`getWorkflowConfig`, `getWorkflowState`, `workflowTransition`) were
removed in [#4451](https://github.com/objectstack-ai/objectstack/issues/4451):
Expand All @@ -350,17 +349,17 @@ flow nodes on the approvals runtime (ADR-0019 — decisions via
`@objectstack/plugin-approvals`), and record-triggered automation is lifecycle
hooks + `record_change` flows.

### 7. automation Service — 1 method ✅ `@objectstack/service-automation`
### 6. automation Service — 1 method ✅ `@objectstack/service-automation`
`triggerAutomation`
Trigger engine, event triggers from ObjectQL hooks, flow executor, scheduled triggers.
The `/automation` dispatcher domain gates on `isServiceServeable`, so a slot filled by
a self-declared stub answers as an empty one.

---

## 8–11. Communication Services
## 7–10. Communication Services

### 8. realtime — 6 methods · `@objectstack/service-realtime`
### 7. realtime — 6 methods · `@objectstack/service-realtime`
`realtimeConnect`, `realtimeDisconnect`, `realtimeSubscribe`, `realtimeUnsubscribe`, `setPresence`, `getPresence`

<Callout type="warn">
Expand All @@ -374,7 +373,7 @@ in-process only and no HTTP/WS surface is mounted. Re-advertising waits on a rea
transport.
</Callout>

### 9. notification — 7 methods · `@objectstack/service-messaging`
### 8. notification — 7 methods · `@objectstack/service-messaging`
`registerDevice`, `unregisterDevice`, `getNotificationPreferences`, `updateNotificationPreferences`, `listNotifications`, `markNotificationsRead`, `markAllNotificationsRead`

The slot name is `notification` (singular) and the package that fills it shares no word
Expand All @@ -386,7 +385,7 @@ on `INotificationService` — a send-only provider (SMTP, Twilio, a webhook) fil
slot legitimately without an inbox, and each route probes its own method and answers
501 when absent.

### 10. ai — contract removed ❌
### 9. ai — contract removed ❌
~~`aiNlq`, `aiSuggest`, `aiInsights`~~

<Callout type="warn">
Expand All @@ -402,7 +401,7 @@ The `ai` slot still exists in `CoreServiceName`, but nothing in this repo fills
it (`CORE_SERVICE_PROVIDER.ai` is `null`).
</Callout>

### 11. i18n — 3 methods
### 10. i18n — 3 methods
`getLocales`, `getTranslations`, `getFieldLabels`

**Service Name**: `i18n` · **Criticality**: `core`
Expand Down Expand Up @@ -467,7 +466,7 @@ AppPlugin will:

---

## 12–16. Infrastructure Services
## 11–15. Infrastructure Services

`cache`, `queue`, and `job` are `core` services: like `i18n`, the kernel auto-injects an in-memory fallback when no plugin registers them (see `CORE_FALLBACK_FACTORIES` in `packages/core/src/fallbacks/`). The `optional` services (`file-storage`, `search`) stay disabled until a plugin provides them.

Expand Down
2 changes: 1 addition & 1 deletion content/docs/permissions/authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ site — the file you read when behavior surprises you.

| # | Gate | What it decides | Enforcement site | Failure direction |
|---|---|---|---|---|
| 1 | **Anonymous deny** | No identity → HTTP 401. **Uniform across every HTTP surface that reaches object data** (#2567): REST `/data` and the metadata endpoints (`/meta`) — the raw-hono standard `/data` routes left the matrix when that duplicate surface was deleted in v17 (#4073), and the dispatcher GraphQL endpoint left when the GraphQL surface was removed (`/graphql` now 404s) — one shared decision, so a caller denied on `/data` can't read the same rows through a sibling door. **Default-on** (ADR-0056 D2): serving the whole data plane publicly requires an explicit `api.requireAuth: false` opt-out, which logs a boot warning. Narrower public surfaces do **not** need it — each derives its own authorization from a declaration rather than from the deployment posture: control plane (`/auth`, `/health`, `/discovery`) is allow-listed; public form submission carries a `publicFormGrant` (ADR-0056 Option A); share-links validate their token then read as SYSTEM; and an anonymous **GET** of the book/doc read surface is admitted so `book.audience: 'public'` works under the secure default, with the ADR-0046 §6.7 audience gate — `'public'` only, fail-closed — doing the authorizing (#3963). | `packages/core/src/security/anonymous-deny.ts` `shouldDenyAnonymous` — called by `rest-server.ts` `enforceAuth` and the dispatcher `handleMetadata`/`handleAI` (default in `packages/spec/src/api/rest-server.zod.ts`); a source-enumerating ratchet in `authz-conformance.test.ts` fails CI if a new surface ships ungated | fail-closed |
| 1 | **Anonymous deny** | No identity → HTTP 401. **Uniform across every HTTP surface that reaches object data** (#2567): REST `/data` and the metadata endpoints (`/meta`) — the raw-hono standard `/data` routes left the matrix when that duplicate surface was deleted in v17 (#4073), and the dispatcher GraphQL endpoint left when the GraphQL surface was removed (`/graphql` now 404s) — one shared decision, so a caller denied on `/data` can't read the same rows through a sibling door. **Unconditional** (#3963, closing out ADR-0056 D2): the `api.requireAuth: false` opt-out was retired in v17 — the key is tombstoned, so authoring it is a parse error, and anonymous callers are denied on every data surface with no deployment-level escape hatch. Narrower public surfaces do **not** need it — each derives its own authorization from a declaration rather than from the deployment posture: control plane (`/auth`, `/health`, `/discovery`) is allow-listed; public form submission carries a `publicFormGrant` (ADR-0056 Option A); share-links validate their token then read as SYSTEM; and an anonymous **GET** of the book/doc read surface is admitted so `book.audience: 'public'` works under the secure default, with the ADR-0046 §6.7 audience gate — `'public'` only, fail-closed — doing the authorizing (#3963). | `packages/core/src/security/anonymous-deny.ts` `shouldDenyAnonymous` — called by `rest-server.ts` `enforceAuth` and the dispatcher `handleMetadata`/`handleAI` (default in `packages/spec/src/api/rest-server.zod.ts`); a source-enumerating ratchet in `authz-conformance.test.ts` fails CI if a new surface ships ungated | fail-closed |
| 2 | **Public-form grant** | An anonymous form submission carries a declaration-derived `publicFormGrant` authorizing ONLY create + read-back on the form's declared target object — never anything else (ADR-0056 Option A). No guest-portal configuration needed (anonymous principals hold the `guest` position). | `packages/plugins/plugin-security/src/security-plugin.ts` (ObjectQL middleware) | scope-limited allow |
| 3 | **Object CRUD** | `allowRead/Create/Edit/Delete` (+ the destructive lifecycle class `allowTransfer/Restore/Purge`, gated ahead of the M2 operations — #1883) resolved across the caller's permission sets. | `packages/plugins/plugin-security/src/permission-evaluator.ts` `checkObjectPermission` | fail-closed 403 |
| 4 | **OWD / sharing** | Org-wide default (`private` / `public_read` / `public_read_write` / `controlled_by_parent`; **unset or unknown ⇒ `private`, fail-closed** — ADR-0090 D1) plus the external dial (`externalSharingModel`, ADR-0090 D11), manual record shares, criteria sharing rules (owner-type rules were removed from the authoring surface in v17 rather than left declared-but-skipped — [Sharing Rules](/docs/permissions/sharing-rules#recipient-types)), business-unit hierarchy widening (ADR-0057 D5: scope-depth hierarchy lives on `sys_business_unit`, not positions). | `packages/plugins/plugin-sharing/src/sharing-service.ts` + `sharing-rule-service.ts` | fail-closed to owner-only |
Expand Down
5 changes: 3 additions & 2 deletions content/docs/permissions/explain.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ through.

## Who may ask

The endpoint is **authenticated-only** — even on `requireAuth: false`
deployments (an access report is sensitive even about oneself). Beyond that,
The endpoint is **authenticated-only** (an access report is sensitive even
about oneself; since v17 anonymous access to object data is always denied
anyway — the `requireAuth: false` opt-out was retired, #3963). Beyond that,
authorization lives in the *service*, so REST and in-process callers share one
rule:

Expand Down
5 changes: 3 additions & 2 deletions content/docs/ui/forms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ Only the spec'd whitelist of form fields is accepted; everything else (status, o
> from the form's own declaration — `{ object: <the form's target object> }` —
> and the SecurityPlugin authorizes **only** create + the immediate read-back on
> exactly that object, never anything else and never the anonymous fall-open. So
> public forms work under secure-by-default (`requireAuth: true`) with **no**
> public forms work under the unconditional anonymous-deny posture (v17
> retired the `requireAuth: false` opt-out, #3963) with **no**
> `guest_portal` permission set. The `guest_portal` permission set + `anonymous` flag
> are still attached for **back-compat** (object hooks that detect a guest via a
> falsy `ctx.user?.id`), but they are no longer the authorization mechanism.
Expand Down Expand Up @@ -231,7 +232,7 @@ The companion `GET /api/v1/forms/:slug` route returns `500 FORM_RESOLVE_FAILED`

### Auth model

- Neither route calls `enforceAuth`, so they work even when the project is configured with `requireAuth: true`.
- Neither route calls `enforceAuth`, so they work under the always-on anonymous-deny default (there is no `requireAuth` knob to configure since v17).
- The execution context handed to ObjectQL is `{ publicFormGrant: { object }, permissions: ['guest_portal'], anonymous: true }` with no `userId`. The Security plugin honors `publicFormGrant` first — a create + read-back grant scoped to exactly the declared object — so authorization holds even without a `guest_portal` profile. `permissions: ['guest_portal']` is retained for back-compat.
- No CSRF or auth header is needed; embed the form on any domain.

Expand Down
58 changes: 58 additions & 0 deletions docs/v17-docs-sweep.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,62 @@ merely *references* changed code, use the `docs-accuracy-audit` workflow the
found seventeen. Grep the pattern repo-wide (including `skills/`), do not work a
fingerprint list file-by-file.

### 2026-08-02 — run 4 (rc.1 catch-up; closes the run-2 gap)

- **Watermark:** framework `ff17642` (origin/main, post-#4590) · spec
`17.0.0-rc.1` · `PROTOCOL_VERSION = '17.0.0'`.
- **Coverage note:** the log holds run 1 (`a641d10`) and run 3 (version-number
pass only) — **no run 2 exists**, so the 362 changesets added
`a641d10..0f9faa2` had never been swept with removal fingerprints. This run's
delta is therefore `a641d10..HEAD` (435 new changesets, ~48 breaking), not
just the run-3 watermark.
- **Fingerprints added:** `api.requireAuth` (retired outright, #3963 — a doc
presenting the opt-out as available is drift); `workflow` kernel service slot
(#4451); standalone `validation` metadata kind (#4509, ADR-0088); `job`
runtime create/org override (#4509); flow `script` `config.actionType` /
`.template` / `.recipients` / `.variables` / inline `script` (#4343);
`query.cursor` / `query.distinct` / `joins` / `windowFunctions` /
nested-select (#4286, #4196); `EnhancedApiError.fieldErrors` (ADR-0114 D4);
`BatchOptions.validateOnly` (#4052); `registerStandardEndpoints` (#4073);
`readReplicas` (#4481); connector template / trigger-registry Connector
clusters (#4500, #4503); dual-source names (`WebhookConfig` from `./api`,
`CacheStrategyEnum`, `MetadataFormat` `'yml'/'ts'/'js'` aliases,
contracts `ShareRecipientType` → `RecordShareRecipientType`; #4537–#4539,
#4572).
- **Fixed (drift → corrected):**
- `permissions/authorization.mdx` — gate 1 still said serving the data plane
publicly "requires an explicit `api.requireAuth: false` opt-out"; the
opt-out is retired and tombstoned (#3963).
- `permissions/explain.mdx` — "even on `requireAuth: false` deployments", a
posture that no longer exists.
- `ui/forms.mdx` — two mentions of configuring `requireAuth`; rewritten
against the unconditional anonymous-deny posture.
- `kernel/services-checklist.mdx` — partially updated at change time: §6 and
the provider callout documented the `workflow` retirement, but the page
still claimed **16** services, listed `workflow` in the architecture
diagram, and carried its live row in the summary table. Now 15 services,
row removed, tables and section numbering re-aligned (enum verified:
15 members).
- **Judged, not drift (skip re-checking):** `actionType` in
`deployment/validating-metadata.mdx` / `skills/objectstack-ui` (dashboard
action buttons — same key name, different surface); flows.mdx / automation
skill (document the #4343 retirement); `bulkActionDefs` in ui docs (teach
the new typed shape, #4457); `having` (documents "enforced since #4286");
`fieldErrors` in api docs (documents the tombstone); `windowFunctions` /
`query.distinct` hits (all "removed in spec 17" notices, run 3 fixed their
version numbers); `maxRetries` in flows.mdx (explicit count — the required
posture, #4247).
- **Zero-hit fingerprints (nothing to fix):** `validateOnly`,
`registerStandardEndpoints`, `readReplicas`, `CacheStrategyEnum`,
`WebhookConfig`, `MetadataFormat` aliases, `ShareRecipientType`, standalone
`.validation.ts`, job `allowRuntimeCreate`, connector template cluster,
`storage.notNull` misclaims, plugin lifecycle-hook family.
- **Method note:** docs-at-change-time discipline improved markedly in this
window — most removals landed with their retirement callouts already written
(flows.mdx #4343, query-syntax #4286, error-catalog ADR-0114). The drift
that survives is the *second-order* kind: a page partially updated (checklist
count vs narrative) or a cross-reference in a page the change didn't touch
(requireAuth in explain/forms). Grep the fingerprint repo-wide even when the
primary page looks done.

<!-- Append the next run above this line, newest last. -->
Loading