@phreshos/server 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Zohayr SLILEH
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md CHANGED
@@ -17,30 +17,96 @@ It uses the domain objects and shared contracts from `@phreshos/core` through
17
17
  a peer dependency. It does not redefine those objects, own client-side
18
18
  capabilities, or contain host and transport implementations.
19
19
 
20
- Its `Host` contract exposes authoritative Program and Process discovery,
21
- runtime Program creation, lifecycle events, and publicly served values.
20
+ Its `Host` contract exposes the observable system Theme with replacement
21
+ authority, authoritative Program and Process discovery, runtime Program
22
+ creation, lifecycle events, and publicly served values. `host.theme.snapshot()`
23
+ explicitly and asynchronously reads the current value,
24
+ `subscribe("change", listener)` receives only complete replacements published
25
+ after registration, and `update()` asynchronously validates and replaces the
26
+ Theme through the system authority. A subscription has no initial delivery or
27
+ replay.
28
+
29
+ `host.signInWallpaper` and `host.desktopWallpaper` are independent direct Host
30
+ capabilities. A served image or HTML file is selected by its generated
31
+ filename, while the desktop may instead select a Program that declares a
32
+ Client:
33
+
34
+ ```ts
35
+ const served = await host.serve(file)
36
+
37
+ await host.signInWallpaper.set(served.file)
38
+ await host.desktopWallpaper.set(served.file)
39
+ await host.desktopWallpaper.setProgram(program, {
40
+ name: "wallpaper",
41
+ server: false,
42
+ client: { location: "/ambient" },
43
+ options: { mode: "calm" }
44
+ })
45
+
46
+ await host.signInWallpaper.remove()
47
+ await host.desktopWallpaper.remove()
48
+ ```
49
+
50
+ Selecting or removing a desktop choice exits the previous wallpaper Process.
51
+ Its Client and visual state are system-managed; complete Process exit remains
52
+ available and reveals the bundled desktop fallback.
22
53
 
23
54
  Its JavaScript entry point adapts the Process IPC boundary to these contracts.
24
55
  The SDK owns callbacks, waits, queues, and their cleanup; the boundary owns
25
56
  only the forwarding registrations requested by the SDK.
26
57
 
58
+ Importing the SDK injects no message into the child process. The endpoint may
59
+ announce its readiness outward, but identity, Theme, Process, readiness,
60
+ lifecycle, and application values enter only in response to an explicit request
61
+ or a live registration made by Program code.
62
+
27
63
  `Current` combines navigation into the executing Server's Process with its
28
- inbound subscription and answer Channel. The paired Client is explicitly named
64
+ Channel and answer registry. The paired Client is explicitly named
29
65
  as `current.client`; its publishing, existence, lifecycle, and Window operations
30
66
  never masquerade as properties of `current`. `current.stop()` stops the
31
67
  executing Server, while complete Process exit remains available only through
32
- `current.process()`.
68
+ `current.process()`. It is the canonical Process-owned handle, so
69
+ `current.client === (await current.process()).client`.
70
+ Endpoint `process()` navigation is asynchronous; contextual ownership is
71
+ requested only when navigation needs it and then retained by the SDK.
72
+
73
+ All domain handles are canonical within this Server runtime's JavaScript realm.
74
+ Lookup, navigation, event payloads, and message metadata reuse the same weakly
75
+ retained handle. Server and Client handles remain stable for their Process
76
+ lifetime. Each Client permanently owns one synchronous `window` capability,
77
+ whose operations address that Client's current live presentation state.
78
+
79
+ An `under` or `over` Client Window may own an authoritative host Surface through
80
+ `client.window.surface`. It is command-only: `set()` or `remove()` changes the
81
+ server-owned value, while Program endpoints cannot read or subscribe to it.
82
+ Radius may be a
83
+ nonnegative pixel number, `"full"`, or a semantic `ScaleLevel` resolved by the
84
+ desktop against its current Theme. Ordinary `window` and system `wallpaper`
85
+ layers reject Surface mutations.
33
86
 
34
- The package provides two contextual values rather than constructors:
87
+ The package provides two contextual runtime entry points:
35
88
 
36
89
  ```ts
37
90
  import { host, current } from "@phreshos/server"
38
91
  ```
39
92
 
93
+ It also re-exports the shared Core runtime classes—`Program`, `Process`,
94
+ `Endpoint`, `Server`, and `Client`—and refines the handles returned through
95
+ them. These are the same domain classes used by the Client SDK, so
96
+ `instanceof Server` and `instanceof Client` retain one meaning. `Window`, like
97
+ `ClientTraffic`, is a type-only capability owned by Client and has no
98
+ independent `instanceof` identity.
99
+
40
100
  The current Server's Channel is composed directly into `current`. It receives
41
- addressed events and registers answerers for questions arriving at this Server.
42
- Answer registration returns its sole cleanup function. Publishing to the paired
43
- Client belongs to `current.client`.
101
+ addressed events, emits destinationless events through `publish()`, and
102
+ registers answerers for questions arriving at this Server. An answerer that
103
+ omits its return value successfully answers with `undefined`. Answer
104
+ registration returns its sole cleanup function. Directed publishing to the
105
+ paired Client belongs to `current.client`.
106
+
107
+ An Endpoint handle is also a selective source: `endpoint.subscribe()` follows
108
+ destinationless events emitted by that Endpoint. Its `traffic` property remains
109
+ reserved for directed publications, questions, and answers.
44
110
 
45
111
  `server.ask()` does not route a question before the addressed Server
46
112
  incarnation is ready. The Server SDK owns one deadline across readiness and the
@@ -52,3 +118,28 @@ also expose `path()` and traversal-safe `resolve()`, because filesystem work is
52
118
  performed locally in the Server SDK after the Host supplies only the area root.
53
119
  Object descriptions passed to `host.createProgram()` therefore require an
54
120
  explicit absolute storage root as well as at least one declared Endpoint.
121
+
122
+ `program.icon()` requests a guaranteed PNG `Blob` in `small`, `medium`, or
123
+ `large` form without exposing the Program's source path or the system's private
124
+ asset-hosting address. Omitting the size selects `medium`.
125
+
126
+ Installed Programs may persist one ordinary Process launch for the next system
127
+ startup. This capability exists only in the Server SDK:
128
+
129
+ ```ts
130
+ await program.startup.enable({
131
+ name: "main",
132
+ server: true,
133
+ client: false,
134
+ options: { mode: "worker" }
135
+ })
136
+
137
+ const launch = await program.startup.get() // Launch | null
138
+
139
+ await program.startup.disable()
140
+ ```
141
+
142
+ The system validates this through the same launch contract as
143
+ `createProcess()`. Setting startup does not create a Process immediately.
144
+ `uninstall(false)` preserves the configuration but makes it inactive until the
145
+ Program is installed again; removing everything deletes it.
package/dist/channel.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import type { Channel as CoreChannel, ChannelMessage, Cleanup } from "@phreshos/core";
2
- export type Answerer<Payload = unknown, Result = unknown> = (message: ChannelMessage<Payload>) => Result | Promise<Result>;
2
+ /** Handles one question addressed to the current Server. */
3
+ export type Answerer<Payload = unknown, Result = undefined> = (message: ChannelMessage<Payload>) => Result | Promise<Result>;
4
+ /** Events and questions explicitly accepted by the current Server. */
3
5
  export interface Channel<Events extends object = {}> extends CoreChannel<Events> {
4
- answer<Payload = unknown, Result = unknown>(event: string, answerer: Answerer<Payload, Result>): Cleanup;
6
+ /** Registers one answerer; omitting its return produces `undefined`. */
7
+ answer<Payload = unknown, Result = undefined>(event: string, answerer: Answerer<Payload, Result>): Cleanup;
5
8
  }
6
9
  export declare const channel: Channel;
package/dist/channel.js CHANGED
@@ -8,6 +8,9 @@ class ServerChannel extends Events {
8
8
  observer(event, message(value));
9
9
  }));
10
10
  }
11
+ publish(event, payload = undefined) {
12
+ wire.send("end-host", "emit", event, payload);
13
+ }
11
14
  answer(event, answerer) {
12
15
  return wire.answer("end-end", event, value => answerer(message(value)));
13
16
  }
package/dist/current.d.ts CHANGED
@@ -1,21 +1,24 @@
1
- import type { Channel as CoreChannel, LaunchClient, Publishable } from "@phreshos/core";
1
+ import type { Channel as CoreChannel } from "@phreshos/core";
2
2
  import { type Answerer } from "./channel.js";
3
- import { type Process, type Program, type Window } from "./domain.js";
4
- /** The paired Client as addressed from the currently executing Server. */
5
- export interface CurrentClient extends Publishable {
6
- exists(): Promise<boolean>;
7
- start(overrides?: LaunchClient): Promise<void>;
8
- stop(): Promise<void>;
9
- window(): Promise<Window>;
10
- }
3
+ import { Client, type Process, type Program } from "./domain.js";
4
+ /** The current Process's canonical Client handle. */
5
+ export type CurrentClient<Events extends object = {}> = Client<Events>;
11
6
  /** Current Server context: its inbound Channel, owner hierarchy, and paired Client. */
12
7
  export interface Current<Events extends object = {}> extends CoreChannel<Events> {
8
+ /** The same Client handle exposed by the current Process. */
13
9
  readonly client: CurrentClient;
14
- answer<Payload = unknown, Result = unknown>(event: string, answerer: Answerer<Payload, Result>): () => void;
10
+ /** Registers one answerer; omitting its return produces `undefined`. */
11
+ answer<Payload = unknown, Result = undefined>(event: string, answerer: Answerer<Payload, Result>): () => void;
12
+ /** Returns the Process represented by this Server. */
15
13
  process(): Promise<Process>;
14
+ /** Returns the parent Process, or `null` when this Process has none. */
16
15
  parent(): Promise<Process | null>;
16
+ /** Returns the Program that owns this Server. */
17
17
  program(): Promise<Program>;
18
+ /** Returns one immutable option supplied when this Process was created. */
18
19
  option(name: string): Promise<string | undefined>;
20
+ /** Stops the current Server; rejects when it is the final live Endpoint. */
19
21
  stop(): Promise<void>;
20
22
  }
23
+ /** Inbound events, owner hierarchy, and paired Client for the current Server. */
21
24
  export declare const current: Current;
package/dist/current.js CHANGED
@@ -1,8 +1,21 @@
1
1
  import { channel } from "./channel.js";
2
- import { process, program } from "./domain.js";
2
+ import { Client, TrafficHandle, bindEvents, endpointEvents, process, program, window } from "./domain.js";
3
3
  import wire from "./wire.js";
4
- class PairedClient {
5
- publish(event, payload) { wire.send("end-end", event, payload); }
4
+ const ClientBase = Client;
5
+ class CurrentClientHandle extends ClientBase {
6
+ owner;
7
+ traffic = new TrafficHandle(null, "client");
8
+ window = window(async () => {
9
+ const identity = await wire.identity();
10
+ return { identity: identity.process, reference: identity.reference };
11
+ });
12
+ constructor(owner) {
13
+ super();
14
+ this.owner = owner;
15
+ bindEvents(this, endpointEvents(null, "client"));
16
+ }
17
+ process() { return this.owner(); }
18
+ publish(event, payload = undefined) { wire.send("end-end", event, payload); }
6
19
  async exists() {
7
20
  const answer = await wire.request(["exists", "client"]);
8
21
  return answer[0];
@@ -11,20 +24,33 @@ class PairedClient {
11
24
  await wire.request(["start-endpoint", undefined, "client", overrides]);
12
25
  }
13
26
  async stop() { await wire.request(["stop-endpoint", undefined, "client"]); }
14
- async window() { return (await current.process()).client.window(); }
15
27
  }
28
+ let ownerPromise = null;
29
+ let currentClient;
30
+ function owner() {
31
+ if (!ownerPromise) {
32
+ const resolving = wire.request(["process"]).then(answer => {
33
+ return process(answer[0], { client: currentClient });
34
+ });
35
+ const retained = resolving.catch(error => {
36
+ if (ownerPromise === retained)
37
+ ownerPromise = null;
38
+ throw error;
39
+ });
40
+ ownerPromise = retained;
41
+ }
42
+ return ownerPromise;
43
+ }
44
+ currentClient = new CurrentClientHandle(owner);
16
45
  class ServerCurrent {
17
- client = new PairedClient();
46
+ client = currentClient;
18
47
  constructor() {
19
48
  bindChannel(this, channel);
20
49
  }
21
50
  answer(event, answerer) {
22
51
  return channel.answer(event, answerer);
23
52
  }
24
- async process() {
25
- const answer = await wire.request(["process"]);
26
- return process(answer[0]);
27
- }
53
+ process() { return owner(); }
28
54
  async parent() {
29
55
  const answer = await wire.request(["parent"]);
30
56
  return answer[0] ? process(answer[0]) : null;
@@ -41,10 +67,12 @@ class ServerCurrent {
41
67
  }
42
68
  function bindChannel(target, source) {
43
69
  Object.assign(target, {
70
+ publish: source.publish.bind(source),
44
71
  subscribe: source.subscribe.bind(source),
45
72
  waitFor: source.waitFor.bind(source),
46
73
  events: source.events.bind(source),
47
74
  observe: source.observe.bind(source)
48
75
  });
49
76
  }
77
+ /** Inbound events, owner hierarchy, and paired Client for the current Server. */
50
78
  export const current = new ServerCurrent();
package/dist/domain.d.ts CHANGED
@@ -1,23 +1,31 @@
1
- import { Client as CoreClient, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, Window as CoreWindow, type ClientDeclaration, type EndpointDeclaration, type Exit, type Launch, type ProgramArea as CoreProgramArea, type TrafficMessage } from "@phreshos/core";
1
+ import { Client as CoreClient, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, type AnswerCapture, type AskCapture, type Cleanup, type ClientDeclaration, type EndpointDeclaration, type Exit, type Launch, type ProgramPermissions, type ProgramArea as CoreProgramArea, type TrafficMessage, type Window as CoreWindow, type WindowState } from "@phreshos/core";
2
2
  import Events from "./events.js";
3
+ import { type ProgramStartup } from "./startup.js";
4
+ export interface HandleAddress {
5
+ identity: string;
6
+ reference: string;
7
+ }
3
8
  /** Server-side filesystem storage with access to its resolved host path. */
4
9
  export interface ProgramArea extends CoreProgramArea {
10
+ /** Returns the absolute host path of this storage area. */
5
11
  path(): Promise<string>;
12
+ /** Resolves path segments within this storage area without permitting escape. */
6
13
  resolve(...path: string[]): Promise<string>;
7
14
  }
8
15
  /** Client-safe Program data transported by the authoritative host. */
9
16
  export interface ProgramRecord {
17
+ reference: string;
10
18
  identity: string;
11
19
  installed?: boolean;
12
20
  name: string;
13
21
  version: string | null;
14
22
  description: string | null;
15
- icons?: boolean;
16
23
  server: EndpointDeclaration | null;
17
24
  client: ClientDeclaration | null;
18
25
  }
19
26
  /** Process data transported with every Endpoint reference. */
20
27
  export interface ProcessRecord {
28
+ reference: string;
21
29
  identity: string;
22
30
  name: string | null;
23
31
  program: ProgramRecord;
@@ -30,62 +38,97 @@ export interface EndpointReference {
30
38
  kind: "server" | "client";
31
39
  process: ProcessRecord;
32
40
  }
41
+ export type WindowRecord = WindowState;
33
42
  /** Server-visible Program handle and privileged Program operations. */
34
43
  export interface Program<Events extends object = {}> extends CoreProgram<Events> {
44
+ /** Persistent filesystem data shared by every Process of this Program. */
35
45
  readonly data: ProgramArea;
46
+ /** Disposable filesystem data shared by every Process of this Program. */
36
47
  readonly cache: ProgramArea;
48
+ /** Persistent Process launch used when the system starts. */
49
+ readonly startup: ProgramStartup;
50
+ /** Persistent permission decisions owned by this Program. */
51
+ readonly permissions: ProgramPermissions;
52
+ /** Returns every live Process of this Program. */
37
53
  processes(): Promise<Process[]>;
54
+ /** Returns the earliest-started live Process, or `null` when none exist. */
55
+ firstProcess(): Promise<Process | null>;
56
+ /** Returns the latest-started live Process, or `null` when none exist. */
57
+ lastProcess(): Promise<Process | null>;
58
+ /** Finds a live Process by runtime identity or Program-local name. */
38
59
  getProcess(identityOrName: string): Promise<Process | null>;
60
+ /** Creates one Process of this Program. */
39
61
  createProcess(launch?: Launch): Promise<Process>;
62
+ /** Installs this Program and returns the same handle. */
40
63
  install(): Promise<this>;
64
+ /** Creates a new runtime Program with the supplied stable identity. */
41
65
  fork(identity: string): Promise<Program>;
42
66
  }
43
67
  /** Server-visible Process handle. */
44
68
  export interface Process<Events extends object = {}> extends CoreProcess<Events> {
69
+ /** Permanent handle to this Process's Server. */
45
70
  readonly server: Server;
71
+ /** Permanent handle to this Process's Client. */
46
72
  readonly client: Client;
73
+ /** Returns the Program that owns this Process. */
47
74
  program(): Program;
75
+ /** Returns the parent Process, or `null` when this Process has none. */
48
76
  parent(): Promise<Process | null>;
49
77
  }
50
78
  /** Server-visible common Endpoint handle. */
51
79
  export interface Endpoint<Events extends object = {}> extends CoreEndpoint<Events> {
52
- process(): Process;
80
+ /** Returns the Process that owns this Endpoint. */
81
+ process(): Promise<Process>;
53
82
  }
54
83
  /** Server-visible Server handle. */
55
84
  export interface Server<Events extends object = {}> extends CoreServer<Events> {
56
- process(): Process;
85
+ /** Returns the Process that owns this Server. */
86
+ process(): Promise<Process>;
57
87
  }
58
88
  /** Server-visible Client handle. */
59
89
  export interface Client<Events extends object = {}> extends CoreClient<Events> {
60
- process(): Process;
61
- window(): Promise<Window>;
90
+ /** Presentation capability permanently owned by this Client handle. */
91
+ readonly window: Window;
92
+ /** Returns the Process that owns this Client. */
93
+ process(): Promise<Process>;
94
+ }
95
+ /** Server-visible Client-owned Window capability. */
96
+ export type Window = CoreWindow;
97
+ /** Internal transport address for a Program handle created by this SDK. */
98
+ export declare function programAddress(value: Program): HandleAddress;
99
+ export declare class TrafficHandle extends Events {
100
+ protected readonly target: HandleAddress | null;
101
+ protected readonly kind: "server" | "client";
102
+ constructor(target: HandleAddress | null, kind: "server" | "client");
103
+ observeAsks(observer: (capture: AskCapture) => unknown): Cleanup;
62
104
  }
63
- /** Server-visible authoritative Window handle. */
64
- export interface Window<Events extends object = {}> extends CoreWindow<Events> {
65
- client(): Client;
105
+ export declare class ServerTrafficHandle extends TrafficHandle {
106
+ observeAnswers(observer: (capture: AnswerCapture) => unknown): Cleanup;
66
107
  }
108
+ type WindowTarget = () => Promise<HandleAddress>;
67
109
  export declare function scoped(route: string, subject: string | null, convert: (event: string, values: unknown[]) => unknown): Events;
110
+ /** Destinationless events originating from one Endpoint handle. */
111
+ export declare function endpointEvents(target: HandleAddress | null, half: "server" | "client"): Events;
112
+ export declare function lifecycleEndpoint(record: unknown, kind: unknown): Server<{}> | Client<{}>;
68
113
  export declare function exit(code: unknown, signal: unknown): Exit;
69
114
  export declare function trafficMessage(value: unknown): TrafficMessage;
70
115
  export declare function bindEvents(target: object, events: Events): void;
71
116
  export declare function program(record: ProgramRecord): Program;
72
- export declare function process(record: ProcessRecord): Process;
117
+ export declare function process(record: ProcessRecord, endpoints?: {
118
+ server?: Server;
119
+ client?: Client;
120
+ }): Process;
73
121
  export declare function endpoint(reference: EndpointReference | undefined): Endpoint;
74
- export declare const Program: {
75
- new <Events extends object = {}>(): Program<Events>;
76
- };
77
- export declare const Process: {
78
- new <Events extends object = {}>(): Process<Events>;
79
- };
80
- export declare const Endpoint: {
81
- new <Events extends object = {}>(): Endpoint<Events>;
82
- };
83
- export declare const Server: {
84
- new <Events extends object = {}>(): Server<Events>;
85
- };
86
- export declare const Client: {
87
- new <Events extends object = {}>(): Client<Events>;
88
- };
89
- export declare const Window: {
90
- new <Events extends object = {}>(): Window<Events>;
91
- };
122
+ export declare function claimEndpoint(reference: string, kind: "server" | "client", endpoint: Endpoint): Endpoint<{}>;
123
+ export declare function window(target: WindowTarget): Window;
124
+ /** Runtime constructor used to identify and type Server-visible Program handles. */
125
+ export declare const Program: typeof CoreProgram;
126
+ /** Runtime constructor used to identify and type Server-visible Process handles. */
127
+ export declare const Process: typeof CoreProcess;
128
+ /** Runtime constructor used to identify and type Server-visible Endpoint handles. */
129
+ export declare const Endpoint: typeof CoreEndpoint;
130
+ /** Runtime constructor used to identify and type Server-visible Server handles. */
131
+ export declare const Server: typeof CoreServer;
132
+ /** Runtime constructor used to identify and type Server-visible Client handles. */
133
+ export declare const Client: typeof CoreClient;
134
+ export {};