@usehercules/convex 0.0.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/LICENSE +21 -0
  2. package/README.md +478 -0
  3. package/dist/_generated/component.d.ts +184 -0
  4. package/dist/_generated/component.d.ts.map +1 -0
  5. package/dist/_generated/component.js +11 -0
  6. package/dist/_generated/component.js.map +1 -0
  7. package/dist/checker/cli.d.ts +3 -0
  8. package/dist/checker/cli.d.ts.map +1 -0
  9. package/dist/checker/cli.js +71 -0
  10. package/dist/checker/cli.js.map +1 -0
  11. package/dist/checker/index.d.ts +28 -0
  12. package/dist/checker/index.d.ts.map +1 -0
  13. package/dist/checker/index.js +1928 -0
  14. package/dist/checker/index.js.map +1 -0
  15. package/dist/client/access-admin.d.ts +818 -0
  16. package/dist/client/access-admin.d.ts.map +1 -0
  17. package/dist/client/access-admin.js +1830 -0
  18. package/dist/client/access-admin.js.map +1 -0
  19. package/dist/client/http.d.ts +19 -0
  20. package/dist/client/http.d.ts.map +1 -0
  21. package/dist/client/http.js +76 -0
  22. package/dist/client/http.js.map +1 -0
  23. package/dist/client/index.d.ts +440 -0
  24. package/dist/client/index.d.ts.map +1 -0
  25. package/dist/client/index.js +654 -0
  26. package/dist/client/index.js.map +1 -0
  27. package/dist/component/authz.d.ts +114 -0
  28. package/dist/component/authz.d.ts.map +1 -0
  29. package/dist/component/authz.js +168 -0
  30. package/dist/component/authz.js.map +1 -0
  31. package/dist/component/checks.d.ts +86 -0
  32. package/dist/component/checks.d.ts.map +1 -0
  33. package/dist/component/checks.js +184 -0
  34. package/dist/component/checks.js.map +1 -0
  35. package/dist/component/convex.config.d.ts +3 -0
  36. package/dist/component/convex.config.d.ts.map +1 -0
  37. package/dist/component/convex.config.js +3 -0
  38. package/dist/component/convex.config.js.map +1 -0
  39. package/dist/component/effective.d.ts +82 -0
  40. package/dist/component/effective.d.ts.map +1 -0
  41. package/dist/component/effective.js +757 -0
  42. package/dist/component/effective.js.map +1 -0
  43. package/dist/component/queries.d.ts +170 -0
  44. package/dist/component/queries.d.ts.map +1 -0
  45. package/dist/component/queries.js +633 -0
  46. package/dist/component/queries.js.map +1 -0
  47. package/dist/component/schema.d.ts +258 -0
  48. package/dist/component/schema.d.ts.map +1 -0
  49. package/dist/component/schema.js +222 -0
  50. package/dist/component/schema.js.map +1 -0
  51. package/dist/component/sync.d.ts +85 -0
  52. package/dist/component/sync.d.ts.map +1 -0
  53. package/dist/component/sync.js +851 -0
  54. package/dist/component/sync.js.map +1 -0
  55. package/dist/shared/projection-protocol.d.ts +1624 -0
  56. package/dist/shared/projection-protocol.d.ts.map +1 -0
  57. package/dist/shared/projection-protocol.js +561 -0
  58. package/dist/shared/projection-protocol.js.map +1 -0
  59. package/dist/shared/sync.d.ts +24 -0
  60. package/dist/shared/sync.d.ts.map +1 -0
  61. package/dist/shared/sync.js +18 -0
  62. package/dist/shared/sync.js.map +1 -0
  63. package/dist/shared/token.d.ts +5 -0
  64. package/dist/shared/token.d.ts.map +1 -0
  65. package/dist/shared/token.js +19 -0
  66. package/dist/shared/token.js.map +1 -0
  67. package/package.json +89 -0
@@ -0,0 +1,1928 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
2
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
3
+ import * as ts from "typescript";
4
+ const rawBuilderNames = new Set(["query", "mutation", "action"]);
5
+ const publicBuilderNames = new Set([
6
+ "publicQuery",
7
+ "publicMutation",
8
+ "publicAction",
9
+ "authenticatedQuery",
10
+ "authenticatedMutation",
11
+ "authenticatedAction",
12
+ "accessQuery",
13
+ "accessMutation",
14
+ "accessAction",
15
+ ]);
16
+ const sourceExtensions = new Set([".ts", ".tsx", ".js", ".jsx"]);
17
+ const ignoredDirectories = new Set([
18
+ "_generated",
19
+ "node_modules",
20
+ "dist",
21
+ ".git",
22
+ ]);
23
+ const exemptFileNames = new Set([
24
+ "access.ts",
25
+ "access.tsx",
26
+ "hercules.ts",
27
+ "hercules.tsx",
28
+ "http.ts",
29
+ "convex.config.ts",
30
+ ]);
31
+ const exemptionMarkers = [
32
+ "hercules-access-control: allow-raw-builder",
33
+ "hercules-access-control: allow-raw-builders",
34
+ ];
35
+ const accessControlPackageName = "@usehercules/convex";
36
+ const accessAdminPackageNames = new Set([
37
+ `${accessControlPackageName}/access-admin`,
38
+ `${accessControlPackageName}/access-admin.js`,
39
+ ]);
40
+ const serviceAuthorityHelperNames = new Set([
41
+ "createAccessInvitation",
42
+ "createResourceInvitation",
43
+ ]);
44
+ const accessAdminActionNames = new Set([
45
+ "archiveScope",
46
+ "setDefaultRole",
47
+ "createInvitation",
48
+ "revokeInvitation",
49
+ "assignRole",
50
+ "removeRole",
51
+ "createOrgCustomRole",
52
+ "updateRolePermissions",
53
+ "setUserExceptions",
54
+ "createResourceGrant",
55
+ "replaceResourceGrants",
56
+ "replaceMemberRoles",
57
+ "createResourceInvitation",
58
+ "setResourcePermissionRule",
59
+ "setResourcePermissionRules",
60
+ "revokeResourceGrant",
61
+ "setGrantExpiry",
62
+ "setRoleOverride",
63
+ "addMember",
64
+ "setMemberStatus",
65
+ "removeMember",
66
+ "approveMember",
67
+ "upsertAdmissionRule",
68
+ "archiveAdmissionRule",
69
+ "setAccountEntryMode",
70
+ "createGroup",
71
+ "renameGroup",
72
+ "archiveGroup",
73
+ "listGroups",
74
+ "addGroupMember",
75
+ "removeGroupMember",
76
+ "listResourceInvitations",
77
+ "getRoleOverrides",
78
+ "getUserExceptions",
79
+ ]);
80
+ export function checkAccessControlSource(options = {}) {
81
+ const cwd = resolve(options.cwd ?? process.cwd());
82
+ const convexDir = resolve(cwd, options.convexDir ?? "convex");
83
+ if (!existsSync(convexDir) || !statSync(convexDir).isDirectory()) {
84
+ const displayPath = displayPathFor(cwd, convexDir);
85
+ return {
86
+ ok: false,
87
+ convexDir,
88
+ filesChecked: 0,
89
+ fixedFiles: 0,
90
+ findings: [
91
+ {
92
+ code: "convex_dir_missing",
93
+ severity: "error",
94
+ filePath: displayPath,
95
+ line: 1,
96
+ column: 1,
97
+ message: `Convex directory not found: ${displayPath}`,
98
+ suggestion: "Run this command from the app root or pass the Convex directory path.",
99
+ },
100
+ ],
101
+ };
102
+ }
103
+ // The managed-pattern rules only apply to apps that actually wire managed
104
+ // Access Control into their Convex functions. A plain Convex app keeps raw
105
+ // builder behavior, so the whole check is a pass-through no-op for it.
106
+ const markerFiles = collectSourceFiles(convexDir, {
107
+ includeExemptFiles: true,
108
+ });
109
+ if (!markerFiles.some((filePath) => fileUsesManagedAccessControl(filePath, convexDir))) {
110
+ return {
111
+ ok: true,
112
+ convexDir,
113
+ filesChecked: markerFiles.length,
114
+ fixedFiles: 0,
115
+ findings: [],
116
+ };
117
+ }
118
+ const sourceFiles = collectSourceFiles(convexDir);
119
+ const appSourceFiles = collectAppSourceFiles(cwd, convexDir);
120
+ const authoritySourceFiles = [
121
+ ...new Set([
122
+ ...markerFiles,
123
+ ...collectAppSourceFiles(cwd, convexDir, {
124
+ includeExemptFiles: true,
125
+ }),
126
+ ]),
127
+ ];
128
+ const orgOwnedTables = collectOrgOwnedTables(sourceFiles);
129
+ const catalogPermissionKeys = loadCatalogPermissionKeys(cwd);
130
+ const fixedFiles = options.fixAuthenticated
131
+ ? sourceFiles.filter((filePath) => fixSourceFileToAuthenticatedBuilders(filePath, convexDir)).length
132
+ : 0;
133
+ const findings = [
134
+ ...sourceFiles.flatMap((filePath) => checkSourceFile(cwd, filePath)),
135
+ ...checkPublicServiceAuthorityCalls(cwd, convexDir, markerFiles, authoritySourceFiles),
136
+ ...markerFiles.flatMap((filePath) => checkHardcodedAccessScopeIds(cwd, filePath)),
137
+ ...markerFiles.flatMap((filePath) => checkPrivilegedResourcePermissionRules(cwd, filePath)),
138
+ ...sourceFiles.flatMap((filePath) => checkCanonicalPermissionKeys(cwd, filePath, catalogPermissionKeys)),
139
+ ...[...sourceFiles, ...appSourceFiles].flatMap((filePath) => checkAccessControlOrgPatterns(cwd, filePath, orgOwnedTables)),
140
+ ];
141
+ return {
142
+ ok: findings.length === 0,
143
+ convexDir,
144
+ filesChecked: sourceFiles.length + appSourceFiles.length,
145
+ fixedFiles,
146
+ findings,
147
+ };
148
+ }
149
+ export function formatAccessControlCheckResult(result) {
150
+ if (result.ok) {
151
+ const fileLabel = result.filesChecked === 1 ? "file" : "files";
152
+ const fixedLabel = result.fixedFiles > 0
153
+ ? ` ${result.fixedFiles} ${result.fixedFiles === 1 ? "file was" : "files were"} updated.`
154
+ : "";
155
+ return `Hercules Access Control static check passed (${result.filesChecked} ${fileLabel} checked).${fixedLabel} This static check does not prove runtime role decisions or control-plane writes are authorized.`;
156
+ }
157
+ const lines = [
158
+ `Hercules Access Control check failed with ${result.findings.length} finding(s):`,
159
+ ];
160
+ for (const finding of result.findings) {
161
+ lines.push(`- ${finding.filePath}:${finding.line}:${finding.column} ${finding.message}`);
162
+ if (finding.suggestion) {
163
+ lines.push(` ${finding.suggestion}`);
164
+ }
165
+ }
166
+ return lines.join("\n");
167
+ }
168
+ function fixSourceFileToAuthenticatedBuilders(filePath, convexDir) {
169
+ const sourceText = readFileSync(filePath, "utf8");
170
+ const sourceFile = createSourceFile(filePath, sourceText);
171
+ const rawBuilderImports = collectRawBuilderImports(sourceFile);
172
+ if (rawBuilderImports.size === 0) {
173
+ return false;
174
+ }
175
+ const exportedNames = collectExportedNames(sourceFile);
176
+ const candidates = collectRawBuilderCandidates(sourceFile, rawBuilderImports)
177
+ .filter((candidate) => candidate.isDirectExport || exportedNames.has(candidate.functionName))
178
+ .filter((candidate) => !hasLocalExemption(sourceFile, sourceText, candidate.declaration));
179
+ if (candidates.length === 0) {
180
+ return false;
181
+ }
182
+ const replacements = candidates.map((candidate) => ({
183
+ start: candidate.builderNode.getStart(sourceFile),
184
+ end: candidate.builderNode.getEnd(),
185
+ text: authenticatedBuilderName(candidate.builder),
186
+ }));
187
+ const accessImports = new Set(replacements.map((replacement) => replacement.text));
188
+ replacements.push(...buildGeneratedServerImportRemovals(sourceFile, sourceText, candidates));
189
+ replacements.push(buildAccessImportReplacement(sourceFile, sourceText, accessImports, convexDir));
190
+ const nextSourceText = applyTextReplacements(sourceText, replacements);
191
+ if (nextSourceText === sourceText) {
192
+ return false;
193
+ }
194
+ writeFileSync(filePath, nextSourceText);
195
+ return true;
196
+ }
197
+ function collectSourceFiles(directory, options = {}) {
198
+ const entries = readdirSync(directory, { withFileTypes: true });
199
+ const sourceFiles = [];
200
+ for (const entry of entries) {
201
+ if (entry.name.startsWith(".")) {
202
+ continue;
203
+ }
204
+ const entryPath = join(directory, entry.name);
205
+ if (entry.isDirectory()) {
206
+ if (!ignoredDirectories.has(entry.name)) {
207
+ sourceFiles.push(...collectSourceFiles(entryPath, options));
208
+ }
209
+ continue;
210
+ }
211
+ if (isSourceFile(entryPath) &&
212
+ (options.includeExemptFiles || !exemptFileNames.has(basename(entryPath)))) {
213
+ sourceFiles.push(entryPath);
214
+ }
215
+ }
216
+ return sourceFiles.sort((left, right) => left.localeCompare(right));
217
+ }
218
+ function collectAppSourceFiles(cwd, convexDir, options = {}) {
219
+ const srcDir = resolve(cwd, "src");
220
+ if (!existsSync(srcDir) || !statSync(srcDir).isDirectory())
221
+ return [];
222
+ if (srcDir === convexDir)
223
+ return [];
224
+ return collectSourceFiles(srcDir, options);
225
+ }
226
+ function checkSourceFile(cwd, filePath) {
227
+ const sourceText = readFileSync(filePath, "utf8");
228
+ const sourceFile = createSourceFile(filePath, sourceText);
229
+ const rawBuilderImports = collectRawBuilderImports(sourceFile);
230
+ if (rawBuilderImports.size === 0) {
231
+ return [];
232
+ }
233
+ const exportedNames = collectExportedNames(sourceFile);
234
+ const candidates = collectRawBuilderCandidates(sourceFile, rawBuilderImports);
235
+ return candidates
236
+ .filter((candidate) => candidate.isDirectExport || exportedNames.has(candidate.functionName))
237
+ .filter((candidate) => !hasLocalExemption(sourceFile, sourceText, candidate.declaration))
238
+ .map((candidate) => createFinding(cwd, sourceFile, candidate));
239
+ }
240
+ function checkAccessControlOrgPatterns(cwd, filePath, orgOwnedTables) {
241
+ const sourceText = readFileSync(filePath, "utf8");
242
+ const sourceFile = createSourceFile(filePath, sourceText);
243
+ const findings = [];
244
+ addPatternFinding({
245
+ findings,
246
+ cwd,
247
+ filePath,
248
+ sourceText,
249
+ code: "placeholder_access_scope_id",
250
+ pattern: /\b(?:herculesScopeId|accessScopeId|orgScopeId)\s*:\s*["']{2}/,
251
+ message: "Do not store a blank Hercules Access Control scope id. Create a Hercules Access Control scope first, then persist the returned accessScopeId.",
252
+ suggestion: "Use createAccessScope from @usehercules/convex/access-admin before inserting org metadata.",
253
+ });
254
+ addPatternFinding({
255
+ findings,
256
+ cwd,
257
+ filePath,
258
+ sourceText,
259
+ code: "local_org_membership_table",
260
+ pattern: /\b(?:memberships|membership|orgMembers|organizationMembers)\s*:\s*defineTable\b/,
261
+ message: "Managed Access Control apps should not define app-local org membership tables.",
262
+ suggestion: "Use Hercules Access Control scopes, principals, and role grants. Store only org metadata in app tables.",
263
+ });
264
+ addPatternFinding({
265
+ findings,
266
+ cwd,
267
+ filePath,
268
+ sourceText,
269
+ code: "optional_org_scope_id",
270
+ pattern: /\borgScopeId\s*:\s*v\.optional\s*\(\s*v\.string\s*\(\s*\)\s*\)/,
271
+ message: "Org-owned rows should require orgScopeId.",
272
+ suggestion: "Backfill existing rows during conversion, then store orgScopeId as v.string() on org-owned tables.",
273
+ });
274
+ if (/\borgScopeId\b/.test(sourceText) &&
275
+ /\.withIndex\s*\(\s*["']by_slug["']/.test(sourceText)) {
276
+ addPatternFinding({
277
+ findings,
278
+ cwd,
279
+ filePath,
280
+ sourceText,
281
+ code: "org_scoped_global_slug_lookup",
282
+ pattern: /\.withIndex\s*\(\s*["']by_slug["']/,
283
+ message: "Org-scoped slug lookups must include the org scope id in the index.",
284
+ suggestion: 'Use an index such as by_org_and_slug on ["orgScopeId", "slug"] and query both values together.',
285
+ });
286
+ }
287
+ for (const definition of collectManagedBuilderDefinitions(sourceFile, [
288
+ "accessQuery",
289
+ "accessMutation",
290
+ "accessAction",
291
+ ])) {
292
+ if (/\bscopeFromArg\s*\(\s*["']orgScopeId["']\s*\)/.test(definition.text) &&
293
+ /\bctx\.db\.(?:get|patch|replace|delete)\s*\(\s*args\.[A-Za-z_$][\w$]*/.test(definition.text)) {
294
+ findings.push(createPatternFindingAtNode({
295
+ cwd,
296
+ sourceFile,
297
+ node: definition.node,
298
+ code: "org_row_scope_from_arg",
299
+ message: "Operations on an org-owned row id must authorize against the stored row scope, not a caller supplied scope id.",
300
+ suggestion: 'Use scopeFromResource("tableName", "rowIdArg") for row read, update, publish, moderation, and delete operations.',
301
+ }));
302
+ }
303
+ }
304
+ for (const definition of collectManagedBuilderDefinitions(sourceFile, [
305
+ "authenticatedQuery",
306
+ ])) {
307
+ const readsOrgOwnedTable = [...orgOwnedTables].some((tableName) => {
308
+ const escapedName = escapeRegExp(tableName);
309
+ return (new RegExp(`\\.query\\s*\\(\\s*["']${escapedName}["']`).test(definition.text) ||
310
+ (new RegExp(`v\\.id\\s*\\(\\s*["']${escapedName}["']`).test(definition.text) &&
311
+ /\bctx\.db\.get\s*\(\s*args\.[A-Za-z_$][\w$]*/.test(definition.text)));
312
+ });
313
+ if (readsOrgOwnedTable) {
314
+ findings.push(createPatternFindingAtNode({
315
+ cwd,
316
+ sourceFile,
317
+ node: definition.node,
318
+ code: "authenticated_org_data_read",
319
+ message: "Authenticated reads of org-owned data do not prove organization membership.",
320
+ suggestion: "Use accessQuery for private organization data. Use publicQuery only for explicitly public rows filtered to public state.",
321
+ }));
322
+ }
323
+ }
324
+ return findings;
325
+ }
326
+ function collectOrgOwnedTables(sourceFiles) {
327
+ const tableNames = new Set();
328
+ for (const filePath of sourceFiles) {
329
+ if (!/^schema\.(?:ts|tsx|js|jsx)$/.test(basename(filePath)))
330
+ continue;
331
+ const sourceText = readFileSync(filePath, "utf8");
332
+ const tablePattern = /\b([A-Za-z_$][\w$]*)\s*:\s*defineTable\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
333
+ for (const match of sourceText.matchAll(tablePattern)) {
334
+ if (/\borgScopeId\s*:/.test(match[2] ?? "")) {
335
+ tableNames.add(match[1]);
336
+ }
337
+ }
338
+ }
339
+ return tableNames;
340
+ }
341
+ function collectManagedBuilderDefinitions(sourceFile, builderNames) {
342
+ const acceptedNames = new Set(builderNames);
343
+ const definitions = [];
344
+ function visit(node) {
345
+ if (ts.isCallExpression(node)) {
346
+ const target = unwrapExpression(node.expression);
347
+ const definition = node.arguments[0];
348
+ if (ts.isIdentifier(target) &&
349
+ acceptedNames.has(target.text) &&
350
+ definition) {
351
+ definitions.push({
352
+ node,
353
+ text: definition.getText(sourceFile),
354
+ definition,
355
+ });
356
+ }
357
+ }
358
+ ts.forEachChild(node, visit);
359
+ }
360
+ visit(sourceFile);
361
+ return definitions;
362
+ }
363
+ function checkHardcodedAccessScopeIds(cwd, filePath) {
364
+ const sourceText = readFileSync(filePath, "utf8");
365
+ const findings = [];
366
+ addPatternFinding({
367
+ findings,
368
+ cwd,
369
+ filePath,
370
+ sourceText,
371
+ code: "hardcoded_access_scope_id",
372
+ pattern: /\b(?:[A-Z][A-Z0-9_]*_)?(?:ACCESS_)?SCOPE_ID\b\s*=\s*["']01[A-Z0-9]{24}["']|\bscopeId\s*:\s*["']01[A-Z0-9]{24}["']/,
373
+ message: "Do not hardcode Access Control scope ids.",
374
+ suggestion: 'Use the "default" scope sentinel for the app scope, or store org scope ids returned by createAccessScope/createOrgScope on app rows and load them from the row.',
375
+ });
376
+ return findings;
377
+ }
378
+ function checkPrivilegedResourcePermissionRules(cwd, filePath) {
379
+ const sourceText = readFileSync(filePath, "utf8");
380
+ const sourceFile = createSourceFile(filePath, sourceText);
381
+ const findings = [];
382
+ function visit(node) {
383
+ if (ts.isObjectLiteralExpression(node)) {
384
+ const permission = getStringProperty(node, "permissionKey");
385
+ const effect = getStringProperty(node, "effect");
386
+ if (permission &&
387
+ effect?.value === "allow" &&
388
+ isPrivilegedResourceRuleKey(permission.value)) {
389
+ findings.push(createPatternFindingAtNode({
390
+ cwd,
391
+ sourceFile,
392
+ node: permission.node,
393
+ code: "privileged_resource_permission_rule",
394
+ message: "Do not grant manage_members, manage_access, system.*, or wildcard permissions through resource permission rules.",
395
+ suggestion: "Use resource role grants for scoped management authority. Resource permission rules are for ordinary allow/deny exceptions.",
396
+ }));
397
+ }
398
+ }
399
+ ts.forEachChild(node, visit);
400
+ }
401
+ visit(sourceFile);
402
+ return findings;
403
+ }
404
+ function checkPublicServiceAuthorityCalls(cwd, convexDir, rootFilePaths, filePaths) {
405
+ const sourceFiles = new Map();
406
+ for (const filePath of filePaths) {
407
+ const sourceFile = createSourceFile(filePath, readFileSync(filePath, "utf8"));
408
+ sourceFiles.set(filePath, {
409
+ filePath,
410
+ sourceFile,
411
+ internalApiNames: collectInternalApiNames(sourceFile),
412
+ bindings: collectTopLevelBindings(sourceFile),
413
+ imports: new Map(),
414
+ namespaceImports: new Map(),
415
+ exportBindings: collectExportBindings(sourceFile),
416
+ reExports: new Map(),
417
+ namespaceReExports: new Map(),
418
+ exportAllTargets: [],
419
+ });
420
+ }
421
+ for (const info of sourceFiles.values()) {
422
+ const imports = collectModuleImports(info.sourceFile, sourceFiles);
423
+ info.imports = imports.bindings;
424
+ info.namespaceImports = imports.namespaces;
425
+ const reExports = collectModuleReExports(info.sourceFile, sourceFiles);
426
+ info.reExports = reExports.bindings;
427
+ info.namespaceReExports = reExports.namespaces;
428
+ info.exportAllTargets = reExports.exportAllTargets;
429
+ }
430
+ const convexModules = new Map();
431
+ for (const info of sourceFiles.values()) {
432
+ const relativePath = normalizePath(relative(convexDir, info.filePath));
433
+ if (relativePath.startsWith("../"))
434
+ continue;
435
+ convexModules.set(stripKnownModuleExtension(relativePath), info);
436
+ }
437
+ const findings = [];
438
+ const findingKeys = new Set();
439
+ const visitedCallables = new Set();
440
+ const addFinding = (info, node) => {
441
+ const key = `${info.filePath}:${node.getStart(info.sourceFile)}`;
442
+ if (findingKeys.has(key))
443
+ return;
444
+ findingKeys.add(key);
445
+ findings.push(createPatternFindingAtNode({
446
+ cwd,
447
+ sourceFile: info.sourceFile,
448
+ node,
449
+ code: "public_service_authority_call",
450
+ message: "Exported public Convex functions must not call service-authority Access Control actions.",
451
+ suggestion: "Use createAccessUserActions for public access changes, or keep the createAccessAdminActions caller internal.",
452
+ }));
453
+ };
454
+ const visitCallableNode = (info, node, declarationScope) => {
455
+ const callableKey = `${info.filePath}:${node.getStart(info.sourceFile)}`;
456
+ if (visitedCallables.has(callableKey))
457
+ return;
458
+ visitedCallables.add(callableKey);
459
+ if (!isCallableNode(node))
460
+ return;
461
+ const functionScope = {
462
+ parent: declarationScope,
463
+ bindings: new Map(),
464
+ };
465
+ for (const parameter of node.parameters) {
466
+ addBindingNames(functionScope, parameter.name, parameter, functionScope);
467
+ }
468
+ const body = node.body;
469
+ if (!body)
470
+ return;
471
+ if (ts.isBlock(body)) {
472
+ visitBlock(info, body, functionScope);
473
+ }
474
+ else {
475
+ visitReachable(info, body, functionScope);
476
+ }
477
+ };
478
+ function resolveBindingValues(info, binding, resolving) {
479
+ const bindingKey = `${info.filePath}:binding:${binding.id}`;
480
+ if (resolving.has(bindingKey))
481
+ return [];
482
+ const nextResolving = new Set(resolving).add(bindingKey);
483
+ if (binding.value.kind === "unknown")
484
+ return [];
485
+ if (binding.value.kind === "callable") {
486
+ return [
487
+ {
488
+ kind: "node",
489
+ info,
490
+ node: binding.value.node,
491
+ declarationScope: binding.value.scope,
492
+ },
493
+ ];
494
+ }
495
+ let values = resolveStaticValues(info, binding.value.expression, binding.value.scope, nextResolving);
496
+ for (const propertyName of binding.value.propertyPath) {
497
+ values = values.flatMap((value) => resolvePropertyValues(value, propertyName, nextResolving));
498
+ }
499
+ return values;
500
+ }
501
+ function resolveModuleExportValues(target, exportedName, resolving) {
502
+ if (target.kind === "local") {
503
+ const targetInfo = sourceFiles.get(target.filePath);
504
+ return targetInfo
505
+ ? resolveExportedValues(targetInfo, exportedName, resolving)
506
+ : [];
507
+ }
508
+ if (!accessAdminPackageNames.has(target.moduleSpecifier))
509
+ return [];
510
+ if (serviceAuthorityHelperNames.has(exportedName)) {
511
+ return [{ kind: "known", value: "serviceAuthority" }];
512
+ }
513
+ if (exportedName === "createAccessAdminActions") {
514
+ return [{ kind: "known", value: "accessAdminFactory" }];
515
+ }
516
+ return [];
517
+ }
518
+ function resolveExportedValues(info, exportedName, resolving) {
519
+ const symbolKey = `${info.filePath}:export:${exportedName}`;
520
+ if (resolving.has(symbolKey))
521
+ return [];
522
+ const nextResolving = new Set(resolving).add(symbolKey);
523
+ if (isAccessWiringSourceFile(info.filePath, convexDir) &&
524
+ publicBuilderNames.has(exportedName)) {
525
+ return [{ kind: "known", value: "publicBuilder" }];
526
+ }
527
+ const localName = info.exportBindings.get(exportedName);
528
+ if (localName) {
529
+ const binding = info.bindings.get(localName);
530
+ if (binding) {
531
+ return resolveBindingValues(info, binding, nextResolving);
532
+ }
533
+ const imported = info.imports.get(localName);
534
+ if (imported) {
535
+ return resolveModuleExportValues(imported.target, imported.exportedName, nextResolving);
536
+ }
537
+ const namespaceImport = info.namespaceImports.get(localName);
538
+ if (namespaceImport) {
539
+ return [{ kind: "module", target: namespaceImport }];
540
+ }
541
+ }
542
+ const reExport = info.reExports.get(exportedName);
543
+ if (reExport) {
544
+ return resolveModuleExportValues(reExport.target, reExport.exportedName, nextResolving);
545
+ }
546
+ const namespaceReExport = info.namespaceReExports.get(exportedName);
547
+ if (namespaceReExport) {
548
+ return [{ kind: "module", target: namespaceReExport }];
549
+ }
550
+ const values = [];
551
+ for (const target of info.exportAllTargets) {
552
+ values.push(...resolveModuleExportValues(target, exportedName, nextResolving));
553
+ }
554
+ return values;
555
+ }
556
+ function resolveObjectPropertyValues(info, objectLiteral, propertyName, scope, resolving) {
557
+ for (let index = objectLiteral.properties.length - 1; index >= 0; index -= 1) {
558
+ const property = objectLiteral.properties[index];
559
+ if (ts.isSpreadAssignment(property)) {
560
+ const spreadValues = resolveStaticValues(info, property.expression, scope, resolving).flatMap((value) => resolvePropertyValues(value, propertyName, resolving));
561
+ if (spreadValues.length > 0)
562
+ return spreadValues;
563
+ continue;
564
+ }
565
+ const name = property.name;
566
+ const nameText = name && (ts.isIdentifier(name) || ts.isStringLiteralLike(name))
567
+ ? name.text
568
+ : null;
569
+ if (nameText !== propertyName)
570
+ continue;
571
+ if (ts.isMethodDeclaration(property)) {
572
+ return [
573
+ {
574
+ kind: "node",
575
+ info,
576
+ node: property,
577
+ declarationScope: scope,
578
+ },
579
+ ];
580
+ }
581
+ if (ts.isPropertyAssignment(property)) {
582
+ return resolveStaticValues(info, property.initializer, scope, resolving);
583
+ }
584
+ if (ts.isShorthandPropertyAssignment(property)) {
585
+ return resolveStaticValues(info, property.name, scope, resolving);
586
+ }
587
+ }
588
+ return [];
589
+ }
590
+ function resolvePropertyValues(value, propertyName, resolving) {
591
+ if (value.kind === "module") {
592
+ return resolveModuleExportValues(value.target, propertyName, resolving);
593
+ }
594
+ if (value.kind === "known") {
595
+ return value.value === "accessAdminActions" &&
596
+ accessAdminActionNames.has(propertyName)
597
+ ? [{ kind: "known", value: "serviceAuthority" }]
598
+ : [];
599
+ }
600
+ if (ts.isObjectLiteralExpression(value.node)) {
601
+ return resolveObjectPropertyValues(value.info, value.node, propertyName, value.declarationScope, resolving);
602
+ }
603
+ if (ts.isArrayLiteralExpression(value.node)) {
604
+ const index = Number(propertyName);
605
+ if (!Number.isInteger(index) || index < 0)
606
+ return [];
607
+ const element = value.node.elements[index];
608
+ return element && !ts.isOmittedExpression(element)
609
+ ? resolveStaticValues(value.info, element, value.declarationScope, resolving)
610
+ : [];
611
+ }
612
+ return [];
613
+ }
614
+ function resolveStaticValues(info, expression, scope, resolving = new Set()) {
615
+ const target = unwrapExpression(expression);
616
+ if (isCallableNode(target)) {
617
+ return [{ kind: "node", info, node: target, declarationScope: scope }];
618
+ }
619
+ if (ts.isObjectLiteralExpression(target) ||
620
+ ts.isArrayLiteralExpression(target)) {
621
+ return [{ kind: "node", info, node: target, declarationScope: scope }];
622
+ }
623
+ if (ts.isAwaitExpression(target)) {
624
+ return resolveStaticValues(info, target.expression, scope, resolving);
625
+ }
626
+ if (ts.isConditionalExpression(target)) {
627
+ return [
628
+ ...resolveStaticValues(info, target.whenTrue, scope, new Set(resolving)),
629
+ ...resolveStaticValues(info, target.whenFalse, scope, new Set(resolving)),
630
+ ];
631
+ }
632
+ if (ts.isIdentifier(target)) {
633
+ const lexicalBinding = findLexicalBinding(scope, target.text);
634
+ if (lexicalBinding) {
635
+ return resolveBindingValues(info, lexicalBinding, resolving);
636
+ }
637
+ const symbolKey = `${info.filePath}:local:${target.text}`;
638
+ if (resolving.has(symbolKey))
639
+ return [];
640
+ const nextResolving = new Set(resolving).add(symbolKey);
641
+ const binding = info.bindings.get(target.text);
642
+ if (binding) {
643
+ return resolveBindingValues(info, binding, nextResolving);
644
+ }
645
+ const imported = info.imports.get(target.text);
646
+ if (imported) {
647
+ return resolveModuleExportValues(imported.target, imported.exportedName, nextResolving);
648
+ }
649
+ const namespaceImport = info.namespaceImports.get(target.text);
650
+ if (namespaceImport) {
651
+ return [{ kind: "module", target: namespaceImport }];
652
+ }
653
+ return [];
654
+ }
655
+ if (ts.isPropertyAccessExpression(target)) {
656
+ if (target.name.text === "call" ||
657
+ target.name.text === "apply" ||
658
+ target.name.text === "bind") {
659
+ return resolveStaticValues(info, target.expression, scope, resolving);
660
+ }
661
+ const objectValues = resolveStaticValues(info, target.expression, scope, resolving);
662
+ return objectValues.flatMap((value) => resolvePropertyValues(value, target.name.text, resolving));
663
+ }
664
+ if (ts.isElementAccessExpression(target)) {
665
+ const argument = target.argumentExpression &&
666
+ unwrapExpression(target.argumentExpression);
667
+ if (!argument || !ts.isStringLiteralLike(argument))
668
+ return [];
669
+ const objectValues = resolveStaticValues(info, target.expression, scope, resolving);
670
+ return objectValues.flatMap((value) => resolvePropertyValues(value, argument.text, resolving));
671
+ }
672
+ if (ts.isCallExpression(target)) {
673
+ const callTarget = unwrapExpression(target.expression);
674
+ if (ts.isPropertyAccessExpression(callTarget) &&
675
+ callTarget.name.text === "bind") {
676
+ return resolveStaticValues(info, callTarget.expression, scope, resolving);
677
+ }
678
+ const values = [];
679
+ for (const callable of resolveStaticValues(info, target.expression, scope, resolving)) {
680
+ if (callable.kind === "known" &&
681
+ callable.value === "accessAdminFactory") {
682
+ values.push({ kind: "known", value: "accessAdminActions" });
683
+ }
684
+ else if (callable.kind === "node" && isCallableNode(callable.node)) {
685
+ values.push(...resolveCallableReturnValues(callable, new Set(resolving)));
686
+ }
687
+ }
688
+ return values;
689
+ }
690
+ if (ts.isBinaryExpression(target) &&
691
+ (target.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
692
+ target.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
693
+ target.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)) {
694
+ return [
695
+ ...resolveStaticValues(info, target.left, scope, new Set(resolving)),
696
+ ...resolveStaticValues(info, target.right, scope, new Set(resolving)),
697
+ ];
698
+ }
699
+ return [];
700
+ }
701
+ function resolveCallableReturnValues(callable, resolving) {
702
+ if (!isCallableNode(callable.node) || !callable.node.body)
703
+ return [];
704
+ const returnKey = `${callable.info.filePath}:returns:${callable.node.pos}`;
705
+ if (resolving.has(returnKey))
706
+ return [];
707
+ const nextResolving = new Set(resolving).add(returnKey);
708
+ const functionScope = createChildScope(callable.declarationScope);
709
+ for (const parameter of callable.node.parameters) {
710
+ addBindingNames(functionScope, parameter.name, parameter, functionScope);
711
+ }
712
+ if (!ts.isBlock(callable.node.body)) {
713
+ return resolveStaticValues(callable.info, callable.node.body, functionScope, nextResolving);
714
+ }
715
+ const values = [];
716
+ const collectReturns = (node, scope) => {
717
+ if (ts.isReturnStatement(node)) {
718
+ if (node.expression) {
719
+ values.push(...resolveStaticValues(callable.info, node.expression, scope, nextResolving));
720
+ }
721
+ return;
722
+ }
723
+ if (isCallableNode(node))
724
+ return;
725
+ if (ts.isBlock(node)) {
726
+ const blockScope = createChildScope(scope);
727
+ collectDirectBlockBindings(node, blockScope);
728
+ for (const statement of node.statements) {
729
+ if (ts.isFunctionDeclaration(statement))
730
+ continue;
731
+ collectReturns(statement, blockScope);
732
+ }
733
+ return;
734
+ }
735
+ if (ts.isIfStatement(node)) {
736
+ collectReturns(node.expression, scope);
737
+ const before = snapshotBindingValues(scope);
738
+ collectReturns(node.thenStatement, scope);
739
+ const afterThen = snapshotBindingValues(scope);
740
+ restoreBindingValues(before);
741
+ if (node.elseStatement) {
742
+ collectReturns(node.elseStatement, scope);
743
+ }
744
+ const afterElse = snapshotBindingValues(scope);
745
+ mergeBindingValues(before, [afterThen, afterElse]);
746
+ return;
747
+ }
748
+ if (ts.isBinaryExpression(node) &&
749
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
750
+ assignBindingExpression(node.left, node.right, scope);
751
+ return;
752
+ }
753
+ ts.forEachChild(node, (child) => collectReturns(child, scope));
754
+ };
755
+ collectReturns(callable.node.body, functionScope);
756
+ return values;
757
+ }
758
+ function isGeneratedServiceAuthorityReference(node, info, scope) {
759
+ const path = getInternalApiReferencePath(node, info, scope, new Set());
760
+ if (path === null || path.length === 0)
761
+ return false;
762
+ let moduleInfo;
763
+ let moduleSegmentCount = 0;
764
+ for (let length = path.length - 1; length >= 1; length -= 1) {
765
+ const candidate = convexModules.get(path.slice(0, length).join("/"));
766
+ if (candidate) {
767
+ moduleInfo = candidate;
768
+ moduleSegmentCount = length;
769
+ break;
770
+ }
771
+ }
772
+ if (!moduleInfo) {
773
+ return path[0] === "accessAdmin";
774
+ }
775
+ const exportedPath = path.slice(moduleSegmentCount);
776
+ if (exportedPath.length === 0)
777
+ return false;
778
+ let values = resolveExportedValues(moduleInfo, exportedPath[0], new Set());
779
+ for (const propertyName of exportedPath.slice(1)) {
780
+ values = values.flatMap((value) => resolvePropertyValues(value, propertyName, new Set()));
781
+ }
782
+ return values.some((value) => value.kind === "known" && value.value === "serviceAuthority");
783
+ }
784
+ const visitCallableReference = (info, expression, scope, resolving = new Set()) => {
785
+ for (const value of resolveStaticValues(info, expression, scope, resolving)) {
786
+ if (value.kind === "known" && value.value === "serviceAuthority") {
787
+ addFinding(info, expression);
788
+ }
789
+ else if (value.kind === "node" && isCallableNode(value.node)) {
790
+ visitCallableNode(value.info, value.node, value.declarationScope);
791
+ }
792
+ }
793
+ };
794
+ const visitBlock = (info, block, parentScope) => {
795
+ const scope = {
796
+ parent: parentScope,
797
+ bindings: new Map(),
798
+ };
799
+ collectDirectBlockBindings(block, scope);
800
+ for (const statement of block.statements) {
801
+ if (ts.isFunctionDeclaration(statement))
802
+ continue;
803
+ visitReachable(info, statement, scope);
804
+ }
805
+ };
806
+ const visitReachable = (info, node, scope) => {
807
+ if (isGeneratedServiceAuthorityReference(node, info, scope)) {
808
+ addFinding(info, node);
809
+ return;
810
+ }
811
+ if (isCallableNode(node))
812
+ return;
813
+ if (ts.isIfStatement(node)) {
814
+ visitReachable(info, node.expression, scope);
815
+ const before = snapshotBindingValues(scope);
816
+ visitReachable(info, node.thenStatement, scope);
817
+ const afterThen = snapshotBindingValues(scope);
818
+ restoreBindingValues(before);
819
+ if (node.elseStatement) {
820
+ visitReachable(info, node.elseStatement, scope);
821
+ }
822
+ const afterElse = snapshotBindingValues(scope);
823
+ mergeBindingValues(before, [afterThen, afterElse]);
824
+ return;
825
+ }
826
+ if (ts.isBlock(node)) {
827
+ visitBlock(info, node, scope ?? { parent: null, bindings: new Map() });
828
+ return;
829
+ }
830
+ if (ts.isForStatement(node)) {
831
+ const before = snapshotBindingValues(scope);
832
+ const loopScope = createChildScope(scope);
833
+ if (node.initializer) {
834
+ collectForInitializerBindings(node.initializer, loopScope);
835
+ visitReachable(info, node.initializer, loopScope);
836
+ }
837
+ if (node.condition)
838
+ visitReachable(info, node.condition, loopScope);
839
+ if (node.incrementor) {
840
+ visitReachable(info, node.incrementor, loopScope);
841
+ }
842
+ visitReachable(info, node.statement, loopScope);
843
+ restoreBindingValues(before);
844
+ return;
845
+ }
846
+ if (ts.isForInStatement(node) || ts.isForOfStatement(node)) {
847
+ const before = snapshotBindingValues(scope);
848
+ const loopScope = createChildScope(scope);
849
+ collectForInitializerBindings(node.initializer, loopScope);
850
+ visitReachable(info, node.initializer, loopScope);
851
+ visitReachable(info, node.expression, loopScope);
852
+ visitReachable(info, node.statement, loopScope);
853
+ restoreBindingValues(before);
854
+ return;
855
+ }
856
+ if (ts.isCatchClause(node)) {
857
+ const catchScope = createChildScope(scope);
858
+ if (node.variableDeclaration) {
859
+ addBindingNames(catchScope, node.variableDeclaration.name, node.variableDeclaration, catchScope);
860
+ }
861
+ visitReachable(info, node.block, catchScope);
862
+ return;
863
+ }
864
+ if (ts.isCaseBlock(node)) {
865
+ const before = snapshotBindingValues(scope);
866
+ const switchScope = createChildScope(scope);
867
+ collectDirectCaseBlockBindings(node, switchScope);
868
+ for (const clause of node.clauses) {
869
+ if (ts.isCaseClause(clause)) {
870
+ visitReachable(info, clause.expression, switchScope);
871
+ }
872
+ for (const statement of clause.statements) {
873
+ visitReachable(info, statement, switchScope);
874
+ }
875
+ }
876
+ restoreBindingValues(before);
877
+ return;
878
+ }
879
+ if (ts.isBinaryExpression(node) &&
880
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
881
+ visitReachable(info, node.right, scope);
882
+ assignBindingExpression(node.left, node.right, scope);
883
+ return;
884
+ }
885
+ if (ts.isCallExpression(node)) {
886
+ const target = unwrapExpression(node.expression);
887
+ visitCallableReference(info, target, scope);
888
+ visitReachable(info, target, scope);
889
+ for (const argument of node.arguments) {
890
+ // Do not infer whether an arbitrary higher-order callee invokes a
891
+ // callback. A statically dangerous callable passed from a public
892
+ // handler is itself service-authority exposure, so classify callable
893
+ // arguments syntactically and still inspect the argument expression.
894
+ visitCallableReference(info, argument, scope);
895
+ visitReachable(info, argument, scope);
896
+ }
897
+ return;
898
+ }
899
+ ts.forEachChild(node, (child) => visitReachable(info, child, scope));
900
+ };
901
+ const collectPublicBuilderRoots = (info) => {
902
+ const roots = [];
903
+ const exportedNames = collectExportedNames(info.sourceFile);
904
+ const addRoot = (expression) => {
905
+ const target = unwrapExpression(expression);
906
+ if (!ts.isCallExpression(target))
907
+ return;
908
+ const isPublicBuilder = resolveStaticValues(info, target.expression, null).some((value) => value.kind === "known" && value.value === "publicBuilder");
909
+ if (isPublicBuilder)
910
+ roots.push(target);
911
+ };
912
+ for (const statement of info.sourceFile.statements) {
913
+ if (ts.isVariableStatement(statement)) {
914
+ const isDirectExport = hasExportModifier(statement);
915
+ for (const declaration of statement.declarationList.declarations) {
916
+ if (ts.isIdentifier(declaration.name) &&
917
+ declaration.initializer &&
918
+ (isDirectExport || exportedNames.has(declaration.name.text))) {
919
+ addRoot(declaration.initializer);
920
+ }
921
+ }
922
+ }
923
+ else if (ts.isExportAssignment(statement)) {
924
+ addRoot(statement.expression);
925
+ }
926
+ }
927
+ return roots;
928
+ };
929
+ const visitConfigObject = (info, config, scope, visitedProperties = new Set()) => {
930
+ for (let index = config.properties.length - 1; index >= 0; index -= 1) {
931
+ const property = config.properties[index];
932
+ if (ts.isSpreadAssignment(property)) {
933
+ const objectValues = resolveStaticValues(info, property.expression, scope).filter((value) => value.kind === "node" && ts.isObjectLiteralExpression(value.node));
934
+ if (objectValues.length === 1) {
935
+ const value = objectValues[0];
936
+ visitConfigObject(value.info, value.node, value.declarationScope, visitedProperties);
937
+ }
938
+ continue;
939
+ }
940
+ const propertyName = property.name &&
941
+ (ts.isIdentifier(property.name) ||
942
+ ts.isStringLiteralLike(property.name) ||
943
+ ts.isNumericLiteral(property.name))
944
+ ? property.name.text
945
+ : null;
946
+ if (propertyName && visitedProperties.has(propertyName)) {
947
+ continue;
948
+ }
949
+ if (propertyName) {
950
+ visitedProperties.add(propertyName);
951
+ }
952
+ if (ts.isMethodDeclaration(property)) {
953
+ visitCallableNode(info, property, scope);
954
+ continue;
955
+ }
956
+ if (ts.isShorthandPropertyAssignment(property)) {
957
+ if (property.name.text === "handler") {
958
+ visitCallableReference(info, property.name, scope);
959
+ }
960
+ else {
961
+ visitReachable(info, property, scope);
962
+ }
963
+ continue;
964
+ }
965
+ if (!ts.isPropertyAssignment(property)) {
966
+ visitReachable(info, property, scope);
967
+ continue;
968
+ }
969
+ const initializer = unwrapExpression(property.initializer);
970
+ if (isCallableNode(initializer) || propertyName === "handler") {
971
+ visitCallableReference(info, initializer, scope);
972
+ }
973
+ else {
974
+ visitReachable(info, initializer, scope);
975
+ }
976
+ }
977
+ };
978
+ for (const filePath of rootFilePaths) {
979
+ const info = sourceFiles.get(filePath);
980
+ if (!info)
981
+ continue;
982
+ for (const root of collectPublicBuilderRoots(info)) {
983
+ for (const argument of root.arguments) {
984
+ let resolvedObject = false;
985
+ for (const value of resolveStaticValues(info, argument, null)) {
986
+ if (value.kind === "node" &&
987
+ ts.isObjectLiteralExpression(value.node)) {
988
+ resolvedObject = true;
989
+ visitConfigObject(value.info, value.node, value.declarationScope);
990
+ }
991
+ }
992
+ if (!resolvedObject) {
993
+ visitReachable(info, argument, null);
994
+ }
995
+ }
996
+ }
997
+ }
998
+ return findings;
999
+ }
1000
+ function collectTopLevelBindings(sourceFile) {
1001
+ const bindings = new Map();
1002
+ for (const statement of sourceFile.statements) {
1003
+ if (ts.isFunctionDeclaration(statement)) {
1004
+ if (statement.name) {
1005
+ bindings.set(statement.name.text, {
1006
+ id: `${statement.pos}:${statement.name.text}`,
1007
+ node: statement,
1008
+ value: { kind: "callable", node: statement, scope: null },
1009
+ });
1010
+ }
1011
+ if (hasDefaultModifier(statement)) {
1012
+ bindings.set("default", {
1013
+ id: `${statement.pos}:default`,
1014
+ node: statement,
1015
+ value: { kind: "callable", node: statement, scope: null },
1016
+ });
1017
+ }
1018
+ continue;
1019
+ }
1020
+ if (ts.isExportAssignment(statement)) {
1021
+ bindings.set("default", {
1022
+ id: `${statement.pos}:default`,
1023
+ node: statement,
1024
+ value: {
1025
+ kind: "expression",
1026
+ expression: statement.expression,
1027
+ scope: null,
1028
+ propertyPath: [],
1029
+ },
1030
+ });
1031
+ continue;
1032
+ }
1033
+ if (!ts.isVariableStatement(statement))
1034
+ continue;
1035
+ for (const declaration of statement.declarationList.declarations) {
1036
+ addBindingNamesToMap(bindings, declaration.name, declaration, null, declaration.initializer);
1037
+ }
1038
+ }
1039
+ return bindings;
1040
+ }
1041
+ function collectExportBindings(sourceFile) {
1042
+ const bindings = new Map();
1043
+ for (const statement of sourceFile.statements) {
1044
+ if (ts.isFunctionDeclaration(statement) && hasExportModifier(statement)) {
1045
+ if (statement.name) {
1046
+ bindings.set(statement.name.text, statement.name.text);
1047
+ }
1048
+ if (hasDefaultModifier(statement)) {
1049
+ bindings.set("default", statement.name?.text ?? "default");
1050
+ }
1051
+ continue;
1052
+ }
1053
+ if (ts.isVariableStatement(statement) && hasExportModifier(statement)) {
1054
+ for (const declaration of statement.declarationList.declarations) {
1055
+ for (const name of collectBindingNames(declaration.name)) {
1056
+ bindings.set(name, name);
1057
+ }
1058
+ }
1059
+ continue;
1060
+ }
1061
+ if (ts.isExportAssignment(statement)) {
1062
+ const expression = unwrapExpression(statement.expression);
1063
+ bindings.set("default", ts.isIdentifier(expression) ? expression.text : "default");
1064
+ continue;
1065
+ }
1066
+ if (!ts.isExportDeclaration(statement) || statement.moduleSpecifier)
1067
+ continue;
1068
+ const exportClause = statement.exportClause;
1069
+ if (!exportClause || !ts.isNamedExports(exportClause))
1070
+ continue;
1071
+ for (const specifier of exportClause.elements) {
1072
+ bindings.set(specifier.name.text, (specifier.propertyName ?? specifier.name).text);
1073
+ }
1074
+ }
1075
+ return bindings;
1076
+ }
1077
+ function collectModuleImports(sourceFile, sourceFiles) {
1078
+ const bindings = new Map();
1079
+ const namespaces = new Map();
1080
+ for (const statement of sourceFile.statements) {
1081
+ if (!ts.isImportDeclaration(statement) ||
1082
+ statement.importClause?.isTypeOnly ||
1083
+ !ts.isStringLiteral(statement.moduleSpecifier)) {
1084
+ continue;
1085
+ }
1086
+ const target = resolveModuleTarget(sourceFile.fileName, statement.moduleSpecifier.text, sourceFiles);
1087
+ if (!target)
1088
+ continue;
1089
+ const importClause = statement.importClause;
1090
+ if (!importClause)
1091
+ continue;
1092
+ if (importClause.name) {
1093
+ bindings.set(importClause.name.text, {
1094
+ target,
1095
+ exportedName: "default",
1096
+ });
1097
+ }
1098
+ const namedBindings = importClause.namedBindings;
1099
+ if (namedBindings && ts.isNamespaceImport(namedBindings)) {
1100
+ namespaces.set(namedBindings.name.text, target);
1101
+ }
1102
+ else if (namedBindings && ts.isNamedImports(namedBindings)) {
1103
+ for (const specifier of namedBindings.elements) {
1104
+ if (specifier.isTypeOnly)
1105
+ continue;
1106
+ bindings.set(specifier.name.text, {
1107
+ target,
1108
+ exportedName: (specifier.propertyName ?? specifier.name).text,
1109
+ });
1110
+ }
1111
+ }
1112
+ }
1113
+ return { bindings, namespaces };
1114
+ }
1115
+ function collectModuleReExports(sourceFile, sourceFiles) {
1116
+ const bindings = new Map();
1117
+ const namespaces = new Map();
1118
+ const exportAllTargets = [];
1119
+ for (const statement of sourceFile.statements) {
1120
+ if (!ts.isExportDeclaration(statement) ||
1121
+ statement.isTypeOnly ||
1122
+ !statement.moduleSpecifier ||
1123
+ !ts.isStringLiteral(statement.moduleSpecifier)) {
1124
+ continue;
1125
+ }
1126
+ const target = resolveModuleTarget(sourceFile.fileName, statement.moduleSpecifier.text, sourceFiles);
1127
+ if (!target)
1128
+ continue;
1129
+ const exportClause = statement.exportClause;
1130
+ if (!exportClause) {
1131
+ exportAllTargets.push(target);
1132
+ continue;
1133
+ }
1134
+ if (ts.isNamespaceExport(exportClause)) {
1135
+ namespaces.set(exportClause.name.text, target);
1136
+ }
1137
+ else if (ts.isNamedExports(exportClause)) {
1138
+ for (const specifier of exportClause.elements) {
1139
+ if (specifier.isTypeOnly)
1140
+ continue;
1141
+ bindings.set(specifier.name.text, {
1142
+ target,
1143
+ exportedName: (specifier.propertyName ?? specifier.name).text,
1144
+ });
1145
+ }
1146
+ }
1147
+ }
1148
+ return { bindings, namespaces, exportAllTargets };
1149
+ }
1150
+ function resolveModuleTarget(fromFilePath, moduleSpecifier, sourceFiles) {
1151
+ if (moduleSpecifier.startsWith(".")) {
1152
+ const targetFilePath = resolveLocalSourceFile(fromFilePath, moduleSpecifier, sourceFiles);
1153
+ return targetFilePath ? { kind: "local", filePath: targetFilePath } : null;
1154
+ }
1155
+ return { kind: "external", moduleSpecifier };
1156
+ }
1157
+ function resolveLocalSourceFile(fromFilePath, moduleSpecifier, sourceFiles) {
1158
+ if (!moduleSpecifier.startsWith("."))
1159
+ return null;
1160
+ const basePath = resolve(dirname(fromFilePath), moduleSpecifier);
1161
+ const baseExtension = extname(basePath);
1162
+ const extensionlessBasePath = sourceExtensions.has(baseExtension)
1163
+ ? basePath.slice(0, -baseExtension.length)
1164
+ : basePath;
1165
+ const candidates = [
1166
+ basePath,
1167
+ ...[...sourceExtensions].map((extension) => `${extensionlessBasePath}${extension}`),
1168
+ ...[...sourceExtensions].map((extension) => join(extensionlessBasePath, `index${extension}`)),
1169
+ ];
1170
+ return candidates.find((candidate) => sourceFiles.has(candidate)) ?? null;
1171
+ }
1172
+ function isCallableNode(node) {
1173
+ return (ts.isFunctionDeclaration(node) ||
1174
+ ts.isFunctionExpression(node) ||
1175
+ ts.isArrowFunction(node) ||
1176
+ ts.isMethodDeclaration(node));
1177
+ }
1178
+ function collectDirectBlockBindings(block, scope) {
1179
+ collectDirectStatementBindings(block.statements, scope);
1180
+ }
1181
+ function collectDirectCaseBlockBindings(caseBlock, scope) {
1182
+ for (const clause of caseBlock.clauses) {
1183
+ collectDirectStatementBindings(clause.statements, scope);
1184
+ }
1185
+ }
1186
+ function collectDirectStatementBindings(statements, scope) {
1187
+ for (const statement of statements) {
1188
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
1189
+ addBindingNames(scope, statement.name, statement, scope);
1190
+ continue;
1191
+ }
1192
+ if (!ts.isVariableStatement(statement))
1193
+ continue;
1194
+ for (const declaration of statement.declarationList.declarations) {
1195
+ addBindingNames(scope, declaration.name, declaration, scope);
1196
+ }
1197
+ }
1198
+ }
1199
+ function createChildScope(parent) {
1200
+ return { parent, bindings: new Map() };
1201
+ }
1202
+ function addBindingNames(scope, name, node, declarationScope) {
1203
+ addBindingNamesToMap(scope.bindings, name, node, declarationScope, ts.isVariableDeclaration(node) ? node.initializer : undefined);
1204
+ }
1205
+ function addBindingNamesToMap(bindings, name, node, declarationScope, initializer, propertyPath = []) {
1206
+ if (ts.isIdentifier(name)) {
1207
+ const value = isCallableNode(node)
1208
+ ? { kind: "callable", node, scope: declarationScope }
1209
+ : initializer
1210
+ ? {
1211
+ kind: "expression",
1212
+ expression: initializer,
1213
+ scope: declarationScope,
1214
+ propertyPath,
1215
+ }
1216
+ : { kind: "unknown" };
1217
+ bindings.set(name.text, {
1218
+ id: `${node.pos}:${name.text}:${propertyPath.join(".")}`,
1219
+ node,
1220
+ value,
1221
+ });
1222
+ return;
1223
+ }
1224
+ if (ts.isObjectBindingPattern(name)) {
1225
+ for (const element of name.elements) {
1226
+ if (element.dotDotDotToken) {
1227
+ addBindingNamesToMap(bindings, element.name, element, declarationScope, undefined);
1228
+ continue;
1229
+ }
1230
+ const propertyName = bindingElementPropertyName(element);
1231
+ addBindingNamesToMap(bindings, element.name, node, declarationScope, initializer, propertyName === null ? [] : [...propertyPath, propertyName]);
1232
+ }
1233
+ return;
1234
+ }
1235
+ name.elements.forEach((element, index) => {
1236
+ if (ts.isOmittedExpression(element))
1237
+ return;
1238
+ if (element.dotDotDotToken) {
1239
+ addBindingNamesToMap(bindings, element.name, element, declarationScope, undefined);
1240
+ return;
1241
+ }
1242
+ addBindingNamesToMap(bindings, element.name, node, declarationScope, initializer, [...propertyPath, String(index)]);
1243
+ });
1244
+ }
1245
+ function bindingElementPropertyName(element) {
1246
+ const propertyName = element.propertyName;
1247
+ if (propertyName &&
1248
+ (ts.isIdentifier(propertyName) ||
1249
+ ts.isStringLiteralLike(propertyName) ||
1250
+ ts.isNumericLiteral(propertyName))) {
1251
+ return propertyName.text;
1252
+ }
1253
+ return ts.isIdentifier(element.name) ? element.name.text : null;
1254
+ }
1255
+ function collectBindingNames(name) {
1256
+ if (ts.isIdentifier(name))
1257
+ return [name.text];
1258
+ return name.elements.flatMap((element) => ts.isOmittedExpression(element) ? [] : collectBindingNames(element.name));
1259
+ }
1260
+ function assignBindingExpression(left, right, scope) {
1261
+ const target = unwrapExpression(left);
1262
+ if (!ts.isIdentifier(target))
1263
+ return;
1264
+ const binding = findLexicalBinding(scope, target.text);
1265
+ if (!binding)
1266
+ return;
1267
+ binding.value = {
1268
+ kind: "expression",
1269
+ expression: right,
1270
+ scope,
1271
+ propertyPath: [],
1272
+ };
1273
+ }
1274
+ function snapshotBindingValues(scope) {
1275
+ const snapshot = new Map();
1276
+ let current = scope;
1277
+ while (current) {
1278
+ for (const binding of current.bindings.values()) {
1279
+ if (!snapshot.has(binding)) {
1280
+ snapshot.set(binding, binding.value);
1281
+ }
1282
+ }
1283
+ current = current.parent;
1284
+ }
1285
+ return snapshot;
1286
+ }
1287
+ function restoreBindingValues(snapshot) {
1288
+ for (const [binding, value] of snapshot) {
1289
+ binding.value = value;
1290
+ }
1291
+ }
1292
+ function mergeBindingValues(before, alternatives) {
1293
+ for (const [binding, initialValue] of before) {
1294
+ const values = alternatives.map((alternative) => alternative.get(binding) ?? initialValue);
1295
+ binding.value = values.every((value) => bindingValuesEqual(value, values[0]))
1296
+ ? values[0]
1297
+ : { kind: "unknown" };
1298
+ }
1299
+ }
1300
+ function bindingValuesEqual(left, right) {
1301
+ if (left.kind !== right.kind)
1302
+ return false;
1303
+ if (left.kind === "unknown" || right.kind === "unknown")
1304
+ return true;
1305
+ if (left.kind === "callable" && right.kind === "callable") {
1306
+ return left.node === right.node && left.scope === right.scope;
1307
+ }
1308
+ if (left.kind === "expression" && right.kind === "expression") {
1309
+ return (left.expression === right.expression &&
1310
+ left.scope === right.scope &&
1311
+ left.propertyPath.length === right.propertyPath.length &&
1312
+ left.propertyPath.every((propertyName, index) => propertyName === right.propertyPath[index]));
1313
+ }
1314
+ return false;
1315
+ }
1316
+ function collectForInitializerBindings(initializer, scope) {
1317
+ if (!ts.isVariableDeclarationList(initializer))
1318
+ return;
1319
+ for (const declaration of initializer.declarations) {
1320
+ addBindingNames(scope, declaration.name, declaration, scope);
1321
+ }
1322
+ }
1323
+ function findLexicalBinding(scope, name) {
1324
+ let current = scope;
1325
+ while (current) {
1326
+ const binding = current.bindings.get(name);
1327
+ if (binding)
1328
+ return binding;
1329
+ current = current.parent;
1330
+ }
1331
+ return null;
1332
+ }
1333
+ function collectInternalApiNames(sourceFile) {
1334
+ const names = new Set();
1335
+ for (const statement of sourceFile.statements) {
1336
+ if (!ts.isImportDeclaration(statement) ||
1337
+ statement.importClause?.isTypeOnly) {
1338
+ continue;
1339
+ }
1340
+ if (!ts.isStringLiteral(statement.moduleSpecifier) ||
1341
+ !isGeneratedApiImport(statement.moduleSpecifier.text)) {
1342
+ continue;
1343
+ }
1344
+ const namedBindings = statement.importClause?.namedBindings;
1345
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) {
1346
+ continue;
1347
+ }
1348
+ for (const importSpecifier of namedBindings.elements) {
1349
+ const importedName = (importSpecifier.propertyName ?? importSpecifier.name).text;
1350
+ if (importedName === "internal") {
1351
+ names.add(importSpecifier.name.text);
1352
+ }
1353
+ }
1354
+ }
1355
+ return names;
1356
+ }
1357
+ function getInternalApiReferencePath(node, info, scope, resolving) {
1358
+ if (ts.isIdentifier(node)) {
1359
+ const lexicalBinding = findLexicalBinding(scope, node.text);
1360
+ if (lexicalBinding) {
1361
+ return getInternalApiPathFromBinding(lexicalBinding, info, resolving);
1362
+ }
1363
+ const key = `${info.filePath}:internal:${node.text}`;
1364
+ if (resolving.has(key))
1365
+ return null;
1366
+ const binding = info.bindings.get(node.text);
1367
+ if (binding) {
1368
+ return getInternalApiPathFromBinding(binding, info, new Set(resolving).add(key));
1369
+ }
1370
+ return info.internalApiNames.has(node.text) ? [] : null;
1371
+ }
1372
+ if (ts.isPropertyAccessExpression(node)) {
1373
+ const parent = getInternalApiReferencePath(unwrapExpression(node.expression), info, scope, resolving);
1374
+ return parent === null ? null : [...parent, node.name.text];
1375
+ }
1376
+ if (ts.isElementAccessExpression(node)) {
1377
+ const argument = node.argumentExpression && unwrapExpression(node.argumentExpression);
1378
+ if (!argument || !ts.isStringLiteralLike(argument))
1379
+ return null;
1380
+ const parent = getInternalApiReferencePath(unwrapExpression(node.expression), info, scope, resolving);
1381
+ return parent === null ? null : [...parent, argument.text];
1382
+ }
1383
+ return null;
1384
+ }
1385
+ function getInternalApiPathFromBinding(binding, info, resolving) {
1386
+ const bindingKey = `${info.filePath}:internal-binding:${binding.id}`;
1387
+ if (resolving.has(bindingKey))
1388
+ return null;
1389
+ const nextResolving = new Set(resolving).add(bindingKey);
1390
+ if (binding.value.kind !== "expression")
1391
+ return null;
1392
+ const path = getInternalApiReferencePath(unwrapExpression(binding.value.expression), info, binding.value.scope, nextResolving);
1393
+ return path === null ? null : [...path, ...binding.value.propertyPath];
1394
+ }
1395
+ function getStringProperty(objectLiteral, propertyName) {
1396
+ for (const property of objectLiteral.properties) {
1397
+ if (!ts.isPropertyAssignment(property))
1398
+ continue;
1399
+ const name = property.name;
1400
+ const nameText = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
1401
+ if (nameText !== propertyName)
1402
+ continue;
1403
+ const value = unwrapExpression(property.initializer);
1404
+ if (ts.isStringLiteral(value) ||
1405
+ ts.isNoSubstitutionTemplateLiteral(value)) {
1406
+ return { value: value.text, node: value };
1407
+ }
1408
+ }
1409
+ return null;
1410
+ }
1411
+ function isPrivilegedResourceRuleKey(permissionKey) {
1412
+ if (permissionKey.startsWith("system."))
1413
+ return true;
1414
+ if (!permissionKey.startsWith("app."))
1415
+ return false;
1416
+ const actionSeparatorIndex = permissionKey.lastIndexOf(":");
1417
+ const action = actionSeparatorIndex === -1
1418
+ ? ""
1419
+ : permissionKey.slice(actionSeparatorIndex + 1);
1420
+ return (action === "*" || action === "manage_members" || action === "manage_access");
1421
+ }
1422
+ // The IAM catalog is file-only: hercules/iam.jsonc at the app root declares
1423
+ // every app permission key, and the control plane seeds the platform
1424
+ // system.* keys. Returns null when the file is missing or does not parse as
1425
+ // a permissions catalog, which disables the noncanonical_permission_key
1426
+ // check instead of risking false positives.
1427
+ function loadCatalogPermissionKeys(cwd) {
1428
+ const iamFilePath = join(cwd, "hercules", "iam.jsonc");
1429
+ if (!existsSync(iamFilePath) || !statSync(iamFilePath).isFile()) {
1430
+ return null;
1431
+ }
1432
+ // parseConfigFileTextToJson parses JSONC (comments and trailing commas),
1433
+ // matching how the build applies the catalog file.
1434
+ const parsed = ts.parseConfigFileTextToJson(iamFilePath, readFileSync(iamFilePath, "utf8"));
1435
+ if (parsed.error) {
1436
+ return null;
1437
+ }
1438
+ const permissions = parsed.config
1439
+ ?.permissions;
1440
+ if (typeof permissions !== "object" ||
1441
+ permissions === null ||
1442
+ Array.isArray(permissions)) {
1443
+ return null;
1444
+ }
1445
+ return new Set(Object.keys(permissions));
1446
+ }
1447
+ // The runtime authorize gate resolves a requested permission by exact key
1448
+ // lookup in the catalog and denies a miss with permission_missing, so a
1449
+ // builder-level permission literal that is not a declared catalog key always
1450
+ // fails at runtime. Membership only: no key grammar is parsed, dynamic
1451
+ // permission values are skipped, and system.* keys are platform-seeded.
1452
+ function checkCanonicalPermissionKeys(cwd, filePath, catalogPermissionKeys) {
1453
+ if (!catalogPermissionKeys) {
1454
+ return [];
1455
+ }
1456
+ const sourceText = readFileSync(filePath, "utf8");
1457
+ const sourceFile = createSourceFile(filePath, sourceText);
1458
+ const findings = [];
1459
+ for (const definition of collectManagedBuilderDefinitions(sourceFile, [
1460
+ "accessQuery",
1461
+ "accessMutation",
1462
+ "accessAction",
1463
+ ])) {
1464
+ const permission = getLiteralPermissionProperty(definition.definition);
1465
+ if (!permission)
1466
+ continue;
1467
+ if (catalogPermissionKeys.has(permission.key) ||
1468
+ permission.key.startsWith("system.")) {
1469
+ continue;
1470
+ }
1471
+ const prefixedKey = `app.${permission.key}`;
1472
+ findings.push(createPatternFindingAtNode({
1473
+ cwd,
1474
+ sourceFile,
1475
+ node: permission.node,
1476
+ code: "noncanonical_permission_key",
1477
+ message: `Permission key "${permission.key}" is not declared in hercules/iam.jsonc.`,
1478
+ suggestion: catalogPermissionKeys.has(prefixedKey)
1479
+ ? `Use the catalog key "${prefixedKey}" exactly as declared in hercules/iam.jsonc.`
1480
+ : "Use a permission key exactly as declared in hercules/iam.jsonc, or add it to the catalog and rebuild.",
1481
+ }));
1482
+ }
1483
+ return findings;
1484
+ }
1485
+ function getLiteralPermissionProperty(definition) {
1486
+ const objectLiteral = unwrapExpression(definition);
1487
+ if (!ts.isObjectLiteralExpression(objectLiteral)) {
1488
+ return null;
1489
+ }
1490
+ for (const property of objectLiteral.properties) {
1491
+ if (!ts.isPropertyAssignment(property))
1492
+ continue;
1493
+ const name = property.name;
1494
+ const nameText = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
1495
+ if (nameText !== "permission")
1496
+ continue;
1497
+ const initializer = unwrapExpression(property.initializer);
1498
+ return ts.isStringLiteralLike(initializer)
1499
+ ? { key: initializer.text, node: initializer }
1500
+ : null;
1501
+ }
1502
+ return null;
1503
+ }
1504
+ function createPatternFindingAtNode(args) {
1505
+ const position = args.sourceFile.getLineAndCharacterOfPosition(args.node.getStart(args.sourceFile));
1506
+ return {
1507
+ code: args.code,
1508
+ severity: "error",
1509
+ filePath: displayPathFor(args.cwd, args.sourceFile.fileName),
1510
+ line: position.line + 1,
1511
+ column: position.character + 1,
1512
+ message: args.message,
1513
+ suggestion: args.suggestion,
1514
+ };
1515
+ }
1516
+ function escapeRegExp(value) {
1517
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1518
+ }
1519
+ function addPatternFinding(args) {
1520
+ const match = args.pattern.exec(args.sourceText);
1521
+ if (!match?.index && match?.index !== 0)
1522
+ return;
1523
+ const position = lineAndColumnAt(args.sourceText, match.index);
1524
+ args.findings.push({
1525
+ code: args.code,
1526
+ severity: "error",
1527
+ filePath: displayPathFor(args.cwd, args.filePath),
1528
+ line: position.line,
1529
+ column: position.column,
1530
+ message: args.message,
1531
+ suggestion: args.suggestion,
1532
+ });
1533
+ }
1534
+ function lineAndColumnAt(sourceText, index) {
1535
+ let line = 1;
1536
+ let column = 1;
1537
+ for (let offset = 0; offset < index; offset += 1) {
1538
+ if (sourceText[offset] === "\n") {
1539
+ line += 1;
1540
+ column = 1;
1541
+ }
1542
+ else {
1543
+ column += 1;
1544
+ }
1545
+ }
1546
+ return { line, column };
1547
+ }
1548
+ function createSourceFile(filePath, sourceText) {
1549
+ return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, filePath.endsWith(".tsx") || filePath.endsWith(".jsx")
1550
+ ? ts.ScriptKind.TSX
1551
+ : ts.ScriptKind.TS);
1552
+ }
1553
+ function collectRawBuilderImports(sourceFile) {
1554
+ const imports = new Map();
1555
+ for (const statement of sourceFile.statements) {
1556
+ if (!ts.isImportDeclaration(statement) ||
1557
+ statement.importClause?.isTypeOnly) {
1558
+ continue;
1559
+ }
1560
+ if (!ts.isStringLiteral(statement.moduleSpecifier) ||
1561
+ !isGeneratedServerImport(statement.moduleSpecifier.text)) {
1562
+ continue;
1563
+ }
1564
+ const namedBindings = statement.importClause?.namedBindings;
1565
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) {
1566
+ continue;
1567
+ }
1568
+ for (const importSpecifier of namedBindings.elements) {
1569
+ const importedName = (importSpecifier.propertyName ?? importSpecifier.name).text;
1570
+ if (isRawBuilderName(importedName)) {
1571
+ imports.set(importSpecifier.name.text, importedName);
1572
+ }
1573
+ }
1574
+ }
1575
+ return imports;
1576
+ }
1577
+ function buildGeneratedServerImportRemovals(sourceFile, sourceText, candidates) {
1578
+ const builderNodeStarts = new Set(candidates.map((candidate) => candidate.builderNode.getStart(sourceFile)));
1579
+ const builderNamesToReplace = new Set(candidates.map((candidate) => candidate.builderNode.text));
1580
+ const identifierUses = collectIdentifierUses(sourceFile, builderNamesToReplace);
1581
+ const removableNames = new Set();
1582
+ for (const name of builderNamesToReplace) {
1583
+ const uses = identifierUses.get(name) ?? [];
1584
+ if (uses.length > 0 &&
1585
+ uses.every((position) => builderNodeStarts.has(position))) {
1586
+ removableNames.add(name);
1587
+ }
1588
+ }
1589
+ if (removableNames.size === 0) {
1590
+ return [];
1591
+ }
1592
+ const replacements = [];
1593
+ for (const statement of sourceFile.statements) {
1594
+ if (!ts.isImportDeclaration(statement) ||
1595
+ statement.importClause?.isTypeOnly) {
1596
+ continue;
1597
+ }
1598
+ if (!ts.isStringLiteral(statement.moduleSpecifier) ||
1599
+ !isGeneratedServerImport(statement.moduleSpecifier.text)) {
1600
+ continue;
1601
+ }
1602
+ const namedBindings = statement.importClause?.namedBindings;
1603
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) {
1604
+ continue;
1605
+ }
1606
+ const rawSpecifiersToRemove = namedBindings.elements.filter((specifier) => removableNames.has(specifier.name.text));
1607
+ if (rawSpecifiersToRemove.length === 0) {
1608
+ continue;
1609
+ }
1610
+ if (rawSpecifiersToRemove.length === namedBindings.elements.length) {
1611
+ const start = statement.getFullStart();
1612
+ const end = includeTrailingNewline(sourceText, statement.getEnd());
1613
+ replacements.push({ start, end, text: "" });
1614
+ continue;
1615
+ }
1616
+ for (const specifier of rawSpecifiersToRemove) {
1617
+ replacements.push(buildImportSpecifierRemoval(namedBindings, specifier));
1618
+ }
1619
+ }
1620
+ return replacements;
1621
+ }
1622
+ function collectIdentifierUses(sourceFile, names) {
1623
+ const uses = new Map();
1624
+ function visit(node) {
1625
+ if (ts.isImportSpecifier(node) && names.has(node.name.text)) {
1626
+ return;
1627
+ }
1628
+ if (ts.isIdentifier(node) && names.has(node.text)) {
1629
+ const positions = uses.get(node.text) ?? [];
1630
+ positions.push(node.getStart(sourceFile));
1631
+ uses.set(node.text, positions);
1632
+ }
1633
+ ts.forEachChild(node, visit);
1634
+ }
1635
+ visit(sourceFile);
1636
+ return uses;
1637
+ }
1638
+ function buildImportSpecifierRemoval(namedImports, specifier) {
1639
+ const elements = namedImports.elements;
1640
+ const index = elements.findIndex((element) => element === specifier);
1641
+ if (index === -1) {
1642
+ return {
1643
+ start: specifier.getFullStart(),
1644
+ end: specifier.getEnd(),
1645
+ text: "",
1646
+ };
1647
+ }
1648
+ const previous = elements[index - 1];
1649
+ const next = elements[index + 1];
1650
+ if (next) {
1651
+ return {
1652
+ start: specifier.getFullStart(),
1653
+ end: next.getFullStart(),
1654
+ text: "",
1655
+ };
1656
+ }
1657
+ if (previous) {
1658
+ return { start: previous.getEnd(), end: specifier.getEnd(), text: "" };
1659
+ }
1660
+ return { start: specifier.getFullStart(), end: specifier.getEnd(), text: "" };
1661
+ }
1662
+ function buildAccessImportReplacement(sourceFile, sourceText, accessImports, convexDir) {
1663
+ const sortedImports = [...accessImports].sort();
1664
+ const accessImport = findAccessImport(sourceFile, convexDir);
1665
+ if (accessImport?.namedBindings &&
1666
+ ts.isNamedImports(accessImport.namedBindings)) {
1667
+ const existingNames = new Set(accessImport.namedBindings.elements.map((specifier) => specifier.name.text));
1668
+ const missingNames = sortedImports.filter((name) => !existingNames.has(name));
1669
+ if (missingNames.length === 0) {
1670
+ return { start: 0, end: 0, text: "" };
1671
+ }
1672
+ const closingBraceStart = accessImport.namedBindings.getEnd() - 1;
1673
+ const prefix = accessImport.namedBindings.elements.length > 0 ? ", " : "";
1674
+ return {
1675
+ start: closingBraceStart,
1676
+ end: closingBraceStart,
1677
+ text: `${prefix}${missingNames.join(", ")}`,
1678
+ };
1679
+ }
1680
+ const accessImportPath = buildAccessImportPath(sourceFile, convexDir);
1681
+ const importLine = `import { ${sortedImports.join(", ")} } from "${accessImportPath}";\n`;
1682
+ const lastImport = sourceFile.statements
1683
+ .filter(ts.isImportDeclaration)
1684
+ .at(-1);
1685
+ if (!lastImport) {
1686
+ return { start: 0, end: 0, text: importLine };
1687
+ }
1688
+ return {
1689
+ start: includeTrailingNewline(sourceText, lastImport.getEnd()),
1690
+ end: includeTrailingNewline(sourceText, lastImport.getEnd()),
1691
+ text: importLine,
1692
+ };
1693
+ }
1694
+ function findAccessImport(sourceFile, convexDir) {
1695
+ for (const statement of sourceFile.statements) {
1696
+ if (!ts.isImportDeclaration(statement) ||
1697
+ statement.importClause?.isTypeOnly) {
1698
+ continue;
1699
+ }
1700
+ if (!ts.isStringLiteral(statement.moduleSpecifier) ||
1701
+ !isAccessImport(sourceFile, statement.moduleSpecifier.text, convexDir)) {
1702
+ continue;
1703
+ }
1704
+ return { namedBindings: statement.importClause?.namedBindings };
1705
+ }
1706
+ return null;
1707
+ }
1708
+ function buildAccessImportPath(sourceFile, convexDir) {
1709
+ const relativePath = normalizePath(relative(dirname(sourceFile.fileName), join(convexDir, "hercules")));
1710
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
1711
+ }
1712
+ function isAccessImport(sourceFile, moduleSpecifier, convexDir) {
1713
+ if (!moduleSpecifier.startsWith(".")) {
1714
+ return false;
1715
+ }
1716
+ return (stripKnownModuleExtension(resolve(dirname(sourceFile.fileName), moduleSpecifier)) === join(convexDir, "hercules") ||
1717
+ stripKnownModuleExtension(resolve(dirname(sourceFile.fileName), moduleSpecifier)) === join(convexDir, "access"));
1718
+ }
1719
+ function isAccessWiringSourceFile(filePath, convexDir) {
1720
+ if (!filePath)
1721
+ return false;
1722
+ const extensionlessPath = stripKnownModuleExtension(filePath);
1723
+ return (extensionlessPath === join(convexDir, "hercules") ||
1724
+ extensionlessPath === join(convexDir, "access"));
1725
+ }
1726
+ // A Convex function file uses managed Access Control when it imports the
1727
+ // @usehercules/convex SDK (including subpaths such as /access-admin and
1728
+ // /convex.config) or the local convex/hercules or convex/access wiring module
1729
+ // the managed builders are re-exported from.
1730
+ function fileUsesManagedAccessControl(filePath, convexDir) {
1731
+ const sourceText = readFileSync(filePath, "utf8");
1732
+ const sourceFile = createSourceFile(filePath, sourceText);
1733
+ for (const statement of sourceFile.statements) {
1734
+ if (!ts.isImportDeclaration(statement) &&
1735
+ !ts.isExportDeclaration(statement)) {
1736
+ continue;
1737
+ }
1738
+ const moduleSpecifier = statement.moduleSpecifier;
1739
+ if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier)) {
1740
+ continue;
1741
+ }
1742
+ if (moduleSpecifier.text === accessControlPackageName ||
1743
+ moduleSpecifier.text.startsWith(`${accessControlPackageName}/`) ||
1744
+ isAccessImport(sourceFile, moduleSpecifier.text, convexDir)) {
1745
+ return true;
1746
+ }
1747
+ }
1748
+ return false;
1749
+ }
1750
+ function collectExportedNames(sourceFile) {
1751
+ const names = new Set();
1752
+ for (const statement of sourceFile.statements) {
1753
+ if (!ts.isExportDeclaration(statement)) {
1754
+ continue;
1755
+ }
1756
+ const exportClause = statement.exportClause;
1757
+ if (!exportClause || !ts.isNamedExports(exportClause)) {
1758
+ continue;
1759
+ }
1760
+ for (const exportSpecifier of exportClause.elements) {
1761
+ names.add((exportSpecifier.propertyName ?? exportSpecifier.name).text);
1762
+ }
1763
+ }
1764
+ return names;
1765
+ }
1766
+ function collectRawBuilderCandidates(sourceFile, rawBuilderImports) {
1767
+ const candidates = [];
1768
+ for (const statement of sourceFile.statements) {
1769
+ if (ts.isVariableStatement(statement)) {
1770
+ const isDirectExport = hasExportModifier(statement);
1771
+ for (const declaration of statement.declarationList.declarations) {
1772
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
1773
+ continue;
1774
+ }
1775
+ const rawCall = getRawBuilderCall(declaration.initializer, rawBuilderImports);
1776
+ if (rawCall) {
1777
+ candidates.push({
1778
+ ...rawCall,
1779
+ functionName: declaration.name.text,
1780
+ isDirectExport,
1781
+ declaration: statement,
1782
+ });
1783
+ }
1784
+ }
1785
+ continue;
1786
+ }
1787
+ if (ts.isExportAssignment(statement)) {
1788
+ const rawCall = getRawBuilderCall(statement.expression, rawBuilderImports);
1789
+ if (rawCall) {
1790
+ candidates.push({
1791
+ ...rawCall,
1792
+ functionName: "default",
1793
+ isDirectExport: true,
1794
+ declaration: statement,
1795
+ });
1796
+ }
1797
+ }
1798
+ }
1799
+ return candidates;
1800
+ }
1801
+ function getRawBuilderCall(initializer, rawBuilderImports) {
1802
+ const expression = unwrapExpression(initializer);
1803
+ if (!ts.isCallExpression(expression)) {
1804
+ return null;
1805
+ }
1806
+ const callTarget = unwrapExpression(expression.expression);
1807
+ if (!ts.isIdentifier(callTarget)) {
1808
+ return null;
1809
+ }
1810
+ const builder = rawBuilderImports.get(callTarget.text);
1811
+ if (!builder) {
1812
+ return null;
1813
+ }
1814
+ return { builder, builderNode: callTarget };
1815
+ }
1816
+ function createFinding(cwd, sourceFile, candidate) {
1817
+ const position = sourceFile.getLineAndCharacterOfPosition(candidate.builderNode.getStart(sourceFile));
1818
+ const builderSuffix = builderDisplaySuffix(candidate.builder);
1819
+ return {
1820
+ code: "raw_exported_convex_builder",
1821
+ severity: "error",
1822
+ filePath: displayPathFor(cwd, sourceFile.fileName),
1823
+ line: position.line + 1,
1824
+ column: position.character + 1,
1825
+ functionName: candidate.functionName,
1826
+ builder: candidate.builder,
1827
+ message: `Exported Convex function "${candidate.functionName}" uses raw ${candidate.builder}().`,
1828
+ suggestion: `Import from ./hercules and choose public${builderSuffix}, authenticated${builderSuffix}, or access${builderSuffix}.`,
1829
+ };
1830
+ }
1831
+ function unwrapExpression(expression) {
1832
+ let current = expression;
1833
+ while (ts.isParenthesizedExpression(current) ||
1834
+ ts.isAsExpression(current) ||
1835
+ ts.isTypeAssertionExpression(current) ||
1836
+ ts.isNonNullExpression(current)) {
1837
+ current = current.expression;
1838
+ }
1839
+ return current;
1840
+ }
1841
+ function hasExportModifier(node) {
1842
+ return (ts.canHaveModifiers(node) &&
1843
+ ts
1844
+ .getModifiers(node)
1845
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ===
1846
+ true);
1847
+ }
1848
+ function hasDefaultModifier(node) {
1849
+ return (ts.canHaveModifiers(node) &&
1850
+ ts
1851
+ .getModifiers(node)
1852
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ===
1853
+ true);
1854
+ }
1855
+ function hasLocalExemption(sourceFile, sourceText, declaration) {
1856
+ const leadingText = sourceText.slice(declaration.getFullStart(), declaration.getStart(sourceFile));
1857
+ return exemptionMarkers.some((marker) => leadingText.includes(marker));
1858
+ }
1859
+ function isSourceFile(filePath) {
1860
+ return sourceExtensions.has(extname(filePath)) && !filePath.endsWith(".d.ts");
1861
+ }
1862
+ function isGeneratedServerImport(moduleSpecifier) {
1863
+ return (moduleSpecifier.endsWith("_generated/server") ||
1864
+ moduleSpecifier.endsWith("_generated/server.js"));
1865
+ }
1866
+ function isGeneratedApiImport(moduleSpecifier) {
1867
+ return (moduleSpecifier.endsWith("_generated/api") ||
1868
+ moduleSpecifier.endsWith("_generated/api.js"));
1869
+ }
1870
+ function isRawBuilderName(value) {
1871
+ return rawBuilderNames.has(value);
1872
+ }
1873
+ function authenticatedBuilderName(builder) {
1874
+ switch (builder) {
1875
+ case "query":
1876
+ return "authenticatedQuery";
1877
+ case "mutation":
1878
+ return "authenticatedMutation";
1879
+ case "action":
1880
+ return "authenticatedAction";
1881
+ }
1882
+ }
1883
+ function builderDisplaySuffix(builder) {
1884
+ switch (builder) {
1885
+ case "query":
1886
+ return "Query";
1887
+ case "mutation":
1888
+ return "Mutation";
1889
+ case "action":
1890
+ return "Action";
1891
+ }
1892
+ }
1893
+ function includeTrailingNewline(sourceText, position) {
1894
+ if (sourceText[position] === "\r" && sourceText[position + 1] === "\n") {
1895
+ return position + 2;
1896
+ }
1897
+ if (sourceText[position] === "\n") {
1898
+ return position + 1;
1899
+ }
1900
+ return position;
1901
+ }
1902
+ function applyTextReplacements(sourceText, replacements) {
1903
+ const sorted = replacements
1904
+ .filter((replacement) => replacement.start !== replacement.end || replacement.text.length > 0)
1905
+ .sort((left, right) => right.start - left.start);
1906
+ let result = sourceText;
1907
+ for (const replacement of sorted) {
1908
+ result =
1909
+ result.slice(0, replacement.start) +
1910
+ replacement.text +
1911
+ result.slice(replacement.end);
1912
+ }
1913
+ return result;
1914
+ }
1915
+ function displayPathFor(cwd, filePath) {
1916
+ const relativePath = relative(cwd, filePath);
1917
+ if (relativePath.startsWith("..")) {
1918
+ return normalizePath(filePath);
1919
+ }
1920
+ return normalizePath(relativePath || basename(filePath));
1921
+ }
1922
+ function normalizePath(filePath) {
1923
+ return filePath.split("\\").join("/");
1924
+ }
1925
+ function stripKnownModuleExtension(filePath) {
1926
+ return filePath.replace(/\.(?:c|m)?(?:t|j)sx?$/, "");
1927
+ }
1928
+ //# sourceMappingURL=index.js.map