Skip to content

Commit 992205a

Browse files
committed
fix: align every remaining /api/ reference to /api/v1/
The v0.7.0 versioning sprint left a long tail of non-route references to the old unversioned paths in: Runtime fetches missed by the original sed: sdk-js/src/codegen/flags.ts — vb-flags CLI 404'd against admin admin/src/theme.ts — public theme endpoint, fixed in b3348a2 Display strings + UI labels: admin/src/pages/{Settings,Hooks,Records,CollectionEdit, NewCollectionModal,AuditLog}.tsx admin/src/components/CodeEditor.tsx admin/src/stores/auth.ts admin/src/App.tsx admin/src/pages/Setup.tsx Server-side comments + log strings: src/api/{audit-log,auth,flags,security,theme,webhooks}.ts src/core/{audit-log,collections,routes,update-check,version}.ts src/db/schema.ts src/realtime/{manager,sse}.ts SDK help/doc strings: sdk-js/src/codegen/{bin,generate}.ts sdk-js/src/flags/manager.ts sdk-js/src/realtime/manager.ts sdk-js/README.md The path-introspection sites in src/api/ratelimit.ts and src/core/audit-log.ts SKIP_PATHS are intentionally left referencing '/api/admin/...' (without /v1/) because they run after normalizeApiPath strips the version prefix. Comments document this; tests verify it. 637 + 65 tests pass; vaultbase + admin + sdk + docs all build clean.
1 parent b3348a2 commit 992205a

22 files changed

Lines changed: 49 additions & 49 deletions

admin/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function RequireUnauth({ children }: { children: React.ReactNode }) {
4545
* Boot-time check: probe whether any admin exists. Runs once at app mount.
4646
* - No admin yet → force redirect to /_/setup (any other route bounces).
4747
* - Admin exists → /_/setup is closed; bounce to /_/login (or /_/ if authed).
48-
* Status endpoint is read-only (`GET /api/admin/setup/status`) so the probe
48+
* Status endpoint is read-only (`GET /api/v1/admin/setup/status`) so the probe
4949
* doesn't write to logs or trip rate limits.
5050
*/
5151
function SetupRedirect({ children }: { children: React.ReactNode }) {

admin/src/components/CodeEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ interface RouteContext {
412412
req: Request;
413413
/** HTTP method (GET, POST, …) */
414414
method: string;
415-
/** Inner path (after the /api/custom prefix) */
415+
/** Inner path (after the /api/v1/custom prefix) */
416416
path: string;
417417
/** Path params from :name segments */
418418
params: Record<string, string>;

admin/src/pages/AuditLog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export default function AuditLog() {
157157
onRowClick={(e) => setOpen(e.data as AuditEntry)}
158158
selection={open}
159159
dataKey="id"
160-
emptyMessage="No audit entries yet. State-changing /api/admin/* requests show up here."
160+
emptyMessage="No audit entries yet. State-changing /api/v1/admin/* requests show up here."
161161
style={{ fontSize: 13 }}
162162
>
163163
<Column

admin/src/pages/CollectionEdit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,7 @@ function FieldOptionsBody({
10241024
<div style={{ fontSize: 13, fontWeight: 500 }}>Protected</div>
10251025
<div className="muted" style={{ fontSize: 11, marginTop: 2 }}>
10261026
Public GETs return 401. Issue a 1h access token via{" "}
1027-
<span className="mono">POST /api/files/.../token</span>, then pass <span className="mono">?token=</span>.
1027+
<span className="mono">POST /api/v1/files/.../token</span>, then pass <span className="mono">?token=</span>.
10281028
</div>
10291029
</div>
10301030
<Toggle

admin/src/pages/Hooks.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const ROUTE_METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"] as const;
129129
const ROUTE_TEMPLATE = `// Available context:
130130
// ctx.req — raw Request
131131
// ctx.method — "GET" | "POST" | …
132-
// ctx.path — inner path after /api/custom
132+
// ctx.path — inner path after /api/v1/custom
133133
// ctx.params — { id: "..." } from :name segments
134134
// ctx.query — query string params
135135
// ctx.body — parsed JSON body (or text/null)
@@ -570,7 +570,7 @@ function RoutesTab() {
570570
async function handleDelete(r: CustomRoute) {
571571
const ok = await confirm({
572572
title: "Delete custom route",
573-
message: `Delete the route ${r.method} /api/custom${r.path}?`,
573+
message: `Delete the route ${r.method} /api/v1/custom${r.path}?`,
574574
danger: true,
575575
});
576576
if (!ok) return;
@@ -593,7 +593,7 @@ function RoutesTab() {
593593
<div className="empty">Loading…</div>
594594
) : routes.length === 0 ? (
595595
<div className="empty">
596-
No custom routes. Routes mount under <code style={{ fontFamily: "var(--font-mono)" }}>/api/custom/&lt;your-path&gt;</code>.
596+
No custom routes. Routes mount under <code style={{ fontFamily: "var(--font-mono)" }}>/api/v1/custom/&lt;your-path&gt;</code>.
597597
</div>
598598
) : (
599599
<table className="table">
@@ -621,7 +621,7 @@ function RoutesTab() {
621621
<span className={`badge method-${r.method.toLowerCase()}`}>{r.method}</span>
622622
</td>
623623
<td className="mono-cell" onClick={() => setEditing(r)} style={{ cursor: "pointer" }}>
624-
<span style={{ color: "var(--text-muted)" }}>/api/custom</span>
624+
<span style={{ color: "var(--text-muted)" }}>/api/v1/custom</span>
625625
<span style={{ color: "var(--accent-light)" }}>{r.path}</span>
626626
</td>
627627
<td
@@ -788,7 +788,7 @@ function RouteEditor({
788788
</div>
789789
<div style={{ display: "flex", flexDirection: "column", gap: 4, flex: 1, minWidth: 240 }}>
790790
<span style={{ fontSize: 10.5, color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.06em" }}>
791-
Path <span className="muted" style={{ textTransform: "none", letterSpacing: 0 }}>(mounts under /api/custom)</span>
791+
Path <span className="muted" style={{ textTransform: "none", letterSpacing: 0 }}>(mounts under /api/v1/custom)</span>
792792
</span>
793793
<input
794794
className="input mono"
@@ -828,7 +828,7 @@ function RouteEditor({
828828
<div className="muted" style={{ fontSize: 11, display: "flex", gap: 16, flexWrap: "wrap" }}>
829829
<span>Type <span className="mono">ctx.</span> for autocomplete</span>
830830
<span>·</span>
831-
<span>Mounts at <span className="mono" style={{ color: "var(--accent-light)" }}>{method} /api/custom{path}</span></span>
831+
<span>Mounts at <span className="mono" style={{ color: "var(--accent-light)" }}>{method} /api/v1/custom{path}</span></span>
832832
<span>·</span>
833833
<span>Throw or call <span className="mono">ctx.helpers.abort(msg)</span> → 422</span>
834834
</div>

admin/src/pages/NewCollectionModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export default function NewCollectionModal({
243243
))}
244244
</div>
245245
<div className="muted" style={{ fontSize: 11, marginTop: 4 }}>
246-
{type === "auth" && <>Email + password sign-up via <span className="mono">/api/auth/{collName || "name"}/register</span>. Field names <span className="mono">email</span>, <span className="mono">password</span>, <span className="mono">verified</span> are managed by the implicit auth schema and cannot be redefined.</>}
246+
{type === "auth" && <>Email + password sign-up via <span className="mono">/api/v1/auth/{collName || "name"}/register</span>. Field names <span className="mono">email</span>, <span className="mono">password</span>, <span className="mono">verified</span> are managed by the implicit auth schema and cannot be redefined.</>}
247247
{type === "view" && <>Read-only collection backed by a SQL <span className="mono">SELECT</span>. Defaults to admin-only access — open it up via the API rules after creation. Writes return 405.</>}
248248
{type === "base" && <>Standard records collection. CRUD via <span className="mono">/api/{collName || "name"}</span>.</>}
249249
</div>

admin/src/pages/Records.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ function useRelationCache(fields: FieldDef[], enabled: boolean): RelationCache {
164164

165165
/**
166166
* In-memory cache of protected-file tokens keyed by filename. Tokens are
167-
* minted lazily via `POST /api/files/:collection/:recordId/:field/:filename/token`
167+
* minted lazily via `POST /api/v1/files/:collection/:recordId/:field/:filename/token`
168168
* and reused until they expire (~1h server-side). We refresh ~60s before
169169
* expiry to dodge edge-of-window failures.
170170
*
@@ -826,7 +826,7 @@ export default function Records() {
826826
onClick={() => setShowNew(true)}
827827
disabled={!collection || collection.type === "auth" || collection.type === "view"}
828828
title={
829-
collection?.type === "auth" ? "Users register via POST /api/auth/<collection>/register"
829+
collection?.type === "auth" ? "Users register via POST /api/v1/auth/<collection>/register"
830830
: collection?.type === "view" ? "View collections are read-only"
831831
: undefined
832832
}

admin/src/pages/Settings.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,7 +1160,7 @@ function MetricsSection() {
11601160
<div className="settings-section-head" style={{ justifyContent: "space-between" }}>
11611161
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
11621162
<h3>Health &amp; metrics</h3>
1163-
<span className="meta">Prometheus exposition at <code style={codeStyle}>/api/metrics</code></span>
1163+
<span className="meta">Prometheus exposition at <code style={codeStyle}>/api/v1/metrics</code></span>
11641164
</div>
11651165
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
11661166
<Toggle on={enabled} onChange={setEnabled} />
@@ -1174,7 +1174,7 @@ function MetricsSection() {
11741174
<label className="label">Bearer token (optional)</label>
11751175
<div className="help">
11761176
When set, scrapers must send <code style={codeStyle}>Authorization: Bearer &lt;token&gt;</code>.
1177-
Leave blank to expose <code style={codeStyle}>/api/metrics</code> publicly — only do this
1177+
Leave blank to expose <code style={codeStyle}>/api/v1/metrics</code> publicly — only do this
11781178
if the endpoint is protected at the proxy layer.
11791179
</div>
11801180
</div>
@@ -1760,7 +1760,7 @@ function EmailTemplatesSection() {
17601760
<div className="label-block span2">
17611761
<label className="label">Verification email</label>
17621762
<div className="help">
1763-
Sent on registration and via <code style={codeStyle}>POST /api/auth/:collection/request-verify</code>.
1763+
Sent on registration and via <code style={codeStyle}>POST /api/v1/auth/:collection/request-verify</code>.
17641764
Variables: <code style={codeStyle}>{`{{email}}`}</code> <code style={codeStyle}>{`{{token}}`}</code> <code style={codeStyle}>{`{{link}}`}</code> <code style={codeStyle}>{`{{appUrl}}`}</code> <code style={codeStyle}>{`{{collection}}`}</code>
17651765
</div>
17661766
</div>
@@ -1792,7 +1792,7 @@ function EmailTemplatesSection() {
17921792
<div className="label-block span2">
17931793
<label className="label">Password reset email</label>
17941794
<div className="help">
1795-
Sent via <code style={codeStyle}>POST /api/auth/:collection/request-password-reset</code>. Same variables as above.
1795+
Sent via <code style={codeStyle}>POST /api/v1/auth/:collection/request-password-reset</code>. Same variables as above.
17961796
</div>
17971797
</div>
17981798
<div className="label-block">
@@ -1840,25 +1840,25 @@ const AUTH_FEATURES: AuthFeatureRow[] = [
18401840
key: "otp",
18411841
label: "OTP / magic link",
18421842
defaultOn: false,
1843-
description: <>Passwordless sign-in via email — both a 6-digit code and a magic link. Requires SMTP. Endpoints: <code style={codeStyle}>POST /api/auth/&lt;col&gt;/otp/&#123;request,auth&#125;</code>.</>,
1843+
description: <>Passwordless sign-in via email — both a 6-digit code and a magic link. Requires SMTP. Endpoints: <code style={codeStyle}>POST /api/v1/auth/&lt;col&gt;/otp/&#123;request,auth&#125;</code>.</>,
18441844
},
18451845
{
18461846
key: "mfa",
18471847
label: "MFA / TOTP (2FA)",
18481848
defaultOn: true,
1849-
description: <>RFC 6238 TOTP with authenticator apps. Disabling blocks new enrollment but lets existing users still sign in and disable their own MFA. Endpoints: <code style={codeStyle}>POST /api/auth/&lt;col&gt;/totp/&#123;setup,confirm,disable&#125;</code>.</>,
1849+
description: <>RFC 6238 TOTP with authenticator apps. Disabling blocks new enrollment but lets existing users still sign in and disable their own MFA. Endpoints: <code style={codeStyle}>POST /api/v1/auth/&lt;col&gt;/totp/&#123;setup,confirm,disable&#125;</code>.</>,
18501850
},
18511851
{
18521852
key: "anonymous",
18531853
label: "Anonymous sign-in",
18541854
defaultOn: false,
1855-
description: <>Mints a guest user with no email/password — useful for guest carts or onboarding before signup. Sessions live 30 days. Endpoint: <code style={codeStyle}>POST /api/auth/&lt;col&gt;/anonymous</code>.</>,
1855+
description: <>Mints a guest user with no email/password — useful for guest carts or onboarding before signup. Sessions live 30 days. Endpoint: <code style={codeStyle}>POST /api/v1/auth/&lt;col&gt;/anonymous</code>.</>,
18561856
},
18571857
{
18581858
key: "impersonation",
18591859
label: "Admin impersonation",
18601860
defaultOn: true,
1861-
description: <>Admin mints a 1-hour user JWT for support purposes. JWT carries <code style={codeStyle}>impersonated_by</code> for audit. Endpoint: <code style={codeStyle}>POST /api/admin/impersonate/&lt;col&gt;/&lt;userId&gt;</code>.</>,
1861+
description: <>Admin mints a 1-hour user JWT for support purposes. JWT carries <code style={codeStyle}>impersonated_by</code> for audit. Endpoint: <code style={codeStyle}>POST /api/v1/admin/impersonate/&lt;col&gt;/&lt;userId&gt;</code>.</>,
18621862
},
18631863
];
18641864

@@ -1956,12 +1956,12 @@ interface SessionKindRow {
19561956
}
19571957

19581958
const SESSION_KINDS: SessionKindRow[] = [
1959-
{ kind: "anonymous", label: "Anonymous", description: "Guest sessions minted by POST /api/auth/:collection/anonymous.", defaultSeconds: 30 * 24 * 3600 },
1959+
{ kind: "anonymous", label: "Anonymous", description: "Guest sessions minted by POST /api/v1/auth/:collection/anonymous.", defaultSeconds: 30 * 24 * 3600 },
19601960
{ kind: "user", label: "User", description: "Standard user JWTs (login, register, OAuth2, magic link).", defaultSeconds: 7 * 24 * 3600 },
1961-
{ kind: "admin", label: "Admin", description: "Admin JWTs minted by POST /api/admin/auth/login.", defaultSeconds: 7 * 24 * 3600 },
1961+
{ kind: "admin", label: "Admin", description: "Admin JWTs minted by POST /api/v1/admin/auth/login.", defaultSeconds: 7 * 24 * 3600 },
19621962
{ kind: "impersonate", label: "Impersonate", description: "JWTs issued by admin impersonation. Keep short — these escalate access.", defaultSeconds: 3600 },
19631963
{ kind: "refresh", label: "Refresh", description: "Window applied when /refresh re-mints a token. Acts as the sliding ratchet.", defaultSeconds: 7 * 24 * 3600 },
1964-
{ kind: "file", label: "File access", description: "Protected-file URLs minted via POST /api/files/.../token.", defaultSeconds: 3600 },
1964+
{ kind: "file", label: "File access", description: "Protected-file URLs minted via POST /api/v1/files/.../token.", defaultSeconds: 3600 },
19651965
];
19661966

19671967
function fmtDuration(seconds: number): string {
@@ -2244,9 +2244,9 @@ function OAuth2Section() {
22442244
<div className="settings-section-body" style={{ gridTemplateColumns: "1fr", padding: "10px 14px" }}>
22452245
<div className="muted" style={{ fontSize: 11, marginBottom: 10 }}>
22462246
Enable a provider to expose it via{" "}
2247-
<code style={codeStyle}>GET /api/auth/&lt;collection&gt;/oauth2/providers</code>
2247+
<code style={codeStyle}>GET /api/v1/auth/&lt;collection&gt;/oauth2/providers</code>
22482248
{" · "}your app drives the popup + state, then POSTs the code to{" "}
2249-
<code style={codeStyle}>/api/auth/&lt;collection&gt;/oauth2/exchange</code>.
2249+
<code style={codeStyle}>/api/v1/auth/&lt;collection&gt;/oauth2/exchange</code>.
22502250
</div>
22512251

22522252
{OAUTH_PROVIDERS.map((p) => {

admin/src/stores/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface AuthState {
88
loaded: boolean;
99
/** Pull auth state from the server (cookie-based). */
1010
load: () => Promise<void>;
11-
/** Called after a successful POST /api/admin/auth/login. */
11+
/** Called after a successful POST /api/v1/admin/auth/login. */
1212
signIn: (_token?: string) => Promise<void>;
1313
signOut: () => Promise<void>;
1414
refresh: () => Promise<void>;

src/api/audit-log.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async function getAdmin(request: Request, jwtSecret: string): Promise<{ id: stri
1414
export function makeAuditLogPlugin(jwtSecret: string) {
1515
return new Elysia({ name: "audit-log" })
1616
// ── Globally-scoped onAfterHandle: capture every state-changing
17-
// /api/admin/* request. Only writes on AUDITED_METHODS — read GETs
17+
// /api/v1/admin/* request. Only writes on AUDITED_METHODS — read GETs
1818
// are skipped inside recordAuditEntry.
1919
.onAfterHandle({ as: "global" }, async ({ request, set }) => {
2020
// No need to skip non-admin paths here — recordAuditEntry filters.

0 commit comments

Comments
 (0)