@hitch42/applet 0.1.1-beta.7 → 0.1.1-beta.8

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.
Files changed (49) hide show
  1. package/dist/_internal/server.d.ts +14 -1
  2. package/dist/_internal/server.js +23 -74
  3. package/dist/applet.js +22 -11
  4. package/dist/build-CFCKKMn7.js +2 -0
  5. package/dist/{build-DauirTcY.js → build-DANx9Hgg.js} +1 -1
  6. package/dist/{ci-gPrwo6C0.js → ci-5qWl7lT3.js} +1 -1
  7. package/dist/client.d.ts +2 -2
  8. package/dist/{contract-DB9QQhD9.d.ts → contract-rP_eV9PK.d.ts} +1 -1
  9. package/dist/{deploy-CqrtNJ1x.js → deploy-MQWLG2EA.js} +3 -3
  10. package/dist/email-DBTVhNB_.js +62 -0
  11. package/dist/{generate-BIifT6PK.js → generate-BxYHdoAQ.js} +9 -8
  12. package/dist/generate-CQ07Hdpo.js +2 -0
  13. package/dist/index.d.ts +114 -3
  14. package/dist/index.js +141 -26
  15. package/dist/{init-Wu6XmyVc.js → init-jDEgYIXg.js} +5 -5
  16. package/dist/{load-config-BknBkdRs.js → load-config-BYSxDVAl.js} +2 -2
  17. package/dist/{login-iT9MRuaw.js → login-C7RKmmTe.js} +1 -1
  18. package/dist/{logs-B-qZGXSK.js → logs-DgjgAxj2.js} +3 -3
  19. package/dist/{manifest-Bd04cWhu.js → manifest-Bp87Uz0a.js} +3 -0
  20. package/dist/{manifest-C489nYUy.d.ts → manifest-DD_SGkS6.d.ts} +3 -0
  21. package/dist/me-BW7Paary.js +32 -0
  22. package/dist/{project-C_rtpjp8.js → project-CbYPtpPq.js} +6 -5
  23. package/dist/prompt-ChS-grYO.js +29 -0
  24. package/dist/schedules-dhU1yyFr.js +42 -0
  25. package/dist/server.d.ts +2 -2
  26. package/dist/telephony-protocol-DRgiYjqQ.d.ts +74 -0
  27. package/dist/telephony.d.ts +77 -0
  28. package/dist/telephony.js +607 -0
  29. package/dist/{variables-DcwMeIau.js → variables-Bwnr7oxf.js} +3 -28
  30. package/dist/vite.d.ts +1 -1
  31. package/dist/vite.js +457 -22
  32. package/dist/workflow-engine-MogejNfF.js +117 -0
  33. package/dist/{workspace-B6lenAS5.js → workspace-XVtM12ZC.js} +5 -2
  34. package/package.json +6 -1
  35. package/scaffold/.agents/skills/hitch-applets/SKILL.md +24 -18
  36. package/scaffold/.agents/skills/hitch-applets/references/email-tracking.md +49 -0
  37. package/scaffold/.agents/skills/hitch-applets/references/files.md +3 -3
  38. package/scaffold/.agents/skills/hitch-applets/references/manifest.md +4 -2
  39. package/scaffold/.agents/skills/hitch-applets/references/performance.md +1 -1
  40. package/scaffold/.agents/skills/hitch-applets/references/records.md +4 -3
  41. package/scaffold/.agents/skills/hitch-applets/references/schedules.md +7 -2
  42. package/scaffold/.agents/skills/hitch-applets/references/server.md +0 -3
  43. package/scaffold/.agents/skills/hitch-applets/references/workflows.md +11 -3
  44. package/scaffold/.agents/skills/hitch-cli/SKILL.md +20 -20
  45. package/scaffold/.agents/skills/hitch-data/SKILL.md +1 -1
  46. package/scaffold/.agents/skills/hitch-data/references/limits.md +10 -10
  47. package/scaffold/.agents/skills/hitch-theory/SKILL.md +150 -0
  48. package/dist/build-DhNDp7ji.js +0 -2
  49. package/dist/generate-DSk8ihsC.js +0 -2
package/dist/vite.js CHANGED
@@ -1,15 +1,299 @@
1
- import { i as APPLET_CODE_PATTERN, n as appletManifestSchema, r as appletSourceDefinitionSchema } from "./manifest-Bd04cWhu.js";
2
- import { l as resolveInstanceUrl, s as organizationInstanceUrl, t as resolveWorkspaceOrg } from "./workspace-B6lenAS5.js";
3
- import { c as writeClientEntry, i as writeRegisterDeclaration, l as writeManifestGen, r as syncContract, s as writeAppletRoot, u as writeServerEntry } from "./generate-BIifT6PK.js";
4
- import { existsSync } from "node:fs";
5
- import { appendFile, readFile, readdir, rm, writeFile } from "node:fs/promises";
6
- import { basename, relative, resolve } from "node:path";
1
+ import { n as meSchema } from "./me-BW7Paary.js";
2
+ import { i as APPLET_CODE_PATTERN, n as appletManifestSchema, r as appletSourceDefinitionSchema } from "./manifest-Bp87Uz0a.js";
3
+ import { a as workflowOutcomeSchema, c as HITCH_SESSION_HEADER, l as HITCH_USER_HEADER, n as runWorkflowJournal } from "./workflow-engine-MogejNfF.js";
4
+ import { c as organizationInstanceUrl, d as resolveInstanceUrl, l as rejectEnvironmentKey, s as bearerToken, t as resolveWorkspaceOrg, u as resolveAuthForOrg } from "./workspace-XVtM12ZC.js";
5
+ import { c as writeClientEntry, i as writeRegisterDeclaration, l as writeManifestGen, r as syncContract, s as writeAppletRoot, u as writeServerEntry } from "./generate-BxYHdoAQ.js";
6
+ import { randomUUID } from "node:crypto";
7
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
8
+ import { appendFile, readdir, rm, writeFile } from "node:fs/promises";
9
+ import { createServer } from "node:http";
10
+ import { basename, dirname, relative, resolve } from "node:path";
7
11
  import { cloudflare } from "@cloudflare/vite-plugin";
8
12
  import { tanstackRouter } from "@tanstack/router-plugin/vite";
9
13
  import react from "@vitejs/plugin-react";
14
+ import { Readable } from "node:stream";
15
+ import { pipeline } from "node:stream/promises";
16
+ //#region src/dev-proxy.ts
17
+ const REQUEST_DROP = /* @__PURE__ */ new Set([
18
+ "host",
19
+ "connection",
20
+ "content-length",
21
+ "cookie",
22
+ "authorization",
23
+ "origin",
24
+ "referer",
25
+ "accept-encoding"
26
+ ]);
27
+ const RESPONSE_DROP = /* @__PURE__ */ new Set([
28
+ "content-encoding",
29
+ "content-length",
30
+ "transfer-encoding",
31
+ "connection",
32
+ "set-cookie"
33
+ ]);
34
+ function refusesClient(headers, clientAddress) {
35
+ if (![
36
+ "127.0.0.1",
37
+ "::1",
38
+ "::ffff:127.0.0.1"
39
+ ].includes(clientAddress ?? "")) return true;
40
+ const get = (name) => headers instanceof Headers ? headers.get(name) : headers[name]?.toString() ?? null;
41
+ const host = get("host") ?? get(":authority");
42
+ const origin = get("origin");
43
+ return origin !== null && (host === null || !URL.canParse(origin) || new URL(origin).host !== host) || get("sec-fetch-site") === "cross-site";
44
+ }
45
+ function createDevProxy(options) {
46
+ let warnedAboutUnauthorized = false;
47
+ return async (request, clientAddress) => {
48
+ if (refusesClient(request.headers, clientAddress)) return new Response(null, { status: 403 });
49
+ const intercepted = await options.intercept?.(request);
50
+ if (intercepted !== void 0) return intercepted;
51
+ const headers = new Headers(request.headers);
52
+ [...headers.keys()].filter((name) => REQUEST_DROP.has(name) || name.startsWith("x-hitch-")).forEach((name) => headers.delete(name));
53
+ headers.set("authorization", `Bearer ${options.token}`);
54
+ const url = new URL(request.url);
55
+ const upstream = await (options.fetch ?? globalThis.fetch)(`${options.orgUrl}${url.pathname}${url.search}`, {
56
+ method: request.method,
57
+ headers,
58
+ body: request.body,
59
+ signal: request.signal,
60
+ ...request.body === null ? {} : { duplex: "half" }
61
+ });
62
+ if (upstream.status === 401 && !warnedAboutUnauthorized) {
63
+ warnedAboutUnauthorized = true;
64
+ options.warn(options.source === "environment" ? "HITCH_API_KEY was rejected." : "Your login expired. Run applet login, then restart the dev server.");
65
+ }
66
+ const responseHeaders = new Headers(upstream.headers);
67
+ [...responseHeaders.keys()].filter((name) => RESPONSE_DROP.has(name)).forEach((name) => responseHeaders.delete(name));
68
+ return new Response(upstream.body, {
69
+ status: upstream.status,
70
+ statusText: upstream.statusText,
71
+ headers: responseHeaders
72
+ });
73
+ };
74
+ }
75
+ async function serveDevProxy(request, response, handler) {
76
+ const controller = new AbortController();
77
+ const onClose = () => {
78
+ if (!response.writableFinished) controller.abort();
79
+ };
80
+ response.on("close", onClose);
81
+ try {
82
+ const authority = request.headers[":authority"];
83
+ const host = request.headers.host ?? (Array.isArray(authority) ? authority[0] : authority);
84
+ const url = `http://${host ?? "localhost"}${request.url ?? "/"}`;
85
+ const result = await handler(new Request(url, {
86
+ method: request.method,
87
+ headers: {
88
+ ...Object.fromEntries(Object.entries(request.headers).filter(([name]) => !name.startsWith(":"))),
89
+ host: host ?? "localhost"
90
+ },
91
+ signal: controller.signal,
92
+ ...["GET", "HEAD"].includes(request.method ?? "GET") ? {} : {
93
+ body: Readable.toWeb(request),
94
+ duplex: "half"
95
+ }
96
+ }), request.socket.remoteAddress);
97
+ response.writeHead(result.status, result.statusText, Object.fromEntries(result.headers));
98
+ if (result.body === null) {
99
+ response.end();
100
+ return;
101
+ }
102
+ await pipeline(Readable.fromWeb(result.body), response);
103
+ } catch (error) {
104
+ if (!controller.signal.aborted || !(error instanceof Error) || error.name !== "AbortError" && error.code !== "ERR_STREAM_PREMATURE_CLOSE") throw error;
105
+ } finally {
106
+ response.off("close", onClose);
107
+ }
108
+ }
109
+ //#endregion
110
+ //#region src/dev-workflows.ts
111
+ const RUN_STATUSES = [
112
+ "in_progress",
113
+ "sleeping",
114
+ "success",
115
+ "error",
116
+ "terminated",
117
+ "open"
118
+ ];
119
+ const badRequest = (message) => Response.json({
120
+ code: "BAD_REQUEST",
121
+ message
122
+ }, { status: 400 });
123
+ const workflowCodeError = (code) => code.length < 1 || code.length > 64 ? badRequest("The workflow code must have 1 to 64 characters.") : void 0;
124
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
125
+ function createDevWorkflows(options) {
126
+ const runs = /* @__PURE__ */ new Map();
127
+ const controller = new AbortController();
128
+ const stopped = () => controller.signal.aborted;
129
+ const wait = (milliseconds) => {
130
+ if (controller.signal.aborted) return Promise.reject(/* @__PURE__ */ new Error("The dev server stopped."));
131
+ return new Promise((resolve, reject) => {
132
+ const deadline = Date.now() + Math.max(0, milliseconds);
133
+ let timer;
134
+ const schedule = () => {
135
+ const remaining = deadline - Date.now();
136
+ if (remaining <= 0) {
137
+ controller.signal.removeEventListener("abort", onAbort);
138
+ resolve();
139
+ return;
140
+ }
141
+ timer = setTimeout(schedule, Math.min(remaining, 2147483647));
142
+ };
143
+ const onAbort = () => {
144
+ clearTimeout(timer);
145
+ controller.signal.removeEventListener("abort", onAbort);
146
+ reject(/* @__PURE__ */ new Error("The dev server stopped."));
147
+ };
148
+ controller.signal.addEventListener("abort", onAbort, { once: true });
149
+ schedule();
150
+ });
151
+ };
152
+ const record = (run, outcome) => {
153
+ switch (outcome.kind) {
154
+ case "step":
155
+ case "step_error":
156
+ run.status = "in_progress";
157
+ run.sleep_until = null;
158
+ break;
159
+ case "sleep":
160
+ run.status = "sleeping";
161
+ run.sleep_until = new Date(outcome.until).toISOString();
162
+ break;
163
+ case "done":
164
+ run.status = "success";
165
+ run.result = outcome.result ?? null;
166
+ run.sleep_until = null;
167
+ run.finished_at = (/* @__PURE__ */ new Date()).toISOString();
168
+ break;
169
+ case "error":
170
+ run.status = "error";
171
+ run.error = outcome.message.slice(0, 2048);
172
+ run.sleep_until = null;
173
+ run.finished_at = (/* @__PURE__ */ new Date()).toISOString();
174
+ }
175
+ };
176
+ const start = (workflow, payload) => {
177
+ const run = {
178
+ id: randomUUID(),
179
+ workflow,
180
+ status: "in_progress",
181
+ result: null,
182
+ error: null,
183
+ sleep_until: null,
184
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
185
+ finished_at: null
186
+ };
187
+ runs.set(run.id, run);
188
+ options.logger.info(`ℹ Workflow ${workflow} run ${run.id} started.`);
189
+ runWorkflowJournal({
190
+ invoke: async (_name, fn) => {
191
+ if (controller.signal.aborted) throw new Error("The dev server stopped.");
192
+ for (let attempt = 0;; attempt += 1) try {
193
+ const outcome = await fn();
194
+ if (stopped()) throw new Error("The dev server stopped.");
195
+ return outcome;
196
+ } catch (error) {
197
+ if (stopped() || attempt === 5) throw error;
198
+ const message = error instanceof Error ? error.message : String(error);
199
+ options.logger.error(`Workflow ${workflow} run ${run.id} invoke failed, retrying: ${message}`);
200
+ await wait(1e3 * 2 ** attempt);
201
+ }
202
+ },
203
+ sleep: (_name, milliseconds) => options.skipSleep ? Promise.resolve() : wait(milliseconds),
204
+ sleepUntil: (_name, until) => options.skipSleep ? Promise.resolve() : wait(until.getTime() - Date.now())
205
+ }, async (journal) => {
206
+ const outcome = await options.invoke(workflow, {
207
+ payload,
208
+ journal
209
+ });
210
+ record(run, outcome);
211
+ return outcome;
212
+ }).then(() => options.logger.info(`ℹ Workflow ${workflow} run ${run.id} succeeded.`), (error) => {
213
+ if (controller.signal.aborted) return;
214
+ const message = (error instanceof Error ? error.message : String(error)).slice(0, 2048);
215
+ run.status = "error";
216
+ run.error = message;
217
+ run.sleep_until = null;
218
+ run.finished_at = (/* @__PURE__ */ new Date()).toISOString();
219
+ options.logger.error(`Workflow ${workflow} run ${run.id} failed: ${message}`);
220
+ });
221
+ return run;
222
+ };
223
+ const handle = (request) => {
224
+ const url = new URL(request.url);
225
+ const prefix = `/api/v1/applets/${encodeURIComponent(options.appletCode)}`;
226
+ if (!url.pathname.startsWith(`${prefix}/`)) return void 0;
227
+ const path = url.pathname.slice(prefix.length);
228
+ const startMatch = /^\/workflows\/([^/]+)\/runs$/.exec(path);
229
+ if (request.method === "POST" && startMatch !== null) return (async () => {
230
+ let body;
231
+ try {
232
+ const text = await request.text();
233
+ body = text === "" ? {} : JSON.parse(text);
234
+ } catch {
235
+ return badRequest("The body must be a JSON object with a payload key.");
236
+ }
237
+ if (body === null || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key) => key !== "payload")) return badRequest("The body must be a JSON object with a payload key.");
238
+ let workflow;
239
+ try {
240
+ workflow = decodeURIComponent(startMatch[1]);
241
+ } catch (error) {
242
+ if (error instanceof URIError) return badRequest(error.message);
243
+ throw error;
244
+ }
245
+ const codeError = workflowCodeError(workflow);
246
+ if (codeError !== void 0) return codeError;
247
+ const run = start(workflow, "payload" in body ? body.payload : void 0);
248
+ return Response.json({ data: {
249
+ id: run.id,
250
+ workflow: run.workflow,
251
+ status: "in_progress"
252
+ } }, { status: 201 });
253
+ })();
254
+ const getMatch = /^\/workflows\/([^/]+)\/runs\/([^/]+)$/.exec(path);
255
+ if (request.method === "GET" && getMatch !== null) {
256
+ let workflow;
257
+ let id;
258
+ try {
259
+ workflow = decodeURIComponent(getMatch[1]);
260
+ id = decodeURIComponent(getMatch[2]);
261
+ } catch (error) {
262
+ if (error instanceof URIError) return Promise.resolve(badRequest(error.message));
263
+ throw error;
264
+ }
265
+ const codeError = workflowCodeError(workflow);
266
+ if (codeError !== void 0) return Promise.resolve(codeError);
267
+ if (!UUID_PATTERN.test(id)) return Promise.resolve(badRequest("The run id must be a uuid."));
268
+ const run = runs.get(id);
269
+ return Promise.resolve(run?.workflow === workflow ? Response.json({ data: run }) : Response.json({
270
+ code: "workflow_run_not_found",
271
+ message: "The workflow run does not exist."
272
+ }, { status: 404 }));
273
+ }
274
+ if (request.method === "GET" && path === "/workflow-runs") {
275
+ const status = url.searchParams.get("status");
276
+ const limitValue = url.searchParams.get("limit");
277
+ const limit = limitValue === null ? 50 : Number(limitValue);
278
+ if (status !== null && !RUN_STATUSES.some((value) => value === status)) return Promise.resolve(badRequest("Invalid workflow run status."));
279
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) return Promise.resolve(badRequest("The limit must be an integer from 1 to 100."));
280
+ const workflow = url.searchParams.get("workflow");
281
+ const codeError = workflow === null ? void 0 : workflowCodeError(workflow);
282
+ if (codeError !== void 0) return Promise.resolve(codeError);
283
+ const data = [...runs.values()].filter((run) => (workflow === null || run.workflow === workflow) && (status === null || status === run.status || status === "open" && ["in_progress", "sleeping"].includes(run.status))).reverse().slice(0, limit).map(({ result: _result, ...summary }) => summary);
284
+ return Promise.resolve(Response.json({ data }));
285
+ }
286
+ };
287
+ return {
288
+ handle,
289
+ stop: () => controller.abort()
290
+ };
291
+ }
292
+ //#endregion
10
293
  //#region src/vite.ts
11
294
  const APPLET_COMPATIBILITY_DATE = "2026-07-14";
12
295
  const APPLET_COMPATIBILITY_FLAGS = ["nodejs_compat"];
296
+ let activeDevListener;
13
297
  function clientBootstrapScript(parentOrigin, appletCode) {
14
298
  return `(function () {
15
299
  var parentOrigin = ${parentOrigin === void 0 ? "null" : JSON.stringify(parentOrigin)};
@@ -151,6 +435,11 @@ function applet(sourceDefinition, options = {}) {
151
435
  let clearedOutDir = false;
152
436
  let contractSync;
153
437
  let themeStylesheetUrl;
438
+ let devApiUrl;
439
+ let devListener;
440
+ let devLogger;
441
+ let devWorkflows;
442
+ let devContext;
154
443
  const virtualRouteConfig = {
155
444
  type: "root",
156
445
  file: "__root.tsx",
@@ -169,6 +458,7 @@ function applet(sourceDefinition, options = {}) {
169
458
  const routesDirectory = resolve(resolvedRoot, "src/client/routes");
170
459
  const hasClient = existsSync(routesDirectory);
171
460
  const hasServer = existsSync(resolve(resolvedRoot, "src/server/main.ts"));
461
+ if (!hasServer && parsedDefinition.data.telephony?.events.length) throw new Error("An applet with telephony events must have src/server/main.ts.");
172
462
  if (!hasServer && (parsedDefinition.data.schedules?.length ?? 0) > 0) throw new Error("An applet with schedules must have src/server/main.ts.");
173
463
  const legacyMain = resolve(resolvedRoot, "src/client/main.tsx");
174
464
  const indexHtml = resolve(resolvedRoot, "index.html");
@@ -235,6 +525,66 @@ function applet(sourceDefinition, options = {}) {
235
525
  hasServer ? writeServerEntry(root, hasHitchGen, manifest.code) : void 0,
236
526
  hasHitchGen ? void 0 : writeRegisterDeclaration(resolve(root, ".hitch"))
237
527
  ]);
528
+ if (environment.command === "serve" && environment.mode !== "test" && environment.isPreview !== true) {
529
+ const instanceUrl = await resolveInstanceUrl(void 0, options.store);
530
+ const orgUrl = organizationInstanceUrl(instanceUrl, manifest.org);
531
+ const auth = await resolveAuthForOrg(instanceUrl, manifest.org, options.store);
532
+ if (auth === void 0) throw new Error("Not logged in. Run applet login.");
533
+ const token = bearerToken(auth);
534
+ const meResponse = await (options.fetch ?? globalThis.fetch)(`${orgUrl}/api/v1/me`, { headers: { authorization: `Bearer ${token}` } });
535
+ rejectEnvironmentKey(meResponse, auth);
536
+ if (meResponse.status === 401) throw new Error("Your login expired. Run applet login.");
537
+ if (!meResponse.ok) throw new Error(`Hitch returned ${meResponse.status} for /api/v1/me.`);
538
+ const { person } = meSchema.parse(await meResponse.json());
539
+ const proxy = createDevProxy({
540
+ orgUrl,
541
+ token,
542
+ source: auth.source,
543
+ fetch: options.fetch,
544
+ warn: (message) => devLogger?.warn(message),
545
+ intercept: (request) => devWorkflows?.handle(request)
546
+ });
547
+ devContext = {
548
+ person,
549
+ proxy,
550
+ orgUrl,
551
+ cacheBuster: randomUUID()
552
+ };
553
+ if (hasServer && devListener === void 0) {
554
+ if (activeDevListener?.listening) activeDevListener.close();
555
+ const nonce = randomUUID();
556
+ devListener = createServer((request, response) => {
557
+ const url = request.url ?? "";
558
+ const prefix = `/${nonce}/api`;
559
+ const address = devListener?.address();
560
+ if (!(url === prefix || url.startsWith(`${prefix}/`) || url.startsWith(`${prefix}?`)) || address === null || address === void 0 || typeof address === "string" || request.headers.host !== `127.0.0.1:${address.port}`) {
561
+ response.writeHead(404).end();
562
+ return;
563
+ }
564
+ request.url = url.slice(nonce.length + 1);
565
+ serveDevProxy(request, response, proxy).catch((error) => {
566
+ const failure = error instanceof Error ? error : new Error(String(error));
567
+ devLogger?.error(`Local Hitch proxy failed: ${failure.message}`);
568
+ response.destroy(failure);
569
+ });
570
+ });
571
+ activeDevListener = devListener;
572
+ try {
573
+ await new Promise((resolvePromise, rejectPromise) => {
574
+ devListener?.once("error", rejectPromise);
575
+ devListener?.listen(0, "127.0.0.1", resolvePromise);
576
+ });
577
+ const address = devListener.address();
578
+ if (address === null || typeof address === "string") throw new Error("The local Hitch proxy has no port.");
579
+ devApiUrl = `http://127.0.0.1:${address.port}/${nonce}/api`;
580
+ } catch (error) {
581
+ if (devListener.listening) devListener.close();
582
+ if (activeDevListener === devListener) activeDevListener = void 0;
583
+ devListener = void 0;
584
+ throw error;
585
+ }
586
+ }
587
+ }
238
588
  return {
239
589
  base: `/${manifest.code}/`,
240
590
  ...hasServer && { environments: { worker: { build: { rollupOptions: { output: { codeSplitting: false } } } } } }
@@ -244,11 +594,84 @@ function applet(sourceDefinition, options = {}) {
244
594
  apiManifest = resolveAppletConfig(config.root).manifest;
245
595
  },
246
596
  configureServer(server) {
247
- const { manifest, root } = requireAppletConfig();
597
+ if (devContext === void 0) return;
598
+ const { manifest } = requireAppletConfig();
599
+ const { person, proxy, orgUrl, cacheBuster } = devContext;
600
+ const orgOrigin = new URL(orgUrl).origin;
601
+ devLogger = server.config.logger;
602
+ const skipSleep = process.env.HITCH_DEV_SKIP_SLEEP === "1";
603
+ devWorkflows = createDevWorkflows({
604
+ appletCode: manifest.code,
605
+ skipSleep,
606
+ logger: server.config.logger,
607
+ invoke: async (workflow, body) => {
608
+ const address = server.httpServer?.address();
609
+ if (address === null || address === void 0 || typeof address === "string") throw new Error("The dev server has no port.");
610
+ const response = await fetch(`http://localhost:${address.port}/${manifest.code}/api/__hitch/workflows/${encodeURIComponent(workflow)}`, {
611
+ method: "POST",
612
+ headers: { "content-type": "application/json" },
613
+ body: JSON.stringify(body)
614
+ });
615
+ if (!response.ok) throw new Error(`Workflow worker returned HTTP ${response.status}.`);
616
+ return workflowOutcomeSchema.parse(await response.json());
617
+ }
618
+ });
619
+ server.config.logger.info(`ℹ Running as ${person.email} in production org "${manifest.org}". Reads and writes use real data with your access.`);
620
+ if (manifest.requiresChangeset !== void 0) server.config.logger.info(`ℹ This applet requires changeset ${manifest.requiresChangeset}. Local dev uses the live schema, so a pending changeset's schema is not available until it applies.`);
621
+ if (skipSleep) server.config.logger.info("ℹ HITCH_DEV_SKIP_SLEEP is set. Workflow sleeps and retry delays run at once.");
622
+ const devServerFile = resolve(requireAppletConfig().root, ".hitch/dev-server.json");
623
+ let devServerUrl;
624
+ const removeDevServerFile = () => {
625
+ if (devServerUrl === void 0) return;
626
+ try {
627
+ const file = JSON.parse(readFileSync(devServerFile, "utf8"));
628
+ if (file !== null && typeof file === "object" && "pid" in file && file.pid === process.pid && "url" in file && file.url === devServerUrl) rmSync(devServerFile, { force: true });
629
+ } catch (error) {
630
+ if (!(error instanceof SyntaxError) && !(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
631
+ }
632
+ };
633
+ server.httpServer?.once("listening", () => {
634
+ const address = server.httpServer?.address();
635
+ if (address === null || address === void 0 || typeof address === "string") return;
636
+ mkdirSync(dirname(devServerFile), { recursive: true });
637
+ devServerUrl = `http://localhost:${address.port}`;
638
+ writeFileSync(devServerFile, JSON.stringify({
639
+ url: devServerUrl,
640
+ pid: process.pid
641
+ }));
642
+ server.config.logger.info(`ℹ Preview in Hitch: ${orgUrl}/applets/${manifest.code}?_hitch_dev=${address.port}`);
643
+ process.once("exit", removeDevServerFile);
644
+ });
645
+ server.httpServer?.once("close", () => {
646
+ devWorkflows?.stop();
647
+ devListener?.close();
648
+ if (activeDevListener === devListener) activeDevListener = void 0;
649
+ process.off("exit", removeDevServerFile);
650
+ removeDevServerFile();
651
+ });
248
652
  const mount = `/${manifest.code}`;
249
653
  const apiPrefix = `${mount}/api`;
250
654
  server.middlewares.use((request, response, next) => {
655
+ if (request.url === mount || request.url?.startsWith(`${mount}?`)) request.url = `${mount}/${request.url.slice(mount.length)}`;
251
656
  const url = request.url ?? "";
657
+ const pathname = url.split("?", 1)[0] ?? "";
658
+ if (pathname.startsWith("/__applet/context/")) {
659
+ if (pathname !== `/__applet/context/${manifest.code}` || request.method !== "GET") {
660
+ response.writeHead(404).end();
661
+ return;
662
+ }
663
+ if (refusesClient(request.headers, request.socket.remoteAddress)) {
664
+ response.writeHead(403).end();
665
+ return;
666
+ }
667
+ response.setHeader("Cache-Control", "no-store");
668
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
669
+ response.end(JSON.stringify({
670
+ cacheBuster,
671
+ cacheScope: person.id
672
+ }));
673
+ return;
674
+ }
252
675
  if (url === "/_hitch/client.js" || url.startsWith("/_hitch/client.js?")) {
253
676
  response.statusCode = 200;
254
677
  response.setHeader("Cache-Control", "no-store");
@@ -256,29 +679,37 @@ function applet(sourceDefinition, options = {}) {
256
679
  response.end("// PostHog is disabled in standalone applet development.\n");
257
680
  return;
258
681
  }
259
- if (url === "/theme.css" || url.startsWith("/theme.css?")) {
260
- readFile(resolve(root, ".hitch/theme.css")).then((theme) => {
261
- response.statusCode = 200;
262
- response.setHeader("Cache-Control", "no-cache");
263
- response.setHeader("Content-Type", "text/css; charset=utf-8");
264
- response.end(theme);
265
- }, (error) => {
266
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
267
- next();
268
- return;
269
- }
270
- next(error);
682
+ if (url === "/api" || url.startsWith("/api/") || url.startsWith("/api?") || url === "/theme.css" || url.startsWith("/theme.css?")) {
683
+ serveDevProxy(request, response, proxy).catch((error) => {
684
+ if (response.headersSent) response.destroy(error instanceof Error ? error : new Error(String(error)));
685
+ else next(error);
271
686
  });
272
687
  return;
273
688
  }
274
- if (url === apiPrefix || url.startsWith(`${apiPrefix}/`) || url.startsWith(`${apiPrefix}?`)) request.url = url.slice(mount.length);
689
+ if (url === apiPrefix || url.startsWith(`${apiPrefix}/`) || url.startsWith(`${apiPrefix}?`)) {
690
+ if (refusesClient(request.headers, request.socket.remoteAddress)) {
691
+ response.writeHead(403).end();
692
+ return;
693
+ }
694
+ request.url = url.slice(mount.length);
695
+ if (request.url.startsWith("/api/__hitch/") && request.headers.origin !== void 0) {
696
+ response.writeHead(404).end();
697
+ return;
698
+ }
699
+ request.originalUrl = request.url;
700
+ request.headers[HITCH_USER_HEADER] = person.id;
701
+ request.headers[HITCH_SESSION_HEADER] = "local-dev";
702
+ request.rawHeaders = request.rawHeaders.flatMap((name, index, headers) => index % 2 === 0 && name.toLowerCase() !== "x-hitch-user" && name.toLowerCase() !== "x-hitch-session" ? [name, headers[index + 1] ?? ""] : []);
703
+ request.rawHeaders.push(HITCH_USER_HEADER, person.id, HITCH_SESSION_HEADER, "local-dev");
704
+ }
705
+ response.setHeader("Content-Security-Policy", `frame-ancestors ${orgOrigin}`);
275
706
  next();
276
707
  });
277
708
  },
278
709
  transformIndexHtml: {
279
710
  order: "post",
280
711
  handler(html) {
281
- const parentOrigin = themeStylesheetUrl === void 0 ? void 0 : new URL(themeStylesheetUrl).origin;
712
+ const parentOrigin = devContext?.orgUrl === void 0 ? themeStylesheetUrl === void 0 ? void 0 : new URL(themeStylesheetUrl).origin : new URL(devContext.orgUrl).origin;
282
713
  return {
283
714
  html: themeStylesheetUrl === void 0 ? html : html.replace("href=\"/theme.css\"", `href="${themeStylesheetUrl}"`),
284
715
  tags: [{
@@ -332,7 +763,11 @@ function applet(sourceDefinition, options = {}) {
332
763
  not_found_handling: "single-page-application",
333
764
  run_worker_first: ["/api/*"]
334
765
  } } : {},
335
- ...hasServer ? { main: resolve(root, ".hitch/entry.server.gen.ts") } : {}
766
+ ...hasServer ? { main: resolve(root, ".hitch/entry.server.gen.ts") } : {},
767
+ ...devApiUrl === void 0 ? {} : { vars: {
768
+ ...workerConfig.vars,
769
+ HITCH_API_URL: devApiUrl
770
+ } }
336
771
  };
337
772
  },
338
773
  get viteEnvironment() {
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ //#region src/headers.ts
3
+ const HITCH_USER_HEADER = "x-hitch-user";
4
+ const HITCH_SESSION_HEADER = "x-hitch-session";
5
+ const HITCH_APPLET_HEADER = "x-hitch-applet";
6
+ const HITCH_APPLET_TOKEN_HEADER = "x-hitch-applet-token";
7
+ //#endregion
8
+ //#region src/server/workflow-protocol.ts
9
+ const journalEntrySchema = z.discriminatedUnion("kind", [
10
+ z.object({
11
+ kind: z.literal("step"),
12
+ id: z.string(),
13
+ name: z.string(),
14
+ result: z.unknown()
15
+ }),
16
+ z.object({
17
+ kind: z.literal("sleep"),
18
+ id: z.string(),
19
+ name: z.string(),
20
+ until: z.number()
21
+ }),
22
+ z.object({
23
+ kind: z.literal("attempt"),
24
+ id: z.string(),
25
+ name: z.string(),
26
+ error: z.string()
27
+ }),
28
+ z.object({
29
+ kind: z.literal("failed"),
30
+ id: z.string(),
31
+ name: z.string(),
32
+ error: z.string()
33
+ })
34
+ ]);
35
+ const workflowInvocationRequestSchema = z.object({
36
+ payload: z.unknown(),
37
+ journal: z.array(journalEntrySchema)
38
+ });
39
+ const workflowOutcomeSchema = z.discriminatedUnion("kind", [
40
+ z.object({
41
+ kind: z.literal("step"),
42
+ id: z.string(),
43
+ name: z.string(),
44
+ result: z.unknown()
45
+ }),
46
+ z.object({
47
+ kind: z.literal("sleep"),
48
+ id: z.string(),
49
+ name: z.string(),
50
+ until: z.number()
51
+ }),
52
+ z.object({
53
+ kind: z.literal("step_error"),
54
+ id: z.string(),
55
+ name: z.string(),
56
+ message: z.string(),
57
+ retries: z.object({
58
+ limit: z.number(),
59
+ delayMs: z.number(),
60
+ backoff: z.enum([
61
+ "constant",
62
+ "linear",
63
+ "exponential"
64
+ ])
65
+ })
66
+ }),
67
+ z.object({
68
+ kind: z.literal("done"),
69
+ result: z.unknown()
70
+ }),
71
+ z.object({
72
+ kind: z.literal("error"),
73
+ message: z.string()
74
+ })
75
+ ]);
76
+ //#endregion
77
+ //#region src/server/workflow-engine.ts
78
+ var WorkflowFailedError = class extends Error {};
79
+ async function runWorkflowJournal(engine, invoke) {
80
+ const journal = [];
81
+ for (let index = 1;; index += 1) {
82
+ const outcome = await engine.invoke(`invoke ${index}`, () => invoke(journal));
83
+ switch (outcome.kind) {
84
+ case "step":
85
+ journal.push(outcome);
86
+ break;
87
+ case "sleep":
88
+ journal.push(outcome);
89
+ await engine.sleepUntil(`sleep ${index}`, new Date(outcome.until));
90
+ break;
91
+ case "step_error": {
92
+ const attempts = 1 + journal.filter((entry) => entry.kind === "attempt" && entry.id === outcome.id).length;
93
+ journal.push({
94
+ kind: "attempt",
95
+ id: outcome.id,
96
+ name: outcome.name,
97
+ error: outcome.message
98
+ });
99
+ if (attempts > outcome.retries.limit) {
100
+ journal.push({
101
+ kind: "failed",
102
+ id: outcome.id,
103
+ name: outcome.name,
104
+ error: outcome.message
105
+ });
106
+ break;
107
+ }
108
+ await engine.sleep(`retry ${index}`, outcome.retries.delayMs * (outcome.retries.backoff === "exponential" ? 2 ** (attempts - 1) : outcome.retries.backoff === "linear" ? attempts : 1));
109
+ break;
110
+ }
111
+ case "done": return outcome.result;
112
+ case "error": throw new WorkflowFailedError(outcome.message);
113
+ }
114
+ }
115
+ }
116
+ //#endregion
117
+ export { workflowOutcomeSchema as a, HITCH_SESSION_HEADER as c, workflowInvocationRequestSchema as i, HITCH_USER_HEADER as l, runWorkflowJournal as n, HITCH_APPLET_HEADER as o, journalEntrySchema as r, HITCH_APPLET_TOKEN_HEADER as s, WorkflowFailedError as t };
@@ -342,9 +342,12 @@ function readWorkspaceConfig(dir) {
342
342
  }
343
343
  const config = module !== null && typeof module === "object" && "default" in module ? module.default : module;
344
344
  if (config === null || typeof config !== "object" || !("sdk" in config) || typeof config.sdk !== "string" || config.sdk.length === 0 || !("org" in config) || typeof config.org !== "string" || !ORG_CODE_PATTERN.test(config.org)) throw new Error(`Invalid Hitch config in ${path}: "sdk" must be a package spec and "org" an organization code.`);
345
+ const githubWorkflow = "githubWorkflow" in config ? config.githubWorkflow : void 0;
346
+ if (githubWorkflow !== void 0 && typeof githubWorkflow !== "boolean") throw new Error(`Invalid Hitch config in ${path}: "githubWorkflow" must be a boolean.`);
345
347
  return {
346
348
  sdk: config.sdk,
347
- org: config.org
349
+ org: config.org,
350
+ githubWorkflow
348
351
  };
349
352
  }
350
353
  function findWorkspaceConfig(cwd) {
@@ -384,4 +387,4 @@ function resolveWorkspaceOrg(appletRoot) {
384
387
  throw new Error(`No Hitch workspace found for ${appletRoot}. An applet must live at <workspace>/applets/<code> with hitch.config.ts in the workspace root.`);
385
388
  }
386
389
  //#endregion
387
- export { loginWithDevice as a, resolveAuthForOrg as c, readWorkspaceConfig as i, resolveInstanceUrl as l, fetchWithOrgAuth as n, AuthenticationError as o, findWorkspaceConfig as r, organizationInstanceUrl as s, resolveWorkspaceOrg as t };
390
+ export { loginWithDevice as a, organizationInstanceUrl as c, resolveInstanceUrl as d, readWorkspaceConfig as i, rejectEnvironmentKey as l, fetchWithOrgAuth as n, AuthenticationError as o, findWorkspaceConfig as r, bearerToken as s, resolveWorkspaceOrg as t, resolveAuthForOrg as u };