mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
Fail the gate on a first-party /api path
Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
This commit is contained in:
@@ -9,7 +9,10 @@
|
||||
# build of @hanzo/esign-remix, which type-checks every workspace package it
|
||||
# depends on. A type error anywhere under packages/ fails here.
|
||||
#
|
||||
# `lint:tx` runs first because it takes seconds and answers a question the build
|
||||
# `lint:paths` runs before install: it needs no dependencies and fails the gate
|
||||
# the moment a first-party /api path is served or called (every route is /v1).
|
||||
#
|
||||
# `lint:tx` runs next because it takes seconds and answers a question the build
|
||||
# cannot: whether anything inside a `$transaction` reaches the global Prisma
|
||||
# client. Base SQLite serves one write connection, so such a call waits on a
|
||||
# lock the transaction holds and the transaction dies (`P2028`) — at runtime,
|
||||
@@ -18,6 +21,7 @@ test:
|
||||
- name: build
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node scripts/lint-paths.mjs
|
||||
npm ci --no-audit --no-fund
|
||||
npm run lint:tx
|
||||
npx turbo run build --filter=@hanzo/esign-remix
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"start": "turbo run start --filter=@hanzo/esign-remix --filter=@hanzo/esign-documentation --filter=@hanzo/esign-openpage-api",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"lint:paths": "node scripts/lint-paths.mjs",
|
||||
"lint:tx": "npm test -w @hanzo/esign-eslint-config && eslint --no-config-lookup --no-inline-config -c packages/eslint-config/transaction.cjs packages apps",
|
||||
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
|
||||
"prepare": "husky && husky install || true",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Every route this app serves or calls is under /v1. Fails when a tracked file
|
||||
* serves or calls a first-party /api path again.
|
||||
*
|
||||
* A path that starts right after a quote, backtick or paren is relative, so it
|
||||
* is ours; an absolute URL is ours when its host is. Third-party hosts
|
||||
* (api.github.com, a vendor's /api/stats) name a different host and do not
|
||||
* match. A route directory named api is a served /api prefix with no string to
|
||||
* find, so the tree is checked too.
|
||||
*
|
||||
* node scripts/lint-paths.mjs
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const relative = /['"`(]\/api(\/|['"`)])/;
|
||||
const ours = /\b(?:e?sign\.hanzo\.ai|localhost(?::\d+)?|127\.0\.0\.1(?::\d+)?)\/api\//;
|
||||
const routeDir = /(^|\/)(routes\/api\+|app\/api)\//;
|
||||
const text = /\.(c?js|mjs|ts|tsx|json|ya?ml|sh|mdx?|html|toml)$/;
|
||||
|
||||
const files = execFileSync('git', ['ls-files'], { encoding: 'utf8' }).split('\n').filter(Boolean);
|
||||
|
||||
const hits = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (routeDir.test(file)) {
|
||||
hits.push(`${file}: serves /api from its directory`);
|
||||
}
|
||||
|
||||
if (!text.test(file) || file.endsWith('package-lock.json')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
readFileSync(file, 'utf8')
|
||||
.split('\n')
|
||||
.forEach((line, i) => {
|
||||
if (relative.test(line) || ours.test(line)) {
|
||||
hits.push(`${file}:${i + 1}: ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (hits.length > 0) {
|
||||
console.error(`first-party /api paths (every route is /v1):\n${hits.join('\n')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`lint:paths: ${files.length} files, no first-party /api path`);
|
||||
Reference in New Issue
Block a user