Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"$schema": "node_modules/fallow/schema.json",
"ignorePatterns": [
"src/gen/**/*_connect.ts",
"src/gen/**/file_*.ts"
"src/gen/**/file_*.ts",
"src/gen/**/*.js"
]
}
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@
"drizzle-kit": "^0.31.6",
"fallow": "^2.64.0",
"grpc-tools": "^1.13.1",
"ts-protoc-gen": "^0.15.0",
"tsx": "^4.20.6",
"vitest": "^4.0.3"
"vitest": "^4.0.3",
"tsx": "^4.20.6"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {

"@grpc/grpc-js": "^1.12.0",
"bullmq": "^5.75.2",
"dodopayments": "^2.30.0",
"drizzle-orm": "^0.44.7",
Expand Down
4 changes: 4 additions & 0 deletions src/context/requestContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class WideEventBuilder {
/**
* Set authentication context after successful auth.
*/
// fallow-ignore-next-line unused-class-member
setAuth(apiKeyId: string | number, cacheHit: boolean): this {
this.event.apiKeyId = apiKeyId;
this.event.cacheHit = cacheHit;
Expand All @@ -69,6 +70,7 @@ export class WideEventBuilder {
/**
* Set event processing context.
*/
// fallow-ignore-next-line unused-class-member
setEventContext(data: { eventType?: string; eventCount?: number }): this {
if (data.eventType !== undefined) this.event.eventType = data.eventType;
if (data.eventCount !== undefined) this.event.eventCount = data.eventCount;
Expand Down Expand Up @@ -97,6 +99,7 @@ export class WideEventBuilder {
/**
* Set API key creation context.
*/
// fallow-ignore-next-line unused-class-member
setApiKeyContext(data: { name?: string; expiration?: string }): this {
if (data.name !== undefined) this.event.apiKeyName = data.name;
if (data.expiration !== undefined)
Expand Down Expand Up @@ -147,6 +150,7 @@ export class WideEventBuilder {
/**
* Build the final wide event with duration calculation.
*/
// fallow-ignore-next-line unused-class-member
build(): WideEvent {
const durationMs = DateTime.utc().toMillis() - this.startTime;

Expand Down
5 changes: 5 additions & 0 deletions src/errors/apikey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class APIKeyError extends Error {
this.code = context.code;
}

// fallow-ignore-next-line unused-class-member
static invalidExpiration(
details?: string,
originalError?: Error
Expand All @@ -44,6 +45,7 @@ export class APIKeyError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static invalidName(details?: string, originalError?: Error): APIKeyError {
return new APIKeyError({
type: APIKeyErrorType.INVALID_NAME,
Expand All @@ -64,6 +66,7 @@ export class APIKeyError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static notFound(apiKeyId?: string, originalError?: Error): APIKeyError {
return new APIKeyError({
type: APIKeyErrorType.NOT_FOUND,
Expand All @@ -75,6 +78,7 @@ export class APIKeyError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static revocationFailed(
details?: string,
originalError?: Error
Expand All @@ -98,6 +102,7 @@ export class APIKeyError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static unknown(originalError?: Error): APIKeyError {
const details = originalError?.message || "No details available";
return new APIKeyError({
Expand Down
2 changes: 2 additions & 0 deletions src/errors/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class AuthError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static databaseError(originalError?: Error): AuthError {
return new AuthError({
type: AuthErrorType.DATABASE_ERROR,
Expand All @@ -79,6 +80,7 @@ export class AuthError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static unknown(originalError?: Error): AuthError {
const details = originalError?.message || "No details available";
return new AuthError({
Expand Down
6 changes: 6 additions & 0 deletions src/errors/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class EventError extends Error {
this.code = context.code;
}

// fallow-ignore-next-line unused-class-member
static invalidPayload(details?: string, originalError?: Error): EventError {
return new EventError({
type: EventErrorType.INVALID_PAYLOAD,
Expand Down Expand Up @@ -63,6 +64,7 @@ export class EventError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static serializationError(
details?: string,
originalError?: Error
Expand All @@ -77,6 +79,7 @@ export class EventError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static invalidUserId(userId?: string, originalError?: Error): EventError {
return new EventError({
type: EventErrorType.INVALID_USER_ID,
Expand All @@ -86,6 +89,7 @@ export class EventError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static missingData(field: string, originalError?: Error): EventError {
return new EventError({
type: EventErrorType.MISSING_DATA,
Expand All @@ -95,6 +99,7 @@ export class EventError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static invalidDataFormat(
field: string,
expectedFormat: string,
Expand All @@ -108,6 +113,7 @@ export class EventError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static unknown(originalError?: Error): EventError {
const details = originalError?.message || "No details available";
return new EventError({
Expand Down
10 changes: 10 additions & 0 deletions src/errors/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class PaymentError extends Error {
this.code = context.code;
}

// fallow-ignore-next-line unused-class-member
static invalidUserId(userId?: string, originalError?: Error): PaymentError {
return new PaymentError({
type: PaymentErrorType.INVALID_USER_ID,
Expand All @@ -45,6 +46,7 @@ export class PaymentError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static checkoutCreationFailed(
details?: string,
originalError?: Error
Expand Down Expand Up @@ -72,6 +74,8 @@ export class PaymentError extends Error {
});
}

// fallow-ignore-next-line unused-class-members
// fallow-ignore-next-line unused-class-member
static paymentProviderApiError(
details?: string,
originalError?: Error
Expand All @@ -95,6 +99,8 @@ export class PaymentError extends Error {
});
}

// fallow-ignore-next-line unused-class-members
// fallow-ignore-next-line unused-class-member
static missingStoreId(originalError?: Error): PaymentError {
return new PaymentError({
type: PaymentErrorType.MISSING_STORE_ID,
Expand All @@ -104,6 +110,8 @@ export class PaymentError extends Error {
});
}

// fallow-ignore-next-line unused-class-members
// fallow-ignore-next-line unused-class-member
static missingVariantId(originalError?: Error): PaymentError {
return new PaymentError({
type: PaymentErrorType.MISSING_VARIANT_ID,
Expand Down Expand Up @@ -164,6 +172,7 @@ export class PaymentError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static configurationError(
details?: string,
originalError?: Error
Expand All @@ -178,6 +187,7 @@ export class PaymentError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static unknown(originalError?: Error): PaymentError {
const details = originalError?.message || "No details available";
return new PaymentError({
Expand Down
4 changes: 4 additions & 0 deletions src/errors/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class StorageError extends Error {
this.code = context.code;
}

// fallow-ignore-next-line unused-class-member
static connectionFailed(
details?: string,
originalError?: Error
Expand Down Expand Up @@ -102,6 +103,7 @@ export class StorageError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static dataNotFound(entity?: string, originalError?: Error): StorageError {
return new StorageError({
type: StorageErrorType.CONNECTION_FAILED,
Expand Down Expand Up @@ -173,6 +175,7 @@ export class StorageError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static userInsertFailed(
userId?: string,
originalError?: Error
Expand Down Expand Up @@ -226,6 +229,7 @@ export class StorageError extends Error {
});
}

// fallow-ignore-next-line unused-class-member
static unknown(originalError?: Error): StorageError {
const details = originalError?.message || "No details available";
return new StorageError({
Expand Down
1 change: 1 addition & 0 deletions src/events/AIEvents/AITokenUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class AITokenUsage implements AITokenUsageEvent {
this.ingested_timestamp = DateTime.utc();
}

// fallow-ignore-next-line unused-class-member
serialize() {
return {
SQL: {
Expand Down
1 change: 1 addition & 0 deletions src/events/RawEvents/SDKCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class SDKCall implements SDKCallEvent {
this.ingested_timestamp = DateTime.utc();
}

// fallow-ignore-next-line unused-class-member
serialize() {
return {
SQL: {
Expand Down
60 changes: 32 additions & 28 deletions src/interceptors/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,45 +63,25 @@ export function authInterceptor<Req, Res>(
handler: GrpcHandler<Req, Res>
): GrpcHandler<Req, Res> {
return (call: GrpcCall<Req, Res>, callback) => {
// Skip auth for whitelisted endpoints
const fullPath = methodPath.startsWith("/") ? methodPath : `/${methodPath}`;
if (no_auth.some((path) => fullPath === path || fullPath.endsWith(path))) {
if (isWhitelistedEndpoint(methodPath)) {
return handler(call, callback);
}

const wideEventBuilder = call[wideEventContextKey];

// Extract authorization from metadata
const authHeader = call.metadata.get("authorization")?.[0] as
| string
| undefined;

if (!authHeader) {
return callback?.(AuthError.missingHeader());
}

if (!authHeader.startsWith("Bearer ")) {
return callback?.(AuthError.invalidHeaderFormat());
}

const apiKey = authHeader.slice("Bearer ".length).trim();

// Validate API key format
if (!apiKey.startsWith("scrn_") || apiKey.length !== 37) {
return callback?.(AuthError.invalidAPIKey("Invalid API key format"));
const authResult = extractAndValidateAuth(call);
if (authResult.error) {
return callback?.(authResult.error);
}

const apiKey = authResult.apiKey!;
const apiKeyHash = hashAPIKey(apiKey);

// Check cache first
const cached = apiKeyCache.get(apiKeyHash);
if (cached) {
call[apiKeyContextKey] = cached.id;
wideEventBuilder?.setAuth(cached.id, true);
call[wideEventContextKey]?.setAuth(cached.id, true);
return handler(call, callback);
}

// Query database for API key
lookupApiKey(apiKeyHash)
.then((apiKeyRecord) => {
if (!apiKeyRecord) {
Expand All @@ -116,14 +96,13 @@ export function authInterceptor<Req, Res>(
return callback?.(AuthError.expiredAPIKey());
}

// Cache and set context
apiKeyCache.set(apiKeyHash, {
id: apiKeyRecord.id,
expiresAt: apiKeyRecord.expiresAt,
});

call[apiKeyContextKey] = apiKeyRecord.id;
wideEventBuilder?.setAuth(apiKeyRecord.id, false);
call[wideEventContextKey]?.setAuth(apiKeyRecord.id, false);

return handler(call, callback);
})
Expand All @@ -133,6 +112,31 @@ export function authInterceptor<Req, Res>(
};
}

function isWhitelistedEndpoint(methodPath: string): boolean {
const fullPath = methodPath.startsWith("/") ? methodPath : `/${methodPath}`;
return no_auth.some((path) => fullPath === path || fullPath.endsWith(path));
}

function extractAndValidateAuth(call: GrpcCall<unknown, unknown>): { apiKey?: string; error?: Error } {
const authHeader = call.metadata.get("authorization")?.[0] as string | undefined;

if (!authHeader) {
return { error: AuthError.missingHeader() };
}

if (!authHeader.startsWith("Bearer ")) {
return { error: AuthError.invalidHeaderFormat() };
}

const apiKey = authHeader.slice("Bearer ".length).trim();

if (!apiKey.startsWith("scrn_") || apiKey.length !== 37) {
return { error: AuthError.invalidAPIKey("Invalid API key format") };
}

return { apiKey };
}

async function lookupApiKey(apiKeyHash: string) {
const db = getPostgresDB();
const result = await db
Expand Down
Loading
Loading