@pylonsync/functions 0.3.385 → 0.4.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.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { AuthInfo } from "./types";
2
+ export type AuthClaims = Omit<AuthInfo, "elevate">;
3
+ /** Normalize the Rust wire envelope into the public function auth shape. */
4
+ export declare function normalizeAuthClaims(raw: Record<string, unknown>): AuthClaims;
package/dist/types.d.ts CHANGED
@@ -40,6 +40,10 @@ export interface AuthInfo<R extends AuthRequirement = "optional"> {
40
40
  /** Active tenant id (selected organization) for multi-tenant apps.
41
41
  * Null when the session hasn't selected one. */
42
42
  tenantId: string | null;
43
+ /** Exact role slugs for the active organization/session. Custom roles do
44
+ * not imply `member` or `admin`; compare explicitly or use
45
+ * `ctx.requireMember` for an authoritative membership lookup. */
46
+ roles: string[];
43
47
  /**
44
48
  * Promote the call's auth context after the handler has done its
45
49
  * own authentication check (HMAC signature verification on a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.3.385",
3
+ "version": "0.4.0",
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,27 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { normalizeAuthClaims } from "./auth";
3
+
4
+ describe("function auth normalization", () => {
5
+ test("preserves custom roles from the Rust wire envelope", () => {
6
+ expect(
7
+ normalizeAuthClaims({
8
+ user_id: "u1",
9
+ is_admin: false,
10
+ tenant_id: "org_1",
11
+ roles: ["reviewer"],
12
+ }),
13
+ ).toEqual({
14
+ userId: "u1",
15
+ isAdmin: false,
16
+ tenantId: "org_1",
17
+ roles: ["reviewer"],
18
+ });
19
+ });
20
+
21
+ test("defaults missing roles to an empty array and drops non-strings", () => {
22
+ expect(normalizeAuthClaims({}).roles).toEqual([]);
23
+ expect(normalizeAuthClaims({ roles: ["billing", 42, null] }).roles).toEqual([
24
+ "billing",
25
+ ]);
26
+ });
27
+ });
package/src/auth.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { AuthInfo } from "./types";
2
+
3
+ export type AuthClaims = Omit<AuthInfo, "elevate">;
4
+
5
+ /** Normalize the Rust wire envelope into the public function auth shape. */
6
+ export function normalizeAuthClaims(
7
+ raw: Record<string, unknown>,
8
+ ): AuthClaims {
9
+ return {
10
+ userId:
11
+ ((raw.userId ?? raw.user_id) as string | null | undefined) ?? null,
12
+ isAdmin: Boolean(raw.isAdmin ?? raw.is_admin),
13
+ tenantId:
14
+ ((raw.tenantId ?? raw.tenant_id) as string | null | undefined) ?? null,
15
+ roles: Array.isArray(raw.roles)
16
+ ? raw.roles.filter((role): role is string => typeof role === "string")
17
+ : [],
18
+ };
19
+ }
@@ -65,6 +65,20 @@ describe("ctx.requireMember", () => {
65
65
  ).toBe("FORBIDDEN");
66
66
  });
67
67
 
68
+ test("custom roles are exact-match and inherit no built-in role", async () => {
69
+ const reviewer = { id: "m1", role: "reviewer" };
70
+ const requireMember = makeRequireMember("u1", spyRead([reviewer]).read);
71
+
72
+ expect(
73
+ await codeOf(() =>
74
+ requireMember("org_1", { role: ["owner", "admin", "member"] }),
75
+ ),
76
+ ).toBe("FORBIDDEN");
77
+ expect(await requireMember("org_1", { role: ["reviewer"] })).toEqual(
78
+ reviewer,
79
+ );
80
+ });
81
+
68
82
  test("role gate: passes when the member's role IS allowed (string or array)", async () => {
69
83
  const owner = { id: "m1", role: "owner" };
70
84
  expect(
package/src/runtime.ts CHANGED
@@ -35,6 +35,7 @@ import type {
35
35
  FnDefinition,
36
36
  AuthInfo,
37
37
  } from "./types";
38
+ import { normalizeAuthClaims } from "./auth";
38
39
  import { makeRequireMember } from "./member";
39
40
  import { isDevMode } from "./ssr-runtime";
40
41
  import { validateArgs } from "./validators";
@@ -1082,11 +1083,7 @@ async function handleCall(msg: CallMessage): Promise<void> {
1082
1083
  // already got camelCase don't regress.
1083
1084
  const rawAuth = msg.auth as unknown as Record<string, unknown>;
1084
1085
  const auth: AuthInfo = {
1085
- userId: ((rawAuth.userId ?? rawAuth.user_id) as string | null | undefined) ?? null,
1086
- isAdmin: Boolean(rawAuth.isAdmin ?? rawAuth.is_admin),
1087
- tenantId:
1088
- ((rawAuth.tenantId ?? rawAuth.tenant_id) as string | null | undefined) ??
1089
- null,
1086
+ ...normalizeAuthClaims(rawAuth),
1090
1087
  // `elevate` round-trips through the host runtime which mutates
1091
1088
  // the per-call caller_is_admin flag — that's what subsequent
1092
1089
  // scheduler.runAfter() reads. We also mutate the local
package/src/types.ts CHANGED
@@ -47,6 +47,10 @@ export interface AuthInfo<R extends AuthRequirement = "optional"> {
47
47
  /** Active tenant id (selected organization) for multi-tenant apps.
48
48
  * Null when the session hasn't selected one. */
49
49
  tenantId: string | null;
50
+ /** Exact role slugs for the active organization/session. Custom roles do
51
+ * not imply `member` or `admin`; compare explicitly or use
52
+ * `ctx.requireMember` for an authoritative membership lookup. */
53
+ roles: string[];
50
54
  /**
51
55
  * Promote the call's auth context after the handler has done its
52
56
  * own authentication check (HMAC signature verification on a