@alivelabs/expo-orchestrator-schemas 0.1.0

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/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@alivelabs/expo-orchestrator-schemas",
3
+ "version": "0.1.0",
4
+ "description": "Shared Zod schemas for the Expo CI Orchestrator wire contract.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/alive-home/alive-expo-orchestrator.git",
10
+ "directory": "packages/schemas"
11
+ },
12
+ "main": "./src/index.ts",
13
+ "types": "./src/index.ts",
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./common": "./src/common.ts",
17
+ "./session": "./src/session.ts",
18
+ "./simulator": "./src/simulator.ts"
19
+ },
20
+ "files": [
21
+ "src"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "zod": "^4.4.3"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^6.0.3"
34
+ }
35
+ }
package/src/common.ts ADDED
@@ -0,0 +1,16 @@
1
+ import * as z from "zod";
2
+
3
+ /** `:sessionId` route parameter, shared by every session-scoped route. */
4
+ export const SessionIdParamSchema = z.object({
5
+ sessionId: z.string().min(1),
6
+ });
7
+ export type SessionIdParam = z.infer<typeof SessionIdParamSchema>;
8
+
9
+ /** Uniform error body returned by the Fastify error handler. */
10
+ export const ErrorResponseSchema = z.object({
11
+ error: z.string(),
12
+ code: z.string(),
13
+ message: z.string(),
14
+ details: z.unknown().optional(),
15
+ });
16
+ export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./common";
2
+ export * from "./session";
3
+ export * from "./simulator";
package/src/session.ts ADDED
@@ -0,0 +1,191 @@
1
+ import * as z from "zod";
2
+
3
+ export const PlatformSchema = z.enum(["ios", "android"]);
4
+ export type Platform = z.infer<typeof PlatformSchema>;
5
+
6
+ export const SessionStatusSchema = z.enum([
7
+ "created",
8
+ "running",
9
+ "completed",
10
+ "failed",
11
+ "timeout",
12
+ "deleted",
13
+ ]);
14
+ export type SessionStatus = z.infer<typeof SessionStatusSchema>;
15
+
16
+ export const SourceTypeSchema = z.enum(["git", "local", "tarball"]);
17
+ export type SourceType = z.infer<typeof SourceTypeSchema>;
18
+
19
+ export const LogLevelSchema = z.enum(["stdout", "stderr", "system"]);
20
+ export type LogLevel = z.infer<typeof LogLevelSchema>;
21
+
22
+ /** A single captured log line. */
23
+ export const LogSchema = z.object({
24
+ timestamp: z.string(),
25
+ level: LogLevelSchema,
26
+ message: z.string(),
27
+ });
28
+ export type Log = z.infer<typeof LogSchema>;
29
+
30
+ /** Optional build secrets — accepted on input, never echoed back. */
31
+ export const SessionSecretsSchema = z.strictObject({
32
+ npmToken: z.string().min(1).optional(),
33
+ appleDeveloperTeam: z.string().min(1).optional(),
34
+ signingCertificate: z.string().min(1).optional(),
35
+ });
36
+
37
+ /** Only http(s) git URLs are accepted — no file://, git://, ssh, etc. */
38
+ const RepoUrlSchema = z
39
+ .string()
40
+ .min(1)
41
+ .refine((value) => {
42
+ try {
43
+ const { protocol } = new URL(value);
44
+ return protocol === "https:" || protocol === "http:";
45
+ } catch {
46
+ return false;
47
+ }
48
+ }, "repoUrl must be an http or https URL");
49
+
50
+ /**
51
+ * Where a build's source code comes from:
52
+ * - `git` — cloned from a public http(s) repo inside the sandbox
53
+ * - `local` — a directory on the orchestrator host (within LOCAL_SOURCE_ROOT)
54
+ * - `tarball` — a gzip tar archive uploaded to POST /api/sessions/:id/source
55
+ */
56
+ export const SessionSourceSchema = z.discriminatedUnion("type", [
57
+ z.strictObject({
58
+ type: z.literal("git"),
59
+ repoUrl: RepoUrlSchema,
60
+ branch: z.string().min(1).max(255).default("main"),
61
+ }),
62
+ z.strictObject({
63
+ type: z.literal("local"),
64
+ path: z.string().min(1).max(4096),
65
+ }),
66
+ z.strictObject({
67
+ type: z.literal("tarball"),
68
+ }),
69
+ ]);
70
+
71
+ /**
72
+ * Where the launched dev-client app should attach for HMR / JS bundle. Must be
73
+ * an http(s) URL reachable from the simulator host. When set, the build is a
74
+ * Debug dev-client pointed at this URL; when omitted, a Release build with the
75
+ * JS bundle embedded is produced instead.
76
+ */
77
+ const DevServerUrlSchema = z
78
+ .string()
79
+ .min(1)
80
+ .max(2048)
81
+ .refine((value) => {
82
+ try {
83
+ const { protocol } = new URL(value);
84
+ return protocol === "https:" || protocol === "http:";
85
+ } catch {
86
+ return false;
87
+ }
88
+ }, "devServerUrl must be an http or https URL");
89
+
90
+ /**
91
+ * Relative path to the Expo app within the source, for monorepos — e.g.
92
+ * `apps/mobile`. Dependencies install at the workspace root (wherever the
93
+ * lockfile lives); `expo prebuild` and the iOS build run here. Defaults to the
94
+ * source root. Must stay inside the source tree (no leading `/`, no `..`).
95
+ */
96
+ const AppRootSchema = z
97
+ .string()
98
+ .min(1)
99
+ .max(255)
100
+ .refine(
101
+ (value) =>
102
+ !value.startsWith("/") &&
103
+ !value.includes("\\") &&
104
+ !value.includes("\0") &&
105
+ !value.split("/").includes(".."),
106
+ "appRoot must be a relative path within the source (no leading '/', no '..')",
107
+ );
108
+
109
+ export const CreateSessionBodySchema = z.strictObject({
110
+ source: SessionSourceSchema,
111
+ platform: PlatformSchema,
112
+ simulatorName: z.string().min(1).max(255).optional(),
113
+ devServerUrl: DevServerUrlSchema.optional(),
114
+ appRoot: AppRootSchema.optional(),
115
+ timeout: z.number().int().min(60).max(3600).default(1800),
116
+ secrets: SessionSecretsSchema.optional(),
117
+ });
118
+ export type CreateSessionBody = z.infer<typeof CreateSessionBodySchema>;
119
+
120
+ export const CreateSessionResponseSchema = z.object({
121
+ sessionId: z.string(),
122
+ status: z.literal("created"),
123
+ buildUser: z.string(),
124
+ wsUrl: z.string(),
125
+ createdAt: z.string(),
126
+ });
127
+ export type CreateSessionResponse = z.infer<typeof CreateSessionResponseSchema>;
128
+
129
+ export const StartSessionResponseSchema = z.object({
130
+ sessionId: z.string(),
131
+ status: z.literal("running"),
132
+ pid: z.number().int(),
133
+ startedAt: z.string(),
134
+ });
135
+ export type StartSessionResponse = z.infer<typeof StartSessionResponseSchema>;
136
+
137
+ export const UploadSourceResponseSchema = z.object({
138
+ sessionId: z.string(),
139
+ sourceStored: z.boolean(),
140
+ bytes: z.number().int(),
141
+ });
142
+ export type UploadSourceResponse = z.infer<typeof UploadSourceResponseSchema>;
143
+
144
+ export const SessionDetailSchema = z.object({
145
+ sessionId: z.string(),
146
+ status: SessionStatusSchema,
147
+ buildUser: z.string(),
148
+ platform: PlatformSchema,
149
+ sourceType: SourceTypeSchema,
150
+ repoUrl: z.string().optional(),
151
+ branch: z.string().optional(),
152
+ sourcePath: z.string().optional(),
153
+ simulatorName: z.string().optional(),
154
+ createdAt: z.string(),
155
+ startTime: z.string().optional(),
156
+ endTime: z.string().optional(),
157
+ duration: z.number().optional(),
158
+ exitCode: z.number().optional(),
159
+ error: z.string().optional(),
160
+ });
161
+ export type SessionDetail = z.infer<typeof SessionDetailSchema>;
162
+
163
+ export const DeleteSessionResponseSchema = z.object({
164
+ sessionId: z.string(),
165
+ status: z.literal("deleted"),
166
+ logsArchived: z.boolean(),
167
+ cleanupDuration: z.number(),
168
+ });
169
+ export type DeleteSessionResponse = z.infer<typeof DeleteSessionResponseSchema>;
170
+
171
+ export const LogsResponseSchema = z.object({
172
+ sessionId: z.string(),
173
+ logs: z.array(LogSchema),
174
+ });
175
+
176
+ // A session-scoped access token: the master-token holder mints one of these to
177
+ // hand an end user, granting them only that session's video stream, input, and
178
+ // screenshots — not session creation, deletion, or access to other sessions.
179
+ export const SessionTokenBodySchema = z.strictObject({
180
+ // Lifetime in seconds; defaults server-side to the session's remaining budget.
181
+ ttlSeconds: z.number().int().min(60).max(3600).optional(),
182
+ });
183
+ export type SessionTokenBody = z.infer<typeof SessionTokenBodySchema>;
184
+
185
+ export const SessionTokenResponseSchema = z.object({
186
+ sessionId: z.string(),
187
+ token: z.string(),
188
+ expiresAt: z.string(),
189
+ wsUrl: z.string(),
190
+ });
191
+ export type SessionTokenResponse = z.infer<typeof SessionTokenResponseSchema>;
@@ -0,0 +1,180 @@
1
+ import * as z from "zod";
2
+
3
+ /**
4
+ * Shapes mirror baguette's wire-protocol gestures one-for-one
5
+ * (`.claude/skills/baguette/references/wire-protocol.md`). The orchestrator
6
+ * is a pass-through: it serialises the payload as the JSON line baguette
7
+ * expects, and for pointer types (`tap`, `swipe`) it injects the simulator
8
+ * frame's `width`/`height` server-side so the client doesn't have to know
9
+ * the device dimensions.
10
+ *
11
+ * Pointer coordinates are in the same units as the streamed frame
12
+ * (screenshot pixels) — baguette normalises by `x / width` internally.
13
+ *
14
+ * Gestures intentionally omitted for now (add as needed):
15
+ * touch1-down/move/up, touch2-down/move/up, pinch, pan.
16
+ */
17
+
18
+ const Coord = z.number();
19
+ const Duration = z.number().min(0).max(10).optional();
20
+
21
+ export const TapInputSchema = z.strictObject({
22
+ type: z.literal("tap"),
23
+ x: Coord,
24
+ y: Coord,
25
+ duration: Duration,
26
+ });
27
+
28
+ export const SwipeInputSchema = z.strictObject({
29
+ type: z.literal("swipe"),
30
+ startX: Coord,
31
+ startY: Coord,
32
+ endX: Coord,
33
+ endY: Coord,
34
+ duration: Duration,
35
+ });
36
+
37
+ export const ScrollInputSchema = z.strictObject({
38
+ type: z.literal("scroll"),
39
+ deltaX: z.number(),
40
+ deltaY: z.number(),
41
+ });
42
+
43
+ /** Hardware + virtual buttons. `siri` is intentionally excluded (crashes backboardd). */
44
+ export const ButtonNameSchema = z.enum([
45
+ "home",
46
+ "lock",
47
+ "power",
48
+ "volume-up",
49
+ "volume-down",
50
+ "action",
51
+ "app-switcher",
52
+ "swipe-to-app-switcher",
53
+ "swipe-to-home",
54
+ "pull-down-to-lock-screen",
55
+ "pull-down-to-notification-center",
56
+ ]);
57
+
58
+ export const ButtonInputSchema = z.strictObject({
59
+ type: z.literal("button"),
60
+ button: ButtonNameSchema,
61
+ duration: Duration,
62
+ });
63
+
64
+ /** W3C `KeyboardEvent.code` — baguette's supported set. */
65
+ export const KeyCodeSchema = z.enum([
66
+ "KeyA",
67
+ "KeyB",
68
+ "KeyC",
69
+ "KeyD",
70
+ "KeyE",
71
+ "KeyF",
72
+ "KeyG",
73
+ "KeyH",
74
+ "KeyI",
75
+ "KeyJ",
76
+ "KeyK",
77
+ "KeyL",
78
+ "KeyM",
79
+ "KeyN",
80
+ "KeyO",
81
+ "KeyP",
82
+ "KeyQ",
83
+ "KeyR",
84
+ "KeyS",
85
+ "KeyT",
86
+ "KeyU",
87
+ "KeyV",
88
+ "KeyW",
89
+ "KeyX",
90
+ "KeyY",
91
+ "KeyZ",
92
+ "Digit0",
93
+ "Digit1",
94
+ "Digit2",
95
+ "Digit3",
96
+ "Digit4",
97
+ "Digit5",
98
+ "Digit6",
99
+ "Digit7",
100
+ "Digit8",
101
+ "Digit9",
102
+ "Enter",
103
+ "Escape",
104
+ "Backspace",
105
+ "Tab",
106
+ "Space",
107
+ "ArrowUp",
108
+ "ArrowDown",
109
+ "ArrowLeft",
110
+ "ArrowRight",
111
+ "Minus",
112
+ "Equal",
113
+ "BracketLeft",
114
+ "BracketRight",
115
+ "Backslash",
116
+ "Semicolon",
117
+ "Quote",
118
+ "Backquote",
119
+ "Comma",
120
+ "Period",
121
+ "Slash",
122
+ ]);
123
+
124
+ export const KeyModifierSchema = z.enum(["shift", "control", "option", "command"]);
125
+
126
+ export const KeyInputSchema = z.strictObject({
127
+ type: z.literal("key"),
128
+ code: KeyCodeSchema,
129
+ modifiers: z.array(KeyModifierSchema).optional(),
130
+ duration: Duration,
131
+ });
132
+
133
+ export const TypeInputSchema = z.strictObject({
134
+ type: z.literal("type"),
135
+ text: z.string().min(1).max(1000),
136
+ });
137
+
138
+ /** Discriminated union of every supported simulator interaction. */
139
+ export const SimulatorInputSchema = z.discriminatedUnion("type", [
140
+ TapInputSchema,
141
+ SwipeInputSchema,
142
+ ScrollInputSchema,
143
+ ButtonInputSchema,
144
+ KeyInputSchema,
145
+ TypeInputSchema,
146
+ ]);
147
+ export type SimulatorInput = z.infer<typeof SimulatorInputSchema>;
148
+
149
+ export type SimulatorButton = z.infer<typeof ButtonNameSchema>;
150
+ export type SimulatorKeyCode = z.infer<typeof KeyCodeSchema>;
151
+ export type SimulatorKeyModifier = z.infer<typeof KeyModifierSchema>;
152
+
153
+ export const SimulatorInputResponseSchema = z.object({
154
+ sessionId: z.string(),
155
+ inputType: z.string(),
156
+ processed: z.boolean(),
157
+ screenshotUrl: z.string(),
158
+ });
159
+ export type SimulatorInputResponse = z.infer<typeof SimulatorInputResponseSchema>;
160
+
161
+ // ── Host-level: available simulators ─────────────────────────────────────────
162
+ // `GET /api/simulators` returns every simulator the host can boot, plus a
163
+ // recommended default name the Launcher pre-selects. Shape mirrors what
164
+ // `baguette list --json` emits per device.
165
+
166
+ export const SimulatorDeviceSchema = z.object({
167
+ udid: z.string(),
168
+ name: z.string(),
169
+ runtime: z.string(),
170
+ state: z.string(),
171
+ });
172
+ export type SimulatorDevice = z.infer<typeof SimulatorDeviceSchema>;
173
+
174
+ export const ListSimulatorsResponseSchema = z.object({
175
+ devices: z.array(SimulatorDeviceSchema),
176
+ /** Name of the simulator the Launcher should pre-select. `null` when the
177
+ * host has no available device (`devices` is empty too in that case). */
178
+ recommended: z.string().nullable(),
179
+ });
180
+ export type ListSimulatorsResponse = z.infer<typeof ListSimulatorsResponseSchema>;