@pylonsync/functions 0.9.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "TypeScript function runtime for pylon — defines server-side queries, mutations, and actions.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A handler that RETURNS `ctx.error(...)` instead of throwing it must
3
+ * still fail the call. An Error has no JSON shape, so a returned one
4
+ * used to reach the client as HTTP 200 with `{}` — a forged proof
5
+ * "passed" because the author wrote `return` where `throw` belonged.
6
+ *
7
+ * Same child-process harness as runtime-db.test.ts: runtime.ts runs
8
+ * main() on import, loads the functions dir from argv[2], and speaks
9
+ * NDJSON on stdin/stdout like the Rust host.
10
+ */
11
+ import { expect, test } from "bun:test";
12
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ const RUNTIME = join(import.meta.dir, "runtime.ts");
17
+ const DEFINE = join(import.meta.dir, "index.ts");
18
+
19
+ function frames(text: string): Record<string, unknown>[] {
20
+ return text
21
+ .split("\n")
22
+ .filter((l) => l.trim().startsWith("{"))
23
+ .map((l) => JSON.parse(l) as Record<string, unknown>);
24
+ }
25
+
26
+ test("a returned ctx.error rejects the call with its code", async () => {
27
+ const dir = mkdtempSync(join(tmpdir(), "pylon-fn-returned-error-"));
28
+ mkdirSync(join(dir, "functions"));
29
+ writeFileSync(
30
+ join(dir, "functions", "checkProof.ts"),
31
+ `import { query } from ${JSON.stringify(DEFINE)};
32
+ export default query({
33
+ args: {},
34
+ auth: "public",
35
+ async handler(ctx) {
36
+ // Wrong on purpose: return instead of throw.
37
+ return ctx.error("INVALID_PROOF", "proof rejected") as unknown as { ok: boolean };
38
+ },
39
+ });
40
+ `,
41
+ );
42
+ writeFileSync(
43
+ join(dir, "functions", "plain.ts"),
44
+ `import { query } from ${JSON.stringify(DEFINE)};
45
+ export default query({
46
+ args: {},
47
+ auth: "public",
48
+ async handler() {
49
+ return { ok: true };
50
+ },
51
+ });
52
+ `,
53
+ );
54
+
55
+ const proc = Bun.spawn([process.execPath, RUNTIME, "functions"], {
56
+ cwd: dir,
57
+ stdin: "pipe",
58
+ stdout: "pipe",
59
+ stderr: "pipe",
60
+ });
61
+
62
+ const reader = proc.stdout.getReader();
63
+ const decoder = new TextDecoder();
64
+ let buffered = "";
65
+ let seen: Record<string, unknown>[] = [];
66
+ const until = async (pred: (f: Record<string, unknown>[]) => boolean) => {
67
+ while (!pred(seen)) {
68
+ const { done, value } = await reader.read();
69
+ if (done) break;
70
+ buffered += decoder.decode(value, { stream: true });
71
+ seen = frames(buffered);
72
+ }
73
+ };
74
+
75
+ await until((f) => f.some((m) => m.type === "ready"));
76
+ const send = (msg: Record<string, unknown>) =>
77
+ proc.stdin.write(JSON.stringify(msg) + "\n");
78
+ const auth = { user_id: null, is_admin: false };
79
+ send({ type: "call", call_id: "c_bad", fn_name: "checkProof", fn_type: "query", args: {}, auth });
80
+ send({ type: "call", call_id: "c_ok", fn_name: "plain", fn_type: "query", args: {}, auth });
81
+ await proc.stdin.flush();
82
+ await until((f) => f.some((m) => m.call_id === "c_bad") && f.some((m) => m.call_id === "c_ok"));
83
+ proc.kill();
84
+
85
+ const bad = seen.find((m) => m.call_id === "c_bad");
86
+ expect(bad).toMatchObject({ type: "error", code: "INVALID_PROOF", message: "proof rejected" });
87
+ const ok = seen.find((m) => m.call_id === "c_ok");
88
+ expect(ok).toMatchObject({ type: "return", value: { ok: true } });
89
+ });
package/src/runtime.ts CHANGED
@@ -1394,6 +1394,11 @@ async function handleCall(msg: CallMessage): Promise<void> {
1394
1394
  try {
1395
1395
  const result = await def.handler(ctx, msg.args);
1396
1396
  if (cancelledCalls.has(msg.call_id)) return;
1397
+ // `return ctx.error(...)` instead of `throw ctx.error(...)` is a
1398
+ // common slip. An Error has no JSON shape, so a returned one can
1399
+ // only mean the handler meant to fail: treat it as thrown so the
1400
+ // caller gets the error code, not a 200 with `{}`.
1401
+ if (result instanceof Error) throw result;
1397
1402
  send({
1398
1403
  type: "return",
1399
1404
  call_id: msg.call_id,