Merge dev into update-oauth-docs

This commit is contained in:
Konsti Wohlwend 2025-10-19 04:31:17 -07:00 committed by GitHub
commit fbe3bdb577
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 25 deletions

View File

@ -9,6 +9,10 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { SmartRouter } from './smart-router';
const DEV_RATE_LIMIT_MAX_REQUESTS = 30;
const DEV_RATE_LIMIT_WINDOW_MS = 10_000;
const devRateLimitTimestamps: number[] = [];
const corsAllowedRequestHeaders = [
// General
'content-type',
@ -65,6 +69,50 @@ export async function middleware(request: NextRequest) {
const url = new URL(request.url);
const isApiRequest = url.pathname.startsWith('/api/');
const corsHeadersInit = isApiRequest ? {
// CORS headers
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Max-Age": "86400", // 1 day (capped to lower values, eg. 10min, by some browsers)
"Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '),
"Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '),
} : undefined;
// ensure our clients can handle 429 responses
if (isApiRequest && getNodeEnvironment() === 'development' && request.method !== 'OPTIONS') {
const now = Date.now();
while (devRateLimitTimestamps.length > 0 && now - devRateLimitTimestamps[0] > DEV_RATE_LIMIT_WINDOW_MS) {
devRateLimitTimestamps.shift();
}
console.log('devRateLimitTimestamps', devRateLimitTimestamps.length);
if (devRateLimitTimestamps.length >= DEV_RATE_LIMIT_MAX_REQUESTS) {
const waitMs = Math.max(0, DEV_RATE_LIMIT_WINDOW_MS - (now - devRateLimitTimestamps[0]));
const retryAfterSeconds = Math.max(1, Math.ceil(waitMs / 1000));
const response = NextResponse.json({
message: 'Artificial development rate limit triggered. Wait before retrying.',
}, {
status: 429,
});
// since not all firewalls return CORS headers with their 429 responses, 50% chance that we don't set the CORS headers
if (Math.random() < 0.5 && corsHeadersInit) {
for (const [key, value] of Object.entries(corsHeadersInit)) {
response.headers.set(key, value);
}
}
if (Math.random() < 0.5) {
// for debugging, make sure we don't always set the Retry-After header
response.headers.set('Retry-After', retryAfterSeconds.toString());
}
return response;
} else {
devRateLimitTimestamps.push(now);
}
}
const newRequestHeaders = new Headers(request.headers);
// here we could update the request headers (currently we don't)
@ -72,14 +120,7 @@ export async function middleware(request: NextRequest) {
request: {
headers: newRequestHeaders,
},
headers: {
// CORS headers
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Max-Age": "86400", // 1 day (capped to lower values, eg. 10min, by some browsers)
"Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '),
"Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '),
},
headers: corsHeadersInit,
} as const : undefined;
// we want to allow preflight requests to pass through

View File

@ -21,6 +21,7 @@ import { CurrentUserCrud } from './crud/current-user';
import { ItemCrud } from './crud/items';
import { NotificationPreferenceCrud } from './crud/notification-preferences';
import { OAuthProviderCrud } from './crud/oauth-providers';
import { CustomerProductsListResponse, ListCustomerProductsOptions } from './crud/products';
import { TeamApiKeysCrud, UserApiKeysCrud, teamApiKeysCreateInputSchema, teamApiKeysCreateOutputSchema, userApiKeysCreateInputSchema, userApiKeysCreateOutputSchema } from './crud/project-api-keys';
import { ProjectPermissionsCrud } from './crud/project-permissions';
import { AdminUserProjectsCrud, ClientProjectsCrud } from './crud/projects';
@ -29,7 +30,6 @@ import { TeamInvitationCrud } from './crud/team-invitation';
import { TeamMemberProfilesCrud } from './crud/team-member-profiles';
import { TeamPermissionsCrud } from './crud/team-permissions';
import { TeamsCrud } from './crud/teams';
import { CustomerProductsListResponse, ListCustomerProductsOptions } from './crud/products';
export type ClientInterfaceOptions = {
clientVersion: string,
@ -156,29 +156,33 @@ export class StackClientInterface {
token_endpoint_auth_method: 'client_secret_post',
};
const rawResponse = await this._networkRetryException(
async () => await oauth.refreshTokenGrantRequest(
const response = await this._networkRetryException(async () => {
const rawResponse = await oauth.refreshTokenGrantRequest(
as,
client,
refreshToken.token,
)
);
const response = await this._processResponse(rawResponse);
);
if (response.status === "error") {
const error = response.error;
if (KnownErrors.RefreshTokenError.isInstance(error)) {
return null;
const response = await this._processResponse(rawResponse);
if (response.status === "error") {
const error = response.error;
if (KnownErrors.RefreshTokenError.isInstance(error)) {
return null;
}
throw error;
}
throw error;
}
if (!response.data.ok) {
const body = await response.data.text();
throw new Error(`Failed to send refresh token request: ${response.status} ${body}`);
}
if (!response.data.ok) {
const body = await response.data.text();
throw new Error(`Failed to send refresh token request: ${response.status} ${body}`);
}
const result = await oauth.processRefreshTokenResponse(as, client, response.data);
return response.data;
});
if (!response) return null;
const result = await oauth.processRefreshTokenResponse(as, client, response);
if (oauth.isOAuth2Error(result)) {
// TODO Handle OAuth 2.0 response body error
throw new StackAssertionError("OAuth error", { result });