Skip to content

Commit f60d692

Browse files
committed
fix(audits): accept HEAD on a GET contract and treat empty import/export clauses as runtime edges
1 parent 093680d commit f60d692

4 files changed

Lines changed: 27 additions & 4 deletions

File tree

‎scripts/check-monorepo-boundaries.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ describe('package boundary command', () => {
8787
["import * as contracts from '@/lib/api/contracts/widgets'", useCase, false],
8888
["import { presentWidget } from '@/lib/api/server/widget-presenters'", useCase, false],
8989
["import { run } from '@/lib/mothership/tools/handlers/run-code'", useCase, false],
90+
["import {} from '@/lib/mothership/tools/handlers/run-code'", useCase, false],
91+
["export {} from '@/lib/api/server/widget-presenters'", useCase, false],
9092
["const tool = import('@/lib/mothership/tools/server/widgets')", useCase, false],
9193
["import { listWidgetsContract } from '../../api/contracts/widgets'", useCase, false],
9294
["import { GET } from '../../../app/api/widgets/route'", useCase, false],

‎scripts/check-monorepo-boundaries.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function applicationViolation(
8282

8383
/**
8484
* Runtime (non-type) names an import/export declaration binds: `'all'` for a
85-
* namespace, default, star or side-effect edge, `[]` for one the compiler erases.
85+
* namespace, default, star, empty-clause or side-effect edge, `[]` for one the compiler erases.
8686
*/
8787
function runtimeBindings(node: ts.ImportDeclaration | ts.ExportDeclaration): string[] | 'all' {
8888
if (ts.isImportDeclaration(node)) {
@@ -92,14 +92,14 @@ function runtimeBindings(node: ts.ImportDeclaration | ts.ExportDeclaration): str
9292
if (clause.name) return 'all'
9393
const named = clause.namedBindings
9494
if (!named) return []
95-
if (ts.isNamespaceImport(named)) return 'all'
95+
if (ts.isNamespaceImport(named) || named.elements.length === 0) return 'all'
9696
return named.elements
9797
.filter((element) => !element.isTypeOnly)
9898
.map((element) => (element.propertyName ?? element.name).text)
9999
}
100100
if (node.isTypeOnly) return []
101101
const clause = node.exportClause
102-
if (!clause || ts.isNamespaceExport(clause)) return 'all'
102+
if (!clause || ts.isNamespaceExport(clause) || clause.elements.length === 0) return 'all'
103103
return clause.elements
104104
.filter((element) => !element.isTypeOnly)
105105
.map((element) => (element.propertyName ?? element.name).text)

‎scripts/check-route-verbs.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,23 @@ describe('aliased contract imports', () => {
118118
expect(failures[0]).toContain('which declares PATCH /api/things')
119119
})
120120
})
121+
122+
describe('HEAD handlers', () => {
123+
const route = {
124+
relative: 'route.ts',
125+
bindings: importedNames(`import { readContract } from '@/lib/api/contracts/things'`),
126+
expectedPath: '/api/things',
127+
}
128+
const loadModule = () => Promise.resolve({ readContract: { method: 'GET', path: '/api/things' } })
129+
130+
it('accepts a HEAD handler on a GET contract, as methodMatchesContract does', async () => {
131+
const failures: string[] = []
132+
await checkSite(route, 'raw', 'HEAD', 'readContract', failures, false, loadModule)
133+
expect(failures).toEqual([])
134+
})
135+
it('still rejects any other verb on a GET contract', async () => {
136+
const failures: string[] = []
137+
await checkSite(route, 'raw', 'POST', 'readContract', failures, false, loadModule)
138+
expect(failures).toHaveLength(1)
139+
})
140+
})

‎scripts/check-route-verbs.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,8 @@ export async function checkSite(
429429
kind === 'builder'
430430
? `export const ${verb} is built from \`${identifier}\``
431431
: `export const ${verb} parses with \`${identifier}\``
432-
if (contract.method.toUpperCase() !== verb) {
432+
const method = contract.method.toUpperCase()
433+
if (method !== verb && !(verb === 'HEAD' && method === 'GET')) {
433434
failures.push(
434435
kind === 'builder'
435436
? `${relative}: ${subject}, which declares ${contract.method} ${contract.path}. Next routes by the exported symbol, so ${verb} requests 500 and ${contract.method} requests 404.`

0 commit comments

Comments
 (0)