@prairielearn/eslint-plugin 4.0.0 → 4.2.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.d.ts +14 -9
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +18 -19
  5. package/dist/index.js.map +1 -1
  6. package/dist/rules/aws-client-mandatory-config.d.ts.map +1 -1
  7. package/dist/rules/aws-client-mandatory-config.js +4 -6
  8. package/dist/rules/aws-client-mandatory-config.js.map +1 -1
  9. package/dist/rules/aws-client-shared-config.d.ts.map +1 -1
  10. package/dist/rules/aws-client-shared-config.js +4 -6
  11. package/dist/rules/aws-client-shared-config.js.map +1 -1
  12. package/dist/rules/jsx-no-dollar-interpolation.d.ts.map +1 -1
  13. package/dist/rules/jsx-no-dollar-interpolation.js +2 -4
  14. package/dist/rules/jsx-no-dollar-interpolation.js.map +1 -1
  15. package/dist/rules/no-current-target-in-callback.d.ts +4 -4
  16. package/dist/rules/no-current-target-in-callback.d.ts.map +1 -1
  17. package/dist/rules/no-current-target-in-callback.js +191 -38
  18. package/dist/rules/no-current-target-in-callback.js.map +1 -1
  19. package/dist/rules/no-hydrate-reslocals.d.ts +4 -0
  20. package/dist/rules/no-hydrate-reslocals.d.ts.map +1 -0
  21. package/dist/rules/no-hydrate-reslocals.js +94 -0
  22. package/dist/rules/no-hydrate-reslocals.js.map +1 -0
  23. package/dist/rules/no-unused-sql-blocks.d.ts.map +1 -1
  24. package/dist/rules/no-unused-sql-blocks.js +4 -39
  25. package/dist/rules/no-unused-sql-blocks.js.map +1 -1
  26. package/dist/rules/require-trpc-permission-middleware.d.ts +4 -0
  27. package/dist/rules/require-trpc-permission-middleware.d.ts.map +1 -0
  28. package/dist/rules/require-trpc-permission-middleware.js +133 -0
  29. package/dist/rules/require-trpc-permission-middleware.js.map +1 -0
  30. package/dist/rules/safe-db-types.d.ts +1 -1
  31. package/dist/rules/safe-db-types.d.ts.map +1 -1
  32. package/dist/rules/safe-db-types.js +6 -40
  33. package/dist/rules/safe-db-types.js.map +1 -1
  34. package/dist/tests/aws-client-mandatory-config.test.d.ts.map +1 -1
  35. package/dist/tests/aws-client-mandatory-config.test.js +8 -13
  36. package/dist/tests/aws-client-mandatory-config.test.js.map +1 -1
  37. package/dist/tests/aws-client-shared-config.test.d.ts.map +1 -1
  38. package/dist/tests/aws-client-shared-config.test.js +8 -13
  39. package/dist/tests/aws-client-shared-config.test.js.map +1 -1
  40. package/dist/tests/jsx-no-dollar-interpolation.test.d.ts.map +1 -1
  41. package/dist/tests/jsx-no-dollar-interpolation.test.js +8 -13
  42. package/dist/tests/jsx-no-dollar-interpolation.test.js.map +1 -1
  43. package/dist/tests/no-current-target-in-callback.test.d.ts.map +1 -1
  44. package/dist/tests/no-current-target-in-callback.test.js +75 -23
  45. package/dist/tests/no-current-target-in-callback.test.js.map +1 -1
  46. package/dist/tests/no-hydrate-reslocals.test.d.ts +2 -0
  47. package/dist/tests/no-hydrate-reslocals.test.d.ts.map +1 -0
  48. package/dist/tests/no-hydrate-reslocals.test.js +86 -0
  49. package/dist/tests/no-hydrate-reslocals.test.js.map +1 -0
  50. package/dist/tests/require-trpc-permission-middleware.test.d.ts +2 -0
  51. package/dist/tests/require-trpc-permission-middleware.test.d.ts.map +1 -0
  52. package/dist/tests/require-trpc-permission-middleware.test.js +106 -0
  53. package/dist/tests/require-trpc-permission-middleware.test.js.map +1 -0
  54. package/dist/utils.js +1 -4
  55. package/dist/utils.js.map +1 -1
  56. package/package.json +11 -10
  57. package/src/index.ts +7 -1
  58. package/src/rules/no-current-target-in-callback.ts +273 -39
  59. package/src/rules/no-hydrate-reslocals.ts +102 -0
  60. package/src/rules/require-trpc-permission-middleware.ts +168 -0
  61. package/src/tests/aws-client-mandatory-config.test.ts +1 -1
  62. package/src/tests/aws-client-shared-config.test.ts +1 -1
  63. package/src/tests/jsx-no-dollar-interpolation.test.ts +1 -1
  64. package/src/tests/no-current-target-in-callback.test.ts +67 -10
  65. package/src/tests/no-hydrate-reslocals.test.ts +89 -0
  66. package/src/tests/require-trpc-permission-middleware.test.ts +109 -0
  67. package/tsconfig.json +1 -7
@@ -0,0 +1,168 @@
1
+ import { ASTUtils, ESLintUtils, type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+
3
+ const { findVariable } = ASTUtils;
4
+
5
+ /**
6
+ * tRPC procedures must enforce their own authorization via a `require*`
7
+ * permission middleware in the chain.
8
+ * Individual procedures often need stricter permissions than the page they're
9
+ * mounted under (e.g. an Edit-only mutation under a route that allows View).
10
+ * Requiring an explicit permission middleware per procedure makes the
11
+ * authz gate visible at the call site and helps prevent missing permission checks.
12
+ *
13
+ * Bad:
14
+ * ```ts
15
+ * const list = t.procedure.query(async ({ ctx }) => { ... });
16
+ * ```
17
+ *
18
+ * Good:
19
+ * ```ts
20
+ * const list = t.procedure.use(requireCoursePermissionEdit).query(...);
21
+ * ```
22
+ *
23
+ * Recognized permission middlewares:
24
+ * - requireCoursePermissionPreview / View / Edit / Own
25
+ * - requireCourseInstancePermissionPreview / View / Edit / Own
26
+ * - requireAdministrator
27
+ *
28
+ * Feature gates like `requireEnhancedAccessControl` and `requireAiGradingFeature`
29
+ * don't count — they must be paired with a permission middleware.
30
+ *
31
+ * The rule follows intermediate procedure-base variables so patterns like
32
+ * `const protected = t.procedure.use(...); ...; protected.query(...)` are
33
+ * checked end-to-end.
34
+ */
35
+
36
+ // TODO: This list requires manual maintenance. We should consider making feature gates
37
+ // not use the `require` prefix, and then we can use a more generic pattern here.
38
+ const PERMISSION_MIDDLEWARE_PATTERN =
39
+ /^require(Course|CourseInstance)Permission(Preview|View|Edit|Own)$|^requireAdministrator$/;
40
+ const TERMINAL_METHODS = new Set(['query', 'mutation', 'subscription']);
41
+
42
+ function unwrapMemberObject(expr: TSESTree.Expression): TSESTree.Expression {
43
+ return expr.type === 'TSNonNullExpression' || expr.type === 'TSAsExpression'
44
+ ? unwrapMemberObject(expr.expression)
45
+ : expr;
46
+ }
47
+
48
+ function isTProcedure(expr: TSESTree.Expression): boolean {
49
+ return (
50
+ expr.type === 'MemberExpression' &&
51
+ !expr.computed &&
52
+ expr.property.type === 'Identifier' &&
53
+ expr.property.name === 'procedure' &&
54
+ expr.object.type === 'Identifier' &&
55
+ expr.object.name === 't'
56
+ );
57
+ }
58
+
59
+ interface ChainInfo {
60
+ matchesTProcedure: boolean;
61
+ middlewares: string[];
62
+ }
63
+
64
+ /**
65
+ * Walks back through a chain like `expr.use(A).input(B).use(C)` collecting
66
+ * `.use(...)` argument identifiers. If the base of the chain is `t.procedure`,
67
+ * returns the collected middlewares. If the base is a variable, follows the
68
+ * variable's initializer (e.g. `const protected = t.procedure.use(X)`).
69
+ */
70
+ function getProcedureChainInfo(
71
+ expr: TSESTree.Expression,
72
+ scope: TSESLint.Scope.Scope,
73
+ visited: Set<TSESTree.Node>,
74
+ ): ChainInfo {
75
+ if (visited.has(expr)) return { matchesTProcedure: false, middlewares: [] };
76
+ visited.add(expr);
77
+
78
+ const middlewares: string[] = [];
79
+ let cursor: TSESTree.Expression = expr;
80
+
81
+ while (cursor.type === 'CallExpression') {
82
+ const callee = cursor.callee;
83
+ if (
84
+ callee.type !== 'MemberExpression' ||
85
+ callee.computed ||
86
+ callee.property.type !== 'Identifier'
87
+ ) {
88
+ return { matchesTProcedure: false, middlewares: [] };
89
+ }
90
+ if (callee.property.name === 'use' && cursor.arguments[0]?.type === 'Identifier') {
91
+ middlewares.push(cursor.arguments[0].name);
92
+ }
93
+ cursor = callee.object;
94
+ }
95
+
96
+ cursor = unwrapMemberObject(cursor);
97
+
98
+ if (isTProcedure(cursor)) {
99
+ return { matchesTProcedure: true, middlewares };
100
+ }
101
+
102
+ // Follow an intermediate procedure-base variable defined in the same module:
103
+ // const protectedProcedure = t.procedure.use(...);
104
+ // protectedProcedure.query(...);
105
+ if (cursor.type === 'Identifier') {
106
+ const variable = findVariable(scope, cursor);
107
+ if (variable && variable.defs.length === 1) {
108
+ const def = variable.defs[0];
109
+ if (
110
+ def.node.type === 'VariableDeclarator' &&
111
+ def.node.init &&
112
+ // Conservative: only follow `const` bindings to avoid stale data
113
+ // from let/var rebinding.
114
+ def.parent?.type === 'VariableDeclaration' &&
115
+ def.parent.kind === 'const'
116
+ ) {
117
+ const upstream = getProcedureChainInfo(def.node.init, scope, visited);
118
+ if (upstream.matchesTProcedure) {
119
+ return {
120
+ matchesTProcedure: true,
121
+ middlewares: [...middlewares, ...upstream.middlewares],
122
+ };
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ return { matchesTProcedure: false, middlewares: [] };
129
+ }
130
+
131
+ export default ESLintUtils.RuleCreator.withoutDocs({
132
+ meta: {
133
+ type: 'problem',
134
+ messages: {
135
+ missingPermissionMiddleware:
136
+ 'tRPC procedure must call .use() with a permission middleware (e.g. requireCoursePermissionEdit)',
137
+ },
138
+ schema: [],
139
+ },
140
+ defaultOptions: [],
141
+
142
+ create(context) {
143
+ return {
144
+ CallExpression(node) {
145
+ if (
146
+ node.callee.type !== 'MemberExpression' ||
147
+ node.callee.computed ||
148
+ node.callee.property.type !== 'Identifier' ||
149
+ !TERMINAL_METHODS.has(node.callee.property.name)
150
+ ) {
151
+ return;
152
+ }
153
+
154
+ const scope = context.sourceCode.getScope(node);
155
+ const info = getProcedureChainInfo(node.callee.object, scope, new Set());
156
+
157
+ if (!info.matchesTProcedure) return;
158
+
159
+ const hasPermissionGate = info.middlewares.some((name) =>
160
+ PERMISSION_MIDDLEWARE_PATTERN.test(name),
161
+ );
162
+ if (!hasPermissionGate) {
163
+ context.report({ node, messageId: 'missingPermissionMiddleware' });
164
+ }
165
+ },
166
+ };
167
+ },
168
+ });
@@ -1,7 +1,7 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
2
  import { afterAll, describe, it } from 'vitest';
3
3
 
4
- import rule from '../rules/aws-client-mandatory-config';
4
+ import rule from '../rules/aws-client-mandatory-config.js';
5
5
 
6
6
  RuleTester.afterAll = afterAll;
7
7
  RuleTester.describe = describe;
@@ -1,7 +1,7 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
2
  import { afterAll, describe, it } from 'vitest';
3
3
 
4
- import rule from '../rules/aws-client-shared-config';
4
+ import rule from '../rules/aws-client-shared-config.js';
5
5
 
6
6
  RuleTester.afterAll = afterAll;
7
7
  RuleTester.describe = describe;
@@ -1,7 +1,7 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
2
  import { afterAll, describe, it } from 'vitest';
3
3
 
4
- import rule from '../rules/jsx-no-dollar-interpolation';
4
+ import rule from '../rules/jsx-no-dollar-interpolation.js';
5
5
 
6
6
  RuleTester.afterAll = afterAll;
7
7
  RuleTester.describe = describe;
@@ -38,10 +38,40 @@ ruleTester.run('no-current-target-in-callback', rule, {
38
38
  {
39
39
  code: '<Component render={(e) => setFoo(() => e.currentTarget)} />',
40
40
  },
41
+ // Synchronous nested callbacks are safe
42
+ {
43
+ code: '<input onChange={(e) => items.map(() => e.currentTarget.value)} />',
44
+ },
45
+ {
46
+ code: `const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
47
+ fields.find((field) => field.name === e.currentTarget.name);
48
+ };`,
49
+ },
50
+ {
51
+ code: `const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
52
+ const cursorPosition = run(() => e.currentTarget.selectionStart ?? 0);
53
+ return cursorPosition;
54
+ };`,
55
+ },
41
56
  // Direct setState without callback
42
57
  {
43
58
  code: '<input onChange={(e) => setChecks({ value: e.currentTarget.checked })} />',
44
59
  },
60
+ // Capturing currentTarget before await is safe
61
+ {
62
+ code: `const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
63
+ const form = e.currentTarget;
64
+ await trigger();
65
+ form.submit();
66
+ };`,
67
+ },
68
+ // Destructuring currentTarget before await is safe
69
+ {
70
+ code: `const handleSubmit = async ({ currentTarget }: React.SubmitEvent<HTMLFormElement>) => {
71
+ await trigger();
72
+ currentTarget.submit();
73
+ };`,
74
+ },
45
75
  // Event target (not currentTarget) - different issue, not covered by this rule
46
76
  {
47
77
  code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.target.checked }))} />',
@@ -79,21 +109,11 @@ ruleTester.run('no-current-target-in-callback', rule, {
79
109
  code: '<button onClick={(e) => setFoo((prev) => ({ ...prev, clicked: e.currentTarget.id }))} />',
80
110
  errors: [{ messageId: 'noCurrentTargetInCallback' }],
81
111
  },
82
- // Nested arrow function in array method
83
- {
84
- code: '<input onChange={(e) => items.map(() => e.currentTarget.value)} />',
85
- errors: [{ messageId: 'noCurrentTargetInCallback' }],
86
- },
87
112
  // Function expression callback
88
113
  {
89
114
  code: '<input onChange={(e) => setChecks(function(c) { return { ...c, value: e.currentTarget.checked }; })} />',
90
115
  errors: [{ messageId: 'noCurrentTargetInCallback' }],
91
116
  },
92
- // Deeply nested
93
- {
94
- code: '<input onChange={(e) => outer(() => inner(() => e.currentTarget.value))} />',
95
- errors: [{ messageId: 'noCurrentTargetInCallback' }],
96
- },
97
117
  // With block body
98
118
  {
99
119
  code: `<input onChange={(e) => {
@@ -103,5 +123,42 @@ ruleTester.run('no-current-target-in-callback', rule, {
103
123
  }} />`,
104
124
  errors: [{ messageId: 'noCurrentTargetInCallback' }],
105
125
  },
126
+ // Promise callbacks are deferred
127
+ {
128
+ code: `<input onChange={(e) => {
129
+ Promise.resolve().then(() => e.currentTarget.value);
130
+ }} />`,
131
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
132
+ },
133
+ // Timer callbacks are deferred
134
+ {
135
+ code: `<input onChange={(e) => {
136
+ setTimeout(() => e.currentTarget.value, 0);
137
+ }} />`,
138
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
139
+ },
140
+ // Access after await in a typed event handler
141
+ {
142
+ code: `const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
143
+ await trigger();
144
+ e.currentTarget.submit();
145
+ };`,
146
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
147
+ },
148
+ // Access after await in an inline JSX handler
149
+ {
150
+ code: `<form onSubmit={async (e) => {
151
+ await trigger();
152
+ e.currentTarget.submit();
153
+ }} />`,
154
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
155
+ },
156
+ // react-hook-form handleSubmit callbacks are also after the original event boundary
157
+ {
158
+ code: `const onSubmit = handleSubmit((_data, e) => {
159
+ e.currentTarget.submit();
160
+ });`,
161
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
162
+ },
106
163
  ],
107
164
  });
@@ -0,0 +1,89 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { afterAll, describe, it } from 'vitest';
3
+
4
+ import rule from '../rules/no-hydrate-reslocals.js';
5
+
6
+ RuleTester.afterAll = afterAll;
7
+ RuleTester.describe = describe;
8
+ RuleTester.it = it;
9
+
10
+ const ruleTester = new RuleTester({
11
+ languageOptions: {
12
+ parserOptions: {
13
+ ecmaFeatures: {
14
+ jsx: true,
15
+ },
16
+ },
17
+ },
18
+ });
19
+
20
+ ruleTester.run('no-hydrate-reslocals', rule, {
21
+ valid: [
22
+ {
23
+ code: '<Hydrate><Child foo={1} bar="baz" /></Hydrate>',
24
+ },
25
+ {
26
+ code: '<Hydrate><Child course={course} assessment={assessment} /></Hydrate>',
27
+ },
28
+ {
29
+ code: '<SomeOther resLocals={res.locals}><Child /></SomeOther>',
30
+ },
31
+ {
32
+ code: '<Hydrate><Child {...otherProps} /></Hydrate>',
33
+ },
34
+ {
35
+ code: '<Hydrate resLocals={foo}><Child /></Hydrate>',
36
+ },
37
+ {
38
+ code: 'hydrateHtml(<Child foo={1} bar="baz" />)',
39
+ },
40
+ {
41
+ code: 'hydrateHtml(<Child {...otherProps} />)',
42
+ },
43
+ {
44
+ code: 'otherFunction(<Child resLocals={res.locals} />)',
45
+ },
46
+ ],
47
+ invalid: [
48
+ {
49
+ code: '<Hydrate><Child resLocals={res.locals} /></Hydrate>',
50
+ errors: [{ messageId: 'forbiddenProp', data: { name: 'resLocals' } }],
51
+ },
52
+ {
53
+ code: '<Hydrate><Child locals={res.locals} /></Hydrate>',
54
+ errors: [{ messageId: 'forbiddenProp', data: { name: 'locals' } }],
55
+ },
56
+ {
57
+ code: '<Hydrate><Child {...res.locals} /></Hydrate>',
58
+ errors: [{ messageId: 'forbiddenSpread' }],
59
+ },
60
+ {
61
+ code: '<Hydrate><Child {...resLocals} /></Hydrate>',
62
+ errors: [{ messageId: 'forbiddenSpread' }],
63
+ },
64
+ {
65
+ code: '<Hydrate><Child {...locals} /></Hydrate>',
66
+ errors: [{ messageId: 'forbiddenSpread' }],
67
+ },
68
+ {
69
+ code: '<Hydrate fullHeight><Child foo={1} resLocals={res.locals} /></Hydrate>',
70
+ errors: [{ messageId: 'forbiddenProp', data: { name: 'resLocals' } }],
71
+ },
72
+ {
73
+ code: 'hydrateHtml(<Child resLocals={res.locals} />)',
74
+ errors: [{ messageId: 'forbiddenProp', data: { name: 'resLocals' } }],
75
+ },
76
+ {
77
+ code: 'hydrateHtml(<Child locals={res.locals} />)',
78
+ errors: [{ messageId: 'forbiddenProp', data: { name: 'locals' } }],
79
+ },
80
+ {
81
+ code: 'hydrateHtml(<Child {...res.locals} />)',
82
+ errors: [{ messageId: 'forbiddenSpread' }],
83
+ },
84
+ {
85
+ code: 'hydrateHtml(<Child {...resLocals} />, { fullHeight: true })',
86
+ errors: [{ messageId: 'forbiddenSpread' }],
87
+ },
88
+ ],
89
+ });
@@ -0,0 +1,109 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { afterAll, describe, it } from 'vitest';
3
+
4
+ import rule from '../rules/require-trpc-permission-middleware.js';
5
+
6
+ RuleTester.afterAll = afterAll;
7
+ RuleTester.describe = describe;
8
+ RuleTester.it = it;
9
+
10
+ const ruleTester = new RuleTester();
11
+
12
+ ruleTester.run('require-trpc-permission-middleware', rule, {
13
+ valid: [
14
+ {
15
+ code: 'const list = t.procedure.use(requireCoursePermissionEdit).query(async () => {});',
16
+ },
17
+ {
18
+ code: 'const m = t.procedure.use(requireCoursePermissionPreview).mutation(async () => {});',
19
+ },
20
+ {
21
+ code: `const m = t.procedure
22
+ .use(requireEnhancedAccessControl)
23
+ .use(requireCourseInstancePermissionView)
24
+ .query(async () => {});`,
25
+ },
26
+ {
27
+ code: 'const m = t.procedure.use(requireAdministrator).mutation(async () => {});',
28
+ },
29
+ {
30
+ code: `const m = t.procedure
31
+ .use(requireCourseInstancePermissionEdit)
32
+ .input(z.object({}))
33
+ .mutation(async () => {});`,
34
+ },
35
+ {
36
+ code: 'const list = t.procedure.use(requireCoursePermissionOwn).query(async () => {});',
37
+ },
38
+ // Not a t.procedure chain — should be ignored
39
+ {
40
+ code: 'const fn = somethingElse.query(async () => {});',
41
+ },
42
+ {
43
+ code: 'const fn = otherRouter.procedure.query(async () => {});',
44
+ },
45
+ // Intermediate procedure-base variable: permission middleware on the base
46
+ {
47
+ code: `
48
+ const protectedProcedure = t.procedure.use(requireCoursePermissionOwn);
49
+ const m = protectedProcedure.query(async () => {});
50
+ `,
51
+ },
52
+ // Intermediate procedure-base variable: permission added downstream
53
+ {
54
+ code: `
55
+ const featureProcedure = t.procedure.use(requireAiGradingFeature);
56
+ const m = featureProcedure.use(requireAdministrator).mutation(async () => {});
57
+ `,
58
+ },
59
+ // Imported procedure base — we can't follow across files, so don't fire
60
+ {
61
+ code: `
62
+ import { protectedProcedure } from './foo.js';
63
+ const m = protectedProcedure.query(async () => {});
64
+ `,
65
+ languageOptions: { parserOptions: { sourceType: 'module' } },
66
+ },
67
+ ],
68
+ invalid: [
69
+ {
70
+ code: 'const list = t.procedure.query(async () => {});',
71
+ errors: [{ messageId: 'missingPermissionMiddleware' }],
72
+ },
73
+ {
74
+ code: 'const m = t.procedure.input(z.object({})).mutation(async () => {});',
75
+ errors: [{ messageId: 'missingPermissionMiddleware' }],
76
+ },
77
+ // Feature gate alone is not a permission gate
78
+ {
79
+ code: 'const m = t.procedure.use(requireEnhancedAccessControl).mutation(async () => {});',
80
+ errors: [{ messageId: 'missingPermissionMiddleware' }],
81
+ },
82
+ {
83
+ code: 'const m = t.procedure.use(requireAiGradingFeature).mutation(async () => {});',
84
+ errors: [{ messageId: 'missingPermissionMiddleware' }],
85
+ },
86
+ {
87
+ code: `const m = t.procedure
88
+ .input(z.object({}))
89
+ .output(z.object({}))
90
+ .subscription(async () => {});`,
91
+ errors: [{ messageId: 'missingPermissionMiddleware' }],
92
+ },
93
+ // Intermediate procedure-base variable with only a feature gate is still a miss
94
+ {
95
+ code: `
96
+ const featureProcedure = t.procedure.use(requireAiGradingFeature);
97
+ const m = featureProcedure.query(async () => {});
98
+ `,
99
+ errors: [{ messageId: 'missingPermissionMiddleware' }],
100
+ },
101
+ {
102
+ code: `
103
+ const featureProcedure = t.procedure.use(requireAiGradingFeature);
104
+ const m = featureProcedure.input(z.object({})).mutation(async () => {});
105
+ `,
106
+ errors: [{ messageId: 'missingPermissionMiddleware' }],
107
+ },
108
+ ],
109
+ });
package/tsconfig.json CHANGED
@@ -1,9 +1,3 @@
1
1
  {
2
- // This must be a CJS package because ESLint doesn't support ESM plugins.
3
- "extends": "@prairielearn/tsconfig/tsconfig.package-cjs.json",
4
- "compilerOptions": {
5
- "outDir": "./dist",
6
- "rootDir": "./src",
7
- "types": ["node"]
8
- }
2
+ "extends": "@prairielearn/tsconfig/tsconfig.package-node.json"
9
3
  }