@prosopo/database 3.15.18 → 3.15.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,258 +1,215 @@
1
+ import { MongoDatabase } from "../base/mongo.js";
1
2
  import { ProsopoDBError } from "@prosopo/common";
2
3
  import { getLogger } from "@prosopo/logger";
3
- import { StoredSessionRecordSchema, StoredPoWCaptchaRecordSchema, StoredUserCommitmentRecordSchema, StoredPuzzleCaptchaRecordSchema } from "@prosopo/types-database";
4
+ import { StoredPoWCaptchaRecordSchema, StoredPuzzleCaptchaRecordSchema, StoredSessionRecordSchema, StoredUserCommitmentRecordSchema } from "@prosopo/types-database";
4
5
  import { Decimal128, Long } from "bson";
5
- import "../base/index.js";
6
- import { MongoDatabase } from "../base/mongo.js";
7
- const isBsonLong = (value) => typeof value === "object" && value !== null && "_bsontype" in value && value._bsontype === "Long";
8
- const logger = getLogger("info", "database:captcha");
9
- var TableNames = /* @__PURE__ */ ((TableNames2) => {
10
- TableNames2["frictionlessToken"] = "frictionlessToken";
11
- TableNames2["session"] = "session";
12
- TableNames2["commitment"] = "commitment";
13
- TableNames2["powcaptcha"] = "powcaptcha";
14
- TableNames2["puzzlecaptcha"] = "puzzlecaptcha";
15
- return TableNames2;
16
- })(TableNames || {});
17
- const CAPTCHA_TABLES = [
18
- {
19
- collectionName: "session",
20
- modelName: "Session",
21
- schema: StoredSessionRecordSchema
22
- },
23
- {
24
- collectionName: "powcaptcha",
25
- modelName: "PowCaptcha",
26
- schema: StoredPoWCaptchaRecordSchema
27
- },
28
- {
29
- collectionName: "commitment",
30
- modelName: "UserCommitment",
31
- schema: StoredUserCommitmentRecordSchema
32
- },
33
- {
34
- collectionName: "puzzlecaptcha",
35
- modelName: "PuzzleCaptcha",
36
- schema: StoredPuzzleCaptchaRecordSchema
37
- }
6
+ //#region src/databases/captcha.ts
7
+ var isBsonLong = (value) => typeof value === "object" && value !== null && "_bsontype" in value && value._bsontype === "Long";
8
+ var logger = getLogger("info", "database:captcha");
9
+ var CAPTCHA_TABLES = [
10
+ {
11
+ collectionName: "session",
12
+ modelName: "Session",
13
+ schema: StoredSessionRecordSchema
14
+ },
15
+ {
16
+ collectionName: "powcaptcha",
17
+ modelName: "PowCaptcha",
18
+ schema: StoredPoWCaptchaRecordSchema
19
+ },
20
+ {
21
+ collectionName: "commitment",
22
+ modelName: "UserCommitment",
23
+ schema: StoredUserCommitmentRecordSchema
24
+ },
25
+ {
26
+ collectionName: "puzzlecaptcha",
27
+ modelName: "PuzzleCaptcha",
28
+ schema: StoredPuzzleCaptchaRecordSchema
29
+ }
38
30
  ];
39
- class CaptchaDatabase extends MongoDatabase {
40
- constructor(url, dbname, authSource, logger2) {
41
- super(url, dbname, authSource, logger2);
42
- this.indexesEnsured = false;
43
- this.tables = {};
44
- }
45
- async connect() {
46
- await super.connect();
47
- CAPTCHA_TABLES.map(({ collectionName, modelName, schema }) => {
48
- if (this.connection) {
49
- this.tables[collectionName] = this.connection.model(modelName, schema);
50
- }
51
- });
52
- }
53
- getTables() {
54
- if (!this.tables) {
55
- throw new ProsopoDBError("DATABASE.TABLES_UNDEFINED", {
56
- context: { failedFuncName: this.getTables.name },
57
- logger: this.logger
58
- });
59
- }
60
- return this.tables;
61
- }
62
- async ensureIndexes() {
63
- const indexPromises = [];
64
- if (!this.indexesEnsured) {
65
- CAPTCHA_TABLES.map(({ collectionName }) => {
66
- indexPromises.push(
67
- new Promise((resolve) => {
68
- if (this.connected) {
69
- this.tables[collectionName].collection.dropIndexes().then(() => {
70
- this.tables[collectionName].ensureIndexes().then(() => {
71
- resolve();
72
- }).catch((err) => {
73
- this.logger.warn(() => ({
74
- err,
75
- msg: `Error creating indexes for collection ${collectionName}`
76
- }));
77
- resolve();
78
- });
79
- });
80
- } else {
81
- this.logger.info(() => ({
82
- msg: `Skipping index creation for collection ${collectionName} as not connected`
83
- }));
84
- resolve();
85
- }
86
- })
87
- );
88
- });
89
- }
90
- await Promise.all(indexPromises);
91
- this.indexesEnsured = true;
92
- }
93
- /**
94
- * Convert a BSON `Long` (which Mongoose's Decimal128 caster rejects)
95
- * into a Decimal128 with the *unsigned* numeric value — so a Long
96
- * representing an IPv6 lower half with bit 63 set (signed Long shows
97
- * as a negative integer) converts to a positive Decimal128 that
98
- * matches what `bigint→string→Decimal128` would have produced if the
99
- * schema setter had run on the original write.
100
- *
101
- * Production background: pipeline-form updates (`updateOne(filter,
102
- * [{ $set: ... }])`) bypass Mongoose schema casting, so a `bigint`
103
- * passed for an IP half is serialised by the driver as BSON Int64
104
- * (Long) instead of going through the `Decimal128` setter. The
105
- * central-streaming sweep below reads those docs via `.lean()` and
106
- * tries to replay them with `bulkWrite`, but `bulkWrite` also skips
107
- * setters — so the cast fires raw and throws. Normalising the lean
108
- * doc here is the only place that's guaranteed to run before the
109
- * bulkWrite sees it.
110
- */
111
- static normaliseCompositeIp(ip) {
112
- if (!ip) return ip;
113
- const normaliseHalf = (v) => {
114
- if (isBsonLong(v)) {
115
- const lng = v;
116
- return Decimal128.fromString(
117
- Long.fromBits(lng.low, lng.high, true).toString()
118
- );
119
- }
120
- return v;
121
- };
122
- return {
123
- ...ip,
124
- lower: normaliseHalf(ip.lower),
125
- upper: normaliseHalf(ip.upper)
126
- };
127
- }
128
- static normaliseDocCompositeIps(doc) {
129
- return {
130
- ...doc,
131
- ipAddress: CaptchaDatabase.normaliseCompositeIp(doc.ipAddress),
132
- providedIp: CaptchaDatabase.normaliseCompositeIp(doc.providedIp)
133
- };
134
- }
135
- async saveCaptchas(sessionEvents, imageCaptchaEvents, powCaptchaEvents, puzzleCaptchaEvents = []) {
136
- await this.connect();
137
- if (sessionEvents.length) {
138
- const result = await this.tables.session.bulkWrite(
139
- sessionEvents.map((document) => {
140
- const { _id, ...safeDoc } = document;
141
- return {
142
- insertOne: {
143
- document: CaptchaDatabase.normaliseDocCompositeIps(safeDoc)
144
- }
145
- };
146
- })
147
- );
148
- logger.info(() => ({
149
- data: {
150
- insertedCount: result.insertedCount,
151
- totalProcessed: sessionEvents.length
152
- },
153
- msg: "Mongo Saved Session Events"
154
- }));
155
- }
156
- if (imageCaptchaEvents.length) {
157
- const result = await this.tables.commitment.bulkWrite(
158
- imageCaptchaEvents.map((doc) => {
159
- const { _id, ...safeDoc } = doc;
160
- const normalised = CaptchaDatabase.normaliseDocCompositeIps(safeDoc);
161
- return {
162
- updateOne: {
163
- filter: { id: normalised.id },
164
- update: { $set: normalised },
165
- upsert: true
166
- }
167
- };
168
- })
169
- );
170
- logger.info(() => ({
171
- data: {
172
- upsertedCount: result.upsertedCount,
173
- matchedCount: result.matchedCount,
174
- modifiedCount: result.modifiedCount,
175
- totalProcessed: imageCaptchaEvents.length
176
- },
177
- msg: "Mongo Saved Image Events"
178
- }));
179
- }
180
- if (powCaptchaEvents.length) {
181
- const result = await this.tables.powcaptcha.bulkWrite(
182
- powCaptchaEvents.map((doc) => {
183
- const { _id, ...safeDoc } = doc;
184
- const normalised = CaptchaDatabase.normaliseDocCompositeIps(safeDoc);
185
- return {
186
- updateOne: {
187
- filter: { challenge: normalised.challenge },
188
- update: { $set: normalised },
189
- upsert: true
190
- }
191
- };
192
- })
193
- );
194
- logger.info(() => ({
195
- data: {
196
- upsertedCount: result.upsertedCount,
197
- matchedCount: result.matchedCount,
198
- modifiedCount: result.modifiedCount,
199
- totalProcessed: powCaptchaEvents.length
200
- },
201
- msg: "Mongo Saved PoW Events"
202
- }));
203
- }
204
- if (puzzleCaptchaEvents.length) {
205
- const result = await this.tables.puzzlecaptcha.bulkWrite(
206
- puzzleCaptchaEvents.map((doc) => {
207
- const { _id, ...safeDoc } = doc;
208
- const normalised = CaptchaDatabase.normaliseDocCompositeIps(safeDoc);
209
- return {
210
- updateOne: {
211
- filter: { challenge: normalised.challenge },
212
- update: { $set: normalised },
213
- upsert: true
214
- }
215
- };
216
- })
217
- );
218
- logger.info(() => ({
219
- data: {
220
- upsertedCount: result.upsertedCount,
221
- matchedCount: result.matchedCount,
222
- modifiedCount: result.modifiedCount,
223
- totalProcessed: puzzleCaptchaEvents.length
224
- },
225
- msg: "Mongo Saved Puzzle Events"
226
- }));
227
- }
228
- await this.close();
229
- }
230
- async getCaptchas(filter = {}, limit = 100) {
231
- await this.connect();
232
- try {
233
- const commitmentResults = await this.tables.commitment.find(filter).limit(limit).lean();
234
- const powCaptchaResults = await this.tables.powcaptcha.find(filter).limit(limit).lean();
235
- const puzzleCaptchaResults = await this.tables.puzzlecaptcha.find(filter).limit(limit).lean();
236
- return {
237
- userCommitmentRecords: commitmentResults,
238
- powCaptchaRecords: powCaptchaResults,
239
- puzzleCaptchaRecords: puzzleCaptchaResults
240
- };
241
- } catch (error) {
242
- throw new ProsopoDBError("DATABASE.QUERY_ERROR", {
243
- context: {
244
- error,
245
- filter,
246
- limit,
247
- failedFuncName: this.getCaptchas.name
248
- },
249
- logger: this.logger
250
- });
251
- } finally {
252
- await this.close();
253
- }
254
- }
255
- }
256
- export {
257
- CaptchaDatabase
31
+ var CaptchaDatabase = class CaptchaDatabase extends MongoDatabase {
32
+ constructor(url, dbname, authSource, logger) {
33
+ super(url, dbname, authSource, logger);
34
+ this.indexesEnsured = false;
35
+ this.tables = {};
36
+ }
37
+ async connect() {
38
+ await super.connect();
39
+ CAPTCHA_TABLES.map(({ collectionName, modelName, schema }) => {
40
+ if (this.connection) this.tables[collectionName] = this.connection.model(modelName, schema);
41
+ });
42
+ }
43
+ getTables() {
44
+ if (!this.tables) throw new ProsopoDBError("DATABASE.TABLES_UNDEFINED", {
45
+ context: { failedFuncName: this.getTables.name },
46
+ logger: this.logger
47
+ });
48
+ return this.tables;
49
+ }
50
+ async ensureIndexes() {
51
+ const indexPromises = [];
52
+ if (!this.indexesEnsured) CAPTCHA_TABLES.map(({ collectionName }) => {
53
+ indexPromises.push(new Promise((resolve) => {
54
+ if (this.connected) this.tables[collectionName].collection.dropIndexes().then(() => {
55
+ this.tables[collectionName].ensureIndexes().then(() => {
56
+ resolve();
57
+ }).catch((err) => {
58
+ this.logger.warn(() => ({
59
+ err,
60
+ msg: `Error creating indexes for collection ${collectionName}`
61
+ }));
62
+ resolve();
63
+ });
64
+ });
65
+ else {
66
+ this.logger.info(() => ({ msg: `Skipping index creation for collection ${collectionName} as not connected` }));
67
+ resolve();
68
+ }
69
+ }));
70
+ });
71
+ await Promise.all(indexPromises);
72
+ this.indexesEnsured = true;
73
+ }
74
+ /**
75
+ * Convert a BSON `Long` (which Mongoose's Decimal128 caster rejects)
76
+ * into a Decimal128 with the *unsigned* numeric value — so a Long
77
+ * representing an IPv6 lower half with bit 63 set (signed Long shows
78
+ * as a negative integer) converts to a positive Decimal128 that
79
+ * matches what `bigint→string→Decimal128` would have produced if the
80
+ * schema setter had run on the original write.
81
+ *
82
+ * Production background: pipeline-form updates (`updateOne(filter,
83
+ * [{ $set: ... }])`) bypass Mongoose schema casting, so a `bigint`
84
+ * passed for an IP half is serialised by the driver as BSON Int64
85
+ * (Long) instead of going through the `Decimal128` setter. The
86
+ * central-streaming sweep below reads those docs via `.lean()` and
87
+ * tries to replay them with `bulkWrite`, but `bulkWrite` also skips
88
+ * setters — so the cast fires raw and throws. Normalising the lean
89
+ * doc here is the only place that's guaranteed to run before the
90
+ * bulkWrite sees it.
91
+ */
92
+ static normaliseCompositeIp(ip) {
93
+ if (!ip) return ip;
94
+ const normaliseHalf = (v) => {
95
+ if (isBsonLong(v)) {
96
+ const lng = v;
97
+ return Decimal128.fromString(Long.fromBits(lng.low, lng.high, true).toString());
98
+ }
99
+ return v;
100
+ };
101
+ return {
102
+ ...ip,
103
+ lower: normaliseHalf(ip.lower),
104
+ upper: normaliseHalf(ip.upper)
105
+ };
106
+ }
107
+ static normaliseDocCompositeIps(doc) {
108
+ return {
109
+ ...doc,
110
+ ipAddress: CaptchaDatabase.normaliseCompositeIp(doc.ipAddress),
111
+ providedIp: CaptchaDatabase.normaliseCompositeIp(doc.providedIp)
112
+ };
113
+ }
114
+ async saveCaptchas(sessionEvents, imageCaptchaEvents, powCaptchaEvents, puzzleCaptchaEvents = []) {
115
+ await this.connect();
116
+ if (sessionEvents.length) {
117
+ const result = await this.tables.session.bulkWrite(sessionEvents.map((document) => {
118
+ const { _id, ...safeDoc } = document;
119
+ return { insertOne: { document: CaptchaDatabase.normaliseDocCompositeIps(safeDoc) } };
120
+ }));
121
+ logger.info(() => ({
122
+ data: {
123
+ insertedCount: result.insertedCount,
124
+ totalProcessed: sessionEvents.length
125
+ },
126
+ msg: "Mongo Saved Session Events"
127
+ }));
128
+ }
129
+ if (imageCaptchaEvents.length) {
130
+ const result = await this.tables.commitment.bulkWrite(imageCaptchaEvents.map((doc) => {
131
+ const { _id, ...safeDoc } = doc;
132
+ const normalised = CaptchaDatabase.normaliseDocCompositeIps(safeDoc);
133
+ return { updateOne: {
134
+ filter: { id: normalised.id },
135
+ update: { $set: normalised },
136
+ upsert: true
137
+ } };
138
+ }));
139
+ logger.info(() => ({
140
+ data: {
141
+ upsertedCount: result.upsertedCount,
142
+ matchedCount: result.matchedCount,
143
+ modifiedCount: result.modifiedCount,
144
+ totalProcessed: imageCaptchaEvents.length
145
+ },
146
+ msg: "Mongo Saved Image Events"
147
+ }));
148
+ }
149
+ if (powCaptchaEvents.length) {
150
+ const result = await this.tables.powcaptcha.bulkWrite(powCaptchaEvents.map((doc) => {
151
+ const { _id, ...safeDoc } = doc;
152
+ const normalised = CaptchaDatabase.normaliseDocCompositeIps(safeDoc);
153
+ return { updateOne: {
154
+ filter: { challenge: normalised.challenge },
155
+ update: { $set: normalised },
156
+ upsert: true
157
+ } };
158
+ }));
159
+ logger.info(() => ({
160
+ data: {
161
+ upsertedCount: result.upsertedCount,
162
+ matchedCount: result.matchedCount,
163
+ modifiedCount: result.modifiedCount,
164
+ totalProcessed: powCaptchaEvents.length
165
+ },
166
+ msg: "Mongo Saved PoW Events"
167
+ }));
168
+ }
169
+ if (puzzleCaptchaEvents.length) {
170
+ const result = await this.tables.puzzlecaptcha.bulkWrite(puzzleCaptchaEvents.map((doc) => {
171
+ const { _id, ...safeDoc } = doc;
172
+ const normalised = CaptchaDatabase.normaliseDocCompositeIps(safeDoc);
173
+ return { updateOne: {
174
+ filter: { challenge: normalised.challenge },
175
+ update: { $set: normalised },
176
+ upsert: true
177
+ } };
178
+ }));
179
+ logger.info(() => ({
180
+ data: {
181
+ upsertedCount: result.upsertedCount,
182
+ matchedCount: result.matchedCount,
183
+ modifiedCount: result.modifiedCount,
184
+ totalProcessed: puzzleCaptchaEvents.length
185
+ },
186
+ msg: "Mongo Saved Puzzle Events"
187
+ }));
188
+ }
189
+ await this.close();
190
+ }
191
+ async getCaptchas(filter = {}, limit = 100) {
192
+ await this.connect();
193
+ try {
194
+ return {
195
+ userCommitmentRecords: await this.tables.commitment.find(filter).limit(limit).lean(),
196
+ powCaptchaRecords: await this.tables.powcaptcha.find(filter).limit(limit).lean(),
197
+ puzzleCaptchaRecords: await this.tables.puzzlecaptcha.find(filter).limit(limit).lean()
198
+ };
199
+ } catch (error) {
200
+ throw new ProsopoDBError("DATABASE.QUERY_ERROR", {
201
+ context: {
202
+ error,
203
+ filter,
204
+ limit,
205
+ failedFuncName: this.getCaptchas.name
206
+ },
207
+ logger: this.logger
208
+ });
209
+ } finally {
210
+ await this.close();
211
+ }
212
+ }
258
213
  };
214
+ //#endregion
215
+ export { CaptchaDatabase };