@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 +1 -1
- package/src/runtime-returned-error.test.ts +89 -0
- package/src/runtime.ts +5 -0
package/package.json
CHANGED
|
@@ -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,
|