1import { defineRule } from "@oxlint/plugins";
2import * as Option from "effect/Option";
3import { getPropertyName } from "../utils.ts";
4
5const readOptions = (options: ReadonlyArray<unknown>) => {
6 const [first] = options;
7 return {
8 allowRawClientAccess:
9 typeof first === "object" &&
10 first !== null &&
11 "allowRawClientAccess" in first &&
12 first.allowRawClientAccess === true,
13 allowGuardInstallation:
14 typeof first === "object" &&
15 first !== null &&
16 "allowGuardInstallation" in first &&
17 first.allowGuardInstallation === true,
18 };
19};
20
21/** A guardrail for ordinary edits, not a security boundary against deliberate casts or aliases. */
22export default defineRule({
23 meta: {
24 type: "problem",
25 docs: {
26 description:
27 "Keep raw RPC access and permission guard installation inside their shared boundaries.",
28 },
29 schema: [
30 {
31 type: "object",
32 properties: {
33 allowRawClientAccess: {
34 type: "boolean",
35 description: "Permit .client access for non-RPC clients in the configured files.",
36 },
37 allowGuardInstallation: {
38 type: "boolean",
39 description: "Permit installing RpcPermissionGuard in a reviewed command boundary.",
40 },
41 },
42 additionalProperties: false,
43 },
44 ],
45 defaultOptions: [{ allowRawClientAccess: false, allowGuardInstallation: false }],
46 },
47 create(context) {
48 const { allowRawClientAccess, allowGuardInstallation } = readOptions(context.options);
49 const report = (node: NonNullable<Parameters<typeof context.report>[0]["node"]>) =>
50 context.report({
51 node,
52 message:
53 "Use permission-aware environment commands. Raw RPC access and RpcPermissionGuard belong in the shared RPC/command boundary.",
54 });
55 return {
56 ImportDeclaration(node) {
57 if (typeof node.source.value !== "string") return;
58 const source = node.source.value;
59 if (/(?:^|\/)rpc\/protocol(?:\.ts)?$/.test(source) && node.importKind !== "type")
60 report(node);
61 if (
62 !allowGuardInstallation &&
63 /(?:^|\/)rpc(?:\/(?:client|index)(?:\.ts)?)?$/.test(source)
64 ) {
65 for (const specifier of node.specifiers) {
66 if (
67 specifier.type === "ImportSpecifier" &&
68 Option.getOrNull(getPropertyName(specifier.imported)) === "RpcPermissionGuard"
69 )
70 report(specifier);
71 }
72 }
73 },
74 VariableDeclarator(node) {
75 if (allowRawClientAccess || node.id.type !== "ObjectPattern") return;
76 for (const property of node.id.properties) {
77 if (
78 property.type === "Property" &&
79 Option.getOrNull(getPropertyName(property.key)) === "client"
80 )
81 report(property);
82 }
83 },
84 MemberExpression(node) {
85 const property = Option.getOrNull(getPropertyName(node.property));
86 if (!allowGuardInstallation && property === "RpcPermissionGuard") report(node);
87 if (!allowRawClientAccess && property === "client") report(node);
88 },
89 };
90 },
91});