@pylonsync/functions 0.3.116 → 0.3.118

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.3.116",
3
+ "version": "0.3.118",
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",
package/src/runtime.ts CHANGED
@@ -593,6 +593,25 @@ async function handleCall(msg: CallMessage): Promise<void> {
593
593
  tenantId:
594
594
  ((rawAuth.tenantId ?? rawAuth.tenant_id) as string | null | undefined) ??
595
595
  null,
596
+ // `elevate` round-trips through the host runtime which mutates
597
+ // the per-call caller_is_admin flag — that's what subsequent
598
+ // scheduler.runAfter() reads. We also mutate the local
599
+ // `auth.isAdmin` so handler code that re-checks `ctx.auth.isAdmin`
600
+ // after elevation sees the new value (it would otherwise stay
601
+ // false even though scheduling now works, which is confusing).
602
+ async elevate(options: { admin: boolean; reason: string }) {
603
+ await rpc(msg.call_id, {
604
+ type: "elevate_auth",
605
+ admin: options.admin,
606
+ reason: options.reason,
607
+ });
608
+ if (options.admin) {
609
+ // Mutate in-place. AuthInfo isn't frozen and handlers hold a
610
+ // reference, so the read on the next line of their code
611
+ // reflects the elevated state.
612
+ (auth as { isAdmin: boolean }).isAdmin = true;
613
+ }
614
+ },
596
615
  };
597
616
 
598
617
  // Env is read-only config — safe to expose on every ctx variant. Without
@@ -712,9 +731,18 @@ async function main() {
712
731
  const name = basename(file, file.endsWith(".ts") ? ".ts" : ".js");
713
732
  try {
714
733
  const mod = await import(join(process.cwd(), fnDir, file));
715
- const def = mod.default as FnDefinition;
716
- if (def && def.type && def.handler) {
717
- registry.set(name, def);
734
+ const def = mod.default as FnDefinition | undefined;
735
+ // Runtime shape check a misnamed/malformed export should
736
+ // log + skip, not crash the loader. TS narrows `def.handler`
737
+ // as always-defined because the FnDefinition type says so,
738
+ // but at runtime we don't know what the user actually exported.
739
+ const anyDef = def as unknown as Record<string, unknown> | undefined;
740
+ if (
741
+ anyDef &&
742
+ typeof anyDef.type === "string" &&
743
+ typeof anyDef.handler === "function"
744
+ ) {
745
+ registry.set(name, def as FnDefinition);
718
746
  }
719
747
  } catch (err) {
720
748
  console.error(`[functions] Failed to load ${file}:`, err);
package/src/types.ts CHANGED
@@ -12,6 +12,36 @@ export interface AuthInfo {
12
12
  /** Active tenant id (selected organization) for multi-tenant apps.
13
13
  * Null when the session hasn't selected one. */
14
14
  tenantId: string | null;
15
+ /**
16
+ * Promote the call's auth context after the handler has done its
17
+ * own authentication check (HMAC signature verification on a
18
+ * webhook, JWT validation, custom token check). Used by webhook
19
+ * receivers — they're necessarily public (external systems POST
20
+ * to them) but want to schedule internal:true workers after
21
+ * they've proven the request came from a trusted source.
22
+ *
23
+ * The framework does NOT verify the developer actually checked
24
+ * anything before calling this — that's on you. The `reason` is
25
+ * mandatory and gets logged at INFO with the function name so
26
+ * every elevation is auditable.
27
+ *
28
+ * ```ts
29
+ * // Github webhook example:
30
+ * const ok = await verifyGithubSignature(secret, rawBody, sig);
31
+ * if (!ok) throw ctx.error("INVALID_SIGNATURE", "bad sig");
32
+ * await ctx.auth.elevate({
33
+ * admin: true,
34
+ * reason: "github webhook hmac verified",
35
+ * });
36
+ * // Now this works — caller_is_admin=true for the gate:
37
+ * await ctx.scheduler.runAfter(0, "deployProject", { deploymentId });
38
+ * ```
39
+ *
40
+ * After calling `elevate({ admin: true })`, `auth.isAdmin` is also
41
+ * mutated to true locally so subsequent reads in the same handler
42
+ * see the new value.
43
+ */
44
+ elevate(options: { admin: boolean; reason: string }): Promise<void>;
15
45
  }
16
46
 
17
47
  // ---------------------------------------------------------------------------