Skip to content
Draft
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
615 changes: 615 additions & 0 deletions docs/260212-personal-server-refactor.md

Large diffs are not rendered by default.

711 changes: 711 additions & 0 deletions docs/260213-dataconnect-integration-refactor.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"dependencies": {
"@opendatalabs/personal-server-ts-core": "*",
"@opendatalabs/personal-server-ts-runtime": "*",
"@opendatalabs/personal-server-ts-server": "*"
}
}
21 changes: 21 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,29 @@ export {
type LoadConfigOptions,
} from "@opendatalabs/personal-server-ts-core/config";
export type { ServerConfig } from "@opendatalabs/personal-server-ts-core/schemas";
export {
RuntimeStateMachine,
type RuntimeState,
type StateTransitionEvent,
} from "@opendatalabs/personal-server-ts-core/lifecycle";
export {
createServer,
type CreateServerOptions,
type ServerContext,
} from "@opendatalabs/personal-server-ts-server";
export {
writePidFile,
readPidFile,
removePidFile,
checkRunningServer,
Supervisor,
daemonize,
resolveSocketPath,
createIpcServer,
IpcClient,
type ServerMetadata,
type SupervisorOptions,
type DaemonizeOptions,
type IpcClientOptions,
type IpcResponse,
} from "@opendatalabs/personal-server-ts-runtime";
6 changes: 5 additions & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts"],
"references": [{ "path": "../core" }, { "path": "../server" }]
"references": [
{ "path": "../core" },
{ "path": "../runtime" },
{ "path": "../server" }
]
}
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@
"./test-utils": {
"types": "./dist/test-utils/index.d.ts",
"import": "./dist/test-utils/index.js"
},
"./lifecycle": {
"types": "./dist/lifecycle/index.d.ts",
"import": "./dist/lifecycle/index.js"
}
},
"scripts": {
Expand Down
120 changes: 120 additions & 0 deletions packages/core/src/lifecycle/endpoint-ownership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Endpoint ownership matrix.
*
* Defines which routes are served on which transport (HTTP vs IPC)
* and what auth model applies to each.
*/

export type Transport = "http" | "ipc" | "both";

export type AuthModel =
| "none"
| "local-only"
| "web3-signed"
| "web3-signed+owner"
| "web3-signed+builder"
| "web3-signed+builder+grant";

export interface EndpointSpec {
method: "GET" | "POST" | "PUT" | "DELETE";
path: string;
transport: Transport;
auth: AuthModel;
description: string;
}

/**
* Canonical endpoint ownership matrix.
*
* HTTP transport: routes reachable through FRP tunnel by builders.
* IPC transport: routes reachable only via UDS by local clients (DataBridge/CLI).
*/
export const ENDPOINT_MATRIX: readonly EndpointSpec[] = [
// --- Protocol routes (HTTP) ---
{
method: "GET",
path: "/health",
transport: "http",
auth: "none",
description: "Health check and server info",
},
{
method: "GET",
path: "/v1/data",
transport: "http",
auth: "web3-signed+builder",
description: "List available scopes",
},
{
method: "GET",
path: "/v1/data/:scope/versions",
transport: "http",
auth: "web3-signed+builder",
description: "List versions for a scope",
},
{
method: "GET",
path: "/v1/data/:scope",
transport: "http",
auth: "web3-signed+builder+grant",
description: "Read data for a scope (grant required)",
},
{
method: "POST",
path: "/v1/grants/verify",
transport: "http",
auth: "none",
description: "Verify a grant EIP-712 signature",
},

// --- Admin routes (IPC) ---
{
method: "POST",
path: "/v1/data/:scope",
transport: "ipc",
auth: "none",
description: "Ingest data (local-only)",
},
{
method: "DELETE",
path: "/v1/data/:scope",
transport: "ipc",
auth: "none",
description: "Delete scope data (local-only)",
},
{
method: "GET",
path: "/v1/grants",
transport: "ipc",
auth: "none",
description: "List grants (local-only)",
},
{
method: "POST",
path: "/v1/grants",
transport: "ipc",
auth: "none",
description: "Create grant (local-only)",
},
{
method: "GET",
path: "/v1/access-logs",
transport: "ipc",
auth: "none",
description: "Read access logs (local-only)",
},
{
method: "GET",
path: "/v1/sync",
transport: "ipc",
auth: "none",
description: "Get sync status (local-only)",
},
{
method: "POST",
path: "/v1/sync",
transport: "ipc",
auth: "none",
description: "Trigger sync (local-only)",
},
] as const;
12 changes: 12 additions & 0 deletions packages/core/src/lifecycle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export {
RuntimeStateMachine,
type RuntimeState,
type StateTransitionEvent,
type StateChangeListener,
} from "./state-machine.js";
export {
ENDPOINT_MATRIX,
type Transport,
type AuthModel,
type EndpointSpec,
} from "./endpoint-ownership.js";
134 changes: 134 additions & 0 deletions packages/core/src/lifecycle/state-machine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, it, expect, vi } from "vitest";
import {
RuntimeStateMachine,
type StateTransitionEvent,
} from "./state-machine.js";

describe("RuntimeStateMachine", () => {
it("starts in uninitialized state", () => {
const sm = new RuntimeStateMachine();
expect(sm.getState()).toBe("uninitialized");
});

it("transitions through the happy path", () => {
const sm = new RuntimeStateMachine();
sm.transition("starting");
expect(sm.getState()).toBe("starting");

sm.transition("ready-local");
expect(sm.getState()).toBe("ready-local");

sm.transition("ready-authenticated");
expect(sm.getState()).toBe("ready-authenticated");

sm.transition("shutting-down");
expect(sm.getState()).toBe("shutting-down");

sm.transition("stopped");
expect(sm.getState()).toBe("stopped");
});

it("allows transition to error from any non-terminal state", () => {
for (const state of [
"uninitialized",
"starting",
"ready-local",
"ready-authenticated",
"shutting-down",
] as const) {
const sm = new RuntimeStateMachine();
// Walk to the target state
if (state === "starting") sm.transition("starting");
if (state === "ready-local") {
sm.transition("starting");
sm.transition("ready-local");
}
if (state === "ready-authenticated") {
sm.transition("starting");
sm.transition("ready-local");
sm.transition("ready-authenticated");
}
if (state === "shutting-down") {
sm.transition("starting");
sm.transition("ready-local");
sm.transition("shutting-down");
}

expect(sm.canTransition("error")).toBe(true);
sm.transition("error");
expect(sm.getState()).toBe("error");
}
});

it("rejects invalid transitions", () => {
const sm = new RuntimeStateMachine();
expect(sm.canTransition("ready-authenticated")).toBe(false);
expect(() => sm.transition("ready-authenticated")).toThrow(
"Invalid state transition: uninitialized -> ready-authenticated",
);
});

it("does not allow transitions from stopped", () => {
const sm = new RuntimeStateMachine();
sm.transition("starting");
sm.transition("ready-local");
sm.transition("shutting-down");
sm.transition("stopped");

expect(sm.canTransition("starting")).toBe(false);
expect(sm.canTransition("error")).toBe(false);
});

it("allows recovery from error to starting", () => {
const sm = new RuntimeStateMachine();
sm.transition("error");
expect(sm.canTransition("starting")).toBe(true);
sm.transition("starting");
expect(sm.getState()).toBe("starting");
});

it("allows transition from error to stopped", () => {
const sm = new RuntimeStateMachine();
sm.transition("error");
sm.transition("stopped");
expect(sm.getState()).toBe("stopped");
});

it("notifies listeners on state change", () => {
const sm = new RuntimeStateMachine();
const events: StateTransitionEvent[] = [];
sm.onStateChange((e) => events.push(e));

sm.transition("starting", "boot");
sm.transition("ready-local");

expect(events).toHaveLength(2);
expect(events[0].from).toBe("uninitialized");
expect(events[0].to).toBe("starting");
expect(events[0].reason).toBe("boot");
expect(events[1].from).toBe("starting");
expect(events[1].to).toBe("ready-local");
expect(events[1].reason).toBeUndefined();
});

it("unsubscribes listener", () => {
const sm = new RuntimeStateMachine();
const listener = vi.fn();
const unsub = sm.onStateChange(listener);

sm.transition("starting");
expect(listener).toHaveBeenCalledTimes(1);

unsub();
sm.transition("ready-local");
expect(listener).toHaveBeenCalledTimes(1);
});

it("allows shutting down from ready-local (skipping auth)", () => {
const sm = new RuntimeStateMachine();
sm.transition("starting");
sm.transition("ready-local");
sm.transition("shutting-down");
expect(sm.getState()).toBe("shutting-down");
});
});
Loading