Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9cea4cb
server: Remove stale eslint-disable and simplify nullish operations
pawiecz Apr 15, 2026
fed9e5d
server: Type apiKey resolvers with generated GQLQueryResolvers/GQLMut…
pawiecz Apr 28, 2026
0ca5836
server: Cast GET route arrays to ControllerRouteList to remove per-ro…
pawiecz Apr 28, 2026
647e434
server: Fix passport typing without unsafe casts
pawiecz Apr 28, 2026
a124c2e
server: Parameterise Kysely instances with concrete schema types
pawiecz Apr 28, 2026
9516238
server: Remove as-any casts in analytics loggers by completing Analyt…
pawiecz Apr 28, 2026
6a25e58
server: Replace deprecated SEMATTRS_EXCEPTION_* otel constants
pawiecz Apr 30, 2026
b6e02db
server: Drop unnecessary conditionals and type assertions
pawiecz Apr 30, 2026
1b0548f
server: lower NCMEC file-annotation mapping complexity from 21 to 3
pawiecz Apr 30, 2026
3b392a8
server: Replace "any" with real types in production code
pawiecz Apr 30, 2026
1903965
server: Scope "any" suppressions to documented TODOs
pawiecz Apr 30, 2026
6875f4f
server: Tighten test mocks and clean up test-file "any" usage
pawiecz Apr 30, 2026
7c836be
server: Keep single-case exhaustiveness switches, scope the warning
pawiecz Apr 30, 2026
39dc340
Merge branch 'main' into fix-eslint-server
pawiecz Apr 30, 2026
6f6295d
Fix merge conflicts
pawiecz Apr 30, 2026
5f784c4
server: Drop residual Sequelize toJSON() calls
pawiecz Apr 30, 2026
abba19c
server: Fix unreachable null check in MRT job
pawiecz Apr 30, 2026
6ef6f07
Merge remote-tracking branch 'origin/main' into pr-331
juanmrad May 21, 2026
6e2494b
address PR review feedback
juanmrad May 21, 2026
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
14 changes: 7 additions & 7 deletions server/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { MapperKind, mapSchema } from '@graphql-tools/utils';
import { MultiSamlStrategy } from '@node-saml/passport-saml';
import { SpanStatusCode } from '@opentelemetry/api';
import {
SEMATTRS_EXCEPTION_MESSAGE,
SEMATTRS_EXCEPTION_STACKTRACE,
SEMATTRS_EXCEPTION_TYPE,
ATTR_EXCEPTION_MESSAGE,
ATTR_EXCEPTION_STACKTRACE,
ATTR_EXCEPTION_TYPE,
} from '@opentelemetry/semantic-conventions';
import connectPgSimple from 'connect-pg-simple';
import cors from 'cors';
Expand Down Expand Up @@ -276,7 +276,7 @@ export default async function makeApiServer(deps: Dependencies) {
},
);

passport.serializeUser((user: any, done) => {
passport.serializeUser((user, done) => {
done(null, user.id);
});

Expand Down Expand Up @@ -422,11 +422,11 @@ export default async function makeApiServer(deps: Dependencies) {
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });

// I don't know if these attributes are necessary, with recordException
span.setAttribute(SEMATTRS_EXCEPTION_MESSAGE, err.message);
span.setAttribute(ATTR_EXCEPTION_MESSAGE, err.message);
if (err.stack) {
span.setAttribute(SEMATTRS_EXCEPTION_STACKTRACE, err.stack);
span.setAttribute(ATTR_EXCEPTION_STACKTRACE, err.stack);
}
span.setAttribute(SEMATTRS_EXCEPTION_TYPE, err.name);
span.setAttribute(ATTR_EXCEPTION_TYPE, err.name);

const errors = (() => {
if (err instanceof AggregateError) {
Expand Down
6 changes: 4 additions & 2 deletions server/bin/run-worker-or-job.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
#!/usr/bin/env node
import _ from 'lodash';

import getBottle from '../iocContainer/index.js';
import getBottle, { type Dependencies } from '../iocContainer/index.js';
import { logErrorJson } from '../utils/logging.js';
import { type WorkerOrJob } from '../workers_jobs/index.js';

const { container } = await getBottle();

const workerOrJobName = process.argv[2];
const workerOrJob = (container as any)[workerOrJobName] as WorkerOrJob;
const workerOrJob = container[
workerOrJobName as keyof Dependencies
] as WorkerOrJob;
const controller = new AbortController();

// When the worker/job finishes naturally (which only applies to jobs, as
Expand Down
4 changes: 4 additions & 0 deletions server/condition_evaluator/conditionSet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,12 @@ describe('Condition Evaluation', () => {

await getConditionSetResults(
{ conditions, conjunction: ConditionConjunction.OR },
// Tests only exercise getSignalCost / tracer.getActiveSpan;
// a full RuleEvaluationContext / SafeTracer is unnecessary.
/* eslint-disable @typescript-eslint/no-explicit-any */
{ getSignalCost } as any,
jest.fn() as any,
/* eslint-enable @typescript-eslint/no-explicit-any */
stubRunLeafCondition,
);

Expand Down
39 changes: 24 additions & 15 deletions server/condition_evaluator/conditionSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,19 @@ export async function getConditionSetResults(
runLeafCondition = defaultRunLeafCondition,
): Promise<Required<ConditionSetWithResult>> {
const { conjunction } = conditionSet;
const result: ConditionSetWithResult = {
conjunction,
conditions: [] as unknown as
| NonEmptyArray<LeafConditionWithResult>
| NonEmptyArray<ConditionSetWithResult>,
};
// We collect mixed condition results during the loop, then narrow back to
// the public "ConditionSetWithResult.conditions" shape at return time. The
// public type is a discriminated union of "all leaves" or "all sets", which
// can't be safely pushed into; the array's runtime contents stay uniform
// because the input "conditionSet.conditions" is uniform.
//
// The element type is "ReadonlyDeep<...>" here because the inputs we push
// come from "ReadonlyDeep<ConditionSet>". We cast back to the mutable
// shape at the return site below; the cast is sound because the data is
// freshly built and never mutated by callers.
const conditionsWithResult: Array<
ReadonlyDeep<LeafConditionWithResult | ConditionSetWithResult>
> = [];

// The conditions, sorted with lowest cost ones first.
const getSignalCost = evaluationContext.getSignalCost.bind(evaluationContext);
Expand All @@ -99,17 +106,17 @@ export async function getConditionSetResults(
// if we can determine it before evaluating all conditions.
let finalOutcome: ConditionOutcome | undefined;

// This is basically mapping `conditions` to ConditionWithResult, and putting
// the mapped array in result.conditions. But we don't use `map` because we
// want to run the conditions in sequence (i.e., with an `await` on each loop
// This is basically mapping "conditions" to ConditionWithResult, and pushing
// them onto "conditionsWithResult". But we don't use "map" because we want to
// run the conditions in sequence (i.e., with an "await" on each loop
// iteration), and map would run them in parallel.
for (const condition of sortedConditions) {
// If we already know the final outcome for the condition set, then we can
// skip all subsequent conditions, and a condition that's skipped has an
// identical representation for its ConditionWithResult, so we just push
// the skipped condition into result.conditions.
// the skipped condition through.
if (finalOutcome !== undefined) {
result.conditions.push(condition as any);
conditionsWithResult.push(condition);
continue;
}

Expand All @@ -133,14 +140,14 @@ export async function getConditionSetResults(
),
};

result.conditions.push(conditionWithResult as any);
conditionsWithResult.push(conditionWithResult);
// console.log('RESULT ' + conditionWithResult.result.outcome);

// Attempt to determine the result for the whole condition set from the
// outcomes so far. If we can, save that result to skip running each
// condition for the rest of the loop
const conditionSetOutcome = tryGetOutcomeFromPartialOutcomes(
result.conditions.map((c) => c.result!.outcome),
conditionsWithResult.map((c) => c.result!.outcome),
conjunction,
);

Expand All @@ -150,12 +157,14 @@ export async function getConditionSetResults(
}

return {
...result,
conjunction,
conditions:
conditionsWithResult as ConditionSetWithResult['conditions'],
result: {
outcome:
finalOutcome ??
getConditionSetOutcome(
result.conditions.map((c) => c.result!.outcome),
conditionsWithResult.map((c) => c.result!.outcome),
conjunction,
),
},
Expand Down
4 changes: 2 additions & 2 deletions server/condition_evaluator/leafCondition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export async function runLeafCondition(
});
}),
)
: signalInputValues.map((it): SignalResult<any> => {
: signalInputValues.map((it): SignalResult<SignalOutputType> => {
// If the condition specified no signal, then we should act as though
// the "identity" signal was used; i.e., the value that the condition
// picked out (with `condition.input`) should be returned as-is.
Expand Down Expand Up @@ -259,7 +259,7 @@ export async function runLeafCondition(
it.score,
condition.threshold,
condition.comparator,
it.outputType as SignalOutputType,
it.outputType,
)
: (it as SignalResult<{ scalarType: ScalarTypes['BOOLEAN'] }>).score,
),
Expand Down
8 changes: 8 additions & 0 deletions server/decs.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
declare namespace Express {
// Extend Express.User so passport callbacks (serializeUser, etc.) see the
// fields we actually use without per-call `as any` casts.
interface User {
id: string;
}
}

declare module 'homoglyph-search';
declare module 'nilsimsa';

Expand Down
11 changes: 9 additions & 2 deletions server/graphql/datasources/UserApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
} from '../../utils/errors.js';
import { safePick } from '../../utils/misc.js';
import { WEEK_MS } from '../../utils/time.js';
import {
type GQLMutationLoginArgs,
type GQLMutationSignUpArgs,
} from '../generated.js';
import { type PassportGqlContext } from '../utils/passportContext.js';
import { buildGraphqlRuleParent } from './buildGraphqlRuleParent.js';
import { type GraphQLRuleParent } from './ruleKyselyPersistence.js';
Expand Down Expand Up @@ -74,7 +78,7 @@ class UserAPI {
return kyselyUserFindByIds(this.kyselyPg, ids);
}

async login(params: any, context: PassportGqlContext) {
async login(params: GQLMutationLoginArgs, context: PassportGqlContext) {
const { email, password } = safePick(params.input, ['email', 'password']);

// Reject missing/empty credentials as a bad request before verifying.
Expand Down Expand Up @@ -118,7 +122,10 @@ class UserAPI {
}
}

async signUp(params: any, _: any): Promise<GraphQLUserParent> {
async signUp(
params: GQLMutationSignUpArgs,
_: unknown,
): Promise<GraphQLUserParent> {
const { role } = params.input;
const {
email,
Expand Down
34 changes: 10 additions & 24 deletions server/graphql/modules/apiKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,7 @@ import { ErrorType, CoopError } from '../../utils/errors.js';
import { logErrorJson } from '../../utils/logging.js';
import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js';
import { forbiddenError } from '../utils/errors.js';

/** Context shape required by rotateWebhookSigningKey (avoids importing resolvers). */
type RotateWebhookSigningKeyContext = {
getUser: () => {
orgId: string;
getPermissions: () => readonly string[];
} | null | undefined;
dataSources: {
orgAPI: { rotateWebhookSigningKey: (orgId: string) => Promise<string> };
};
};
import { type GQLMutationResolvers, type GQLQueryResolvers } from '../generated.js';

const typeDefs = /* GraphQL */ `
type ApiKey {
Expand Down Expand Up @@ -73,8 +63,8 @@ const typeDefs = /* GraphQL */ `
}
`;

const Query: any = {
async apiKey(_: any, __: any, context: any) {
const Query: GQLQueryResolvers = {
async apiKey(_, __, context) {
const user = context.getUser();
if (!user || !user.orgId) {
throw forbiddenError('User does not have permission to check if key exists');
Expand All @@ -89,8 +79,8 @@ const Query: any = {
},
};

const Mutation: any = {
async rotateApiKey(_: any, { input }: any, context: any) {
const Mutation: GQLMutationResolvers = {
async rotateApiKey(_, { input }, context) {
const user = context.getUser();
if (!user || !user.orgId) {
throw forbiddenError('User does not have permission to rotate the API key');
Expand All @@ -103,7 +93,7 @@ const Mutation: any = {
const { apiKey, record } = await context.services.ApiKeyService.rotateApiKey(
user.orgId,
input.name,
input.description || null,
input.description ?? null,
user.id
);

Expand All @@ -116,7 +106,7 @@ const Mutation: any = {
description: record.description,
isActive: record.isActive,
createdAt: record.createdAt.toISOString(),
lastUsedAt: record.lastUsedAt?.toISOString() || null,
lastUsedAt: record.lastUsedAt?.toISOString() ?? null,
createdBy: record.createdBy,
},
},
Expand All @@ -132,17 +122,13 @@ const Mutation: any = {
type: [ErrorType.InternalServerError],
title: 'Failed to rotate API key',
detail: 'An error occurred while rotating the API key',
name: 'InternalServerError',
name: 'RotateApiKeyError',
shouldErrorSpan: true,
}),
);
}
},
async rotateWebhookSigningKey(
_: unknown,
__: Record<string, never>,
context: RotateWebhookSigningKeyContext,
) {
async rotateWebhookSigningKey(_, __, context) {
const user = context.getUser();
if (!user || !user.orgId) {
throw forbiddenError('User does not have permission to rotate the webhook signing key');
Expand Down Expand Up @@ -171,7 +157,7 @@ const Mutation: any = {
type: [ErrorType.InternalServerError],
title: 'Failed to rotate webhook signing key',
detail: 'An error occurred while rotating the webhook signing key',
name: 'InternalServerError',
name: 'RotateWebhookSigningKeyError',
shouldErrorSpan: true,
}),
);
Expand Down
Loading
Loading