Skip to content

Commit 7dcde27

Browse files
hotlongCopilot
andcommitted
fix(rest+runtime): plug anonymous /data/* leak and decouple runtime from service-cloud
Two related fixes that together close the CRM cross-account data leak reported at crm.objectos.app. ## Phase R - runtime ↔ service-cloud decoupling `packages/runtime` no longer depends on `@objectstack/service-cloud`. Cloud-side runtime helpers physically moved to `packages/runtime/src/cloud/`: - artifact-api-client / file-artifact-api-client - artifact-environment-registry / environment-registry - artifact-kernel-factory - auth-proxy-plugin - kernel-manager (+ tests) - objectos-stack apps/objectos/server/{ensure-local-identity, fs-app-bundle-resolver, single-project-plugin}.ts removed (logic absorbed into runtime/cloud or the per-project stack). apps/cloud and apps/objectos `objectstack.config.ts` updated to import the cloud helpers from `@objectstack/runtime/cloud` instead of `@objectstack/service-cloud`. ## Phase S - REST requireAuth gate (CF leak root fix) Anonymous requests to `/api/v1/data/*` were bypassing security checks entirely because plugin-security short-circuits when userId/roles are absent (kept for standalone public demos). Added an opt-in REST-layer gate so deployments that mount the auth tier reject anon callers *before* hitting ObjectQL. - `RestApiConfigSchema.requireAuth: z.boolean().default(false)` (packages/spec/src/api/rest-server.zod.ts) - `RestServer.enforceAuth(req, res, ctx)` returns 401 `{error: "unauthenticated"}` (OPTIONS preflight excluded) (packages/rest/src/rest-server.ts) - All 9 `/data/*` routes gated: list, read, create, update, delete, batch, createMany, updateMany, deleteMany. The 4 batch routes also now resolve and forward exec context, so authenticated batch ops finally get proper RBAC enforcement. - CLI auto-enables `requireAuth` whenever `tierEnabled('auth')` is true; stack-level `api.requireAuth` overrides (packages/cli/src/commands/serve.ts). Verified locally: - anon GET /api/v1/data/account -> 401 (was leaking data) - anon POST /api/v1/data/account/createMany -> 401 - authed cloud GET /data/sys_project -> 200 - authed cloud GET /cloud/projects -> 200 - packages/rest test suite (53 tests) -> all pass ROADMAP updated with S1 entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fcc92dc commit 7dcde27

30 files changed

Lines changed: 1650 additions & 1710 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,4 @@ playwright-report/
104104
test-results/
105105
playwright/.cache/
106106
apps/cloud/artifacts/
107+
studio-crm.png

ROADMAP.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ Code that exists and matches the intended architecture. Do not regress these.
3939
|:---|:---|
4040
| Organization CRUD + member/invitation system | [apps/studio/src/hooks/useSession.ts](apps/studio/src/hooks/useSession.ts) |
4141
| Project CRUD + per-project Turso/memory DB provisioning | [packages/services/service-tenant/](packages/services/service-tenant/) |
42-
| Per-project ObjectKernel with LRU cache | [packages/runtime/src/project-kernel-factory.ts](packages/runtime/src/project-kernel-factory.ts) |
43-
| Hostname-based routing: `sys_project.hostname` -> kernel resolution | [packages/runtime/src/environment-registry.ts](packages/runtime/src/environment-registry.ts) |
44-
| `ControlPlaneProxyDriver` - org-scoped data isolation | [packages/runtime/src/control-plane-proxy-driver.ts](packages/runtime/src/control-plane-proxy-driver.ts) |
42+
| Per-project ObjectKernel with LRU cache | [packages/runtime/src/cloud/kernel-manager.ts](packages/runtime/src/cloud/kernel-manager.ts) |
43+
| Hostname-based routing: `sys_project.hostname` -> kernel resolution | [packages/runtime/src/cloud/artifact-environment-registry.ts](packages/runtime/src/cloud/artifact-environment-registry.ts) |
44+
| `ControlPlaneProxyDriver` - org-scoped data isolation | [apps/cloud/src/control-plane-proxy-driver.ts](apps/cloud/src/control-plane-proxy-driver.ts) |
4545
| `AppCatalogService` - per-project app events -> org-scoped `sys_app` catalog | [packages/services/service-tenant/src/services/app-catalog.service.ts](packages/services/service-tenant/src/services/app-catalog.service.ts) |
4646
| TS -> JSON compile pipeline (`objectstack compile`) | [packages/cli/src/commands/compile.ts](packages/cli/src/commands/compile.ts) |
4747
| Zod -> JSON Schema publishing (`z.toJSONSchema`) - TS/JSON bridge | [packages/spec/scripts/build-schemas.ts](packages/spec/scripts/build-schemas.ts) |
@@ -56,6 +56,7 @@ Code that exists and matches the intended architecture. Do not regress these.
5656
| Studio Flow Viewer + Flow Test Runner + Flow Runs panel | [apps/studio/src/components/FlowViewer.tsx](apps/studio/src/components/FlowViewer.tsx) |
5757
| Automation: flow auto-discovery from ObjectQL registry | [packages/services/service-automation/src/plugin.ts](packages/services/service-automation/src/plugin.ts) |
5858
| **D1** ObjectOS metadata DB bridge removed - `MetadataPlugin` no longer registers `sys_metadata` / `sys_metadata_history` or auto-bridges ObjectQL to `DatabaseLoader` | [packages/metadata/src/plugin.ts](packages/metadata/src/plugin.ts) |
59+
| **S1** REST `requireAuth` gate (resolved 2026-05-17) — anonymous `/api/v1/data/*` returns 401 when `auth` tier is enabled (CRUD + batch routes both gated); auto-enabled by CLI when `tierEnabled('auth')`. Plugs the CF data-leak reported via crm.objectos.app. | [packages/rest/src/rest-server.ts](packages/rest/src/rest-server.ts), [packages/cli/src/commands/serve.ts](packages/cli/src/commands/serve.ts) |
5960

6061
---
6162

@@ -111,6 +112,24 @@ The deprecated `'platform'` and `'environment'` aliases have been removed
111112

112113
`ScopedServiceManager` and `SharedProjectPlugin` were added but their integration into the request path is incomplete. Either finish them or remove them.
113114

115+
### D6b - `packages/services/service-cloud` mixes runtime + control-plane (resolved 2026-05-17 — Phase R)
116+
117+
The package historically housed three unrelated concerns under one name:
118+
119+
1. **ObjectOS runtime** (artifact-fetching, per-project kernel manager, auth proxy) — used by `apps/objectos`
120+
2. **Cloud control plane** (multi-project orchestration, templates, local-identity seeding) — used by `apps/cloud`
121+
3. **Legacy single-project shells** (`single-project-plugin`, `shared-project-plugin`, `multi-project-plugin`) — pre-artifact-API code
122+
123+
This forced `apps/objectos` to depend on `@objectstack/service-cloud` — a "cloud service" package — even though objectos only needs a runtime that pulls compiled artifacts over HTTP. The dispatcher (`boot-stack.ts`) that bridged the two was an `if/else` over `OS_MODE`.
124+
125+
**Phase R — completed:**
126+
127+
- Runtime-side files (`artifact-api-client`, `artifact-environment-registry`, `artifact-kernel-factory`, `auth-proxy-plugin`, `kernel-manager`, `objectos-stack`, plus new `file-artifact-api-client`) live in [`packages/runtime/src/cloud/`](packages/runtime/src/cloud/) and are exported from `@objectstack/runtime`.
128+
- `apps/objectos/objectstack.config.ts` now imports `createObjectOSStack` from `@objectstack/runtime` directly. No `@objectstack/service-cloud` dependency.
129+
- `apps/cloud/objectstack.config.ts` calls `createCloudStack` from `@objectstack/service-cloud` directly. The `createBootStack` dispatcher is removed.
130+
- Dead duplicates (`packages/runtime/src/{kernel-manager,multi-project-plugin,project-kernel-factory,environment-registry,control-plane-proxy-driver}.ts`) and legacy plugins in `service-cloud` (`single-project-plugin`, `shared-project-plugin`, `multi-project-plugin`, …) are deleted.
131+
- Result: `service-cloud` shrinks from 5 294 LOC / 27 files to the cloud-only control-plane core; the runtime no longer has a "cloud" dependency.
132+
114133
### D7 - ✅ Plugin-config churn converged (resolved 2026-04-26)
115134

116135
Each plugin's manifest header + objects list now lives in a single canonical

apps/cloud/objectstack.config.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,36 @@
44
* ObjectStack Cloud — Host Configuration
55
*
66
* Booted by `objectstack dev` / `objectstack serve` (see `package.json`)
7-
* and by the Vercel serverless entrypoint (`server/index.ts`).
7+
* and by the Vercel / Cloudflare serverless entrypoints.
88
*
9-
* ## Boot mode
10-
*
11-
* This config is **cloud-only** — multi-project, control-plane connected,
12-
* with the Studio template registry and filesystem-backed app bundle
13-
* resolver wired in. Local single-project / standalone modes live in
14-
* `apps/objectos`. All boot orchestration lives in
15-
* `@objectstack/service-cloud`; this file only supplies the
16-
* apps/cloud-specific knobs (templates, app bundle resolution).
9+
* This config is **cloud-only** — multi-project control plane with the
10+
* Studio template registry and filesystem-backed app bundle resolver
11+
* wired in. `apps/objectos` is the runtime that talks to this control
12+
* plane; their boot configs are now fully independent (no shared
13+
* `createBootStack` dispatcher).
1714
*/
1815

19-
import { createBootStack } from '@objectstack/service-cloud';
16+
import { createCloudStack } from '@objectstack/service-cloud';
2017
import { createFsAppBundleResolver } from './server/fs-app-bundle-resolver.js';
2118
import { templateRegistry } from './server/templates/registry.js';
2219

23-
const config = await createBootStack({
24-
mode: 'cloud',
25-
cloud: {
26-
templates: templateRegistry,
27-
appBundles: createFsAppBundleResolver(),
28-
},
20+
const authSecret = process.env.AUTH_SECRET
21+
?? process.env.BETTER_AUTH_SECRET
22+
?? process.env.OS_AUTH_SECRET
23+
?? '';
24+
if (!authSecret) {
25+
throw new Error('apps/cloud: AUTH_SECRET (or BETTER_AUTH_SECRET / OS_AUTH_SECRET) is required.');
26+
}
27+
28+
const baseUrl = process.env.OS_BASE_URL
29+
?? process.env.BETTER_AUTH_URL
30+
?? `http://localhost:${process.env.PORT ?? '4000'}`;
31+
32+
const config = await createCloudStack({
33+
authSecret,
34+
baseUrl,
35+
templates: templateRegistry,
36+
appBundles: createFsAppBundleResolver(),
2937
});
3038

3139
export default config;
Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,50 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* ObjectStack Server — Host Configuration (runtime node)
4+
* ObjectStack ObjectOS — Host Configuration (runtime node)
55
*
66
* Booted by `objectstack dev` / `objectstack serve` (see `package.json`).
77
*
8+
* ObjectOS is a **stateless multi-tenant runtime**. The host kernel here is
9+
* a routing shell: every incoming request is resolved by hostname to a
10+
* project, the project's compiled artifact is fetched (either from a
11+
* control plane over HTTP or from a local file), and a per-project
12+
* ObjectKernel is built on demand and cached.
13+
*
814
* ## Boot modes
915
*
10-
* Default: single-project **local** mode. `pnpm dev` runs a self-contained
11-
* server with one control DB on disk and Studio UI in single-project mode
12-
* (no org/project picker — platform metadata only).
16+
* Default (no env): connects to a locally-running `apps/cloud` on
17+
* `http://localhost:4000`. Spin both up side-by-side for the natural
18+
* dev loop (`apps/cloud` for control plane + Studio, `apps/objectos`
19+
* for the actual runtime that serves project data).
1320
*
1421
* Override via env:
15-
* - `OS_CLOUD_URL=http://localhost:4000` — connect to a
16-
* locally-running `apps/cloud` (multi-project control plane).
17-
* - `OS_CLOUD_URL=https://cloud.objectstack.ai` — hosted
18-
* control plane.
19-
* - `OS_MODE=cloud` — boot the
20-
* multi-project control plane in this very process (lives in
21-
* `apps/cloud`).
22+
* - `OS_CLOUD_URL=https://cloud.objectstack.ai` — point at the
23+
* hosted ObjectStack Cloud (or any other control plane).
24+
* - `OS_CLOUD_URL=file` — file-backed single-project mode. Reads one
25+
* compiled artifact from `dist/objectstack.json` (or
26+
* `OS_ARTIFACT_PATH`) and serves it on every hostname. Use this
27+
* for smoke tests / one-shot demos where bringing up a separate
28+
* control plane is overkill.
2229
*
23-
* All boot orchestration lives in `@objectstack/service-cloud`. This file
24-
* only supplies the apps/objectos-specific knobs (filesystem app bundle
25-
* resolution).
30+
* No `@objectstack/service-cloud` dependency — `apps/objectos` only
31+
* needs the runtime + the artifact loader, both of which live in
32+
* `@objectstack/runtime`.
2633
*/
2734

28-
import { resolve as resolvePath, dirname } from 'node:path';
29-
import { fileURLToPath } from 'node:url';
30-
import { createBootStack } from '@objectstack/service-cloud';
31-
import { createFsAppBundleResolver } from './server/fs-app-bundle-resolver.js';
35+
import { createObjectOSStack } from '@objectstack/runtime';
3236

33-
const serverDir = dirname(fileURLToPath(import.meta.url));
34-
const dataDir = resolvePath(serverDir, '.objectstack/data');
35-
const localArtifactPath = process.env.OS_ARTIFACT_PATH
36-
?? resolvePath(serverDir, 'dist/objectstack.json');
37+
const cloudUrl = process.env.OS_CLOUD_URL?.trim() || 'http://localhost:4000';
3738

38-
const config = await createBootStack({
39-
runtime: {
40-
dataDir,
41-
artifactPath: localArtifactPath,
42-
appBundles: createFsAppBundleResolver(),
43-
// Default to single-project local mode (`cloudUrl: 'local'`) so
44-
// `pnpm dev` boots a self-contained server: one project, one
45-
// control DB on disk, Studio UI in single-project mode (no
46-
// org/project picker — platform metadata only).
47-
//
48-
// Override with:
49-
// - `OS_CLOUD_URL=http://localhost:4000` to connect to
50-
// a locally-running `apps/cloud` (multi-project control plane)
51-
// - `OS_CLOUD_URL=https://cloud.objectstack.ai` for the
52-
// hosted control plane
53-
cloudUrl: process.env.OS_CLOUD_URL ?? 'local',
54-
cloudApiKey: process.env.OS_CLOUD_API_KEY,
55-
},
39+
const config = await createObjectOSStack({
40+
controlPlaneUrl: cloudUrl,
41+
controlPlaneApiKey: process.env.OS_CLOUD_API_KEY,
42+
fileConfig: cloudUrl === 'file'
43+
? {
44+
artifactPath: process.env.OS_ARTIFACT_PATH,
45+
projectId: process.env.OS_PROJECT_ID,
46+
}
47+
: undefined,
5648
});
5749

5850
export default config;

apps/objectos/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
"@objectstack/service-ai": "workspace:*",
4747
"@objectstack/service-analytics": "workspace:*",
4848
"@objectstack/service-automation": "workspace:*",
49-
"@objectstack/service-cloud": "workspace:*",
5049
"@objectstack/service-feed": "workspace:*",
5150
"@objectstack/service-i18n": "workspace:*",
5251
"@objectstack/service-package": "workspace:*",

apps/objectos/server/ensure-local-identity.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

apps/objectos/server/fs-app-bundle-resolver.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

apps/objectos/server/single-project-plugin.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

packages/cli/src/commands/serve.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,11 +765,17 @@ export default class Serve extends Command {
765765
const apiConfig = (config as any).api ?? {};
766766
const enableProjectScoping = apiConfig.enableProjectScoping ?? false;
767767
const projectResolution = apiConfig.projectResolution ?? 'auto';
768+
// `requireAuth: true` rejects anonymous requests on `/api/v1/data/*`
769+
// with HTTP 401 before they reach ObjectQL. Default-on when the
770+
// stack opts in OR when the resolved tier set includes `auth`
771+
// (because anonymous data access is almost never desirable when
772+
// auth is enabled). Apps can override via stack `api.requireAuth`.
773+
const requireAuth = apiConfig.requireAuth ?? (tierEnabled('auth') ? true : false);
768774

769775
try {
770776
const { createRestApiPlugin } = await import('@objectstack/rest');
771777
await kernel.use(
772-
createRestApiPlugin({ api: { api: { enableProjectScoping, projectResolution } } as any }),
778+
createRestApiPlugin({ api: { api: { enableProjectScoping, projectResolution, requireAuth } } as any }),
773779
);
774780
trackPlugin('RestAPI');
775781
} catch (e: any) {

0 commit comments

Comments
 (0)