Merge dev into update-oauth-docs

This commit is contained in:
Konsti Wohlwend 2025-10-14 04:31:50 -07:00 committed by GitHub
commit 598c3c50a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 508 additions and 38 deletions

View File

@ -53,6 +53,9 @@ jobs:
- name: Build packages
run: pnpm run build:packages
- name: Codegen
run: pnpm run codegen
- name: Initialize database
run: pnpm run db:init

View File

@ -2,7 +2,7 @@
-- SPLIT_STATEMENT_SENTINEL
-- SINGLE_STATEMENT_SENTINEL
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
CREATE INDEX CONCURRENTLY IF NOT EXISTS "temp_eco_config_apps_idx" ON "EnvironmentConfigOverride" USING GIN ("config");
CREATE INDEX CONCURRENTLY IF NOT EXISTS "temp_eco_config_apps_idx" ON /* SCHEMA_NAME_SENTINEL */."EnvironmentConfigOverride" USING GIN ("config");
-- SPLIT_STATEMENT_SENTINEL
-- SPLIT_STATEMENT_SENTINEL

View File

@ -212,9 +212,12 @@ import.meta.vitest?.test("applies migrations concurrently", runTest(async ({ exp
}));
import.meta.vitest?.test("applies migrations concurrently with 20 concurrent migrations", runTest(async ({ expect, prismaClient }) => {
const promises = Array.from({ length: 20 }, () =>
applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, artificialDelayInSeconds: 1, schema: 'public' })
);
const promises = Array.from({ length: 20 }, async (_, i) => {
console.log("Applying migration", i);
const result = await applyMigrations({ prismaClient, migrationFiles: exampleMigrationFiles1, artificialDelayInSeconds: 1, schema: 'public', logging: true });
console.log("Migration", i, "applied", result.newlyAppliedMigrationNames);
return result;
});
const results = await Promise.all(promises);

View File

@ -1,4 +1,4 @@
import { sqlQuoteIdent } from '@/prisma-client';
import { sqlQuoteIdent, sqlQuoteIdentToString } from '@/prisma-client';
import { Prisma, PrismaClient } from '@prisma/client';
import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
import { MIGRATION_FILES } from './../generated/migration-files';
@ -125,7 +125,8 @@ export async function applyMigrations(options: {
return;
}
for (const statement of migration.sql.split('SPLIT_STATEMENT_SENTINEL')) {
for (const statementRaw of migration.sql.split('SPLIT_STATEMENT_SENTINEL')) {
const statement = statementRaw.replace('/* SCHEMA_NAME_SENTINEL */', sqlQuoteIdentToString(options.schema));
const runOutside = statement.includes('RUN_OUTSIDE_TRANSACTION_SENTINEL');
const isSingleStatement = statement.includes('SINGLE_STATEMENT_SENTINEL');
const isConditionallyRepeatMigration = statement.includes('CONDITIONALLY_REPEAT_MIGRATION_SENTINEL');
@ -197,6 +198,7 @@ export async function runMigrationNeeded(options: {
schema: string,
migrationFiles?: { migrationName: string, sql: string }[],
artificialDelayInSeconds?: number,
logging?: boolean,
}): Promise<void> {
const migrationFiles = options.migrationFiles ?? MIGRATION_FILES;
@ -217,7 +219,7 @@ export async function runMigrationNeeded(options: {
migrationFiles: options.migrationFiles,
artificialDelayInSeconds: options.artificialDelayInSeconds,
schema: options.schema,
logging: true,
logging: options.logging,
});
} else {
throw e;

View File

@ -93,12 +93,12 @@ export async function getPrismaClientForSourceOfTruth(sourceOfTruth: CompleteCon
const entry = sourceOfTruth.connectionStrings[branchId];
const connectionString = await resolveNeonConnectionString(entry);
const neonPrismaClient = getNeonPrismaClient(connectionString);
await runMigrationNeeded({ prismaClient: neonPrismaClient, schema: getSchemaFromConnectionString(connectionString) });
await runMigrationNeeded({ prismaClient: neonPrismaClient, schema: getSchemaFromConnectionString(connectionString), logging: true });
return neonPrismaClient;
}
case 'postgres': {
const postgresPrismaClient = getPostgresPrismaClient(sourceOfTruth.connectionString);
await runMigrationNeeded({ prismaClient: postgresPrismaClient.client, schema: getSchemaFromConnectionString(sourceOfTruth.connectionString) });
await runMigrationNeeded({ prismaClient: postgresPrismaClient.client, schema: getSchemaFromConnectionString(sourceOfTruth.connectionString), logging: true });
return postgresPrismaClient.client;
}
case 'hosted': {
@ -365,11 +365,14 @@ export function isPrismaUniqueConstraintViolation(error: unknown, modelName: str
return error.meta.modelName === modelName && deepPlainEquals(error.meta.target, target);
}
export function sqlQuoteIdent(id: string) {
// accept letters, numbers, underscore, $, and dash (adjust as needed)
export function sqlQuoteIdentToString(id: string) {
if (!/^[A-Za-z_][A-Za-z0-9_\-$]*$/.test(id)) {
throw new Error(`Invalid identifier: ${id}`);
}
// escape embedded double quotes just in case
return Prisma.raw(`"${id.replace(/"/g, '""')}"`);
return `"${id.replace(/"/g, '""')}"`;
}
export function sqlQuoteIdent(id: string) {
return Prisma.raw(sqlQuoteIdentToString(id));
}

View File

@ -2,7 +2,6 @@ import { wait } from "@stackframe/stack-shared/dist/utils/promises";
import { expect } from "vitest";
import { NiceResponse, it } from "../../../../helpers";
import { Auth, InternalApiKey, Project, backendContext, createMailbox, niceBackendFetch } from "../../../backend-helpers";
import { Result } from "@stackframe/stack-shared/dist/utils/results";
async function ensureAnonymousUsersAreStillExcluded(metricsResponse: NiceResponse) {
for (let i = 0; i < 2; i++) {
@ -127,37 +126,33 @@ it("should exclude anonymous users from metrics", async ({ expect }) => {
}
// the event log is async, so let's give it some time to be written to the DB
const result = await Result.retry(async () => {
const response = await niceBackendFetch("/api/v1/internal/metrics", { accessType: 'admin' });
if (JSON.stringify(response.body) === JSON.stringify(beforeMetrics.body)) {
return Result.ok(response);
let result!: NiceResponse;
for (let i = 0; i < 5; i++) {
result = await niceBackendFetch("/api/v1/internal/metrics", { accessType: 'admin' });
if (JSON.stringify(result.body) === JSON.stringify(beforeMetrics.body)) {
break;
}
return Result.error(response);
}, 5, { exponentialDelayBase: 200 });
if (result.status === "error") {
expect(beforeMetrics.body).toEqual(result.error);
throw new Error("Metrics response mismatch, should never be reached");
await wait(2_000);
}
// Verify that total_users only counts the 1 regular user, not the anonymous ones
expect(result.data.body.total_users).toBe(1);
expect(result.body.total_users).toBe(1);
// Verify anonymous users don't appear in recently_registered
expect(result.data.body.recently_registered.length).toBe(1);
expect(result.data.body.recently_registered.every((user: any) => !user.is_anonymous)).toBe(true);
expect(result.body.recently_registered.length).toBe(1);
expect(result.body.recently_registered.every((user: any) => !user.is_anonymous)).toBe(true);
// Verify anonymous users don't appear in recently_active
expect(result.data.body.recently_active.every((user: any) => !user.is_anonymous)).toBe(true);
expect(result.body.recently_active.every((user: any) => !user.is_anonymous)).toBe(true);
// Verify anonymous users aren't counted in daily_users
const lastDayUsers = result.data.body.daily_users[result.data.body.daily_users.length - 1];
const lastDayUsers = result.body.daily_users[result.body.daily_users.length - 1];
expect(lastDayUsers.activity).toBe(1);
// Verify users_by_country only includes regular users
expect(result.data.body.users_by_country["US"]).toBe(1);
expect(result.body.users_by_country["US"]).toBe(1);
await ensureAnonymousUsersAreStillExcluded(result.data);
await ensureAnonymousUsersAreStillExcluded(result);
});
it("should handle anonymous users with activity correctly", async ({ expect }) => {

View File

@ -50,8 +50,8 @@ export default {
Then, update your Convex client to use Stack Auth:
```ts
convexClient.setAuth(stackServerApp.getConvexClientAuth()); // browser JS
convexReactClient.setAuth(stackServerApp.getConvexClientAuth()); // React
convexClient.setAuth(stackServerApp.getConvexClientAuth({})); // browser JS
convexReactClient.setAuth(stackServerApp.getConvexClientAuth({})); // React
convexHttpClient.setAuth(stackServerApp.getAuthForConvexHttpClient({ tokenStore: requestObject })); // HTTP, see Stack Auth docs for more information on tokenStore
```

View File

@ -1,12 +1,12 @@
"use client";
import { ReactNode } from "react";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { stackClientApp } from "@/stack/client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ReactNode } from "react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
convex.setAuth(
stackClientApp.getConvexClientAuth({ tokenStore: "nextjs-cookie" })
stackClientApp.getConvexClientAuth({})
);
export default function ConvexClientProvider({

View File

@ -237,6 +237,10 @@ async function main(): Promise<void> {
if (isNeon) packagesToInstall.push('@neondatabase/serverless');
await Steps.writeEnvVars(type);
const convexIntegration = await Steps.maybeInstallConvexIntegration({ packageJson: projectPackageJson, type });
if (convexIntegration) {
nextSteps.push(...convexIntegration.instructions);
}
if (type === "next") {
const projectInfo = await Steps.getNextProjectInfo({ packageJson: projectPackageJson });
@ -440,6 +444,10 @@ type StackAppFileResult = {
fileName: string,
}
type ConvexIntegrationResult = {
instructions: string[],
}
const Steps = {
async getProject(): Promise<{ packageJson: PackageJson }> {
let projectPath = await getProjectPath();
@ -599,6 +607,65 @@ const Steps = {
return false;
},
async maybeInstallConvexIntegration({ packageJson, type }: { packageJson: PackageJson, type: "js" | "next" | "react" }): Promise<ConvexIntegrationResult | null> {
const hasConvexDependency = Boolean(packageJson.dependencies?.["convex"] || packageJson.devDependencies?.["convex"]);
if (!hasConvexDependency) {
return null;
}
const projectPath = await getProjectPath();
const convexDir = path.join(projectPath, "convex");
if (!fs.existsSync(convexDir)) {
return null;
}
const stackPackageName = await Steps.getStackPackageName(type);
const instructions: string[] = [];
const authConfigPath = path.join(convexDir, "auth.config.ts");
const desiredAuthConfig = createConvexAuthConfigContent({ stackPackageName, type });
const existingAuthConfig = await readFile(authConfigPath);
if (!existingAuthConfig || (!existingAuthConfig.includes("getConvexProvidersConfig") && !existingAuthConfig.includes("@stackframe/"))) {
laterWriteFile(authConfigPath, desiredAuthConfig);
}
const convexConfigPath = path.join(convexDir, "convex.config.ts");
const existingConvexConfig = await readFile(convexConfigPath);
const desiredConvexConfig = createConvexIntegrationConvexConfigContent(stackPackageName);
let needsManualConvexConfig = false;
if (!existingConvexConfig) {
laterWriteFile(convexConfigPath, desiredConvexConfig);
} else if (existingConvexConfig.includes("app.use(stackAuthComponent") && existingConvexConfig.includes("/convex.config") && existingConvexConfig.includes("stackframe")) {
// already integrated
} else {
const integratedContent = integrateConvexConfig(existingConvexConfig, stackPackageName);
if (integratedContent) {
laterWriteFile(convexConfigPath, integratedContent);
} else if (isSimpleConvexConfig(existingConvexConfig)) {
laterWriteFile(convexConfigPath, desiredConvexConfig);
} else {
needsManualConvexConfig = true;
}
}
if (needsManualConvexConfig) {
instructions.push(`Update convex/convex.config.ts to import ${stackPackageName}/convex.config and call app.use(stackAuthComponent).`);
}
const convexClientUpdateResult = await updateConvexClients({ projectPath, type });
if (convexClientUpdateResult.skippedFiles.length > 0) {
instructions.push("Review your Convex client setup and call stackClientApp.getConvexClientAuth({}) or stackServerApp.getConvexClientAuth({}) manually where needed.");
}
instructions.push(
"Set the Stack Auth environment variables in Convex (Deployment → Settings → Environment Variables).",
"Verify your Convex clients call stackClientApp.getConvexClientAuth({}) or stackServerApp.getConvexClientAuth({}) so they share authentication with Stack Auth."
);
return { instructions };
},
async dryUpdateNextLayoutFile({ appPath, defaultExtension }: { appPath: string, defaultExtension: string }): Promise<{
path: string,
updatedContent: string,
@ -1131,6 +1198,403 @@ function laterWriteFileIfNotExists(fullPath: string, content: string): void {
});
}
function createConvexAuthConfigContent(options: { stackPackageName: string, type: "js" | "next" | "react" }): string {
const envVarName = getPublicProjectEnvVarName(options.type);
return `import { getConvexProvidersConfig } from ${JSON.stringify(options.stackPackageName)};
export default {
providers: getConvexProvidersConfig({
projectId: process.env.${envVarName},
}),
};
`;
}
function createConvexIntegrationConvexConfigContent(stackPackageName: string): string {
const importPath = `${stackPackageName}/convex.config`;
return `import stackAuthComponent from ${JSON.stringify(importPath)};
import { defineApp } from "convex/server";
const app = defineApp();
app.use(stackAuthComponent);
export default app;
`;
}
function integrateConvexConfig(existingContent: string, stackPackageName: string): string | null {
if (!existingContent.includes("defineApp")) {
return null;
}
const newline = existingContent.includes("\r\n") ? "\r\n" : "\n";
const normalizedLines = existingContent.replace(/\r\n/g, "\n").split("\n");
const importPath = `${stackPackageName}/convex.config`;
const hasImport = normalizedLines.some((line) => line.includes(importPath));
if (!hasImport) {
let insertIndex = 0;
while (insertIndex < normalizedLines.length && normalizedLines[insertIndex].trim() === "") {
insertIndex++;
}
while (insertIndex < normalizedLines.length && normalizedLines[insertIndex].trim().startsWith("import")) {
insertIndex++;
}
normalizedLines.splice(insertIndex, 0, `import stackAuthComponent from "${importPath}";`);
}
let lastImportIndex = -1;
for (let i = 0; i < normalizedLines.length; i++) {
if (normalizedLines[i].trim().startsWith("import")) {
lastImportIndex = i;
continue;
}
if (normalizedLines[i].trim() === "") {
continue;
}
break;
}
if (lastImportIndex >= 0) {
const nextIndex = lastImportIndex + 1;
if (!normalizedLines[nextIndex] || normalizedLines[nextIndex].trim() !== "") {
normalizedLines.splice(nextIndex, 0, "");
}
}
const hasStackUse = normalizedLines.some((line) => line.includes("app.use(stackAuthComponent"));
if (!hasStackUse) {
const appLineIndex = normalizedLines.findIndex((line) => /const\s+app\s*=\s*defineApp/.test(line));
if (appLineIndex === -1) {
return null;
}
const indent = normalizedLines[appLineIndex].match(/^\s*/)?.[0] ?? "";
const insertIndexForUse = appLineIndex + 1;
normalizedLines.splice(insertIndexForUse, 0, `${indent}app.use(stackAuthComponent);`);
const nextLineIndex = insertIndexForUse + 1;
if (!normalizedLines[nextLineIndex] || normalizedLines[nextLineIndex].trim() !== "") {
normalizedLines.splice(nextLineIndex, 0, "");
}
}
let updated = normalizedLines.join(newline);
if (!updated.endsWith(newline)) {
updated += newline;
}
return updated;
}
function isSimpleConvexConfig(content: string): boolean {
const normalized = content
.replace(/\r\n/g, "\n")
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
if (normalized.length !== 3) {
return false;
}
const [line1, line2, line3] = normalized;
const importRegex = /^import\s+\{\s*defineApp\s*\}\s+from\s+['"]convex\/server['"];?$/;
const appRegex = /^const\s+app\s*=\s*defineApp\(\s*\);?$/;
const exportRegex = /^export\s+default\s+app;?$/;
return importRegex.test(line1) && appRegex.test(line2) && exportRegex.test(line3);
}
function getPublicProjectEnvVarName(type: "js" | "next" | "react"): string {
if (type === "react") {
return "VITE_PUBLIC_STACK_PROJECT_ID";
}
if (type === "next") {
return "NEXT_PUBLIC_STACK_PROJECT_ID";
}
return "STACK_PROJECT_ID";
}
type ConvexClientUpdateResult = {
updatedFiles: string[],
skippedFiles: string[],
};
type AddSetAuthResult = {
updatedContent: string,
changed: boolean,
usedClientApp: boolean,
usedServerApp: boolean,
instantiationCount: number,
skippedHttpCount: number,
};
async function updateConvexClients({ projectPath, type }: { projectPath: string, type: "js" | "next" | "react" }): Promise<ConvexClientUpdateResult> {
const files = collectConvexClientCandidateFiles(projectPath);
const updatedFiles: string[] = [];
const skippedFiles: string[] = [];
for (const filePath of files) {
const fileContent = await readFile(filePath);
if (!fileContent) continue;
if (!/new\s+Convex(?:React|Http)?Client\b/.test(fileContent)) continue;
const addResult = addSetAuthToConvexClients(fileContent, type);
if (!addResult.changed) {
if (addResult.instantiationCount > 0 && addResult.skippedHttpCount > 0) {
skippedFiles.push(filePath);
}
continue;
}
let finalContent = addResult.updatedContent;
if (addResult.usedClientApp) {
finalContent = await ensureStackAppImport(finalContent, filePath, "client");
}
if (addResult.usedServerApp) {
finalContent = await ensureStackAppImport(finalContent, filePath, "server");
}
if (finalContent !== fileContent) {
laterWriteFile(filePath, finalContent);
updatedFiles.push(filePath);
}
}
return {
updatedFiles,
skippedFiles,
};
}
type StackAppKind = "client" | "server";
async function ensureStackAppImport(content: string, filePath: string, kind: StackAppKind): Promise<string> {
const identifier = kind === "client" ? "stackClientApp" : "stackServerApp";
if (new RegExp(`import\\s+[^;]*\\b${identifier}\\b`).test(content)) {
return content;
}
const stackBasePath = await getStackAppBasePath(kind);
const relativeImportPath = convertToModuleSpecifier(path.relative(path.dirname(filePath), stackBasePath));
const newline = content.includes("\r\n") ? "\r\n" : "\n";
const lines = content.split(/\r?\n/);
const importLine = `import { ${identifier} } from "${relativeImportPath}";`;
let insertIndex = 0;
while (insertIndex < lines.length) {
const line = lines[insertIndex];
if (/^\s*['"]use (client|server)['"];?\s*$/.test(line)) {
insertIndex += 1;
continue;
}
if (/^\s*import\b/.test(line)) {
insertIndex += 1;
continue;
}
if (line.trim() === "") {
insertIndex += 1;
continue;
}
break;
}
lines.splice(insertIndex, 0, importLine);
const nextLine = lines[insertIndex + 1];
if (nextLine && nextLine.trim() !== "" && !/^\s*import\b/.test(nextLine)) {
lines.splice(insertIndex + 1, 0, "");
}
return lines.join(newline);
}
function convertToModuleSpecifier(relativePath: string): string {
let specifier = relativePath.replace(/\\/g, "/");
if (!specifier.startsWith(".")) {
specifier = "./" + specifier;
}
return specifier;
}
async function getStackAppBasePath(kind: StackAppKind): Promise<string> {
const srcPath = await Steps.guessSrcPath();
return path.join(srcPath, "stack", kind);
}
function addSetAuthToConvexClients(content: string, type: "js" | "next" | "react"): AddSetAuthResult {
const newline = content.includes("\r\n") ? "\r\n" : "\n";
const instantiationRegex = /^[ \t]*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+(Convex(?:React|Http)?Client)\b([\s\S]*?);/gm;
const replacements: Array<{ start: number, end: number, text: string }> = [];
let instantiationCount = 0;
let skippedHttpCount = 0;
let usedClientApp = false;
let usedServerApp = false;
let match: RegExpExecArray | null;
while ((match = instantiationRegex.exec(content)) !== null) {
instantiationCount += 1;
const fullMatch = match[0];
const variableName = match[1];
const className = match[2];
if (className === "ConvexHttpClient") {
skippedHttpCount += 1;
continue;
}
const remainder = content.slice(match.index + fullMatch.length);
const setAuthRegex = new RegExp(`\\b${escapeRegExp(variableName)}\\s*\\.setAuth\\s*\\(`);
if (setAuthRegex.test(remainder)) {
continue;
}
const indentation = fullMatch.match(/^[\t ]*/)?.[0] ?? "";
const authCall = determineAuthCallExpression({ type, className, content });
if (authCall.identifier === "stackClientApp") {
usedClientApp = true;
} else {
usedServerApp = true;
}
const replacementText = `${fullMatch}${newline}${indentation}${variableName}.setAuth(${authCall.expression});`;
replacements.push({
start: match.index,
end: match.index + fullMatch.length,
text: replacementText,
});
}
if (replacements.length === 0) {
return {
updatedContent: content,
changed: false,
usedClientApp,
usedServerApp,
instantiationCount,
skippedHttpCount,
};
}
let updatedContent = content;
for (let i = replacements.length - 1; i >= 0; i--) {
const replacement = replacements[i];
updatedContent = `${updatedContent.slice(0, replacement.start)}${replacement.text}${updatedContent.slice(replacement.end)}`;
}
return {
updatedContent,
changed: true,
usedClientApp,
usedServerApp,
instantiationCount,
skippedHttpCount,
};
}
function determineAuthCallExpression({ type, className, content }: { type: "js" | "next" | "react", className: string, content: string }): { expression: string, identifier: "stackClientApp" | "stackServerApp" } {
const hasClientAppReference = /\bstackClientApp\b/.test(content);
const hasServerAppReference = /\bstackServerApp\b/.test(content);
if (type === "js") {
return { expression: "stackServerApp.getConvexClientAuth({})", identifier: "stackServerApp" };
}
if (hasClientAppReference) {
return { expression: getClientAuthCall(type), identifier: "stackClientApp" };
}
if (hasServerAppReference && className !== "ConvexReactClient") {
return { expression: "stackServerApp.getConvexClientAuth({})", identifier: "stackServerApp" };
}
return { expression: getClientAuthCall(type), identifier: "stackClientApp" };
}
function getClientAuthCall(type: "js" | "next" | "react"): string {
return "stackClientApp.getConvexClientAuth({})";
}
function collectConvexClientCandidateFiles(projectPath: string): string[] {
const roots = getConvexSearchRoots(projectPath);
const files = new Set<string>();
const visited = new Set<string>();
for (const root of roots) {
walkDirectory(root, files, visited);
}
return Array.from(files);
}
function getConvexSearchRoots(projectPath: string): string[] {
const candidateDirs = ["convex", "src", "app", "components"];
const existing = candidateDirs
.map((dir) => path.join(projectPath, dir))
.filter((dirPath) => {
try {
return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
} catch {
return false;
}
});
if (existing.length > 0) {
return existing;
}
return [projectPath];
}
const directorySkipList = new Set([
"node_modules",
".git",
".next",
".turbo",
".output",
".vercel",
"dist",
"build",
"coverage",
".cache",
".storybook",
"storybook-static",
]);
function walkDirectory(currentDir: string, files: Set<string>, visited: Set<string>): void {
const realPath = (() => {
try {
return fs.realpathSync(currentDir);
} catch {
return currentDir;
}
})();
if (visited.has(realPath)) return;
visited.add(realPath);
let dirEntries: fs.Dirent[];
try {
dirEntries = fs.readdirSync(realPath, { withFileTypes: true });
} catch {
return;
}
for (const entry of dirEntries) {
const entryName = entry.name;
if (entry.isDirectory()) {
if (directorySkipList.has(entryName)) continue;
if (entryName.startsWith(".") || entryName.startsWith("_")) continue;
walkDirectory(path.join(realPath, entryName), files, visited);
continue;
}
if (!entry.isFile()) continue;
if (entryName.endsWith(".d.ts")) continue;
if (!hasJsLikeExtension(entryName)) continue;
files.add(path.join(realPath, entryName));
}
}
function hasJsLikeExtension(fileName: string): boolean {
return jsLikeFileExtensions.some((ext) => fileName.endsWith(`.${ext}`));
}
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function throwErr(message: string): never {
throw new Error(message);
}

View File

@ -1,11 +1,11 @@
import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
import { KnownErrors, StackClientInterface } from "@stackframe/stack-shared";
import type { CustomerProductsListResponse } from "@stackframe/stack-shared/dist/interface/crud/products";
import { ContactChannelsCrud } from "@stackframe/stack-shared/dist/interface/crud/contact-channels";
import { CurrentUserCrud } from "@stackframe/stack-shared/dist/interface/crud/current-user";
import { ItemCrud } from "@stackframe/stack-shared/dist/interface/crud/items";
import { NotificationPreferenceCrud } from "@stackframe/stack-shared/dist/interface/crud/notification-preferences";
import { OAuthProviderCrud } from "@stackframe/stack-shared/dist/interface/crud/oauth-providers";
import type { CustomerProductsListResponse } from "@stackframe/stack-shared/dist/interface/crud/products";
import { TeamApiKeysCrud, UserApiKeysCrud, teamApiKeysCreateOutputSchema, userApiKeysCreateOutputSchema } from "@stackframe/stack-shared/dist/interface/crud/project-api-keys";
import { ProjectPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/project-permissions";
import { ClientProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
@ -1749,7 +1749,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
// END_PLATFORM
getConvexClientAuth(options: { tokenStore: TokenStoreInit }): (args: { forceRefreshToken: boolean }) => Promise<string | null> {
return async (args: { forceRefreshToken: boolean }) => {
const session = await this._getSession(options.tokenStore);
const session = await this._getSession(options.tokenStore ?? this._tokenStoreInit);
if (!args.forceRefreshToken) {
const tokens = await session.getOrFetchLikelyValidTokens(20_000);
return tokens?.accessToken.token ?? null;

View File

@ -56,7 +56,7 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
redirectToOAuthCallback(): Promise<void>,
getConvexClientAuth(options: { tokenStore: TokenStoreInit }): (args: { forceRefreshToken: boolean }) => Promise<string | null>,
getConvexClientAuth(options: HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit }): (args: { forceRefreshToken: boolean }) => Promise<string | null>,
getConvexHttpClientAuth(options: { tokenStore: TokenStoreInit }): Promise<string>,
// IF_PLATFORM react-like