@riocrypto/common-server 1.0.2926 → 1.0.2928

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.
@@ -8,72 +8,140 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
12
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
13
+ var m = o[Symbol.asyncIterator], i;
14
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
15
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
16
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
17
+ };
11
18
  Object.defineProperty(exports, "__esModule", { value: true });
12
19
  exports.getOrderFromReference = void 0;
13
20
  const order_1 = require("../models/order");
14
21
  const common_1 = require("@riocrypto/common");
22
+ // A reference is 8 hex characters of the order id (two from the random bytes
23
+ // plus the last six); older orders were referenced by the last six alone. The
24
+ // payer types it into a concepto that can carry anything else around it, so
25
+ // matching has always been "does the text contain the token", and that is what
26
+ // is preserved here.
27
+ const NEW_FORMAT_LENGTH = 8;
28
+ const OLD_FORMAT_LENGTH = 6;
29
+ // Statuses a payment can no longer be applied to. Anything failed is included
30
+ // by name so that a status added later is excluded without a change here.
31
+ const excludedStatuses = Object.values(common_1.OrderStatus).filter((status) => {
32
+ const statusLower = status.toLowerCase();
33
+ return (statusLower === "complete" ||
34
+ statusLower === "cancelled" ||
35
+ statusLower === "expired" ||
36
+ statusLower === "refundcomplete" ||
37
+ statusLower.includes("failed"));
38
+ });
39
+ const HEX_DIGITS = "0123456789abcdef";
40
+ // Every window of `length` hex characters in the text, lowercase and in the
41
+ // order they appear. A reference has to be a run of hex, so anything else in
42
+ // the concepto is a boundary rather than a candidate.
43
+ const hexWindows = (text, length) => {
44
+ var _a;
45
+ const windows = new Set();
46
+ for (const run of (_a = text.toLowerCase().match(/[0-9a-f]+/g)) !== null && _a !== void 0 ? _a : []) {
47
+ for (let i = 0; i + length <= run.length; i += 1) {
48
+ windows.add(run.slice(i, i + length));
49
+ }
50
+ }
51
+ return Array.from(windows);
52
+ };
53
+ // A concepto is short, so a bound this size is never reached by a real payment.
54
+ // It is here so that a pathological text cannot turn the old-format fallback
55
+ // into a query with tens of thousands of keys.
56
+ const MAX_OLD_FORMAT_WINDOWS = 24;
15
57
  const getOrderFromReference = (reference, mongoose) => __awaiter(void 0, void 0, void 0, function* () {
16
- // Ensure reference is defined
58
+ var _a, e_1, _b, _c;
17
59
  if (!reference) {
18
60
  throw new Error("Invalid reference. Reference cannot be empty.");
19
61
  }
20
- // Convert reference to lowercase to ensure case-insensitive comparison
21
- const referenceText = reference.toLowerCase();
22
62
  const Order = (0, order_1.buildOrder)(mongoose);
23
- // Define statuses to exclude (completed, cancelled, and any failed status)
24
- const excludedStatuses = Object.values(common_1.OrderStatus).filter((status) => {
25
- const statusLower = status.toLowerCase();
26
- return (statusLower === "complete" ||
27
- statusLower === "cancelled" ||
28
- statusLower === "expired" ||
29
- statusLower === "refundcomplete" ||
30
- statusLower.includes("failed"));
31
- });
32
- // Process in batches to avoid memory issues
33
- const batchSize = 1000;
34
- let skip = 0;
35
- let foundOrder = null;
36
- let foundOrderOldFormat = null;
37
- while (!foundOrder) {
38
- // Query only active orders (exclude completed/cancelled/failed) in batches
39
- const orders = yield Order.find({
40
- status: { $nin: excludedStatuses },
41
- })
42
- .select("_id") // Only select the ID field to minimize memory usage
43
- .skip(skip)
44
- .limit(batchSize)
45
- .lean(); // Use lean() for better performance
46
- // If no more orders, break the loop
47
- if (orders.length === 0) {
48
- break;
63
+ const activeStatus = { $nin: excludedStatuses };
64
+ // New format, exact and indexed. The stored reference is what the customer was
65
+ // shown, upper case, so the windows are compared the same way.
66
+ const newFormatCandidates = hexWindows(reference, NEW_FORMAT_LENGTH).map((window) => window.toUpperCase());
67
+ if (newFormatCandidates.length) {
68
+ const order = yield Order.findOne({
69
+ "paymentInstructions.reference": { $in: newFormatCandidates },
70
+ status: activeStatus,
71
+ }).sort({ createdAt: 1 });
72
+ if (order) {
73
+ return order;
49
74
  }
50
- // Check each order ID against the reference
51
- // Priority 1: New format - 2 chars from random portion (8-9) + last 6 chars
52
- // Priority 2: Old format - just last 6 chars (backwards compatibility)
53
- for (const order of orders) {
54
- const orderId = order._id.toString().toLowerCase();
55
- const randomPrefix = orderId.slice(8, 10);
56
- const last6 = orderId.slice(-6);
57
- const reference8Chars = randomPrefix + last6;
58
- // Check new 8-char format first (takes priority)
59
- if (referenceText.includes(reference8Chars)) {
60
- foundOrder = yield Order.findById(order._id);
61
- break;
75
+ }
76
+ const oldFormatWindows = hexWindows(reference, OLD_FORMAT_LENGTH).slice(0, MAX_OLD_FORMAT_WINDOWS);
77
+ // Buy orders that were never given a stored reference, from before the bank
78
+ // payment listener wrote one. Only these are still compared against the id in
79
+ // code. Walked once, with a cursor over a bounded query rather than paging
80
+ // through every active order with skip, and before the old-format lookup so a
81
+ // full match here still beats a six-character one anywhere, as it always has.
82
+ const newFormatWindowSet = new Set(newFormatCandidates.map((candidate) => candidate.toLowerCase()));
83
+ const oldFormatWindowSet = new Set(oldFormatWindows);
84
+ let legacyOldFormatMatchId = null;
85
+ const legacyOrders = Order.find({
86
+ status: activeStatus,
87
+ side: common_1.Side.Buy,
88
+ "paymentInstructions.reference": { $exists: false },
89
+ })
90
+ .select("_id")
91
+ .lean()
92
+ .cursor();
93
+ try {
94
+ for (var _d = true, legacyOrders_1 = __asyncValues(legacyOrders), legacyOrders_1_1; legacyOrders_1_1 = yield legacyOrders_1.next(), _a = legacyOrders_1_1.done, !_a;) {
95
+ _c = legacyOrders_1_1.value;
96
+ _d = false;
97
+ try {
98
+ const legacy = _c;
99
+ const orderId = legacy._id.toString().toLowerCase();
100
+ const reference8Chars = orderId.slice(8, 10) + orderId.slice(-6);
101
+ if (newFormatWindowSet.has(reference8Chars)) {
102
+ return Order.findById(legacy._id);
103
+ }
104
+ if (!legacyOldFormatMatchId && oldFormatWindowSet.has(orderId.slice(-6))) {
105
+ legacyOldFormatMatchId = legacy._id;
106
+ }
62
107
  }
63
- // Track old 6-char format match as fallback (only if no 8-char match yet)
64
- if (!foundOrderOldFormat && referenceText.includes(last6)) {
65
- foundOrderOldFormat = yield Order.findById(order._id);
108
+ finally {
109
+ _d = true;
66
110
  }
67
111
  }
68
- skip += batchSize;
69
112
  }
70
- // Use 8-char match if found, otherwise fall back to 6-char match
71
- if (!foundOrder && foundOrderOldFormat) {
72
- foundOrder = foundOrderOldFormat;
113
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
114
+ finally {
115
+ try {
116
+ if (!_d && !_a && (_b = legacyOrders_1.return)) yield _b.call(legacyOrders_1);
117
+ }
118
+ finally { if (e_1) throw e_1.error; }
119
+ }
120
+ // Old format: the payer typed only the last six characters. Those are also the
121
+ // last six of a stored new-format reference, so each six-character window is
122
+ // widened to every two-character prefix it could sit behind and looked up on
123
+ // the same index. 256 keys per window keeps this a point lookup rather than a
124
+ // scan, which a regex with a leading wildcard could not be.
125
+ if (oldFormatWindows.length) {
126
+ const oldFormatCandidates = [];
127
+ for (const window of oldFormatWindows) {
128
+ for (const first of HEX_DIGITS) {
129
+ for (const second of HEX_DIGITS) {
130
+ oldFormatCandidates.push((first + second + window).toUpperCase());
131
+ }
132
+ }
133
+ }
134
+ const order = yield Order.findOne({
135
+ "paymentInstructions.reference": { $in: oldFormatCandidates },
136
+ status: activeStatus,
137
+ }).sort({ createdAt: 1 });
138
+ if (order) {
139
+ return order;
140
+ }
73
141
  }
74
- if (!foundOrder) {
75
- throw new Error("Order not found");
142
+ if (legacyOldFormatMatchId) {
143
+ return Order.findById(legacyOldFormatMatchId);
76
144
  }
77
- return foundOrder;
145
+ throw new Error("Order not found");
78
146
  });
79
147
  exports.getOrderFromReference = getOrderFromReference;
@@ -412,6 +412,11 @@ const buildOrder = (mongoose) => {
412
412
  orderSchema.index({ status: 1, createdAt: 1 });
413
413
  orderSchema.index({ clientReferenceId: 1 }, { sparse: true });
414
414
  orderSchema.index({ "paymentsReceived.blockchainTxId": 1 }, { sparse: true });
415
+ // Every inbound bank deposit is matched to its order by the reference the
416
+ // customer typed. Before this, that meant loading every active order and
417
+ // comparing ids in code on each webhook, which under a burst of payments held
418
+ // the fintoc pod long enough for its liveness probe to kill it mid-deposit.
419
+ orderSchema.index({ "paymentInstructions.reference": 1 }, { sparse: true });
415
420
  // At most one order per real quoteId. Imported orders use quoteId "" and are
416
421
  // excluded by the partial filter (which also serves the createOrder lookup).
417
422
  orderSchema.index({ quoteId: 1 }, { unique: true, partialFilterExpression: { quoteId: { $gt: "" } } });
@@ -25,6 +25,7 @@ declare class LoggerService {
25
25
  private setupConsoleMasking;
26
26
  private setupStreamMasking;
27
27
  getLogger(): Logger;
28
+ private gcpSeverity;
28
29
  initLogger(): Promise<void>;
29
30
  }
30
31
  declare const loggerService: LoggerService;
@@ -278,6 +278,22 @@ class LoggerService {
278
278
  // Bearer tokens: Bearer <token>
279
279
  /(\bBearer\s+)([^\s,;"]+)/gi,
280
280
  ];
281
+ // Cloud Logging reads a top level "severity" field from JSON stdout lines.
282
+ // Without it every stdout line is stored as INFO and stderr as ERROR.
283
+ this.gcpSeverity = (0, winston_1.format)((info) => {
284
+ var _a;
285
+ const severities = {
286
+ error: "ERROR",
287
+ warn: "WARNING",
288
+ info: "INFO",
289
+ http: "INFO",
290
+ verbose: "DEBUG",
291
+ debug: "DEBUG",
292
+ silly: "DEBUG",
293
+ };
294
+ info.severity = (_a = severities[info.level]) !== null && _a !== void 0 ? _a : "DEFAULT";
295
+ return info;
296
+ });
281
297
  this.logger = null;
282
298
  this.setupConsoleMasking();
283
299
  this.setupStreamMasking();
@@ -453,11 +469,16 @@ class LoggerService {
453
469
  service: process.env.OTEL_SERVICE_NAME,
454
470
  }));
455
471
  }
472
+ // One JSON line per log event. prettyPrint spreads each event over many
473
+ // stdout lines and Cloud Logging stores and bills every line as its own
474
+ // entry with roughly 800 bytes of metadata, which multiplied ingestion
475
+ // volume by 7x to 25x depending on the service.
476
+ const outputFormat = process.env.RIO_ENV === common_1.RioEnv.Local ? winston_1.format.prettyPrint() : winston_1.format.json();
456
477
  this.logger = (0, winston_1.createLogger)({
457
478
  level: logLevel,
458
479
  format: winston_1.format.combine(winston_1.format.timestamp(), this.maskSensitiveData(), this.requestMethodFilter(), // Add the request method filter
459
480
  this.handleCustomErrorFormat(), // Add the custom error format
460
- winston_1.format.json(), winston_1.format.prettyPrint()),
481
+ this.gcpSeverity(), outputFormat),
461
482
  transports: transportsArray,
462
483
  });
463
484
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2926",
3
+ "version": "1.0.2928",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",