diff --git a/architecture.mmd b/architecture.mmd
index 519aaa82ec..dc2d9fd2a0 100644
--- a/architecture.mmd
+++ b/architecture.mmd
@@ -67,8 +67,14 @@ graph TB
AccessRules["Access Rules Engine
spam / traffic / geo filters"]
end
+ %% Captcha request records (sessions, powcaptchas, imagecaptchas a.k.a.
+ %% commitments) each carry a `requestType` discriminator
+ %% (session / powcaptcha / imagecaptcha / puzzlecaptcha) so every record
+ %% is self-describing ahead of consolidating them into one `requests`
+ %% collection. Phase 1: discriminator added; physical collections are
+ %% unchanged. "UserCommitment" record/type renamed to "ImageCaptcha".
subgraph Storage["Storage"]
- MongoDB[(MongoDB
captchas, commitments,
clients, sessions)]
+ MongoDB[(MongoDB
captchas, imagecaptchas,
powcaptchas, clients, sessions
+ requestType discriminator)]
Redis[(Redis
session cache,
write queue)]
Dataset[(Dataset
labelled + unlabelled
captcha images)]
end
diff --git a/packages/database/src/databases/captcha.ts b/packages/database/src/databases/captcha.ts
index 37e2b5669d..900bc20a9b 100644
--- a/packages/database/src/databases/captcha.ts
+++ b/packages/database/src/databases/captcha.ts
@@ -17,13 +17,13 @@ import { type Logger, getLogger } from "@prosopo/logger";
import {
type CaptchaProperties,
type ICaptchaDatabase,
+ type ImageCaptchaRecord,
type PoWCaptchaRecord,
+ StoredImageCaptchaRecordSchema,
StoredPoWCaptchaRecordSchema,
type StoredSession,
StoredSessionRecordSchema,
- StoredUserCommitmentRecordSchema,
type Tables,
- type UserCommitmentRecord,
} from "@prosopo/types-database";
import type { RootFilterQuery } from "mongoose";
import { MongoDatabase } from "../base/index.js";
@@ -51,7 +51,7 @@ const CAPTCHA_TABLES = [
{
collectionName: TableNames.commitment,
modelName: "UserCommitment",
- schema: StoredUserCommitmentRecordSchema,
+ schema: StoredImageCaptchaRecordSchema,
},
];
@@ -126,7 +126,7 @@ export class CaptchaDatabase extends MongoDatabase implements ICaptchaDatabase {
async saveCaptchas(
sessionEvents: StoredSession[],
- imageCaptchaEvents: UserCommitmentRecord[],
+ imageCaptchaEvents: ImageCaptchaRecord[],
powCaptchaEvents: PoWCaptchaRecord[],
) {
await this.connect();
@@ -206,7 +206,7 @@ export class CaptchaDatabase extends MongoDatabase implements ICaptchaDatabase {
filter: RootFilterQuery = {},
limit = 100,
): Promise<{
- userCommitmentRecords: UserCommitmentRecord[];
+ imageCaptchaRecords: ImageCaptchaRecord[];
powCaptchaRecords: PoWCaptchaRecord[];
}> {
await this.connect();
@@ -215,7 +215,7 @@ export class CaptchaDatabase extends MongoDatabase implements ICaptchaDatabase {
const commitmentResults = await this.tables.commitment
.find(filter)
.limit(limit)
- .lean();
+ .lean();
const powCaptchaResults = await this.tables.powcaptcha
.find(filter)
@@ -223,7 +223,7 @@ export class CaptchaDatabase extends MongoDatabase implements ICaptchaDatabase {
.lean();
return {
- userCommitmentRecords: commitmentResults,
+ imageCaptchaRecords: commitmentResults,
powCaptchaRecords: powCaptchaResults,
};
} catch (error) {
diff --git a/packages/database/src/databases/centralDbStreamer.ts b/packages/database/src/databases/centralDbStreamer.ts
index a764d8237d..5f70c03083 100644
--- a/packages/database/src/databases/centralDbStreamer.ts
+++ b/packages/database/src/databases/centralDbStreamer.ts
@@ -14,9 +14,9 @@
import { type Logger, getLogger } from "@prosopo/logger";
import type {
+ ImageCaptchaRecord,
PoWCaptchaRecord,
StoredSession,
- UserCommitmentRecord,
} from "@prosopo/types-database";
import { CaptchaDatabase } from "./captcha.js";
@@ -154,7 +154,7 @@ export class CentralDbStreamer {
* Fire-and-forget: errors are logged, never thrown.
*/
streamImageRecord(
- record: UserCommitmentRecord,
+ record: ImageCaptchaRecord,
markStored?: MarkStoredCallback,
): void {
const timestamp = this.getRecordTimestamp(record);
@@ -180,7 +180,7 @@ export class CentralDbStreamer {
* Stream an image captcha update by fetching the full record first.
*/
streamImageUpdate(
- getFullRecord: () => Promise,
+ getFullRecord: () => Promise,
markStored?: MarkStoredCallback,
): void {
getFullRecord()
diff --git a/packages/database/src/databases/provider.ts b/packages/database/src/databases/provider.ts
index ed9459183e..5701d88725 100644
--- a/packages/database/src/databases/provider.ts
+++ b/packages/database/src/databases/provider.ts
@@ -43,6 +43,7 @@ import {
type PoWChallengeId,
type PuzzleCaptchaStored,
type RequestHeaders,
+ RequestType,
type ResultReason,
type ScheduledTaskNames,
type ScheduledTaskResult,
@@ -124,6 +125,12 @@ const PROVIDER_TABLES = [
},
{
collectionName: TableNames.powcaptcha,
+ // NOTE: model name is "PowCaptcha" (lowercase w). The private-DB reader
+ // (@prosopo/database-private PoWCaptchaData) registers "PoWCaptcha"
+ // (capital W); both pluralize to the same physical `powcaptchas`
+ // collection, so they coexist. This inconsistency is resolved when the
+ // records are merged into the unified `requests` collection — do not
+ // "fix" either string in isolation, as changing it moves the collection.
modelName: "PowCaptcha",
schema: PoWCaptchaRecordSchema,
},
@@ -669,7 +676,7 @@ export class ProviderDatabase
/**
* @description Store a Dapp User's captcha solution commitment
*/
- async storeUserImageCaptchaSolution(
+ async storeImageCaptchaSolution(
captchas: CaptchaSolution[],
commit: UserCommitment,
): Promise {
@@ -1239,7 +1246,7 @@ export class ProviderDatabase
/** @description Get serverChecked Dapp User image captcha commitments from the commitments table
*/
- async getCheckedDappUserCommitments(): Promise {
+ async getCheckedImageCaptchas(): Promise {
const filter: {
[key in keyof Pick]: boolean;
} = { [StoredStatusNames.serverChecked]: true };
@@ -1254,13 +1261,13 @@ export class ProviderDatabase
*
* Served by the `pendingStage_partial` index. Records have
* `pendingStage: true` set on insert and on every mutation (see
- * `updateDappUserCommitment`, `markDappUserCommitmentsChecked`,
- * `approveDappUserCommitment`, `disapproveDappUserCommitment`,
- * `storePendingImageCommitment`). `markDappUserCommitmentsStored` clears
+ * `updateImageCaptcha`, `markImageCaptchasChecked`,
+ * `approveImageCaptcha`, `disapproveImageCaptcha`,
+ * `storePendingImageCommitment`). `markImageCaptchasStored` clears
* the flag after a successful stage, guarded by `lastUpdatedTimestamp`
* so an in-flight update isn't lost.
*/
- async getUnstoredDappUserCommitments(
+ async getUnstoredImageCaptchas(
limit = 1000,
skip = 0,
): Promise {
@@ -1281,7 +1288,7 @@ export class ProviderDatabase
* fetch and mark-stored — those records will leave pendingStage set so
* the next sweep picks them up.
*/
- async markDappUserCommitmentsStored(
+ async markImageCaptchasStored(
commitmentIds: Hash[],
asOfTimestamp: Date = new Date(),
): Promise {
@@ -1300,7 +1307,7 @@ export class ProviderDatabase
/** @description Mark a list of captcha commits as checked
*/
- async markDappUserCommitmentsChecked(commitmentIds: Hash[]): Promise {
+ async markImageCaptchasChecked(commitmentIds: Hash[]): Promise {
const updateDoc: Pick<
StoredCaptcha,
"serverChecked" | "lastUpdatedTimestamp" | "pendingStage"
@@ -1319,7 +1326,7 @@ export class ProviderDatabase
/** @description Update an image captcha commitment
*/
- async updateDappUserCommitment(
+ async updateImageCaptcha(
commitmentId: Hash,
updates: Partial,
) {
@@ -1342,7 +1349,7 @@ export class ProviderDatabase
skip = 0,
): Promise {
// Served by the `pendingStage_partial` index — see
- // `getUnstoredDappUserCommitments` for the lifecycle of the flag.
+ // `getUnstoredImageCaptchas` for the lifecycle of the flag.
const docs = await this.tables?.powcaptcha
.find({ pendingStage: true })
.sort({ _id: 1 })
@@ -1355,7 +1362,7 @@ export class ProviderDatabase
/** @description Mark a list of PoW captcha commits as stored.
*
* `asOfTimestamp` defaults to "now" but the sweep should pass the time
- * at which it fetched the batch. See markDappUserCommitmentsStored for
+ * at which it fetched the batch. See markImageCaptchasStored for
* the guard rationale.
*/
async markDappUserPoWCommitmentsStored(
@@ -1623,7 +1630,7 @@ export class ProviderDatabase
* @description Get session records that have not been stored yet.
*
* Served by the `pendingStage_partial` index — see
- * `getUnstoredDappUserCommitments` for the lifecycle of the flag.
+ * `getUnstoredImageCaptchas` for the lifecycle of the flag.
* `checkAndRemoveSession` also flips the flag so consumed sessions
* propagate to the central DB via the next sweep.
* @param limit
@@ -1645,7 +1652,7 @@ export class ProviderDatabase
/** Mark a list of session records as stored.
*
* `asOfTimestamp` defaults to "now" but the sweep should pass the time
- * at which it fetched the batch. See markDappUserCommitmentsStored for
+ * at which it fetched the batch. See markImageCaptchasStored for
* the guard rationale.
*/
async markSessionRecordsStored(
@@ -1688,6 +1695,10 @@ export class ProviderDatabase
});
}
const pendingRecord = {
+ // Stamp the discriminator explicitly: this placeholder is built by
+ // hand and written via `$set`, so we don't rely on the schema
+ // default applying on upsert-insert.
+ requestType: RequestType.imagecaptcha,
userAccount,
pending: true,
salt,
@@ -1701,7 +1712,7 @@ export class ProviderDatabase
// Deliberately NOT setting pendingStage here. Placeholder
// records have id: "" until the user submits a solution; if we
// flag them, the sweep picks them up via the partial index but
- // then `markDappUserCommitmentsStored` runs
+ // then `markImageCaptchasStored` runs
// `{ id: { $in: ["", ...] } }` which collapses to a single ""
// bound and scans every empty-id row via the `id_-1` index —
// turning a cheap sweep into a fresh cache evictor. The real
@@ -1943,7 +1954,7 @@ export class ProviderDatabase
* @description Get dapp user commitment by user account
* @param commitmentId
*/
- async getDappUserCommitmentById(
+ async getImageCaptchaById(
commitmentId: string,
): Promise {
const filter: Pick = { id: commitmentId };
@@ -1972,7 +1983,7 @@ export class ProviderDatabase
* @param {string} userAccount
* @param {string} dappAccount
*/
- async getDappUserCommitmentByAccount(
+ async getImageCaptchaByAccount(
userAccount: string,
dappAccount: string,
): Promise {
@@ -1999,7 +2010,7 @@ export class ProviderDatabase
* @param {string[]} commitmentId
* @param coords
*/
- async approveDappUserCommitment(
+ async approveImageCaptcha(
commitmentId: string,
coords?: [number, number][][],
): Promise {
@@ -2047,7 +2058,7 @@ export class ProviderDatabase
* @param coords
* @param reason
*/
- async disapproveDappUserCommitment(
+ async disapproveImageCaptcha(
commitmentId: string,
reason?: ResultReason,
coords?: [number, number][][],
@@ -2114,7 +2125,7 @@ export class ProviderDatabase
* @description Flag dapp users' commitments as used by calculated solution
* @param {string[]} commitmentIds
*/
- async flagProcessedDappUserCommitments(commitmentIds: Hash[]): Promise {
+ async flagProcessedImageCaptchas(commitmentIds: Hash[]): Promise {
try {
const distinctCommitmentIds = [...new Set(commitmentIds)];
await this.tables?.commitment
diff --git a/packages/database/src/tests/unit/requestType.unit.test.ts b/packages/database/src/tests/unit/requestType.unit.test.ts
new file mode 100644
index 0000000000..2d08cc2d5d
--- /dev/null
+++ b/packages/database/src/tests/unit/requestType.unit.test.ts
@@ -0,0 +1,115 @@
+// Copyright 2021-2026 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+ CaptchaStatus,
+ ImageCaptchaSchema,
+ IpAddressType,
+ PoWCaptchaStoredSchema,
+ RequestType,
+ RequestTypeSchema,
+ SessionSchema,
+ UserCommitmentSchema,
+} from "@prosopo/types";
+import {
+ ImageCaptchaRecordSchema,
+ PoWCaptchaRecordSchema,
+ PuzzleCaptchaRecordSchema,
+ SessionRecordSchema,
+ StoredImageCaptchaRecordSchema,
+ StoredUserCommitmentRecordSchema,
+ UserCommitmentRecordSchema,
+} from "@prosopo/types-database";
+import mongoose, { type Schema } from "mongoose";
+import { describe, expect, it } from "vitest";
+
+describe("RequestType discriminator", () => {
+ it("RequestTypeSchema accepts every RequestType value", () => {
+ for (const value of Object.values(RequestType)) {
+ expect(RequestTypeSchema.parse(value)).toBe(value);
+ }
+ });
+
+ it("RequestTypeSchema rejects unknown request types", () => {
+ expect(() => RequestTypeSchema.parse("not-a-request-type")).toThrow();
+ });
+
+ it("Mongoose record schemas stamp the correct requestType default on new docs", () => {
+ const cases: Array<[string, Schema, RequestType]> = [
+ ["ReqTypeTestSession", SessionRecordSchema, RequestType.session],
+ ["ReqTypeTestPow", PoWCaptchaRecordSchema, RequestType.powcaptcha],
+ ["ReqTypeTestImage", ImageCaptchaRecordSchema, RequestType.imagecaptcha],
+ ["ReqTypeTestPuzzle", PuzzleCaptchaRecordSchema, RequestType.puzzlecaptcha],
+ ];
+ for (const [modelName, schema, expected] of cases) {
+ expect(schema.path("requestType").instance).toBe("String");
+ // A throwaway model (no DB connection needed) applies schema defaults
+ // at construction time — the same path production writes take.
+ const model =
+ mongoose.models[modelName] ?? mongoose.model(modelName, schema);
+ const doc = new model();
+ expect(doc.get("requestType")).toBe(expected);
+ }
+ });
+
+ it("Zod record schemas expose requestType as optional (back-compat)", () => {
+ for (const schema of [
+ ImageCaptchaSchema,
+ PoWCaptchaStoredSchema,
+ SessionSchema,
+ ]) {
+ expect(schema.shape.requestType.isOptional()).toBe(true);
+ }
+ });
+
+ it("ImageCaptchaSchema parses records with and without requestType", () => {
+ const base = {
+ userAccount: "u",
+ dappAccount: "d",
+ datasetId: "ds",
+ providerAccount: "p",
+ id: "id",
+ result: { status: CaptchaStatus.approved },
+ userSignature: "",
+ ipAddress: { lower: 1n, type: IpAddressType.v4 },
+ headers: {},
+ ja4: "",
+ userSubmitted: true,
+ serverChecked: true,
+ requestedAtTimestamp: new Date(),
+ pending: false,
+ salt: "0x0",
+ requestHash: "0x0",
+ deadlineTimestamp: new Date(),
+ threshold: 0.8,
+ };
+ expect(() => ImageCaptchaSchema.parse(base)).not.toThrow();
+ expect(() =>
+ ImageCaptchaSchema.parse({
+ ...base,
+ requestType: RequestType.imagecaptcha,
+ }),
+ ).not.toThrow();
+ });
+});
+
+describe("UserCommitment -> ImageCaptcha back-compat aliases", () => {
+ it("deprecated schema aliases are identity-equal to the new schemas", () => {
+ expect(UserCommitmentSchema).toBe(ImageCaptchaSchema);
+ expect(UserCommitmentRecordSchema).toBe(ImageCaptchaRecordSchema);
+ expect(StoredUserCommitmentRecordSchema).toBe(
+ StoredImageCaptchaRecordSchema,
+ );
+ });
+});
diff --git a/packages/provider/src/tasks/client/clientTasks.ts b/packages/provider/src/tasks/client/clientTasks.ts
index 66e1dec737..57eb3d2f51 100644
--- a/packages/provider/src/tasks/client/clientTasks.ts
+++ b/packages/provider/src/tasks/client/clientTasks.ts
@@ -120,7 +120,7 @@ export class ClientTaskManager {
await this.processBatchesWithCursor(
async (skip: number) =>
- await this.providerDB.getUnstoredDappUserCommitments(
+ await this.providerDB.getUnstoredImageCaptchas(
BATCH_SIZE,
skip,
),
@@ -134,14 +134,14 @@ export class ClientTaskManager {
// inserts with `id: ""` until the user submits a solution.
// Defense in depth: even if a stray placeholder slips into
// the partial index, never pass `id: ""` to
- // markDappUserCommitmentsStored — Mongo collapses
+ // markImageCaptchasStored — Mongo collapses
// `{ id: { $in: ["", "", ...] } }` to a single empty-string
// bound and the IXSCAN on `id_-1` then walks every
// empty-id document on the node (~100K rows).
if (filteredBatch.length > 0) {
await captchaDB.saveCaptchas([], filteredBatch, []);
- await this.providerDB.markDappUserCommitmentsStored(
+ await this.providerDB.markImageCaptchasStored(
filteredBatch.map((commitment) => commitment.id),
sweepStartedAt,
);
diff --git a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts
index fcb8a3d2e0..bf99e47b13 100644
--- a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts
+++ b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts
@@ -399,7 +399,7 @@ export class ImgCaptchaManager extends CaptchaManager {
clientMetaData: { hp: clientMetaData.hp },
}),
};
- await this.db.storeUserImageCaptchaSolution(receivedCaptchas, commit);
+ await this.db.storeImageCaptchaSolution(receivedCaptchas, commit);
const solutionRecords = await Promise.all(
storedCaptchas.map(async (captcha) => {
@@ -420,7 +420,7 @@ export class ImgCaptchaManager extends CaptchaManager {
if (containsIdenticalPairs(pairs) && process.env.NODE_ENV !== "test") {
// Write commitment disapproval and session update in parallel
const writePromises: Promise[] = [
- this.db.disapproveDappUserCommitment(
+ this.db.disapproveImageCaptcha(
commitmentId,
"CAPTCHA.INVALID_SOLUTION",
pairs,
@@ -466,7 +466,7 @@ export class ImgCaptchaManager extends CaptchaManager {
};
// Write commitment approval and session update in parallel
const writePromises: Promise[] = [
- this.db.approveDappUserCommitment(commitmentId, pairs),
+ this.db.approveImageCaptcha(commitmentId, pairs),
];
if (pendingRecord.sessionId) {
writePromises.push(
@@ -481,7 +481,7 @@ export class ImgCaptchaManager extends CaptchaManager {
} else {
// Write commitment disapproval and session update in parallel
const writePromises: Promise[] = [
- this.db.disapproveDappUserCommitment(
+ this.db.disapproveImageCaptcha(
commitmentId,
"CAPTCHA.INVALID_SOLUTION",
pairs,
@@ -599,15 +599,15 @@ export class ImgCaptchaManager extends CaptchaManager {
/*
* Get dapp user solution from database
*/
- async getDappUserCommitmentById(
+ async getImageCaptchaById(
commitmentId: string,
): Promise {
const dappUserSolution =
- await this.db.getDappUserCommitmentById(commitmentId);
+ await this.db.getImageCaptchaById(commitmentId);
if (!dappUserSolution) {
throw new ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
context: {
- failedFuncName: this.getDappUserCommitmentById.name,
+ failedFuncName: this.getImageCaptchaById.name,
commitmentId: commitmentId,
},
});
@@ -616,11 +616,11 @@ export class ImgCaptchaManager extends CaptchaManager {
}
/* Check if dapp user has verified solution in cache */
- async getDappUserCommitmentByAccount(
+ async getImageCaptchaByAccount(
userAccount: string,
dappAccount: string,
): Promise {
- const dappUserSolutions = await this.db.getDappUserCommitmentByAccount(
+ const dappUserSolutions = await this.db.getImageCaptchaByAccount(
userAccount,
dappAccount,
);
@@ -651,8 +651,8 @@ export class ImgCaptchaManager extends CaptchaManager {
storeMetadata = false,
): Promise {
const solution = await (commitmentId
- ? this.getDappUserCommitmentById(commitmentId)
- : this.getDappUserCommitmentByAccount(user, dapp));
+ ? this.getImageCaptchaById(commitmentId)
+ : this.getImageCaptchaByAccount(user, dapp));
// No solution exists
if (!solution) {
@@ -676,7 +676,7 @@ export class ImgCaptchaManager extends CaptchaManager {
};
}
- await this.db.markDappUserCommitmentsChecked([solution.id]);
+ await this.db.markImageCaptchasChecked([solution.id]);
// -- END WARNING --
// A solution exists but is disapproved
@@ -972,7 +972,7 @@ export class ImgCaptchaManager extends CaptchaManager {
}
// Batch writes: separate non-streaming updates from streaming result writes.
- // - providedIp / metadata use updateDappUserCommitment (no central streaming)
+ // - providedIp / metadata use updateImageCaptcha (no central streaming)
// - approve/disapprove use dedicated methods that trigger centralStreamer
const writePromises: Promise[] = [];
@@ -986,17 +986,17 @@ export class ImgCaptchaManager extends CaptchaManager {
}
if (Object.keys(sideUpdates).length > 0) {
writePromises.push(
- this.db.updateDappUserCommitment(solution.id, sideUpdates),
+ this.db.updateImageCaptcha(solution.id, sideUpdates),
);
}
// Write result via the streaming-aware methods
if (commitmentId) {
if (isApproved) {
- writePromises.push(this.db.approveDappUserCommitment(commitmentId));
+ writePromises.push(this.db.approveImageCaptcha(commitmentId));
} else if (commitmentUpdates.result) {
writePromises.push(
- this.db.disapproveDappUserCommitment(
+ this.db.disapproveImageCaptcha(
commitmentId,
commitmentUpdates.result.reason,
),
diff --git a/packages/provider/src/tests/unit/tasks/client/clientTasks.unit.test.ts b/packages/provider/src/tests/unit/tasks/client/clientTasks.unit.test.ts
index aaa0e77370..5ef2590029 100644
--- a/packages/provider/src/tests/unit/tasks/client/clientTasks.unit.test.ts
+++ b/packages/provider/src/tests/unit/tasks/client/clientTasks.unit.test.ts
@@ -139,8 +139,8 @@ describe("ClientTaskManager", () => {
markFrictionlessTokenRecordsStored: vi.fn(),
getUnstoredSessionRecords: vi.fn().mockResolvedValue([]),
markSessionRecordsStored: vi.fn(),
- getUnstoredDappUserCommitments: vi.fn().mockResolvedValue([]),
- markDappUserCommitmentsStored: vi.fn(),
+ getUnstoredImageCaptchas: vi.fn().mockResolvedValue([]),
+ markImageCaptchasStored: vi.fn(),
markDappUserPoWCommitmentsStored: vi.fn(),
getUnstoredDappUserPoWCommitments: vi.fn().mockResolvedValue([]),
createScheduledTaskStatus: vi.fn(
@@ -249,7 +249,7 @@ describe("ClientTaskManager", () => {
msg: "Mongo env not set",
});
- expect(providerDB.getUnstoredDappUserCommitments).not.toHaveBeenCalled();
+ expect(providerDB.getUnstoredImageCaptchas).not.toHaveBeenCalled();
});
it("should store commitments externally if mongoCaptchaUri is set", async () => {
@@ -263,7 +263,7 @@ describe("ClientTaskManager", () => {
];
// biome-ignore lint/suspicious/noExplicitAny: TODO fix
- (providerDB.getUnstoredDappUserCommitments as any).mockResolvedValueOnce(
+ (providerDB.getUnstoredImageCaptchas as any).mockResolvedValueOnce(
mockCommitments,
);
@@ -280,10 +280,10 @@ describe("ClientTaskManager", () => {
await clientTaskManager.storeCommitmentsExternal();
- expect(providerDB.getUnstoredDappUserCommitments).toHaveBeenCalled();
+ expect(providerDB.getUnstoredImageCaptchas).toHaveBeenCalled();
expect(providerDB.getUnstoredDappUserPoWCommitments).toHaveBeenCalled();
- expect(providerDB.markDappUserCommitmentsStored).toHaveBeenCalledWith(
+ expect(providerDB.markImageCaptchasStored).toHaveBeenCalledWith(
mockCommitments.map((c) => c.id),
expect.any(Date),
);
@@ -345,7 +345,7 @@ describe("ClientTaskManager", () => {
}));
// biome-ignore lint/suspicious/noExplicitAny: TODO fix
- (providerDB.getUnstoredDappUserCommitments as any).mockResolvedValueOnce(
+ (providerDB.getUnstoredImageCaptchas as any).mockResolvedValueOnce(
mockCommitments,
);
// biome-ignore lint/suspicious/noExplicitAny: TODO fix
@@ -358,7 +358,7 @@ describe("ClientTaskManager", () => {
logger.info(() => ({ msg: "Test: storeCommitmentsExternal completed" }));
// Verification steps with logging
- expect(providerDB.getUnstoredDappUserCommitments).toHaveBeenCalled();
+ expect(providerDB.getUnstoredImageCaptchas).toHaveBeenCalled();
expect(providerDB.getUnstoredDappUserPoWCommitments).toHaveBeenCalled();
logger.info(() => ({ msg: "Test: Verified DB queries were made" }));
@@ -373,7 +373,7 @@ describe("ClientTaskManager", () => {
);
logger.info(() => ({ msg: "Test: Verified task status creation" }));
- expect(providerDB.markDappUserCommitmentsStored).not.toHaveBeenCalled();
+ expect(providerDB.markImageCaptchasStored).not.toHaveBeenCalled();
logger.info(() => ({
msg: "Test: Verified no image commitments were marked as stored (expected as they're old)",
}));
@@ -423,7 +423,7 @@ describe("ClientTaskManager", () => {
collections.schedulers.time = 2;
// biome-ignore lint/suspicious/noExplicitAny: TODO fix
- (providerDB.getUnstoredDappUserCommitments as any).mockResolvedValueOnce(
+ (providerDB.getUnstoredImageCaptchas as any).mockResolvedValueOnce(
[],
);
@@ -434,7 +434,7 @@ describe("ClientTaskManager", () => {
await clientTaskManager.storeCommitmentsExternal();
- expect(providerDB.markDappUserCommitmentsStored).not.toHaveBeenCalled();
+ expect(providerDB.markImageCaptchasStored).not.toHaveBeenCalled();
expect(providerDB.markDappUserPoWCommitmentsStored).not.toHaveBeenCalled();
expect(providerDB.updateScheduledTaskStatus).toHaveBeenCalledWith(
diff --git a/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.unit.test.ts b/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.unit.test.ts
index 73826fc4d1..b2a7471ae1 100644
--- a/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.unit.test.ts
+++ b/packages/provider/src/tests/unit/tasks/dataset/datasetTasks.unit.test.ts
@@ -95,8 +95,8 @@ describe("DatasetManager", () => {
providerDB = {
storeDataset: vi.fn(),
- getUnstoredDappUserCommitments: vi.fn().mockResolvedValue([]),
- markDappUserCommitmentsStored: vi.fn(),
+ getUnstoredImageCaptchas: vi.fn().mockResolvedValue([]),
+ markImageCaptchasStored: vi.fn(),
markDappUserPoWCommitmentsStored: vi.fn(),
getUnstoredDappUserPoWCommitments: vi.fn().mockResolvedValue([]),
createScheduledTaskStatus: vi.fn(
diff --git a/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.ts b/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.ts
index 7fe223c556..6d9125236d 100644
--- a/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.ts
+++ b/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.ts
@@ -166,19 +166,19 @@ describe("ImgCaptchaManager", () => {
getPendingImageCommitment: vi.fn(),
updatePendingImageCommitmentStatus: vi.fn(),
storeDappUserSolution: vi.fn(),
- approveDappUserCommitment: vi.fn(),
- disapproveDappUserCommitment: vi.fn(),
- updateDappUserCommitment: vi.fn(),
+ approveImageCaptcha: vi.fn(),
+ disapproveImageCaptcha: vi.fn(),
+ updateImageCaptcha: vi.fn(),
getCaptchaById: vi.fn(),
- getDappUserCommitmentById: vi.fn(),
- getDappUserCommitmentByAccount: vi.fn(),
- markDappUserCommitmentsChecked: vi.fn(),
+ getImageCaptchaById: vi.fn(),
+ getImageCaptchaByAccount: vi.fn(),
+ markImageCaptchasChecked: vi.fn(),
getSessionRecordBySessionId: vi.fn(),
updateSessionRecord: vi.fn(),
getSpamEmailDomain: vi.fn(),
getClientRecord: vi.fn(),
getSolutionByCaptchaId: vi.fn(),
- storeUserImageCaptchaSolution: vi.fn(),
+ storeImageCaptchaSolution: vi.fn(),
} as unknown as IProviderDatabase;
pair = {
@@ -550,12 +550,12 @@ describe("ImgCaptchaManager", () => {
expect(result.verified).toBe(false);
expect(compareCaptchaSolutions).toHaveBeenCalled();
- expect(db.disapproveDappUserCommitment).toHaveBeenCalledWith(
+ expect(db.disapproveImageCaptcha).toHaveBeenCalledWith(
"commitmentId",
"CAPTCHA.INVALID_SOLUTION",
expect.anything(),
);
- expect(db.approveDappUserCommitment).not.toHaveBeenCalled();
+ expect(db.approveImageCaptcha).not.toHaveBeenCalled();
});
});
@@ -582,10 +582,10 @@ describe("ImgCaptchaManager", () => {
lastUpdatedTimestamp: new Date(),
};
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(dappUserCommitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(dappUserCommitment);
const result =
- await imgCaptchaManager.getDappUserCommitmentById(commitmentId);
+ await imgCaptchaManager.getImageCaptchaById(commitmentId);
expect(result).toEqual(dappUserCommitment);
});
@@ -593,14 +593,14 @@ describe("ImgCaptchaManager", () => {
it("should throw an error if dapp user commitment is not found by ID", async () => {
const commitmentId = "commitmentId";
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(null);
+ (db.getImageCaptchaById as any).mockResolvedValue(null);
await expect(
- imgCaptchaManager.getDappUserCommitmentById(commitmentId),
+ imgCaptchaManager.getImageCaptchaById(commitmentId),
).rejects.toThrow(
new ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
context: {
- failedFuncName: "getDappUserCommitmentById",
+ failedFuncName: "getImageCaptchaById",
commitmentId: commitmentId,
},
}),
@@ -633,11 +633,11 @@ describe("ImgCaptchaManager", () => {
} as Partial,
];
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentByAccount as any).mockResolvedValue(
+ (db.getImageCaptchaByAccount as any).mockResolvedValue(
dappUserCommitments,
);
- const result = await imgCaptchaManager.getDappUserCommitmentByAccount(
+ const result = await imgCaptchaManager.getImageCaptchaByAccount(
userAccount,
dappAccount,
);
@@ -650,11 +650,11 @@ describe("ImgCaptchaManager", () => {
const dappAccount = "dappAccount";
const dappUserCommitments: UserCommitment[] = [];
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentByAccount as any).mockResolvedValue(
+ (db.getImageCaptchaByAccount as any).mockResolvedValue(
dappUserCommitments,
);
- const result = await imgCaptchaManager.getDappUserCommitmentByAccount(
+ const result = await imgCaptchaManager.getImageCaptchaByAccount(
userAccount,
dappAccount,
);
@@ -693,9 +693,9 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.disapproveDappUserCommitment as any).mockResolvedValue(undefined);
+ (db.disapproveImageCaptcha as any).mockResolvedValue(undefined);
// Mock decision machine to return Deny
const originalDecide =
@@ -723,7 +723,7 @@ describe("ImgCaptchaManager", () => {
expect(result.status).toBe("Suspicious behavior detected");
// Verify commitment was disapproved via streaming-aware method
- expect(db.disapproveDappUserCommitment).toHaveBeenCalledWith(
+ expect(db.disapproveImageCaptcha).toHaveBeenCalledWith(
commitmentId,
expect.stringContaining("Suspicious behavior"),
);
@@ -763,7 +763,7 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// Mock decision machine to throw an error
const originalDecide =
@@ -820,7 +820,7 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// Mock decision machine to return Allow
const originalDecide =
@@ -897,7 +897,7 @@ describe("ImgCaptchaManager", () => {
};
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSessionRecordBySessionId as any).mockResolvedValue(undefined);
@@ -956,11 +956,11 @@ describe("ImgCaptchaManager", () => {
};
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSessionRecordBySessionId as any).mockResolvedValue(undefined);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.disapproveDappUserCommitment as any).mockResolvedValue(undefined);
+ (db.disapproveImageCaptcha as any).mockResolvedValue(undefined);
// Mock decision machine to deny
const originalDecide =
@@ -1023,7 +1023,7 @@ describe("ImgCaptchaManager", () => {
};
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// Mock decision machine to allow
const originalDecide =
@@ -1081,15 +1081,15 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSpamEmailDomain as any).mockResolvedValue({
domain: "spammydomain.com",
});
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.disapproveDappUserCommitment as any).mockResolvedValue(undefined);
+ (db.disapproveImageCaptcha as any).mockResolvedValue(undefined);
const result = await imgCaptchaManager.verifyImageCaptchaSolution(
userAccount,
@@ -1108,7 +1108,7 @@ describe("ImgCaptchaManager", () => {
expect(result.verified).toBe(false);
expect(result.status).toBe("API.SPAM_EMAIL_DOMAIN");
expect(db.getSpamEmailDomain).toHaveBeenCalledWith("spammydomain.com");
- expect(db.disapproveDappUserCommitment).toHaveBeenCalledWith(
+ expect(db.disapproveImageCaptcha).toHaveBeenCalledWith(
commitmentId,
"API.SPAM_EMAIL_DOMAIN",
);
@@ -1145,13 +1145,13 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSpamEmailDomain as any).mockResolvedValue(null);
- // Reset disapproveDappUserCommitment mock
- db.disapproveDappUserCommitment = vi
+ // Reset disapproveImageCaptcha mock
+ db.disapproveImageCaptcha = vi
.fn()
// biome-ignore lint/suspicious/noExplicitAny: tests
.mockResolvedValue(undefined) as any;
@@ -1187,7 +1187,7 @@ describe("ImgCaptchaManager", () => {
expect(result.verified).toBe(true);
expect(result.status).toBe("API.USER_VERIFIED");
expect(db.getSpamEmailDomain).toHaveBeenCalledWith("legitimate.com");
- expect(db.disapproveDappUserCommitment).not.toHaveBeenCalledWith(
+ expect(db.disapproveImageCaptcha).not.toHaveBeenCalledWith(
commitmentId,
"API.SPAM_EMAIL_DOMAIN",
);
@@ -1229,9 +1229,9 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// Mock decision machine to allow
const originalDecide =
@@ -1303,15 +1303,15 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSpamEmailDomain as any).mockResolvedValue({
domain: "spammydomain.com",
});
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.disapproveDappUserCommitment as any).mockResolvedValue(undefined);
+ (db.disapproveImageCaptcha as any).mockResolvedValue(undefined);
const result = await imgCaptchaManager.verifyImageCaptchaSolution(
userAccount,
@@ -1330,7 +1330,7 @@ describe("ImgCaptchaManager", () => {
expect(result.verified).toBe(false);
expect(result.status).toBe("API.SPAM_EMAIL_DOMAIN");
expect(db.getSpamEmailDomain).toHaveBeenCalledWith("spammydomain.com");
- expect(db.disapproveDappUserCommitment).toHaveBeenCalledWith(
+ expect(db.disapproveImageCaptcha).toHaveBeenCalledWith(
commitmentId,
"API.SPAM_EMAIL_DOMAIN",
);
@@ -1367,15 +1367,15 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSpamEmailDomain as any).mockResolvedValue({
domain: "spammydomain.com",
});
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.disapproveDappUserCommitment as any).mockResolvedValue(undefined);
+ (db.disapproveImageCaptcha as any).mockResolvedValue(undefined);
const result = await imgCaptchaManager.verifyImageCaptchaSolution(
userAccount,
@@ -1427,9 +1427,9 @@ describe("ImgCaptchaManager", () => {
// Mock database calls
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(commitment);
+ (db.getImageCaptchaById as any).mockResolvedValue(commitment);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// Mock database error when checking spam
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSpamEmailDomain as any).mockRejectedValue(
@@ -1476,7 +1476,7 @@ describe("ImgCaptchaManager", () => {
}
});
- it("should not call disapproveDappUserCommitment when commitmentId is undefined", async () => {
+ it("should not call disapproveImageCaptcha when commitmentId is undefined", async () => {
const userAccount = "userAccount";
const dappAccount = "dappAccount";
const spamEmail = "user@spammydomain.com";
@@ -1506,16 +1506,16 @@ describe("ImgCaptchaManager", () => {
// Mock database calls to find commitment by account
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentByAccount as any).mockResolvedValue([
+ (db.getImageCaptchaByAccount as any).mockResolvedValue([
commitment,
]);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSpamEmailDomain as any).mockResolvedValue({
domain: "spammydomain.com",
});
- db.disapproveDappUserCommitment = vi
+ db.disapproveImageCaptcha = vi
.fn()
// biome-ignore lint/suspicious/noExplicitAny: tests
.mockResolvedValue(undefined) as any;
@@ -1538,7 +1538,7 @@ describe("ImgCaptchaManager", () => {
expect(result.status).toBe("API.SPAM_EMAIL_DOMAIN");
expect(db.getSpamEmailDomain).toHaveBeenCalledWith("spammydomain.com");
// Should not call disapprove when commitmentId is undefined
- expect(db.disapproveDappUserCommitment).not.toHaveBeenCalled();
+ expect(db.disapproveImageCaptcha).not.toHaveBeenCalled();
});
});
@@ -1573,11 +1573,11 @@ describe("ImgCaptchaManager", () => {
const commitmentId = "webviewScoreCommitmentId";
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(
+ (db.getImageCaptchaById as any).mockResolvedValue(
baseCommitment(commitmentId),
);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSessionRecordBySessionId as any).mockResolvedValue({
sessionId,
@@ -1586,7 +1586,7 @@ describe("ImgCaptchaManager", () => {
reason: "WEBVIEW_DETECTED",
});
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.disapproveDappUserCommitment as any).mockResolvedValue(undefined);
+ (db.disapproveImageCaptcha as any).mockResolvedValue(undefined);
const result = await imgCaptchaManager.verifyImageCaptchaSolution(
"userAccount",
@@ -1600,7 +1600,7 @@ describe("ImgCaptchaManager", () => {
expect(result.verified).toBe(false);
expect(result.status).toBe("API.DISALLOWED_WEBVIEW");
- expect(db.disapproveDappUserCommitment).toHaveBeenCalledWith(
+ expect(db.disapproveImageCaptcha).toHaveBeenCalledWith(
commitmentId,
"API.DISALLOWED_WEBVIEW",
);
@@ -1616,18 +1616,18 @@ describe("ImgCaptchaManager", () => {
true,
);
// Decision machine must not be consulted once the request is blocked.
- expect(db.approveDappUserCommitment).not.toHaveBeenCalled();
+ expect(db.approveImageCaptcha).not.toHaveBeenCalled();
});
it("should disapprove when session.webView is true even if scoreComponents.webView is unset", async () => {
const commitmentId = "webviewBoolCommitmentId";
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(
+ (db.getImageCaptchaById as any).mockResolvedValue(
baseCommitment(commitmentId),
);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// Webview detected but reached image captcha via a non-webview
// frictionless branch (UA mismatch, context-aware, etc.), so
// `scoreComponents.webView` is absent.
@@ -1639,7 +1639,7 @@ describe("ImgCaptchaManager", () => {
reason: "USER_AGENT_MISMATCH",
});
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.disapproveDappUserCommitment as any).mockResolvedValue(undefined);
+ (db.disapproveImageCaptcha as any).mockResolvedValue(undefined);
const result = await imgCaptchaManager.verifyImageCaptchaSolution(
"userAccount",
@@ -1653,7 +1653,7 @@ describe("ImgCaptchaManager", () => {
expect(result.verified).toBe(false);
expect(result.status).toBe("API.DISALLOWED_WEBVIEW");
- expect(db.disapproveDappUserCommitment).toHaveBeenCalledWith(
+ expect(db.disapproveImageCaptcha).toHaveBeenCalledWith(
commitmentId,
"API.DISALLOWED_WEBVIEW",
);
@@ -1663,11 +1663,11 @@ describe("ImgCaptchaManager", () => {
const commitmentId = "webviewAllowedCommitmentId";
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.getDappUserCommitmentById as any).mockResolvedValue(
+ (db.getImageCaptchaById as any).mockResolvedValue(
baseCommitment(commitmentId),
);
// biome-ignore lint/suspicious/noExplicitAny: tests
- (db.markDappUserCommitmentsChecked as any).mockResolvedValue(undefined);
+ (db.markImageCaptchasChecked as any).mockResolvedValue(undefined);
// biome-ignore lint/suspicious/noExplicitAny: tests
(db.getSessionRecordBySessionId as any).mockResolvedValue({
sessionId,
@@ -1696,7 +1696,7 @@ describe("ImgCaptchaManager", () => {
expect(result.verified).toBe(true);
expect(result.status).toBe("API.USER_VERIFIED");
- expect(db.approveDappUserCommitment).toHaveBeenCalledWith(commitmentId);
+ expect(db.approveImageCaptcha).toHaveBeenCalledWith(commitmentId);
} finally {
// biome-ignore lint/suspicious/noExplicitAny: tests
(imgCaptchaManager as any).decisionMachineRunner.decide =
diff --git a/packages/types-database/src/types/captcha.ts b/packages/types-database/src/types/captcha.ts
index 9adc69adb0..a8a4fd0c82 100644
--- a/packages/types-database/src/types/captcha.ts
+++ b/packages/types-database/src/types/captcha.ts
@@ -12,16 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import type { PoWCaptcha, UserCommitment } from "@prosopo/types";
+import type { ImageCaptcha, PoWCaptcha } from "@prosopo/types";
import { type RootFilterQuery, Schema } from "mongoose";
import type { IDatabase } from "./mongo.js";
import {
+ type ImageCaptchaRecord,
+ ImageCaptchaRecordSchema,
type PoWCaptchaRecord,
PoWCaptchaRecordSchema,
type SessionRecord,
SessionRecordSchema,
- type UserCommitmentRecord,
- UserCommitmentRecordSchema,
} from "./provider.js";
// StoredSession is now the same as SessionRecord since we merged the schemas
@@ -29,10 +29,15 @@ export type StoredSession = SessionRecord;
export const StoredSessionRecordSchema: Schema = SessionRecordSchema;
-export const StoredUserCommitmentRecordSchema: Schema = new Schema({
- ...UserCommitmentRecordSchema.obj,
+export const StoredImageCaptchaRecordSchema: Schema = new Schema({
+ ...ImageCaptchaRecordSchema.obj,
});
-StoredUserCommitmentRecordSchema.index({ sessionId: 1 });
+StoredImageCaptchaRecordSchema.index({ sessionId: 1 });
+
+/**
+ * @deprecated Use {@link StoredImageCaptchaRecordSchema}. Back-compat alias.
+ */
+export const StoredUserCommitmentRecordSchema = StoredImageCaptchaRecordSchema;
export const StoredPoWCaptchaRecordSchema: Schema = new Schema({
...PoWCaptchaRecordSchema.obj,
@@ -42,17 +47,17 @@ StoredPoWCaptchaRecordSchema.index({ sessionId: 1 });
export interface ICaptchaDatabase extends IDatabase {
saveCaptchas(
sessionEvents: StoredSession[],
- imageCaptchaEvents: UserCommitmentRecord[],
+ imageCaptchaEvents: ImageCaptchaRecord[],
powCaptchaEvents: PoWCaptchaRecord[],
): Promise;
getCaptchas(
filter: RootFilterQuery,
limit: number,
): Promise<{
- userCommitmentRecords: UserCommitmentRecord[];
+ imageCaptchaRecords: ImageCaptchaRecord[];
powCaptchaRecords: PoWCaptchaRecord[];
}>;
}
export interface CaptchaProperties
- extends Partial {}
+ extends Partial {}
diff --git a/packages/types-database/src/types/provider.ts b/packages/types-database/src/types/provider.ts
index 8920105966..c0f4d5de3b 100644
--- a/packages/types-database/src/types/provider.ts
+++ b/packages/types-database/src/types/provider.ts
@@ -25,16 +25,17 @@ import {
DecisionMachineRuntime,
DecisionMachineScope,
type DetectorKey,
+ type ImageCaptcha,
IpAddressType,
ModeEnum,
type PendingImageCaptchaRequest,
type PoWCaptchaStored,
type PuzzleCaptchaStored,
+ RequestType,
type Session,
type SimdReadingsStage,
type SolutionRecord,
Tier,
- type UserCommitment,
type UserSolutionSchema,
} from "@prosopo/types";
import {
@@ -114,7 +115,13 @@ export type PoWCaptchaRecord = mongoose.Document & PoWCaptchaStored;
export type PuzzleCaptchaRecord = mongoose.Document & PuzzleCaptchaStored;
-export type UserCommitmentRecord = mongoose.Document & UserCommitment;
+export type ImageCaptchaRecord = mongoose.Document & ImageCaptcha;
+
+/**
+ * @deprecated Use {@link ImageCaptchaRecord}. Back-compat alias; remove in a
+ * later cleanup once consumers migrate off the old name.
+ */
+export type UserCommitmentRecord = ImageCaptchaRecord;
export type Tables = {
// biome-ignore lint/suspicious/noExplicitAny:
@@ -152,6 +159,12 @@ CaptchaRecordSchema.index({ datasetId: 1 });
CaptchaRecordSchema.index({ datasetId: 1, solved: 1 });
export const PoWCaptchaRecordSchema = new Schema({
+ requestType: {
+ type: String,
+ enum: RequestType,
+ required: false,
+ default: RequestType.powcaptcha,
+ },
challenge: { type: String, required: true },
dappAccount: { type: String, required: true },
userAccount: { type: String, required: true },
@@ -255,6 +268,12 @@ PoWCaptchaRecordSchema.index(
);
export const PuzzleCaptchaRecordSchema = new Schema({
+ requestType: {
+ type: String,
+ enum: RequestType,
+ required: false,
+ default: RequestType.puzzlecaptcha,
+ },
challenge: { type: String, required: true },
dappAccount: { type: String, required: true },
userAccount: { type: String, required: true },
@@ -354,7 +373,13 @@ PuzzleCaptchaRecordSchema.index(
},
);
-export const UserCommitmentRecordSchema = new Schema({
+export const ImageCaptchaRecordSchema = new Schema({
+ requestType: {
+ type: String,
+ enum: RequestType,
+ required: false,
+ default: RequestType.imagecaptcha,
+ },
userAccount: { type: String, required: true },
dappAccount: { type: String, required: true },
providerAccount: { type: String, required: true },
@@ -429,23 +454,23 @@ export const UserCommitmentRecordSchema = new Schema({
labelledAt: { type: Date, required: false },
});
// Set an index on the commitment id field, descending
-UserCommitmentRecordSchema.index({ id: -1 });
+ImageCaptchaRecordSchema.index({ id: -1 });
// Supports the labelled-dataset export query (`{ label: { $exists: true } }`).
-UserCommitmentRecordSchema.index({ label: 1, dappAccount: 1 });
-UserCommitmentRecordSchema.index({
+ImageCaptchaRecordSchema.index({ label: 1, dappAccount: 1 });
+ImageCaptchaRecordSchema.index({
lastUpdatedTimestamp: 1,
});
-UserCommitmentRecordSchema.index({ userAccount: 1, dappAccount: 1 });
-UserCommitmentRecordSchema.index({ "ipAddress.lower": 1 });
-UserCommitmentRecordSchema.index({ "ipAddress.upper": 1 });
-UserCommitmentRecordSchema.index({ "result.reason": 1 });
-UserCommitmentRecordSchema.index({ "ipInfo.countryCode": 1 });
-UserCommitmentRecordSchema.index({ "ipInfo.isVPN": 1 });
-UserCommitmentRecordSchema.index({ requestHash: -1 });
-UserCommitmentRecordSchema.index({ pending: 1 });
-UserCommitmentRecordSchema.index({ ipInfo: 1 });
-UserCommitmentRecordSchema.index({ parsedUserAgentInfo: 1 });
-UserCommitmentRecordSchema.index(
+ImageCaptchaRecordSchema.index({ userAccount: 1, dappAccount: 1 });
+ImageCaptchaRecordSchema.index({ "ipAddress.lower": 1 });
+ImageCaptchaRecordSchema.index({ "ipAddress.upper": 1 });
+ImageCaptchaRecordSchema.index({ "result.reason": 1 });
+ImageCaptchaRecordSchema.index({ "ipInfo.countryCode": 1 });
+ImageCaptchaRecordSchema.index({ "ipInfo.isVPN": 1 });
+ImageCaptchaRecordSchema.index({ requestHash: -1 });
+ImageCaptchaRecordSchema.index({ pending: 1 });
+ImageCaptchaRecordSchema.index({ ipInfo: 1 });
+ImageCaptchaRecordSchema.index({ parsedUserAgentInfo: 1 });
+ImageCaptchaRecordSchema.index(
{ pendingStage: 1 },
{
name: "pendingStage_partial",
@@ -453,6 +478,12 @@ UserCommitmentRecordSchema.index(
},
);
+/**
+ * @deprecated Use {@link ImageCaptchaRecordSchema}. Back-compat alias; remove in
+ * a later cleanup.
+ */
+export const UserCommitmentRecordSchema = ImageCaptchaRecordSchema;
+
export const DatasetRecordSchema = new Schema({
contentTree: { type: [[String]], required: true },
datasetContentId: { type: String, required: true },
@@ -534,6 +565,12 @@ ScheduledTaskRecordSchema.index({ _id: 1, status: 1 });
export type SessionRecord = mongoose.Document & Session;
export const SessionRecordSchema = new Schema({
+ requestType: {
+ type: String,
+ enum: RequestType,
+ required: false,
+ default: RequestType.session,
+ },
sessionId: { type: String, required: true },
createdAt: { type: Date, required: true },
token: { type: String, required: true },
@@ -568,7 +605,7 @@ export const SessionRecordSchema = new Schema({
blocked: { type: Boolean, required: false },
// Full ipinfo payload — replaces flat `countryCode` / `geolocation`
// fields. Mirrors the captcha record schemas (PoW / Puzzle /
- // UserCommitment).
+ // ImageCaptcha).
ipInfo: { type: Object, required: false },
headers: { type: Object, required: false },
result: {
@@ -726,9 +763,9 @@ export interface IProviderDatabase extends IDatabase {
datasetId: Hash | string | Uint8Array,
): Promise;
- storeUserImageCaptchaSolution(
+ storeImageCaptchaSolution(
captchas: CaptchaSolution[],
- commit: UserCommitment,
+ commit: ImageCaptcha,
): Promise;
storePendingImageCommitment(
@@ -771,43 +808,43 @@ export interface IProviderDatabase extends IDatabase {
commitmentId: string,
): Promise;
- getDappUserCommitmentById(
+ getImageCaptchaById(
commitmentId: string,
- ): Promise;
+ ): Promise;
- getDappUserCommitmentByAccount(
+ getImageCaptchaByAccount(
userAccount: string,
dappAccount: string,
- ): Promise;
+ ): Promise;
- approveDappUserCommitment(
+ approveImageCaptcha(
commitmentId: string,
coords?: [number, number][][],
): Promise;
- disapproveDappUserCommitment(
+ disapproveImageCaptcha(
commitmentId: string,
reason?: TranslationKey,
coords?: [number, number][][],
): Promise;
- getCheckedDappUserCommitments(): Promise;
+ getCheckedImageCaptchas(): Promise;
- getUnstoredDappUserCommitments(
+ getUnstoredImageCaptchas(
limit?: number,
skip?: number,
- ): Promise;
+ ): Promise;
- markDappUserCommitmentsStored(
+ markImageCaptchasStored(
commitmentIds: Hash[],
asOfTimestamp?: Date,
): Promise;
- markDappUserCommitmentsChecked(commitmentIds: Hash[]): Promise;
+ markImageCaptchasChecked(commitmentIds: Hash[]): Promise;
- updateDappUserCommitment(
+ updateImageCaptcha(
commitmentId: Hash,
- updates: Partial,
+ updates: Partial,
): Promise;
getUnstoredDappUserPoWCommitments(
@@ -824,7 +861,7 @@ export interface IProviderDatabase extends IDatabase {
flagProcessedDappUserSolutions(captchaIds: Hash[]): Promise;
- flagProcessedDappUserCommitments(commitmentIds: Hash[]): Promise;
+ flagProcessedImageCaptchas(commitmentIds: Hash[]): Promise;
getLastScheduledTaskStatus(
task: ScheduledTaskNames,
diff --git a/packages/types/src/provider/database.ts b/packages/types/src/provider/database.ts
index f439f66005..6f0190ac95 100644
--- a/packages/types/src/provider/database.ts
+++ b/packages/types/src/provider/database.ts
@@ -50,6 +50,7 @@ import type {
import type { PuzzleEvent, RequestHeaders } from "./api.js";
import type { SimdReadings } from "./detection.js";
import type { FrictionlessReason, ResultReason } from "./reasons.js";
+import { RequestType, RequestTypeSchema } from "./requestType.js";
export interface BrowserInfo {
name: string;
@@ -165,7 +166,30 @@ export enum CaptchaLabel {
export const CaptchaLabelSchema = nativeEnum(CaptchaLabel);
+/**
+ * Fields common to every persisted captcha request, regardless of kind
+ * (session / powcaptcha / imagecaptcha / puzzle). This is the shared shape the
+ * consolidation builds on: the `requestType` discriminator plus the
+ * cross-cutting correlation / staging fields that already exist on every
+ * record. Kept additive for now — `StoredCaptcha` and `Session` carry these
+ * fields directly rather than `extends`ing this, so no existing record shape is
+ * tightened. Once the records live in one `requests` collection this becomes
+ * the required base for the discriminated union.
+ */
+export interface RequestRecord {
+ requestType?: RequestType;
+ sessionId?: string;
+ requestedAtTimestamp?: Date;
+ lastUpdatedTimestamp?: Date;
+ storedAtTimestamp?: Date;
+ pendingStage?: boolean;
+}
+
export interface StoredCaptcha {
+ // Discriminator for the unified request model. Optional for back-compat
+ // with records written before the field existed; writers stamp it via the
+ // mongoose schema default. See {@link RequestRecord}.
+ requestType?: RequestType;
result: {
status: CaptchaStatus;
reason?: ResultReason;
@@ -216,7 +240,7 @@ export interface StoredCaptcha {
labelledAt?: Date;
}
-export interface UserCommitment extends StoredCaptcha {
+export interface ImageCaptcha extends StoredCaptcha {
userAccount: string;
dappAccount: string;
datasetId: string;
@@ -230,6 +254,12 @@ export interface UserCommitment extends StoredCaptcha {
deadlineTimestamp: Date;
}
+/**
+ * @deprecated Use {@link ImageCaptcha}. Retained as a back-compat alias while
+ * consumers migrate off the old name; remove in a later cleanup.
+ */
+export type UserCommitment = ImageCaptcha;
+
// Runtime parsing stays permissive (`string().optional()`) because decision
// machines are operator-authored JS — their `reason` is whatever string the
// machine returns, including values that won't be in `ResultReason`. The
@@ -260,7 +290,7 @@ export const ClientMetaDataDbSchema = object({
hp: string().optional(),
}) satisfies ZodType;
-export const UserCommitmentSchema = object({
+export const ImageCaptchaSchema = object({
userAccount: string(),
dappAccount: string(),
datasetId: string(),
@@ -280,6 +310,7 @@ export const UserCommitmentSchema = object({
requestedAtTimestamp: date(),
lastUpdatedTimestamp: date().optional(),
pendingStage: boolean().optional(),
+ requestType: RequestTypeSchema.optional(),
sessionId: string().optional(),
coords: array(array(tuple([number(), number()]))).optional(),
// Pending request fields for image captcha workflow
@@ -296,7 +327,13 @@ export const UserCommitmentSchema = object({
labelReason: string().optional(),
labelledBy: string().optional(),
labelledAt: date().optional(),
-}) satisfies ZodType;
+}) satisfies ZodType;
+
+/**
+ * @deprecated Use {@link ImageCaptchaSchema}. Back-compat alias; remove in a
+ * later cleanup.
+ */
+export const UserCommitmentSchema = ImageCaptchaSchema;
// Zod schema for ScoreComponents
export const ScoreComponentsSchema = object({
@@ -379,6 +416,7 @@ export const SessionSchema = object({
storedAtTimestamp: date().optional(),
lastUpdatedTimestamp: date().optional(),
pendingStage: boolean().optional(),
+ requestType: RequestTypeSchema.optional(),
deleted: boolean().optional(),
userSitekeyIpHash: string().optional(),
webView: boolean(),
@@ -438,6 +476,10 @@ export type Session = {
lastUpdatedTimestamp?: Date;
// See StoredCaptcha.pendingStage — same semantics on Session records.
pendingStage?: boolean;
+ // Discriminator for the unified request model. Always `RequestType.session`
+ // for session records; stamped via the mongoose schema default. Optional for
+ // back-compat with records written before the field existed.
+ requestType?: RequestType;
deleted?: boolean;
userSitekeyIpHash?: string;
webView: boolean;
@@ -494,6 +536,7 @@ export const PoWCaptchaStoredSchema = object({
parsedUserAgentInfo: any().optional(),
storedAtTimestamp: date().optional(),
lastUpdatedTimestamp: date().optional(),
+ requestType: RequestTypeSchema.optional(),
sessionId: string().optional(),
coords: array(array(tuple([number(), number()]))).optional(),
mouseEvents: array(object({}).catchall(any())).optional(),
@@ -552,14 +595,24 @@ export const UserSolutionSchema = CaptchaSolutionSchema.extend({
export type UserSolution = zInfer;
-export const UserCommitmentWithSolutionsSchema = UserCommitmentSchema.extend({
+export const ImageCaptchaWithSolutionsSchema = ImageCaptchaSchema.extend({
captchas: array(UserSolutionSchema),
});
-export type UserCommitmentWithSolutions = zInfer<
- typeof UserCommitmentWithSolutionsSchema
+export type ImageCaptchaWithSolutions = zInfer<
+ typeof ImageCaptchaWithSolutionsSchema
>;
+/**
+ * @deprecated Use {@link ImageCaptchaWithSolutionsSchema}. Back-compat alias.
+ */
+export const UserCommitmentWithSolutionsSchema = ImageCaptchaWithSolutionsSchema;
+
+/**
+ * @deprecated Use {@link ImageCaptchaWithSolutions}. Back-compat alias.
+ */
+export type UserCommitmentWithSolutions = ImageCaptchaWithSolutions;
+
export type DetectorKey = {
detectorKey: string;
createdAt: Date;
diff --git a/packages/types/src/provider/index.ts b/packages/types/src/provider/index.ts
index 7f0c37d56a..15159f443d 100644
--- a/packages/types/src/provider/index.ts
+++ b/packages/types/src/provider/index.ts
@@ -15,5 +15,6 @@ export * from "./accounts.js";
export * from "./api.js";
export * from "./scheduler.js";
export * from "./detection.js";
+export * from "./requestType.js";
export * from "./database.js";
export * from "./reasons.js";
diff --git a/packages/types/src/provider/requestType.ts b/packages/types/src/provider/requestType.ts
new file mode 100644
index 0000000000..74a3423710
--- /dev/null
+++ b/packages/types/src/provider/requestType.ts
@@ -0,0 +1,40 @@
+// Copyright 2021-2026 Prosopo (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import { nativeEnum } from "zod";
+
+/**
+ * Discriminator for the kind of captcha request a stored record represents.
+ *
+ * This is an orthogonal axis to {@link CaptchaType}: `CaptchaType` describes the
+ * challenge served to the user (image / pow / puzzle / frictionless), whereas
+ * `RequestType` describes which record collection / shape the row belongs to.
+ * A `session` record has no `CaptchaType` equivalent, which is why this is a
+ * separate enum rather than an extension of `CaptchaType`.
+ *
+ * Records are currently stored in separate physical collections; `requestType`
+ * makes each record self-describing ahead of consolidating them into a single
+ * `requests` collection. The discriminator is stamped on every record kind via
+ * the mongoose schema default; the physical collections are unchanged until the
+ * later consolidation phase.
+ */
+export enum RequestType {
+ session = "session",
+ powcaptcha = "powcaptcha",
+ imagecaptcha = "imagecaptcha",
+ // Puzzle records keep their existing shape and collection; the discriminator
+ // is stamped so they can join the unified collection without a backfill.
+ puzzlecaptcha = "puzzlecaptcha",
+}
+
+export const RequestTypeSchema = nativeEnum(RequestType);