@pylonsync/functions 0.11.5 → 0.12.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.
@@ -0,0 +1,69 @@
1
+ // Keeps stdout for the host protocol. See `fenceStdout`.
2
+
3
+ // Bun global used here. Declared locally so apps that type-check this source
4
+ // without bun-types still pass `tsc` (same pattern as runtime.ts).
5
+ declare const Bun: {
6
+ write(dest: unknown, data: string): unknown;
7
+ stderr: unknown;
8
+ };
9
+
10
+ /**
11
+ * Redirect console.* from user code to stderr so handlers can't accidentally
12
+ * emit a line that looks like a protocol frame and confuse the Rust reader.
13
+ *
14
+ * Before this guard, a handler calling `console.log('{"type":"return",...}')`
15
+ * — either intentionally or by logging an object shaped that way — would be
16
+ * parsed by the host as a real protocol message. Moving all console output
17
+ * to stderr keeps stdout reserved for NDJSON protocol frames only.
18
+ *
19
+ * The original console methods are saved on the console object as
20
+ * `__stdoutLog` etc. in case the runtime itself needs to write diagnostics
21
+ * to stdout for some reason (it currently doesn't).
22
+ *
23
+ * Safe to call more than once. A production server bundle calls it from its
24
+ * entry, before the app modules load, and the runtime's `main()` calls it
25
+ * again.
26
+ */
27
+ export function fenceStdout(): void {
28
+ const c = globalThis.console as unknown as Record<string, unknown>;
29
+ if (c.__pylonFenced) return;
30
+ c.__pylonFenced = true;
31
+ const toStderr = (prefix: string) => (...args: unknown[]) => {
32
+ const line = args
33
+ .map((a) => {
34
+ if (typeof a === "string") return a;
35
+ // Error: JSON.stringify yields `{}` because message/stack are
36
+ // non-enumerable. That made `console.error("x:", err)` log as `x: {}`,
37
+ // hiding the real failure from operators. Unwrap by hand.
38
+ if (a instanceof Error) {
39
+ const parts = [a.stack || `${a.name}: ${a.message}`];
40
+ const code = (a as { code?: unknown }).code;
41
+ if (code !== undefined) parts.push(`code=${String(code)}`);
42
+ const cause = (a as { cause?: unknown }).cause;
43
+ if (cause !== undefined) {
44
+ try {
45
+ parts.push(`cause=${cause instanceof Error ? cause.stack || cause.message : JSON.stringify(cause)}`);
46
+ } catch {
47
+ parts.push(`cause=${String(cause)}`);
48
+ }
49
+ }
50
+ return parts.join(" ");
51
+ }
52
+ try {
53
+ return JSON.stringify(a);
54
+ } catch {
55
+ return String(a);
56
+ }
57
+ })
58
+ .join(" ");
59
+ Bun.write(Bun.stderr, `${prefix}${line}\n`);
60
+ };
61
+ // Intentional: we want console.* for user handlers to go to stderr.
62
+ // Overwrite the globals before any user code is loaded.
63
+ c.__stdoutLog = c.log;
64
+ c.log = toStderr("");
65
+ c.info = toStderr("");
66
+ c.warn = toStderr("[warn] ");
67
+ c.error = toStderr("[error] ");
68
+ c.debug = toStderr("[debug] ");
69
+ }
package/src/types.ts CHANGED
@@ -827,6 +827,42 @@ export interface Files {
827
827
  signedUrl(fileId: string, opts?: { ttlSecs?: number }): Promise<string>;
828
828
  }
829
829
 
830
+ /**
831
+ * Shard tickets: short-lived, signed permission to join one realtime shard,
832
+ * with claims the shard's authorization hooks read without a database
833
+ * call. Check what the user may do here, then mint:
834
+ *
835
+ * ```ts
836
+ * export default mutation({
837
+ * args: { characterId: v.id("Character") },
838
+ * async handler(ctx, args) {
839
+ * const c = await ctx.db.get("Character", args.characterId);
840
+ * if (c?.ownerId !== ctx.auth.userId) throw ctx.error("FORBIDDEN", "not your character");
841
+ * const shard = `zone-${c.zone}`;
842
+ * const ticket = await ctx.shards.ticket(shard, {
843
+ * subscriberId: c.id,
844
+ * claims: { character: c.id, realm: c.realm },
845
+ * });
846
+ * return { shard, ticket };
847
+ * },
848
+ * });
849
+ * ```
850
+ *
851
+ * The client passes the ticket when it connects (`connectShard(shard, {
852
+ * ticket })`). The shard rejects a ticket for another shard, another
853
+ * subscriber id, or past its expiry.
854
+ */
855
+ export interface Shards {
856
+ /**
857
+ * Mint a ticket for `shardId`. `subscriberId` defaults to the calling
858
+ * user's id; `ttlSecs` defaults to 60 and is capped at 3600 by the host.
859
+ */
860
+ ticket(
861
+ shardId: string,
862
+ opts?: { subscriberId?: string; claims?: Record<string, unknown>; ttlSecs?: number },
863
+ ): Promise<string>;
864
+ }
865
+
830
866
  /** Context for query handlers (read-only).
831
867
  *
832
868
  * NOTE: `ctx.llm` is NOT exposed here. Queries are reactive: a
@@ -852,6 +888,8 @@ export interface QueryCtx<R extends AuthRequirement = "optional"> {
852
888
  requireMember: RequireMember;
853
889
  /** Signed file-download URLs — see {@link Files}. */
854
890
  files: Files;
891
+ /** Shard tickets — see {@link Shards}. */
892
+ shards: Shards;
855
893
  /**
856
894
  * Fires when the host cancels this call (idle timeout exceeded).
857
895
  * Thread it into `fetch(url, { signal: ctx.signal })` or SDK calls so
@@ -880,6 +918,8 @@ export interface MutationCtx<R extends AuthRequirement = "optional"> {
880
918
  workflows: Workflows;
881
919
  /** Signed file-download URLs — see {@link Files}. */
882
920
  files: Files;
921
+ /** Shard tickets — see {@link Shards}. */
922
+ shards: Shards;
883
923
  /** Create a typed error that triggers rollback. */
884
924
  error(code: string, message: string): Error;
885
925
  /** Assert org membership (optionally a role) — see {@link RequireMember}. */
@@ -1053,6 +1093,8 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
1053
1093
  env: Record<string, string>;
1054
1094
  /** Signed file-download URLs — see {@link Files}. */
1055
1095
  files: Files;
1096
+ /** Shard tickets — see {@link Shards}. */
1097
+ shards: Shards;
1056
1098
  /** Run a registered query within its own read transaction. */
1057
1099
  runQuery<T = unknown>(
1058
1100
  fnName: string,