From 3c1167fc59dd0355d8fafc002fb846327c007ce2 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Tue, 18 Aug 2026 14:42:35 -0500 Subject: [PATCH 1/4] fix(everything-dev): replace hard-coded auth type list with export type * The auth-types.gen generator hard-coded a 14-type re-export list from the auth plugin's auth-export, causing TS2724 errors when the auth plugin renamed or added types (e.g. GetOrganizationInput -> GetFullOrganizationInput, and AuthTeam was missing entirely). Replace the hard-coded list with 'export type * from auth-export' in all five locations (the main generator, the init scaffold, and the test stub). New types added to auth-export now flow through automatically. Updates the init and test stubs with the corrected GetFullOrganizationInput name and adds AuthTeam. Adds a regression test that verifies the generator produces export type * and would not reference a stale hard-coded name like GetOrganizationInput. See NEARBuilders/nearbuilders.org#206 --- .changeset/auth-types-gen-export-wildcard.md | 5 + packages/everything-dev/src/api-contract.ts | 34 +----- packages/everything-dev/src/cli/init.ts | 20 +--- .../tests/integration/typecheck-utils.ts | 3 +- .../tests/unit/api-contract-gen.test.ts | 105 ++++++++++++++++++ 5 files changed, 117 insertions(+), 50 deletions(-) create mode 100644 .changeset/auth-types-gen-export-wildcard.md diff --git a/.changeset/auth-types-gen-export-wildcard.md b/.changeset/auth-types-gen-export-wildcard.md new file mode 100644 index 00000000..41745e4d --- /dev/null +++ b/.changeset/auth-types-gen-export-wildcard.md @@ -0,0 +1,5 @@ +--- +"everything-dev": patch +--- + +Fix stale auth type name in type generation by replacing the hard-coded re-export list in `auth-types.gen.ts` with `export type *`. New types added to the auth plugin's `auth-export.ts` now flow through automatically without generator changes, preventing the class of `TS2724` errors caused by stale type names. diff --git a/packages/everything-dev/src/api-contract.ts b/packages/everything-dev/src/api-contract.ts index 657d1eb4..e0b74045 100644 --- a/packages/everything-dev/src/api-contract.ts +++ b/packages/everything-dev/src/api-contract.ts @@ -190,22 +190,7 @@ async function fetchAuthExportTypes(opts: { function writeAuthTypesGen(targetPath: string, authExportPath: string) { const exportImportPath = toImportPath(targetPath, authExportPath); const content = [ - `export type {`, - ` Auth,`, - ` AuthOrganizationContext,`, - ` AuthOrganization,`, - ` AuthOrganizationSummary,`, - ` AuthOrganizationMember,`, - ` AuthApiKey,`, - ` AuthInvitation,`, - ` GetActiveMemberInput,`, - ` GetOrganizationInput,`, - ` ListMembersInput,`, - ` ListInvitationsInput,`, - ` ListApiKeysInput,`, - ` AuthServices,`, - ` createAuthInstance,`, - `} from "${exportImportPath}";`, + `export type * from "${exportImportPath}";`, `import type { InferOutput, ContractType as AuthContract } from "${toImportPath(targetPath, join(dirname(authExportPath), "contract.d.ts"))}";`, `import type { Auth as BaseAuth } from "${exportImportPath}";`, "", @@ -244,22 +229,7 @@ function writeContractBasedAuthTypesGen(targetPath: string, configDir: string) { ); const content = [ - `export type {`, - ` Auth,`, - ` AuthOrganizationContext,`, - ` AuthOrganization,`, - ` AuthOrganizationSummary,`, - ` AuthOrganizationMember,`, - ` AuthApiKey,`, - ` AuthInvitation,`, - ` GetActiveMemberInput,`, - ` GetOrganizationInput,`, - ` ListMembersInput,`, - ` ListInvitationsInput,`, - ` ListApiKeysInput,`, - ` AuthServices,`, - ` createAuthInstance,`, - `} from "${authExportRel}";`, + `export type * from "${authExportRel}";`, `import type { InferOutput, ContractType as AuthContract } from "${contractRel}";`, `import type { Auth as BaseAuth } from "${authExportRel}";`, "", diff --git a/packages/everything-dev/src/cli/init.ts b/packages/everything-dev/src/cli/init.ts index 1fd382c8..ac301c4a 100644 --- a/packages/everything-dev/src/cli/init.ts +++ b/packages/everything-dev/src/cli/init.ts @@ -935,8 +935,9 @@ export type AuthOrganizationSummary = any; export type AuthOrganizationMember = any; export type AuthApiKey = any; export type AuthInvitation = any; +export type AuthTeam = any; export type GetActiveMemberInput = any; -export type GetOrganizationInput = any; +export type GetFullOrganizationInput = any; export type ListMembersInput = any; export type ListInvitationsInput = any; export type ListApiKeysInput = any; @@ -983,22 +984,7 @@ function generateAuthTypesContent(targetPath: string, configDir: string): string targetPath, ); - return `export type { - Auth, - AuthOrganizationContext, - AuthOrganization, - AuthOrganizationSummary, - AuthOrganizationMember, - AuthApiKey, - AuthInvitation, - GetActiveMemberInput, - GetOrganizationInput, - ListMembersInput, - ListInvitationsInput, - ListApiKeysInput, - AuthServices, - createAuthInstance, -} from "${authExportRel}"; + return `export type * from "${authExportRel}"; import type { InferOutput, ContractType as AuthContract } from "${contractRel}"; import type { Auth as BaseAuth } from "${authExportRel}"; diff --git a/packages/everything-dev/tests/integration/typecheck-utils.ts b/packages/everything-dev/tests/integration/typecheck-utils.ts index f5a0d280..b0480c30 100644 --- a/packages/everything-dev/tests/integration/typecheck-utils.ts +++ b/packages/everything-dev/tests/integration/typecheck-utils.ts @@ -53,8 +53,9 @@ export type AuthOrganizationSummary = any; export type AuthOrganizationMember = any; export type AuthApiKey = any; export type AuthInvitation = any; +export type AuthTeam = any; export type GetActiveMemberInput = any; -export type GetOrganizationInput = any; +export type GetFullOrganizationInput = any; export type ListMembersInput = any; export type ListInvitationsInput = any; export type ListApiKeysInput = any; diff --git a/packages/everything-dev/tests/unit/api-contract-gen.test.ts b/packages/everything-dev/tests/unit/api-contract-gen.test.ts index b3a9dabc..c6c8a4b7 100644 --- a/packages/everything-dev/tests/unit/api-contract-gen.test.ts +++ b/packages/everything-dev/tests/unit/api-contract-gen.test.ts @@ -25,6 +25,111 @@ function writeFile(path: string, content: string) { fsWriteFileSync(path, content, "utf-8"); } +describe("writeGeneratedFiles — auth types gen uses export type *", () => { + it("uses `export type *` so new types added to auth-export flow through automatically", async () => { + const { writeGeneratedFiles } = await import("../../src/api-contract"); + testDir = mkdtempSync(join(tmpdir(), "api-contract-auth-test-")); + + const apiSrc = join(testDir, "api", "src"); + const uiLib = join(testDir, "ui", "src", "lib"); + const authDir = join(testDir, ".bos", "generated", "auth"); + const authSrc = join(testDir, "plugins", "auth", "src"); + + const contractPath = join(apiSrc, "contract.ts"); + writeFile(contractPath, "export type ContractType = { ping: string };"); + + const authExportPath = join(authDir, "auth-export.d.ts"); + writeFile( + authExportPath, + `import type { InferInput, InferOutput } from "./contract"; +export type Auth = { id: string }; +export type { Auth as BaseAuth } from "better-auth"; +export type AuthOrganization = NonNullable>; +export type AuthTeam = InferOutput<"listTeams">[number]; +export type GetFullOrganizationInput = InferInput<"getFullOrganization">; +export type AuthServices = { auth: Auth; handler: (req: Request) => Promise }; +`, + ); + + const authContractPath = join(authDir, "contract.d.ts"); + writeFile( + authContractPath, + `export type ContractType = Record; +export type InferInput = Record; +export type InferOutput = Record; +`, + ); + + writeGeneratedFiles({ + configDir: testDir, + sources: [ + makeContractSource("api", contractPath), + makeContractSource("auth", join(authSrc, "contract.ts"), "authContract"), + ], + pluginKeys: [], + authSource: makeContractSource("auth", join(authSrc, "contract.ts"), "authContract"), + authExportPath, + apiDependsOn: undefined, + }); + + const uiAuthTypes = readFileSync(join(uiLib, "auth-types.gen.ts"), "utf-8"); + + expect(uiAuthTypes).toContain("export type * from"); + expect(uiAuthTypes).not.toContain("GetOrganizationInput"); + expect(uiAuthTypes).not.toContain("export type {\n Auth,"); + expect(uiAuthTypes).toContain("AuthSessionUser"); + expect(uiAuthTypes).toContain("AuthPluginContext"); + expect(uiAuthTypes).toContain("AuthBaseSession"); + expect(uiAuthTypes).toContain("AuthContractType"); + }); + + it("uses `export type *` in the contract-based path when no authExportPath is provided", async () => { + const { writeGeneratedFiles } = await import("../../src/api-contract"); + testDir = mkdtempSync(join(tmpdir(), "api-contract-auth-test-")); + + const apiSrc = join(testDir, "api", "src"); + const authDir = join(testDir, ".bos", "generated", "auth"); + const authSrc = join(testDir, "plugins", "auth", "src"); + + const contractPath = join(apiSrc, "contract.ts"); + writeFile(contractPath, "export type ContractType = { ping: string };"); + + writeFile( + join(authDir, "auth-export.d.ts"), + `import type { InferOutput } from "./contract"; +export type AuthTeam = InferOutput<"listTeams">[number]; +export type GetFullOrganizationInput = { organizationId: string }; +`, + ); + + writeFile( + join(authDir, "contract.d.ts"), + `export type ContractType = Record; +export type InferOutput = Record; +`, + ); + + writeGeneratedFiles({ + configDir: testDir, + sources: [ + makeContractSource("api", contractPath), + makeContractSource("auth", join(authSrc, "contract.ts"), "authContract"), + ], + pluginKeys: [], + authSource: makeContractSource("auth", join(authSrc, "contract.ts"), "authContract"), + apiDependsOn: undefined, + }); + + const uiAuthTypes = readFileSync( + join(testDir, "ui", "src", "lib", "auth-types.gen.ts"), + "utf-8", + ); + + expect(uiAuthTypes).toContain("export type * from"); + expect(uiAuthTypes).not.toContain("GetOrganizationInput"); + }); +}); + describe("writeGeneratedFiles — apiDependsOn filtering", () => { it("includes all plugins + auth when apiDependsOn is not set", async () => { const { writeGeneratedFiles } = await import("../../src/api-contract"); From 091f603a17a67d78189d5cb67e67ec4d2672f3ed Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Tue, 18 Aug 2026 14:43:22 -0500 Subject: [PATCH 2/4] better-near-auth version --- bun.lock | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bun.lock b/bun.lock index 17ee7b3f..2ccc9552 100644 --- a/bun.lock +++ b/bun.lock @@ -39,7 +39,7 @@ }, "api": { "name": "api", - "version": "2.8.0", + "version": "2.8.1", "dependencies": { "@electric-sql/pglite": "catalog:", "@orpc/contract": "catalog:", @@ -71,7 +71,7 @@ }, "host": { "name": "host", - "version": "1.16.0", + "version": "1.16.1", "dependencies": { "@electric-sql/pglite": "catalog:", "@hono/node-server": "^2.0.1", @@ -264,7 +264,7 @@ }, "ui": { "name": "ui", - "version": "1.9.0", + "version": "1.9.1", "dependencies": { "@better-auth/api-key": "catalog:", "@better-auth/core": "catalog:", @@ -389,7 +389,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "better-auth": "1.6.25", - "better-near-auth": "1.8.2", + "better-near-auth": "1.10.0", "drizzle-kit": "^0.31.8", "drizzle-orm": "^0.45.1", "effect": "3.21.2", @@ -1510,7 +1510,7 @@ "better-call": ["better-call@1.3.7", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w=="], - "better-near-auth": ["better-near-auth@1.8.2", "", { "dependencies": { "@scure/base": "^2.2.0", "nanostores": "^1.1.0", "near-kit": "^0.14.0", "zod": "^4.4.3" }, "peerDependencies": { "@better-auth/core": "^1.6.25", "better-auth": "^1.6.25", "typescript": "^5.9.3" } }, "sha512-sZWGaIaFZejENw7Upe77Hg3r6xjG/pN0QOGfvrDUvYoRXbcCLsvdlM0Ra+FLwWYZzidcCGzkarFwJ1ehaoAu8g=="], + "better-near-auth": ["better-near-auth@1.10.0", "", { "dependencies": { "@scure/base": "^2.2.0", "nanostores": "^1.1.0", "near-kit": "^0.14.0", "zod": "^4.4.3" }, "peerDependencies": { "@better-auth/core": "^1.6.25", "better-auth": "^1.6.25", "typescript": "^5.9.3" } }, "sha512-qLMm+pM+3wUHewLvEwpgT0LDFAIXhRwoKr0IgNcKC0Eu+DALaqe6x8KGX7FmIffY9ETevGubNjth2cmG1AaOMg=="], "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], diff --git a/package.json b/package.json index 073c58bc..7ccdb5b7 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "better-auth": "1.6.25", "@better-auth/api-key": "1.6.25", "@better-auth/passkey": "1.6.25", - "better-near-auth": "1.8.2", + "better-near-auth": "1.10.0", "every-plugin": "^2.10.1", "everything-dev": "^1.53.0", "typescript": "^5.9.3", From d6a410031ac8761dc6ac779fe4696d58d0a30e15 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Tue, 18 Aug 2026 15:11:25 -0500 Subject: [PATCH 3/4] refactor(everything-dev): extract auth-types-gen module, use export type * MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three duplicated copies of the auth-types.gen template (api-contract.ts x2, init.ts x1) plus two hard-coded stub type lists (init.ts, typecheck-utils.ts) are replaced with a single shared module: packages/everything-dev/src/auth-types-gen.ts. Also removes three unused derived types (AuthActiveMember, AuthBaseSession, AuthContractType) and all Raw* aliases from the generated content. Adds export type * to host/src/lib/auth.ts, ui/src/lib/auth.ts, and api/src/lib/auth.ts so new auth types propagate automatically — no manual re-export list to maintain when upstream auth-export changes. Deletes the flaky init.typecheck.test.ts which attempted to skip the network fetch. init.full.test.ts already covers the full init + types:gen + typecheck flow in CI. See NEARBuilders/nearbuilders.org/issues/206 --- api/src/lib/auth.ts | 2 + host/src/lib/auth.ts | 17 +-- packages/everything-dev/src/api-contract.ts | 96 ++++----------- packages/everything-dev/src/auth-types-gen.ts | 48 ++++++++ packages/everything-dev/src/cli/init.ts | 77 +++--------- .../tests/integration/init.typecheck.test.ts | 113 ------------------ .../tests/integration/typecheck-utils.ts | 28 +---- .../tests/unit/api-contract-gen.test.ts | 2 - ui/src/lib/auth.ts | 2 + 9 files changed, 97 insertions(+), 288 deletions(-) create mode 100644 packages/everything-dev/src/auth-types-gen.ts delete mode 100644 packages/everything-dev/tests/integration/init.typecheck.test.ts diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts index 1bef6846..7783b82c 100644 --- a/api/src/lib/auth.ts +++ b/api/src/lib/auth.ts @@ -14,6 +14,8 @@ import type { AuthPluginContext, } from "./auth-types.gen"; +export type * from "./auth-types.gen"; + export type AuthContext = AuthPluginContext; export type RequestAuthUser = NonNullable; export type ApiKeyContext = NonNullable; diff --git a/host/src/lib/auth.ts b/host/src/lib/auth.ts index ecaebd6a..b03bcbf9 100644 --- a/host/src/lib/auth.ts +++ b/host/src/lib/auth.ts @@ -1,24 +1,13 @@ import type { - AuthPluginContext, AuthRequestContext, AuthSession, AuthSessionData, AuthSessionUser, - AuthServices as GeneratedAuthServices, } from "@/lib/auth-types.gen"; -export type { - AuthPluginContext, - AuthRequestContext, - AuthSession, - AuthSessionData, - AuthSessionUser, -}; -export type AuthUser = AuthSessionUser; +export type * from "@/lib/auth-types.gen"; -interface AuthServices extends GeneratedAuthServices { - auth: GeneratedAuthServices["auth"]; -} +export type AuthUser = AuthSessionUser; export interface AuthClient { getSession(): Promise; @@ -38,5 +27,3 @@ export type HonoEnv = { Variables: AuthVariables }; export function toAuthClientContext(headers: Headers): Record { return Object.fromEntries(headers.entries()); } - -export type { AuthServices }; diff --git a/packages/everything-dev/src/api-contract.ts b/packages/everything-dev/src/api-contract.ts index e0b74045..a864c7c5 100644 --- a/packages/everything-dev/src/api-contract.ts +++ b/packages/everything-dev/src/api-contract.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, relative } from "node:path"; +import { buildAuthTypesGenContent } from "./auth-types-gen"; import { fetchJsonOrNull, fetchResponse } from "./http-client"; import type { JsonObject, RuntimeConfig, RuntimePluginConfig } from "./types"; @@ -187,76 +188,6 @@ async function fetchAuthExportTypes(opts: { return generatedPath; } -function writeAuthTypesGen(targetPath: string, authExportPath: string) { - const exportImportPath = toImportPath(targetPath, authExportPath); - const content = [ - `export type * from "${exportImportPath}";`, - `import type { InferOutput, ContractType as AuthContract } from "${toImportPath(targetPath, join(dirname(authExportPath), "contract.d.ts"))}";`, - `import type { Auth as BaseAuth } from "${exportImportPath}";`, - "", - 'type RawAuthSession = InferOutput<"getSession">;', - 'type RawAuthRequestContext = InferOutput<"getContext">;', - 'type RawAuthActiveMember = InferOutput<"getActiveMember">;', - "", - 'export type AuthSessionUser = NonNullable;', - 'export type AuthSessionData = NonNullable;', - "export type AuthSession = {", - " user: AuthSessionUser | null;", - " session: AuthSessionData | null;", - "};", - "export type AuthRequestContext = RawAuthRequestContext;", - "export type AuthPluginContext = Partial & {", - " reqHeaders?: Headers;", - " getRawBody?: () => Promise;", - "};", - "export type AuthActiveMember = RawAuthActiveMember;", - 'export type AuthBaseSession = BaseAuth["$Infer"]["Session"];', - "export type AuthContractType = AuthContract;", - "", - ].join("\n"); - mkdirSync(dirname(targetPath), { recursive: true }); - writeFileIfChanged(targetPath, content); -} - -function writeContractBasedAuthTypesGen(targetPath: string, configDir: string) { - const authExportRel = toImportPath( - targetPath, - join(configDir, ".bos", "generated", "auth", "auth-export.d.ts"), - ); - const contractRel = toImportPath( - targetPath, - join(configDir, ".bos", "generated", "auth", "contract.d.ts"), - ); - - const content = [ - `export type * from "${authExportRel}";`, - `import type { InferOutput, ContractType as AuthContract } from "${contractRel}";`, - `import type { Auth as BaseAuth } from "${authExportRel}";`, - "", - 'type RawAuthSession = InferOutput<"getSession">;', - 'type RawAuthRequestContext = InferOutput<"getContext">;', - 'type RawAuthActiveMember = InferOutput<"getActiveMember">;', - "", - 'export type AuthSessionUser = NonNullable;', - 'export type AuthSessionData = NonNullable;', - "export type AuthSession = {", - " user: AuthSessionUser | null;", - " session: AuthSessionData | null;", - "};", - "export type AuthRequestContext = RawAuthRequestContext;", - "export type AuthPluginContext = Partial & {", - " reqHeaders?: Headers;", - " getRawBody?: () => Promise;", - "};", - "export type AuthActiveMember = RawAuthActiveMember;", - 'export type AuthBaseSession = BaseAuth["$Infer"]["Session"];', - "export type AuthContractType = AuthContract;", - "", - ].join("\n"); - mkdirSync(dirname(targetPath), { recursive: true }); - writeFileIfChanged(targetPath, content); -} - async function resolveContractSource(opts: { configDir: string; runtimeDir: string; @@ -504,11 +435,32 @@ export function writeGeneratedFiles(opts: { if (opts.authExportPath) { for (const authTypesPath of authTypeTargets) { - writeAuthTypesGen(authTypesPath, opts.authExportPath); + const exportImportPath = toImportPath(authTypesPath, opts.authExportPath); + const contractImportPath = toImportPath( + authTypesPath, + join(dirname(opts.authExportPath), "contract.d.ts"), + ); + mkdirSync(dirname(authTypesPath), { recursive: true }); + writeFileIfChanged( + authTypesPath, + buildAuthTypesGenContent(exportImportPath, contractImportPath), + ); } } else if (opts.authSource) { for (const authTypesPath of authTypeTargets) { - writeContractBasedAuthTypesGen(authTypesPath, opts.configDir); + const exportImportPath = toImportPath( + authTypesPath, + join(opts.configDir, ".bos", "generated", "auth", "auth-export.d.ts"), + ); + const contractImportPath = toImportPath( + authTypesPath, + join(opts.configDir, ".bos", "generated", "auth", "contract.d.ts"), + ); + mkdirSync(dirname(authTypesPath), { recursive: true }); + writeFileIfChanged( + authTypesPath, + buildAuthTypesGenContent(exportImportPath, contractImportPath), + ); } } diff --git a/packages/everything-dev/src/auth-types-gen.ts b/packages/everything-dev/src/auth-types-gen.ts new file mode 100644 index 00000000..f04b13ec --- /dev/null +++ b/packages/everything-dev/src/auth-types-gen.ts @@ -0,0 +1,48 @@ +export const AUTH_DERIVED_TYPES_BODY = ` +export type AuthSessionUser = NonNullable["user"]>; +export type AuthSessionData = NonNullable["session"]>; +export type AuthSession = { + user: AuthSessionUser | null; + session: AuthSessionData | null; +}; +export type AuthRequestContext = InferOutput<"getContext">; +export type AuthPluginContext = Partial & { + reqHeaders?: Headers; + getRawBody?: () => Promise; +}; +`; + +export function buildAuthTypesGenContent( + authExportImportPath: string, + contractImportPath: string, +): string { + return `export type * from "${authExportImportPath}"; +import type { InferOutput } from "${contractImportPath}"; +${AUTH_DERIVED_TYPES_BODY} +`; +} + +export function buildAuthExportStub(): string { + return `export type Auth = any; +export type AuthOrganizationContext = any; +export type AuthOrganization = any; +export type AuthOrganizationSummary = any; +export type AuthOrganizationMember = any; +export type AuthApiKey = any; +export type AuthInvitation = any; +export type AuthTeam = any; +export type GetActiveMemberInput = any; +export type GetFullOrganizationInput = any; +export type ListMembersInput = any; +export type ListInvitationsInput = any; +export type ListApiKeysInput = any; +export type AuthServices = any; +export type createAuthInstance = any; +`; +} + +export function buildAuthContractStub(): string { + return `export type ContractType = any; +export type InferOutput<_TRoute extends string> = any; +`; +} diff --git a/packages/everything-dev/src/cli/init.ts b/packages/everything-dev/src/cli/init.ts index ac301c4a..bb987ac2 100644 --- a/packages/everything-dev/src/cli/init.ts +++ b/packages/everything-dev/src/cli/init.ts @@ -15,6 +15,11 @@ import { dirname, join, relative, resolve } from "node:path"; import { pipeline } from "node:stream/promises"; import { execa } from "execa"; import { glob } from "glob"; +import { + buildAuthContractStub, + buildAuthExportStub, + buildAuthTypesGenContent, +} from "../auth-types-gen"; import type { OverrideSection } from "../contract"; import { fetchBosConfigFromFastKv } from "../fastkv"; import { fetchResponse } from "../http-client"; @@ -915,7 +920,15 @@ export async function personalizeConfig( for (const authTypesGenPath of authTypesPaths) { if (!existsSync(authTypesGenPath)) { mkdirSync(dirname(authTypesGenPath), { recursive: true }); - writeFileSync(authTypesGenPath, generateAuthTypesContent(authTypesGenPath, destination)); + const authExportRel = toRelativeImportPath( + join(destination, ".bos", "generated", "auth", "auth-export.d.ts"), + authTypesGenPath, + ); + const contractRel = toRelativeImportPath( + join(destination, ".bos", "generated", "auth", "contract.d.ts"), + authTypesGenPath, + ); + writeFileSync(authTypesGenPath, buildAuthTypesGenContent(authExportRel, contractRel)); } } @@ -926,34 +939,11 @@ export async function personalizeConfig( } const authExportStubPath = join(authDir, "auth-export.d.ts"); if (!existsSync(authExportStubPath)) { - writeFileSync( - authExportStubPath, - `export type Auth = any; -export type AuthOrganizationContext = any; -export type AuthOrganization = any; -export type AuthOrganizationSummary = any; -export type AuthOrganizationMember = any; -export type AuthApiKey = any; -export type AuthInvitation = any; -export type AuthTeam = any; -export type GetActiveMemberInput = any; -export type GetFullOrganizationInput = any; -export type ListMembersInput = any; -export type ListInvitationsInput = any; -export type ListApiKeysInput = any; -export type AuthServices = any; -export type createAuthInstance = any; -`, - ); + writeFileSync(authExportStubPath, buildAuthExportStub()); } const contractStubPath = join(authDir, "contract.d.ts"); if (!existsSync(contractStubPath)) { - writeFileSync( - contractStubPath, - `export type ContractType = any; -export type InferOutput<_TRoute extends string> = any; -`, - ); + writeFileSync(contractStubPath, buildAuthContractStub()); } } @@ -974,41 +964,6 @@ export type InferOutput<_TRoute extends string> = any; } } -function generateAuthTypesContent(targetPath: string, configDir: string): string { - const authExportRel = toRelativeImportPath( - join(configDir, ".bos", "generated", "auth", "auth-export.d.ts"), - targetPath, - ); - const contractRel = toRelativeImportPath( - join(configDir, ".bos", "generated", "auth", "contract.d.ts"), - targetPath, - ); - - return `export type * from "${authExportRel}"; -import type { InferOutput, ContractType as AuthContract } from "${contractRel}"; -import type { Auth as BaseAuth } from "${authExportRel}"; - -type RawAuthSession = InferOutput<"getSession">; -type RawAuthRequestContext = InferOutput<"getContext">; -type RawAuthActiveMember = InferOutput<"getActiveMember">; - -export type AuthSessionUser = NonNullable; -export type AuthSessionData = NonNullable; -export type AuthSession = { - user: AuthSessionUser | null; - session: AuthSessionData | null; -}; -export type AuthRequestContext = RawAuthRequestContext; -export type AuthPluginContext = Partial & { - reqHeaders?: Headers; - getRawBody?: () => Promise; -}; -export type AuthActiveMember = RawAuthActiveMember; -export type AuthBaseSession = BaseAuth["$Infer"]["Session"]; -export type AuthContractType = AuthContract; -`; -} - function toRelativeImportPath(fromPath: string, toPath: string): string { const rel = relative(dirname(toPath), fromPath); return rel.startsWith(".") ? rel : `./${rel}`; diff --git a/packages/everything-dev/tests/integration/init.typecheck.test.ts b/packages/everything-dev/tests/integration/init.typecheck.test.ts deleted file mode 100644 index 09579bd3..00000000 --- a/packages/everything-dev/tests/integration/init.typecheck.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { - buildInitPatterns, - copyFilteredFiles, - personalizeConfig, - runBunInstall, -} from "../../src/cli/init"; -import { getFrameworkTarballs, rewriteFrameworkPackageSpecs } from "./framework-packages"; -import { - assertTypecheckSuccess, - renderTypeErrors, - runCommand, - runTypecheck, - unexpectedTypeErrors, - writeGeneratedAuthStubs, -} from "./typecheck-utils"; - -const REPO_ROOT = join(import.meta.dirname, "../../../../"); - -describe("bos init — typecheck", () => { - let testDir: string; - let frameworkTarballs: Awaited>; - - beforeAll(async () => { - testDir = mkdtempSync(join(tmpdir(), "bos-init-typecheck-")); - frameworkTarballs = await getFrameworkTarballs(REPO_ROOT); - }, 180_000); - - afterAll(() => { - rmSync(testDir, { recursive: true, force: true }); - }, 120_000); - - it("scaffolds project with template files", async () => { - const patterns = buildInitPatterns(["ui", "api", "plugins"], ["apps", "template"], { - template: "_template", - }); - await copyFilteredFiles(REPO_ROOT, testDir, patterns, { - overrides: ["ui", "api", "plugins"], - plugins: ["apps", "template"], - }); - - await personalizeConfig(testDir, { - extendsAccount: "dev.everything.near", - extendsGateway: "dev.everything.dev", - account: "test.near", - domain: "test.dev", - workspaceOpts: { sourceDir: REPO_ROOT }, - overrides: ["ui", "api", "plugins"], - plugins: ["apps", "template"], - }); - rewriteFrameworkPackageSpecs(testDir, frameworkTarballs); - - expect(existsSync(join(testDir, "bos.config.json"))).toBe(true); - expect(existsSync(join(testDir, "ui", "src", "lib", "auth-types.gen.ts"))).toBe(true); - expect(existsSync(join(testDir, "api", "src", "lib", "auth-types.gen.ts"))).toBe(true); - const pkg = JSON.parse(readFileSync(join(testDir, "ui", "package.json"), "utf-8")) as { - dependencies?: Record; - }; - expect(pkg.dependencies?.["@better-auth/core"]).toBe("catalog:"); - }); - - it("sets postinstall to 'node node_modules/.bin/bos types gen || true'", async () => { - const pkgPath = join(testDir, "package.json"); - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { scripts?: Record }; - expect(pkg.scripts?.postinstall).toBe("node node_modules/.bin/bos types gen || true"); - }); - - it("sets types:gen to 'node node_modules/.bin/bos types gen'", async () => { - const pkgPath = join(testDir, "package.json"); - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { scripts?: Record }; - expect(pkg.scripts?.["types:gen"]).toBe("node node_modules/.bin/bos types gen"); - }); - - it("installs dependencies", async () => { - await runBunInstall(testDir); - writeGeneratedAuthStubs(testDir); - expect(existsSync(join(testDir, "node_modules"))).toBe(true); - }, 180_000); - - it("generates types", async () => { - const typesGen = await runCommand("bun", ["run", "types:gen"], testDir, 120_000); - expect(typesGen.code).toBe(0); - expect(existsSync(join(testDir, "ui", "src", "lib", "api-types.gen.ts"))).toBe(true); - expect(existsSync(join(testDir, "api", "src", "lib", "plugins-types.gen.ts"))).toBe(true); - - writeGeneratedAuthStubs(testDir); - }, 120_000); - - it("typechecks api with zero unexpected errors", async () => { - const result = await runTypecheck(testDir, "api", { raw: true }); - const unexpected = unexpectedTypeErrors(result.stdout + result.stderr); - - if (unexpected.length > 0) { - console.error(renderTypeErrors("api", unexpected)); - } - - assertTypecheckSuccess(result, "api"); - }, 120_000); - - it("typechecks ui with zero unexpected errors", async () => { - const result = await runTypecheck(testDir, "ui", { raw: true }); - const unexpected = unexpectedTypeErrors(result.stdout + result.stderr); - - if (unexpected.length > 0) { - console.error(renderTypeErrors("ui", unexpected)); - } - - assertTypecheckSuccess(result, "ui"); - }, 120_000); -}); diff --git a/packages/everything-dev/tests/integration/typecheck-utils.ts b/packages/everything-dev/tests/integration/typecheck-utils.ts index b0480c30..2b1b84b2 100644 --- a/packages/everything-dev/tests/integration/typecheck-utils.ts +++ b/packages/everything-dev/tests/integration/typecheck-utils.ts @@ -3,6 +3,7 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { Data } from "effect"; import { expect } from "vitest"; +import { buildAuthContractStub, buildAuthExportStub } from "../../src/auth-types-gen"; export interface CommandResult { code: number; @@ -44,31 +45,8 @@ export function runCommand( export function writeGeneratedAuthStubs(projectDir: string) { const authDir = join(projectDir, ".bos", "generated", "auth"); mkdirSync(authDir, { recursive: true }); - writeFileSync( - join(authDir, "auth-export.d.ts"), - `export type Auth = any; -export type AuthOrganizationContext = any; -export type AuthOrganization = any; -export type AuthOrganizationSummary = any; -export type AuthOrganizationMember = any; -export type AuthApiKey = any; -export type AuthInvitation = any; -export type AuthTeam = any; -export type GetActiveMemberInput = any; -export type GetFullOrganizationInput = any; -export type ListMembersInput = any; -export type ListInvitationsInput = any; -export type ListApiKeysInput = any; -export type AuthServices = any; -export type createAuthInstance = any; -`, - ); - writeFileSync( - join(authDir, "contract.d.ts"), - `export type ContractType = any; -export type InferOutput<_TRoute extends string> = any; -`, - ); + writeFileSync(join(authDir, "auth-export.d.ts"), buildAuthExportStub()); + writeFileSync(join(authDir, "contract.d.ts"), buildAuthContractStub()); } export function writeGeneratedTypeStubsEmpty(projectDir: string) { diff --git a/packages/everything-dev/tests/unit/api-contract-gen.test.ts b/packages/everything-dev/tests/unit/api-contract-gen.test.ts index c6c8a4b7..18804b1f 100644 --- a/packages/everything-dev/tests/unit/api-contract-gen.test.ts +++ b/packages/everything-dev/tests/unit/api-contract-gen.test.ts @@ -79,8 +79,6 @@ export type InferOutput = Record; expect(uiAuthTypes).not.toContain("export type {\n Auth,"); expect(uiAuthTypes).toContain("AuthSessionUser"); expect(uiAuthTypes).toContain("AuthPluginContext"); - expect(uiAuthTypes).toContain("AuthBaseSession"); - expect(uiAuthTypes).toContain("AuthContractType"); }); it("uses `export type *` in the contract-based path when no authExportPath is provided", async () => { diff --git a/ui/src/lib/auth.ts b/ui/src/lib/auth.ts index 32a0b096..f2e814df 100644 --- a/ui/src/lib/auth.ts +++ b/ui/src/lib/auth.ts @@ -23,6 +23,8 @@ import type { ClientRuntimeConfig } from "everything-dev/types"; import { getRuntimeConfig } from "everything-dev/ui/runtime"; import type { Auth } from "./auth-types.gen"; +export type * from "./auth-types.gen"; + type RuntimeAuthVariables = { siwn: { recipient?: string; From 5d4bc60ea3cee1b87c1e91c0945a1e9a5648c53c Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Tue, 18 Aug 2026 15:32:09 -0500 Subject: [PATCH 4/4] fix(ci): rebase+retry push in deploy/staging workflows to avoid fetch-first rejection The final push step in `Deploy` and `Staging` workflows ran unconditionally on a local checkout from the start of the run. Any commit landing on the target branch during the 10-20 minute deploy window (Release's auto-merged `chore: version packages` PR, manual `workflow_dispatch`, Renovate, or human commits) made the push non-fast-forward and Git rejected it with `! [rejected] main -> main (fetch first)`. Replace the push with a fetch + rebase + retry loop: - git fetch origin $TARGET_BRANCH - git rebase origin/$TARGET_BRANCH (clean conflicts fail loudly) - git add bos.config.json - exit 0 if no staged changes after rebase - git push origin $TARGET_BRANCH, retry up to 5x with 5/10/15/20/25s backoff Applied to both the live workflows and the templates that ship to child projects via `bos sync`. `cancel-in-progress` left as-is to preserve the existing 'don't kill in-flight deploy' semantics. --- .changeset/fix-deploy-push-fetch-first.md | 5 +++++ .github/templates/workflows/deploy.yml | 25 +++++++++++++++++++---- .github/templates/workflows/staging.yml | 25 +++++++++++++++++++---- .github/workflows/deploy.yml | 25 +++++++++++++++++++---- .github/workflows/staging.yml | 25 +++++++++++++++++++---- 5 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 .changeset/fix-deploy-push-fetch-first.md diff --git a/.changeset/fix-deploy-push-fetch-first.md b/.changeset/fix-deploy-push-fetch-first.md new file mode 100644 index 00000000..14a038b0 --- /dev/null +++ b/.changeset/fix-deploy-push-fetch-first.md @@ -0,0 +1,5 @@ +--- +"everything-dev": patch +--- + +Fix `Deploy` and `Staging` workflows failing with `! [rejected] main -> main (fetch first)` when remote `main` (or `staging`) advances during the long deploy window. The final push step now `fetch` + `rebase` against the remote ref before pushing, retries up to 5 times with exponential backoff, and exits cleanly when there's nothing to push after rebase. This eliminates races with the `Release` workflow's auto-merged `chore: version packages` PR, manual `workflow_dispatch` triggers, Renovate, and human commits landing during deploy. diff --git a/.github/templates/workflows/deploy.yml b/.github/templates/workflows/deploy.yml index 6f9a44f8..4b6aa7ee 100644 --- a/.github/templates/workflows/deploy.yml +++ b/.github/templates/workflows/deploy.yml @@ -75,8 +75,25 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add bos.config.json - if ! git diff --cached --quiet; then + + for i in 1 2 3 4 5; do + git fetch origin "$TARGET_BRANCH" + if ! git rebase "origin/$TARGET_BRANCH"; then + echo "Rebase conflict — manual intervention required" + exit 1 + fi + git add bos.config.json + if git diff --cached --quiet; then + echo "Nothing to push after rebase (attempt $i)" + exit 0 + fi git commit -m "chore: update deployment URLs [skip ci]" - git push origin "$TARGET_BRANCH" - fi + if git push origin "$TARGET_BRANCH"; then + echo "Push succeeded on attempt $i" + exit 0 + fi + echo "Push attempt $i failed, retrying in $((i*5))s..." + sleep $((i*5)) + done + echo "All push attempts failed" + exit 1 diff --git a/.github/templates/workflows/staging.yml b/.github/templates/workflows/staging.yml index fecfb3c7..cd114156 100644 --- a/.github/templates/workflows/staging.yml +++ b/.github/templates/workflows/staging.yml @@ -76,8 +76,25 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add bos.config.json - if ! git diff --cached --quiet; then + + for i in 1 2 3 4 5; do + git fetch origin "$TARGET_BRANCH" + if ! git rebase "origin/$TARGET_BRANCH"; then + echo "Rebase conflict — manual intervention required" + exit 1 + fi + git add bos.config.json + if git diff --cached --quiet; then + echo "Nothing to push after rebase (attempt $i)" + exit 0 + fi git commit -m "chore: update staging deployment URLs [skip ci]" - git push origin "$TARGET_BRANCH" - fi + if git push origin "$TARGET_BRANCH"; then + echo "Push succeeded on attempt $i" + exit 0 + fi + echo "Push attempt $i failed, retrying in $((i*5))s..." + sleep $((i*5)) + done + echo "All push attempts failed" + exit 1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 300ff4e6..d01f0dad 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -109,8 +109,25 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add bos.config.json - if ! git diff --cached --quiet; then + + for i in 1 2 3 4 5; do + git fetch origin "$TARGET_BRANCH" + if ! git rebase "origin/$TARGET_BRANCH"; then + echo "Rebase conflict — manual intervention required" + exit 1 + fi + git add bos.config.json + if git diff --cached --quiet; then + echo "Nothing to push after rebase (attempt $i)" + exit 0 + fi git commit -m "chore: update deployment URLs [skip ci]" - git push origin "$TARGET_BRANCH" - fi + if git push origin "$TARGET_BRANCH"; then + echo "Push succeeded on attempt $i" + exit 0 + fi + echo "Push attempt $i failed, retrying in $((i*5))s..." + sleep $((i*5)) + done + echo "All push attempts failed" + exit 1 diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 1d9d1eb7..1580dc17 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -80,8 +80,25 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add bos.config.json - if ! git diff --cached --quiet; then + + for i in 1 2 3 4 5; do + git fetch origin "$TARGET_BRANCH" + if ! git rebase "origin/$TARGET_BRANCH"; then + echo "Rebase conflict — manual intervention required" + exit 1 + fi + git add bos.config.json + if git diff --cached --quiet; then + echo "Nothing to push after rebase (attempt $i)" + exit 0 + fi git commit -m "chore: update staging deployment URLs [skip ci]" - git push origin "$TARGET_BRANCH" - fi + if git push origin "$TARGET_BRANCH"; then + echo "Push succeeded on attempt $i" + exit 0 + fi + echo "Push attempt $i failed, retrying in $((i*5))s..." + sleep $((i*5)) + done + echo "All push attempts failed" + exit 1