diff --git a/.ast-grep/rules/no-void-output.yml b/.ast-grep/rules/no-void-output.yml new file mode 100644 index 0000000..7a44f86 --- /dev/null +++ b/.ast-grep/rules/no-void-output.yml @@ -0,0 +1,15 @@ +id: no-void-output +language: typescript +severity: error +message: Do not use z.void() for output - use successResponseSchema instead +note: | + Endpoints should return `{ success: true }` instead of void. + This makes the API more explicit and avoids issues with TypeScript + expecting void-returning Promises. + + Replace `.output(z.void())` with `.output(successResponseSchema)` and ensure + the handler returns `{ success: true }`. +rule: + pattern: $EXPR.output(z.void()) +files: + - packages/api-contract/**/*.ts diff --git a/apps/api-server/src/__tests__/e2e/me.test.ts b/apps/api-server/src/__tests__/e2e/me.test.ts index 4caa9d3..9257cea 100644 --- a/apps/api-server/src/__tests__/e2e/me.test.ts +++ b/apps/api-server/src/__tests__/e2e/me.test.ts @@ -23,9 +23,9 @@ import { } from "bun:test"; import { call } from "@orpc/server"; import { router } from "../../router.js"; -import { hashPassword } from "../../utils/password.js"; -import { hashToken } from "../../utils/crypto.js"; import { COOKIE_NAMES } from "../../utils/cookies.js"; +import { hashToken } from "../../utils/crypto.js"; +import { hashPassword } from "../../utils/password.js"; import { TEST_RP } from "../helpers/test-constants.js"; import { createTestDb, @@ -85,7 +85,7 @@ function createAPIContext(options?: { * Create a real session in the database and return the token */ async function createSession(userId: number): Promise { - const token = "test-session-" + String(Date.now()) + String(Math.random()); + const token = `test-session-${String(Date.now())}${String(Math.random())}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); @@ -110,8 +110,7 @@ async function createSession(userId: number): Promise { async function createApiToken( userId: number, ): Promise<{ token: string; name: string }> { - const token = - "test-api-token-" + String(Date.now()) + String(Math.random()); + const token = `test-api-token-${String(Date.now())}${String(Math.random())}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + API_TOKEN_EXPIRY_MS); @@ -523,6 +522,7 @@ describe("me.setPassword", () => { const sessionToken = await createSession(user.id); const context = createAPIContext({ sessionToken }); + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call( router.me.setPassword, @@ -544,6 +544,7 @@ describe("me.setPassword", () => { const sessionToken = await createSession(user.id); const context = createAPIContext({ sessionToken }); + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call( router.me.setPassword, @@ -567,6 +568,7 @@ describe("me.setPassword", () => { // Password must be at least 8 chars to pass schema validation // "password" passes length check but fails zxcvbn strength check // zxcvbn provides feedback like "This is a top-10 common password" + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call( router.me.setPassword, @@ -611,6 +613,7 @@ describe("me.delete", () => { const sessionToken = await createSession(user.id); const context = createAPIContext({ sessionToken }); + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call(router.me.delete, { password: "anything" }, { context }), ).rejects.toThrow("Cannot delete account without a password"); @@ -626,6 +629,7 @@ describe("me.delete", () => { const sessionToken = await createSession(user.id); const context = createAPIContext({ sessionToken }); + // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression await expect( call(router.me.delete, { password: "WrongPassword123!" }, { context }), ).rejects.toThrow("Incorrect password"); diff --git a/apps/api-server/src/__tests__/helpers/test-db.ts b/apps/api-server/src/__tests__/helpers/test-db.ts index 77bb019..82b406a 100644 --- a/apps/api-server/src/__tests__/helpers/test-db.ts +++ b/apps/api-server/src/__tests__/helpers/test-db.ts @@ -3,9 +3,9 @@ */ import type { Database } from "@reviq/db-schema"; +import type { Kysely } from "kysely"; import { join } from "node:path"; import { createDb } from "@reviq/db"; -import type { Kysely } from "kysely"; import { sql } from "kysely"; import pg from "pg"; @@ -134,14 +134,13 @@ async function ensureTestDatabaseExists(): Promise { * * @throws Error if repo root cannot be found */ -function findRepoRoot(): string { - const { existsSync } = require("node:fs"); +async function findRepoRoot(): Promise { let current = import.meta.dir; // Walk up to 10 levels to find the repo root for (let i = 0; i < 10; i++) { const migrationsPath = join(current, "db", "migrations"); - if (existsSync(migrationsPath)) { + if (await Bun.file(migrationsPath).exists()) { return current; } const parent = join(current, ".."); @@ -167,7 +166,7 @@ export async function runMigrations(): Promise { // Ensure the database exists first await ensureTestDatabaseExists(); - const repoRoot = findRepoRoot(); + const repoRoot = await findRepoRoot(); const proc = Bun.spawn(["dbmate", "up"], { env: { ...process.env, DATABASE_URL: testDbUrl }, diff --git a/apps/api-server/src/procedures/admin/auth/complete-login.ts b/apps/api-server/src/procedures/admin/auth/complete-login.ts index 74e49d7..bb25325 100644 --- a/apps/api-server/src/procedures/admin/auth/complete-login.ts +++ b/apps/api-server/src/procedures/admin/auth/complete-login.ts @@ -46,4 +46,6 @@ export const adminAuthCompleteLogin = os.admin.auth.completeLogin .set({ completed_at: new Date() }) .where("id", "=", anyRequest.id) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/delete.ts b/apps/api-server/src/procedures/admin/orgs/delete.ts index 8b1e609..1cfd440 100644 --- a/apps/api-server/src/procedures/admin/orgs/delete.ts +++ b/apps/api-server/src/procedures/admin/orgs/delete.ts @@ -33,4 +33,6 @@ export const adminOrgsDelete = os.admin.orgs.delete .execute(); await trx.deleteFrom("orgs").where("id", "=", org.id).execute(); }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/sites.ts b/apps/api-server/src/procedures/admin/orgs/sites.ts index 250ebe6..aa0fc7b 100644 --- a/apps/api-server/src/procedures/admin/orgs/sites.ts +++ b/apps/api-server/src/procedures/admin/orgs/sites.ts @@ -68,6 +68,8 @@ export const adminOrgsAddSite = os.admin.orgs.addSite }) .execute(); }); + + return { success: true }; }); export const adminOrgsRemoveSite = os.admin.orgs.removeSite @@ -94,4 +96,6 @@ export const adminOrgsRemoveSite = os.admin.orgs.removeSite if (!result.numDeletedRows || result.numDeletedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Site not found" }); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/update.ts b/apps/api-server/src/procedures/admin/orgs/update.ts index d2b2bb1..27f4aa3 100644 --- a/apps/api-server/src/procedures/admin/orgs/update.ts +++ b/apps/api-server/src/procedures/admin/orgs/update.ts @@ -22,7 +22,7 @@ export const adminOrgsUpdate = os.admin.orgs.update if (!org) { throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); } - return; + return { success: true }; } const updates: Partial<{ @@ -47,4 +47,6 @@ export const adminOrgsUpdate = os.admin.orgs.update if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/users/confirm-email.ts b/apps/api-server/src/procedures/admin/users/confirm-email.ts index a2fa6f1..782306f 100644 --- a/apps/api-server/src/procedures/admin/users/confirm-email.ts +++ b/apps/api-server/src/procedures/admin/users/confirm-email.ts @@ -21,4 +21,6 @@ export const adminUsersConfirmEmail = os.admin.users.confirmEmail if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "User not found" }); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/users/create.ts b/apps/api-server/src/procedures/admin/users/create.ts index 1e431a4..a23029e 100644 --- a/apps/api-server/src/procedures/admin/users/create.ts +++ b/apps/api-server/src/procedures/admin/users/create.ts @@ -60,4 +60,6 @@ export const adminUsersCreate = os.admin.users.create .execute(); } }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/users/update.ts b/apps/api-server/src/procedures/admin/users/update.ts index ddfc353..6e8f3e6 100644 --- a/apps/api-server/src/procedures/admin/users/update.ts +++ b/apps/api-server/src/procedures/admin/users/update.ts @@ -23,7 +23,7 @@ export const adminUsersUpdate = os.admin.users.update if (!user) { throw new ORPCError("NOT_FOUND", { message: "User not found" }); } - return; + return { success: true }; } // Prevent superuser from demoting themselves @@ -45,4 +45,6 @@ export const adminUsersUpdate = os.admin.users.update if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "User not found" }); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/forgot-password.ts b/apps/api-server/src/procedures/auth/forgot-password.ts index 55a676b..40a57c6 100644 --- a/apps/api-server/src/procedures/auth/forgot-password.ts +++ b/apps/api-server/src/procedures/auth/forgot-password.ts @@ -57,5 +57,6 @@ export const forgotPassword = os.auth.forgotPassword.handler( // Always return success (anti-enumeration) // Don't reveal whether the email exists or not + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/login-password-confirm.ts b/apps/api-server/src/procedures/auth/login-password-confirm.ts index 64f6262..655f4ae 100644 --- a/apps/api-server/src/procedures/auth/login-password-confirm.ts +++ b/apps/api-server/src/procedures/auth/login-password-confirm.ts @@ -41,7 +41,7 @@ export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler( // If already completed, return success (idempotent) if (loginRequest.completed_at !== null) { - return; + return { success: true }; } // Mark as completed @@ -50,5 +50,7 @@ export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler( .set({ completed_at: new Date() }) .where("id", "=", loginRequest.id) .execute(); + + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/login-password.ts b/apps/api-server/src/procedures/auth/login-password.ts index 456fd97..6db8663 100644 --- a/apps/api-server/src/procedures/auth/login-password.ts +++ b/apps/api-server/src/procedures/auth/login-password.ts @@ -111,6 +111,6 @@ export const loginPassword = os.auth.loginPassword.handler( await sendLoginConfirmationEmail(result.email, result.token); } - // Return void (success) + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/logout.ts b/apps/api-server/src/procedures/auth/logout.ts index 1fa5762..c964d7c 100644 --- a/apps/api-server/src/procedures/auth/logout.ts +++ b/apps/api-server/src/procedures/auth/logout.ts @@ -23,4 +23,6 @@ export const logout = os.auth.logout // Clear the session cookie deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/resend-verification.ts b/apps/api-server/src/procedures/auth/resend-verification.ts index 5f2eff7..66bade8 100644 --- a/apps/api-server/src/procedures/auth/resend-verification.ts +++ b/apps/api-server/src/procedures/auth/resend-verification.ts @@ -24,7 +24,7 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail // Check if email is already verified if (context.user.emailVerifiedAt !== null) { // Email already verified, return early - return; + return { success: true }; } // Delete any existing verification tokens for this user @@ -49,4 +49,6 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail // Send verification email (stubbed) await sendVerificationEmail(context.user.email, token); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/reset-password.ts b/apps/api-server/src/procedures/auth/reset-password.ts index af51079..f98baf9 100644 --- a/apps/api-server/src/procedures/auth/reset-password.ts +++ b/apps/api-server/src/procedures/auth/reset-password.ts @@ -84,6 +84,6 @@ export const resetPassword = os.auth.resetPassword.handler( .where("revoked_at", "is", null) .execute(); - // Return void on success + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/signup.ts b/apps/api-server/src/procedures/auth/signup.ts index 33338d5..3015c8f 100644 --- a/apps/api-server/src/procedures/auth/signup.ts +++ b/apps/api-server/src/procedures/auth/signup.ts @@ -280,4 +280,6 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => { // Send verification email (stubbed) await sendVerificationEmail(email, verificationToken); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/verify-email.ts b/apps/api-server/src/procedures/auth/verify-email.ts index bdc4139..a39c206 100644 --- a/apps/api-server/src/procedures/auth/verify-email.ts +++ b/apps/api-server/src/procedures/auth/verify-email.ts @@ -54,5 +54,7 @@ export const verifyEmail = os.auth.verifyEmail.handler( .deleteFrom("email_verifications") .where("id", "=", verification.id) .execute(); + + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/me/delete.ts b/apps/api-server/src/procedures/me/delete.ts index a58f0c4..e5cf875 100644 --- a/apps/api-server/src/procedures/me/delete.ts +++ b/apps/api-server/src/procedures/me/delete.ts @@ -47,4 +47,6 @@ export const meDelete = os.me.delete // Clear session cookie deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/devices.ts b/apps/api-server/src/procedures/me/devices.ts index 2941480..5a39c1f 100644 --- a/apps/api-server/src/procedures/me/devices.ts +++ b/apps/api-server/src/procedures/me/devices.ts @@ -64,6 +64,8 @@ export const trustDevice = os.me.trustDevice if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Device not found" }); } + + return { success: true }; }); /** @@ -113,6 +115,8 @@ export const untrustDevice = os.me.untrustDevice if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Device not found" }); } + + return { success: true }; }); /** @@ -128,4 +132,6 @@ export const revokeAllTrustedDevices = os.me.revokeAllTrustedDevices .set({ is_trusted: false }) .where("user_id", "=", context.user.id) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/passkeys.ts b/apps/api-server/src/procedures/me/passkeys.ts index e6ea66d..b093580 100644 --- a/apps/api-server/src/procedures/me/passkeys.ts +++ b/apps/api-server/src/procedures/me/passkeys.ts @@ -45,6 +45,8 @@ export const renamePasskey = os.me.passkeys.rename if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Passkey not found" }); } + + return { success: true }; }); /** @@ -92,4 +94,6 @@ export const deletePasskey = os.me.passkeys.delete throw new ORPCError("NOT_FOUND", { message: "Passkey not found" }); } }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/sessions.ts b/apps/api-server/src/procedures/me/sessions.ts index f8d1fdd..d6cd434 100644 --- a/apps/api-server/src/procedures/me/sessions.ts +++ b/apps/api-server/src/procedures/me/sessions.ts @@ -65,6 +65,8 @@ export const revokeSession = os.me.revokeSession if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Session not found" }); } + + return { success: true }; }); /** @@ -83,4 +85,6 @@ export const revokeAllSessions = os.me.revokeAllSessions .where("id", "!=", context.session.id) .where("revoked_at", "is", null) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/set-password.ts b/apps/api-server/src/procedures/me/set-password.ts index ba4663d..beacc87 100644 --- a/apps/api-server/src/procedures/me/set-password.ts +++ b/apps/api-server/src/procedures/me/set-password.ts @@ -58,4 +58,6 @@ export const setPassword = os.me.setPassword .set({ password_hash: newHash, updated_at: new Date() }) .where("id", "=", context.user.id) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/update-profile.ts b/apps/api-server/src/procedures/me/update-profile.ts index 3d59510..b23c090 100644 --- a/apps/api-server/src/procedures/me/update-profile.ts +++ b/apps/api-server/src/procedures/me/update-profile.ts @@ -36,4 +36,6 @@ export const updateProfile = os.me.updateProfile .where("id", "=", context.user.id) .execute(); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/orgs/invites.ts b/apps/api-server/src/procedures/orgs/invites.ts index 4089ad2..842c90f 100644 --- a/apps/api-server/src/procedures/orgs/invites.ts +++ b/apps/api-server/src/procedures/orgs/invites.ts @@ -123,6 +123,8 @@ export const invitesCreate = os.orgs.invites.create // Send invitation email const inviterName = context.user.displayName ?? context.user.email; await sendOrgInviteEmail(email, token, org.displayName, inviterName, role); + + return { success: true }; }); /** @@ -149,6 +151,8 @@ export const invitesCancel = os.orgs.invites.cancel if (!result.numDeletedRows || result.numDeletedRows === 0n) { throw new ORPCError("NOT_FOUND", { message: "Invitation not found" }); } + + return { success: true }; }); /** @@ -219,4 +223,6 @@ export const invitesAccept = os.orgs.invites.accept } throw error; } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/orgs/management.ts b/apps/api-server/src/procedures/orgs/management.ts index 57ff4cf..00a72ae 100644 --- a/apps/api-server/src/procedures/orgs/management.ts +++ b/apps/api-server/src/procedures/orgs/management.ts @@ -39,6 +39,8 @@ export const orgsUpdate = os.orgs.update .set(updates) .where("id", "=", org.id) .execute(); + + return { success: true }; }); /** @@ -57,6 +59,8 @@ export const orgsDelete = os.orgs.delete requireRole(membership, "owner"); await context.db.deleteFrom("orgs").where("id", "=", org.id).execute(); + + return { success: true }; }); /** @@ -92,4 +96,6 @@ export const orgsLeave = os.orgs.leave .where("user_id", "=", context.user.id) .execute(); }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/orgs/members.ts b/apps/api-server/src/procedures/orgs/members.ts index 1a39aba..8bcc681 100644 --- a/apps/api-server/src/procedures/orgs/members.ts +++ b/apps/api-server/src/procedures/orgs/members.ts @@ -95,6 +95,8 @@ export const membersUpdateRole = os.orgs.members.updateRole .where("id", "=", targetMember.id) .execute(); }); + + return { success: true }; }); /** @@ -155,4 +157,6 @@ export const membersRemove = os.orgs.members.remove .where("id", "=", targetMember.id) .execute(); }); + + return { success: true }; }); diff --git a/apps/api-server/src/router.ts b/apps/api-server/src/router.ts index 08bfef9..e4de764 100644 --- a/apps/api-server/src/router.ts +++ b/apps/api-server/src/router.ts @@ -160,6 +160,8 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication .set({ completed_at: new Date() }) .where("id", "=", String(context.loginRequestId)) .execute(); + + return { success: true }; }); // Me procedures @@ -245,6 +247,8 @@ const setupProfile = os.me.setupProfile }) .where("id", "=", context.user.id) .execute(); + + return { success: true }; }); // Me procedures imported from ./procedures/me/* diff --git a/package.json b/package.json index c2f32e2..b014ed6 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dev": "turbo dev", "build": "turbo build", "build:watch:packages": "turbo watch build --filter=./packages/*", + "build:packages": "turbo build --filter=./packages/*", "lint": "biome check && turbo run lint", "lint:fix": "biome check --write --unsafe && turbo run lint -- --fix", "typecheck": "turbo typecheck", diff --git a/packages/api-contract/src/contract.ts b/packages/api-contract/src/contract.ts index b2e477f..59ed053 100644 --- a/packages/api-contract/src/contract.ts +++ b/packages/api-contract/src/contract.ts @@ -22,7 +22,11 @@ import { signupInputSchema, verifyEmailInputSchema, } from "./schemas/auth.js"; -import { emailSchema, slugSchema } from "./schemas/common.js"; +import { + emailSchema, + slugSchema, + successResponseSchema, +} from "./schemas/common.js"; import { createInviteInputSchema, createOrgInputSchema, @@ -51,26 +55,32 @@ import { export const contract = oc.router({ auth: oc.router({ // Signup and verification - signup: oc.input(signupInputSchema).output(z.void()), - verifyEmail: oc.input(verifyEmailInputSchema).output(z.void()), - resendVerificationEmail: oc.output(z.void()), + signup: oc.input(signupInputSchema).output(successResponseSchema), + verifyEmail: oc.input(verifyEmailInputSchema).output(successResponseSchema), + resendVerificationEmail: oc.output(successResponseSchema), // Login flow createLoginRequest: oc .input(loginRequestInputSchema) .output(loginRequestOutputSchema), - loginPassword: oc.input(loginPasswordInputSchema).output(z.void()), + loginPassword: oc + .input(loginPasswordInputSchema) + .output(successResponseSchema), loginPasswordConfirm: oc .input(z.object({ token: z.string() })) - .output(z.void()), + .output(successResponseSchema), loginIfRequestIsCompleted: oc.output(loginStatusOutputSchema), // Password reset - forgotPassword: oc.input(forgotPasswordInputSchema).output(z.void()), - resetPassword: oc.input(resetPasswordInputSchema).output(z.void()), + forgotPassword: oc + .input(forgotPasswordInputSchema) + .output(successResponseSchema), + resetPassword: oc + .input(resetPasswordInputSchema) + .output(successResponseSchema), // Logout - logout: oc.output(z.void()), + logout: oc.output(successResponseSchema), // WebAuthn procedures webauthn: oc.router({ @@ -103,45 +113,53 @@ export const contract = oc.router({ response: z.custom(), }), ) - .output(z.void()), + .output(successResponseSchema), }), }), me: oc.router({ // Profile get: oc.output(userProfileSchema), - setupProfile: oc.input(setupProfileInputSchema).output(z.void()), - updateProfile: oc.input(updateProfileInputSchema).output(z.void()), - delete: oc.input(z.object({ password: z.string() })).output(z.void()), + setupProfile: oc + .input(setupProfileInputSchema) + .output(successResponseSchema), + updateProfile: oc + .input(updateProfileInputSchema) + .output(successResponseSchema), + delete: oc + .input(z.object({ password: z.string() })) + .output(successResponseSchema), // Auth status (for CLI and debugging) authStatus: oc.output(authStatusOutputSchema), // Authentication settings - setPassword: oc.input(setPasswordInputSchema).output(z.void()), + setPassword: oc.input(setPasswordInputSchema).output(successResponseSchema), // Passkeys passkeys: oc.router({ list: oc.output(z.array(passkeyOutputSchema)), rename: oc .input(z.object({ passkeyId: z.number(), name: z.string() })) - .output(z.void()), - delete: oc.input(z.object({ passkeyId: z.number() })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ passkeyId: z.number() })) + .output(successResponseSchema), }), // Sessions & devices listSessions: oc.output(z.array(sessionOutputSchema)), revokeSession: oc .input(z.object({ sessionId: z.number() })) - .output(z.void()), - revokeAllSessions: oc.output(z.void()), + .output(successResponseSchema), + revokeAllSessions: oc.output(successResponseSchema), getDeviceInfo: oc.output(deviceOutputSchema), - trustDevice: oc.input(trustDeviceInputSchema).output(z.void()), + trustDevice: oc.input(trustDeviceInputSchema).output(successResponseSchema), listTrustedDevices: oc.output(z.array(deviceOutputSchema)), untrustDevice: oc .input(z.object({ deviceId: z.number() })) - .output(z.void()), - revokeAllTrustedDevices: oc.output(z.void()), + .output(successResponseSchema), + revokeAllTrustedDevices: oc.output(successResponseSchema), }), orgs: oc.router({ @@ -159,19 +177,25 @@ export const contract = oc.router({ logoUrl: z.string().optional(), }), ) - .output(z.void()), - delete: oc.input(z.object({ slug: slugSchema })).output(z.void()), - leave: oc.input(z.object({ slug: slugSchema })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), + leave: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), // Members members: oc.router({ list: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgMemberOutputSchema)), - updateRole: oc.input(updateMemberRoleInputSchema).output(z.void()), + updateRole: oc + .input(updateMemberRoleInputSchema) + .output(successResponseSchema), remove: oc .input(z.object({ slug: slugSchema, userId: z.number() })) - .output(z.void()), + .output(successResponseSchema), }), // Invites @@ -179,11 +203,13 @@ export const contract = oc.router({ list: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgInviteOutputSchema)), - create: oc.input(createInviteInputSchema).output(z.void()), + create: oc.input(createInviteInputSchema).output(successResponseSchema), cancel: oc .input(z.object({ slug: slugSchema, inviteId: z.number() })) - .output(z.void()), - accept: oc.input(z.object({ token: z.string() })).output(z.void()), + .output(successResponseSchema), + accept: oc + .input(z.object({ token: z.string() })) + .output(successResponseSchema), }), // Sites @@ -210,31 +236,39 @@ export const contract = oc.router({ logoUrl: z.string().optional(), }), ) - .output(z.void()), - delete: oc.input(z.object({ slug: slugSchema })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), listSites: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgSiteOutputSchema)), - addSite: oc.input(adminAddSiteInputSchema).output(z.void()), + addSite: oc.input(adminAddSiteInputSchema).output(successResponseSchema), removeSite: oc .input(z.object({ slug: slugSchema, domain: z.string() })) - .output(z.void()), + .output(successResponseSchema), }), // Admin user management users: oc.router({ list: oc.output(z.array(userProfileSchema)), get: oc.input(z.object({ email: emailSchema })).output(userProfileSchema), - create: oc.input(adminCreateUserInputSchema).output(z.void()), - update: oc.input(adminUpdateUserInputSchema).output(z.void()), - confirmEmail: oc.input(z.object({ email: emailSchema })).output(z.void()), + create: oc + .input(adminCreateUserInputSchema) + .output(successResponseSchema), + update: oc + .input(adminUpdateUserInputSchema) + .output(successResponseSchema), + confirmEmail: oc + .input(z.object({ email: emailSchema })) + .output(successResponseSchema), }), // Admin auth management auth: oc.router({ completeLogin: oc .input(z.object({ email: emailSchema })) - .output(z.void()), + .output(successResponseSchema), }), }), }); diff --git a/packages/api-contract/src/schemas/common.ts b/packages/api-contract/src/schemas/common.ts index a4cd986..782c97c 100644 --- a/packages/api-contract/src/schemas/common.ts +++ b/packages/api-contract/src/schemas/common.ts @@ -58,3 +58,9 @@ export const phoneSchema = z .refine((val) => !val || isValidPhoneNumber(val), { message: "Invalid phone number", }); + +/** + * Success response schema for operations that don't return data + * Use instead of void to make responses more explicit + */ +export const successResponseSchema = z.object({ success: z.literal(true) });