@codixus/server 0.1.5 → 0.1.7
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.
- package/dist/index.cjs +900 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +96 -1
- package/dist/index.d.ts +96 -1
- package/dist/index.js +899 -0
- package/dist/index.js.map +1 -1
- package/package.json +16 -5
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ var ConnectionManager = class {
|
|
|
5
5
|
this.uri = uri;
|
|
6
6
|
this.dbName = dbName;
|
|
7
7
|
}
|
|
8
|
+
uri;
|
|
9
|
+
dbName;
|
|
8
10
|
client = null;
|
|
9
11
|
db = null;
|
|
10
12
|
async connect() {
|
|
@@ -829,6 +831,7 @@ var RestError = class extends Error {
|
|
|
829
831
|
super(message);
|
|
830
832
|
this.code = code;
|
|
831
833
|
}
|
|
834
|
+
code;
|
|
832
835
|
};
|
|
833
836
|
function param(req, name) {
|
|
834
837
|
const val = req.params[name];
|
|
@@ -1260,6 +1263,724 @@ function createRestRouter(model2, options = {}) {
|
|
|
1260
1263
|
return router;
|
|
1261
1264
|
}
|
|
1262
1265
|
|
|
1266
|
+
// src/push/router.ts
|
|
1267
|
+
import { Router as Router3 } from "express";
|
|
1268
|
+
import { randomUUID } from "crypto";
|
|
1269
|
+
function checkAuth(req, res) {
|
|
1270
|
+
const deviceId = req.user?.deviceId;
|
|
1271
|
+
if (!deviceId) {
|
|
1272
|
+
res.status(401).json({ success: false, error: "Missing authorization header" });
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
return deviceId;
|
|
1276
|
+
}
|
|
1277
|
+
function isNonEmptyString(value) {
|
|
1278
|
+
return typeof value === "string" && value.length > 0;
|
|
1279
|
+
}
|
|
1280
|
+
function createPushRouter(db) {
|
|
1281
|
+
const router = Router3();
|
|
1282
|
+
const devicesCol = db.collection("push_devices");
|
|
1283
|
+
const deliveriesCol = db.collection("push_deliveries");
|
|
1284
|
+
router.post("/register", async (req, res) => {
|
|
1285
|
+
const deviceId = checkAuth(req, res);
|
|
1286
|
+
if (!deviceId) return;
|
|
1287
|
+
const { token, provider, platform, locale, timezone, appVersion, permissionStatus } = req.body ?? {};
|
|
1288
|
+
if (!isNonEmptyString(token) || !isNonEmptyString(provider) || platform !== "ios" && platform !== "android") {
|
|
1289
|
+
res.status(400).json({ success: false, error: "Invalid request body" });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const now = /* @__PURE__ */ new Date();
|
|
1293
|
+
const existing = await devicesCol.findOne({ token });
|
|
1294
|
+
if (existing) {
|
|
1295
|
+
const update = {
|
|
1296
|
+
deviceId,
|
|
1297
|
+
enabled: true,
|
|
1298
|
+
lastSeenAt: now,
|
|
1299
|
+
updatedAt: now,
|
|
1300
|
+
provider,
|
|
1301
|
+
platform
|
|
1302
|
+
};
|
|
1303
|
+
if (locale !== void 0) update.locale = locale;
|
|
1304
|
+
if (timezone !== void 0) update.timezone = timezone;
|
|
1305
|
+
if (appVersion !== void 0) update.appVersion = appVersion;
|
|
1306
|
+
if (permissionStatus !== void 0)
|
|
1307
|
+
update.permissionStatus = permissionStatus;
|
|
1308
|
+
await devicesCol.updateOne({ token }, { $set: update });
|
|
1309
|
+
res.json({ success: true });
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const doc = {
|
|
1313
|
+
_id: randomUUID(),
|
|
1314
|
+
deviceId,
|
|
1315
|
+
token,
|
|
1316
|
+
provider,
|
|
1317
|
+
platform,
|
|
1318
|
+
enabled: true,
|
|
1319
|
+
lastSeenAt: now,
|
|
1320
|
+
createdAt: now,
|
|
1321
|
+
updatedAt: now
|
|
1322
|
+
};
|
|
1323
|
+
if (locale !== void 0) doc.locale = locale;
|
|
1324
|
+
if (timezone !== void 0) doc.timezone = timezone;
|
|
1325
|
+
if (appVersion !== void 0) doc.appVersion = appVersion;
|
|
1326
|
+
if (permissionStatus !== void 0) doc.permissionStatus = permissionStatus;
|
|
1327
|
+
try {
|
|
1328
|
+
await devicesCol.insertOne(doc);
|
|
1329
|
+
} catch (err) {
|
|
1330
|
+
if (!isDuplicateKeyError(err)) throw err;
|
|
1331
|
+
const update = {
|
|
1332
|
+
deviceId,
|
|
1333
|
+
enabled: true,
|
|
1334
|
+
lastSeenAt: now,
|
|
1335
|
+
updatedAt: now,
|
|
1336
|
+
provider,
|
|
1337
|
+
platform
|
|
1338
|
+
};
|
|
1339
|
+
if (locale !== void 0) update.locale = locale;
|
|
1340
|
+
if (timezone !== void 0) update.timezone = timezone;
|
|
1341
|
+
if (appVersion !== void 0) update.appVersion = appVersion;
|
|
1342
|
+
if (permissionStatus !== void 0)
|
|
1343
|
+
update.permissionStatus = permissionStatus;
|
|
1344
|
+
await devicesCol.updateOne({ token }, { $set: update });
|
|
1345
|
+
}
|
|
1346
|
+
res.json({ success: true });
|
|
1347
|
+
});
|
|
1348
|
+
router.post("/unregister", async (req, res) => {
|
|
1349
|
+
const deviceId = checkAuth(req, res);
|
|
1350
|
+
if (!deviceId) return;
|
|
1351
|
+
await devicesCol.updateMany(
|
|
1352
|
+
{ deviceId },
|
|
1353
|
+
{ $set: { enabled: false, updatedAt: /* @__PURE__ */ new Date() } }
|
|
1354
|
+
);
|
|
1355
|
+
res.json({ success: true });
|
|
1356
|
+
});
|
|
1357
|
+
router.post("/open", async (req, res) => {
|
|
1358
|
+
const deviceId = checkAuth(req, res);
|
|
1359
|
+
if (!deviceId) return;
|
|
1360
|
+
const { deliveryId } = req.body ?? {};
|
|
1361
|
+
if (!isNonEmptyString(deliveryId)) {
|
|
1362
|
+
res.status(400).json({ success: false, error: "Invalid request body" });
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const delivery = await deliveriesCol.findOne({ _id: deliveryId });
|
|
1366
|
+
if (!delivery || delivery.deviceId !== deviceId) {
|
|
1367
|
+
res.status(404).json({ success: false, error: "NOT_FOUND" });
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const update = { status: "opened" };
|
|
1371
|
+
if (!delivery.openedAt) {
|
|
1372
|
+
update.openedAt = /* @__PURE__ */ new Date();
|
|
1373
|
+
}
|
|
1374
|
+
await deliveriesCol.updateOne({ _id: deliveryId }, { $set: update });
|
|
1375
|
+
res.json({ success: true });
|
|
1376
|
+
});
|
|
1377
|
+
return router;
|
|
1378
|
+
}
|
|
1379
|
+
function isDuplicateKeyError(err) {
|
|
1380
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === 11e3;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/admin/router.ts
|
|
1384
|
+
import { Router as Router4 } from "express";
|
|
1385
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1386
|
+
import { z } from "zod";
|
|
1387
|
+
|
|
1388
|
+
// src/admin/guard.ts
|
|
1389
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
1390
|
+
var ADMIN_HEADER = "x-codixus-admin";
|
|
1391
|
+
function digestToken(token) {
|
|
1392
|
+
return createHash("sha256").update(token, "utf8").digest();
|
|
1393
|
+
}
|
|
1394
|
+
function tokensMatch(provided, expected) {
|
|
1395
|
+
return timingSafeEqual(digestToken(provided), digestToken(expected));
|
|
1396
|
+
}
|
|
1397
|
+
function readHeader(req) {
|
|
1398
|
+
const value = req.headers[ADMIN_HEADER];
|
|
1399
|
+
if (typeof value === "string") return value;
|
|
1400
|
+
if (Array.isArray(value)) return value[0];
|
|
1401
|
+
return void 0;
|
|
1402
|
+
}
|
|
1403
|
+
function createAdminGuard(expectedToken) {
|
|
1404
|
+
return (req, res, next) => {
|
|
1405
|
+
const token = readHeader(req);
|
|
1406
|
+
if (!token || !tokensMatch(token, expectedToken)) {
|
|
1407
|
+
res.status(401).json({ success: false, error: "UNAUTHORIZED" });
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
next();
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/admin/router.ts
|
|
1415
|
+
var USERS_FIELDS = ["_id", "deviceId", "locale", "createdAt"];
|
|
1416
|
+
var PUSH_DEVICE_FIELDS = [
|
|
1417
|
+
"_id",
|
|
1418
|
+
"deviceId",
|
|
1419
|
+
"token",
|
|
1420
|
+
"provider",
|
|
1421
|
+
"platform",
|
|
1422
|
+
"locale",
|
|
1423
|
+
"timezone",
|
|
1424
|
+
"appVersion",
|
|
1425
|
+
"permissionStatus",
|
|
1426
|
+
"enabled",
|
|
1427
|
+
"lastSeenAt",
|
|
1428
|
+
"properties",
|
|
1429
|
+
"createdAt",
|
|
1430
|
+
"updatedAt"
|
|
1431
|
+
];
|
|
1432
|
+
var PUSH_DELIVERY_FIELDS = [
|
|
1433
|
+
"_id",
|
|
1434
|
+
"deviceId",
|
|
1435
|
+
"token",
|
|
1436
|
+
"provider",
|
|
1437
|
+
"title",
|
|
1438
|
+
"body",
|
|
1439
|
+
"data",
|
|
1440
|
+
"status",
|
|
1441
|
+
"ticketId",
|
|
1442
|
+
"errorCode",
|
|
1443
|
+
"idempotencyKey",
|
|
1444
|
+
"createdAt",
|
|
1445
|
+
"openedAt",
|
|
1446
|
+
"receiptedAt"
|
|
1447
|
+
];
|
|
1448
|
+
function param2(req, name) {
|
|
1449
|
+
const val = req.params[name];
|
|
1450
|
+
return Array.isArray(val) ? val[0] : val ?? "";
|
|
1451
|
+
}
|
|
1452
|
+
function isNonEmptyString2(value) {
|
|
1453
|
+
return typeof value === "string" && value.length > 0;
|
|
1454
|
+
}
|
|
1455
|
+
function getModelSchema(model2) {
|
|
1456
|
+
return model2.definition.schema;
|
|
1457
|
+
}
|
|
1458
|
+
function getModelFields(model2) {
|
|
1459
|
+
const schema = getModelSchema(model2);
|
|
1460
|
+
const fields = schema instanceof z.ZodObject ? Object.keys(schema.shape) : [];
|
|
1461
|
+
if (!fields.includes("_id")) {
|
|
1462
|
+
return ["_id", ...fields];
|
|
1463
|
+
}
|
|
1464
|
+
return fields;
|
|
1465
|
+
}
|
|
1466
|
+
function escapeRegex(value) {
|
|
1467
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1468
|
+
}
|
|
1469
|
+
function parseJsonObject(value, res) {
|
|
1470
|
+
if (value === void 0 || value === "") {
|
|
1471
|
+
return {};
|
|
1472
|
+
}
|
|
1473
|
+
if (typeof value !== "string") {
|
|
1474
|
+
res.status(400).json({ success: false, error: "Invalid request body" });
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
try {
|
|
1478
|
+
const parsed = JSON.parse(value);
|
|
1479
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1480
|
+
res.status(400).json({ success: false, error: "Invalid request body" });
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
return parsed;
|
|
1484
|
+
} catch {
|
|
1485
|
+
res.status(400).json({ success: false, error: "Invalid request body" });
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function parseSort(value, res) {
|
|
1490
|
+
if (value === void 0 || value === "") {
|
|
1491
|
+
return {};
|
|
1492
|
+
}
|
|
1493
|
+
const parsed = parseJsonObject(value, res);
|
|
1494
|
+
if (parsed === null) return null;
|
|
1495
|
+
const sort = {};
|
|
1496
|
+
for (const [field, direction] of Object.entries(parsed)) {
|
|
1497
|
+
if (direction !== 1 && direction !== -1) {
|
|
1498
|
+
res.status(400).json({ success: false, error: "Invalid request body" });
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
sort[field] = direction;
|
|
1502
|
+
}
|
|
1503
|
+
return sort;
|
|
1504
|
+
}
|
|
1505
|
+
function parseLimit(value) {
|
|
1506
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
1507
|
+
if (Number.isNaN(parsed) || parsed < 1) return 50;
|
|
1508
|
+
return Math.min(parsed, 100);
|
|
1509
|
+
}
|
|
1510
|
+
function parseSkip(value) {
|
|
1511
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
1512
|
+
if (Number.isNaN(parsed) || parsed < 0) return 0;
|
|
1513
|
+
return parsed;
|
|
1514
|
+
}
|
|
1515
|
+
var NATIVE_DATE_FIELDS = /* @__PURE__ */ new Set([
|
|
1516
|
+
"createdAt",
|
|
1517
|
+
"updatedAt",
|
|
1518
|
+
"lastSeenAt",
|
|
1519
|
+
"openedAt",
|
|
1520
|
+
"receiptedAt"
|
|
1521
|
+
]);
|
|
1522
|
+
function prepareSetValues(setValues) {
|
|
1523
|
+
const next = { ...setValues };
|
|
1524
|
+
delete next._id;
|
|
1525
|
+
for (const [key, value] of Object.entries(next)) {
|
|
1526
|
+
if (!NATIVE_DATE_FIELDS.has(key) || typeof value !== "string") {
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
const parsed = new Date(value);
|
|
1530
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1531
|
+
next[key] = parsed;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return next;
|
|
1535
|
+
}
|
|
1536
|
+
function extractSetValues(body) {
|
|
1537
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
1538
|
+
return {};
|
|
1539
|
+
}
|
|
1540
|
+
const obj = body;
|
|
1541
|
+
const update = obj.update;
|
|
1542
|
+
if (update !== void 0) {
|
|
1543
|
+
if (!update || typeof update !== "object" || Array.isArray(update)) {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
const setValues = update.$set;
|
|
1547
|
+
if (setValues === void 0 || typeof setValues !== "object" || setValues === null || Array.isArray(setValues)) {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
return setValues;
|
|
1551
|
+
}
|
|
1552
|
+
return obj;
|
|
1553
|
+
}
|
|
1554
|
+
function buildCollectionsList(hasPush) {
|
|
1555
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1556
|
+
for (const model2 of getModelRegistry()) {
|
|
1557
|
+
byName.set(model2.collectionName, {
|
|
1558
|
+
name: model2.collectionName,
|
|
1559
|
+
fields: getModelFields(model2),
|
|
1560
|
+
kind: "model"
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
if (!byName.has("users")) {
|
|
1564
|
+
byName.set("users", {
|
|
1565
|
+
name: "users",
|
|
1566
|
+
fields: [...USERS_FIELDS],
|
|
1567
|
+
kind: "users"
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (hasPush) {
|
|
1571
|
+
if (!byName.has("push_devices")) {
|
|
1572
|
+
byName.set("push_devices", {
|
|
1573
|
+
name: "push_devices",
|
|
1574
|
+
fields: [...PUSH_DEVICE_FIELDS],
|
|
1575
|
+
kind: "push_devices"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
if (!byName.has("push_deliveries")) {
|
|
1579
|
+
byName.set("push_deliveries", {
|
|
1580
|
+
name: "push_deliveries",
|
|
1581
|
+
fields: [...PUSH_DELIVERY_FIELDS],
|
|
1582
|
+
kind: "push_deliveries"
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return Array.from(byName.values()).sort(
|
|
1587
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
function resolveCollection(name, hasPush) {
|
|
1591
|
+
if (name === "refresh_tokens") return null;
|
|
1592
|
+
const model2 = getModelRegistry().find((m) => m.collectionName === name);
|
|
1593
|
+
if (model2) {
|
|
1594
|
+
return { kind: "model", name, model: model2 };
|
|
1595
|
+
}
|
|
1596
|
+
if (name === "users") {
|
|
1597
|
+
return { kind: "users", name: "users" };
|
|
1598
|
+
}
|
|
1599
|
+
if (hasPush && name === "push_devices") {
|
|
1600
|
+
return { kind: "push_devices", name: "push_devices" };
|
|
1601
|
+
}
|
|
1602
|
+
if (hasPush && name === "push_deliveries") {
|
|
1603
|
+
return { kind: "push_deliveries", name: "push_deliveries" };
|
|
1604
|
+
}
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
function notFound(res) {
|
|
1608
|
+
res.status(404).json({ success: false, error: "NOT_FOUND" });
|
|
1609
|
+
}
|
|
1610
|
+
function invalidBody(res) {
|
|
1611
|
+
res.status(400).json({ success: false, error: "Invalid request body" });
|
|
1612
|
+
}
|
|
1613
|
+
function createAdminRouter(deps) {
|
|
1614
|
+
const router = Router4();
|
|
1615
|
+
router.use(createAdminGuard(deps.token));
|
|
1616
|
+
router.get("/session", (_req, res) => {
|
|
1617
|
+
res.status(204).send();
|
|
1618
|
+
});
|
|
1619
|
+
router.get("/collections", (_req, res) => {
|
|
1620
|
+
res.json({
|
|
1621
|
+
success: true,
|
|
1622
|
+
data: buildCollectionsList(deps.hasPush)
|
|
1623
|
+
});
|
|
1624
|
+
});
|
|
1625
|
+
router.get("/collections/:name", async (req, res) => {
|
|
1626
|
+
const name = param2(req, "name");
|
|
1627
|
+
const resolved = resolveCollection(name, deps.hasPush);
|
|
1628
|
+
if (!resolved) {
|
|
1629
|
+
notFound(res);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
const filter = parseJsonObject(req.query.filter, res);
|
|
1633
|
+
if (filter === null) return;
|
|
1634
|
+
const sort = parseSort(req.query.sort, res);
|
|
1635
|
+
if (sort === null) return;
|
|
1636
|
+
const limit = parseLimit(req.query.limit);
|
|
1637
|
+
const skip = parseSkip(req.query.skip);
|
|
1638
|
+
const docs = await deps.db.collection(name).find(filter).sort(sort).skip(skip).limit(limit).toArray();
|
|
1639
|
+
const payload = {
|
|
1640
|
+
success: true,
|
|
1641
|
+
data: docs
|
|
1642
|
+
};
|
|
1643
|
+
if (docs.length === limit) {
|
|
1644
|
+
payload.nextSkip = skip + limit;
|
|
1645
|
+
}
|
|
1646
|
+
res.json(payload);
|
|
1647
|
+
});
|
|
1648
|
+
router.post("/collections/:name", async (req, res) => {
|
|
1649
|
+
const name = param2(req, "name");
|
|
1650
|
+
const resolved = resolveCollection(name, deps.hasPush);
|
|
1651
|
+
if (!resolved) {
|
|
1652
|
+
notFound(res);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
const body = req.body ?? {};
|
|
1656
|
+
if (resolved.kind === "model") {
|
|
1657
|
+
try {
|
|
1658
|
+
const created = await resolved.model.create(body);
|
|
1659
|
+
res.status(200).json({ success: true, data: created });
|
|
1660
|
+
} catch {
|
|
1661
|
+
invalidBody(res);
|
|
1662
|
+
}
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
1666
|
+
invalidBody(res);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const doc = { ...body };
|
|
1670
|
+
if (!doc._id) {
|
|
1671
|
+
doc._id = randomUUID2();
|
|
1672
|
+
}
|
|
1673
|
+
await deps.db.collection(name).insertOne(doc);
|
|
1674
|
+
res.status(200).json({ success: true, data: doc });
|
|
1675
|
+
});
|
|
1676
|
+
router.get("/collections/:name/:id", async (req, res) => {
|
|
1677
|
+
const name = param2(req, "name");
|
|
1678
|
+
const id = param2(req, "id");
|
|
1679
|
+
const resolved = resolveCollection(name, deps.hasPush);
|
|
1680
|
+
if (!resolved) {
|
|
1681
|
+
notFound(res);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const doc = await deps.db.collection(name).findOne({ _id: id });
|
|
1685
|
+
if (!doc) {
|
|
1686
|
+
notFound(res);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
res.json({ success: true, data: doc });
|
|
1690
|
+
});
|
|
1691
|
+
router.patch(
|
|
1692
|
+
"/collections/:name/:id",
|
|
1693
|
+
async (req, res) => {
|
|
1694
|
+
const name = param2(req, "name");
|
|
1695
|
+
const id = param2(req, "id");
|
|
1696
|
+
const resolved = resolveCollection(name, deps.hasPush);
|
|
1697
|
+
if (!resolved) {
|
|
1698
|
+
notFound(res);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const setValues = extractSetValues(req.body);
|
|
1702
|
+
if (setValues === null) {
|
|
1703
|
+
invalidBody(res);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
const col = deps.db.collection(name);
|
|
1707
|
+
const existing = await col.findOne({ _id: id });
|
|
1708
|
+
if (!existing) {
|
|
1709
|
+
notFound(res);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
if (resolved.kind === "model") {
|
|
1713
|
+
const schema = getModelSchema(resolved.model);
|
|
1714
|
+
try {
|
|
1715
|
+
schema.parse({ ...existing, ...setValues });
|
|
1716
|
+
} catch {
|
|
1717
|
+
invalidBody(res);
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
const prepared = prepareSetValues(setValues);
|
|
1722
|
+
if (Object.keys(prepared).length > 0) {
|
|
1723
|
+
await col.updateOne({ _id: id }, { $set: prepared });
|
|
1724
|
+
}
|
|
1725
|
+
const updated = await col.findOne({ _id: id });
|
|
1726
|
+
res.json({ success: true, data: updated });
|
|
1727
|
+
}
|
|
1728
|
+
);
|
|
1729
|
+
router.delete(
|
|
1730
|
+
"/collections/:name/:id",
|
|
1731
|
+
async (req, res) => {
|
|
1732
|
+
const name = param2(req, "name");
|
|
1733
|
+
const id = param2(req, "id");
|
|
1734
|
+
const resolved = resolveCollection(name, deps.hasPush);
|
|
1735
|
+
if (!resolved) {
|
|
1736
|
+
notFound(res);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const result = await deps.db.collection(name).deleteOne({ _id: id });
|
|
1740
|
+
if (result.deletedCount === 0) {
|
|
1741
|
+
notFound(res);
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
res.status(204).send();
|
|
1745
|
+
}
|
|
1746
|
+
);
|
|
1747
|
+
router.get("/push/devices", async (req, res) => {
|
|
1748
|
+
if (!deps.hasPush) {
|
|
1749
|
+
notFound(res);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const q = req.query.q;
|
|
1753
|
+
if (!isNonEmptyString2(q)) {
|
|
1754
|
+
invalidBody(res);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const devicesCol = deps.db.collection("push_devices");
|
|
1758
|
+
const pattern = escapeRegex(q);
|
|
1759
|
+
const docs = await devicesCol.find({
|
|
1760
|
+
$or: [
|
|
1761
|
+
{ deviceId: { $regex: pattern } },
|
|
1762
|
+
{ token: { $regex: pattern } }
|
|
1763
|
+
]
|
|
1764
|
+
}).sort({ enabled: -1, lastSeenAt: -1 }).limit(50).toArray();
|
|
1765
|
+
res.json({ success: true, data: docs });
|
|
1766
|
+
});
|
|
1767
|
+
router.post("/push/send", async (req, res) => {
|
|
1768
|
+
if (!deps.hasPush || !deps.pushSend) {
|
|
1769
|
+
notFound(res);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const { deviceId, title, body, data } = req.body ?? {};
|
|
1773
|
+
if (!isNonEmptyString2(deviceId) || !isNonEmptyString2(title) || !isNonEmptyString2(body)) {
|
|
1774
|
+
invalidBody(res);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const devicesCol = deps.db.collection("push_devices");
|
|
1778
|
+
const device = await devicesCol.findOne({ deviceId, enabled: true });
|
|
1779
|
+
if (!device) {
|
|
1780
|
+
res.status(404).json({ success: false, error: "NO_DEVICE" });
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
const input = { deviceId, title, body };
|
|
1784
|
+
if (data !== void 0) {
|
|
1785
|
+
input.data = data;
|
|
1786
|
+
}
|
|
1787
|
+
await deps.pushSend(input);
|
|
1788
|
+
res.status(200).json({ success: true });
|
|
1789
|
+
});
|
|
1790
|
+
router.get("/push/deliveries", async (req, res) => {
|
|
1791
|
+
if (!deps.hasPush) {
|
|
1792
|
+
notFound(res);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const deviceId = req.query.deviceId;
|
|
1796
|
+
if (!isNonEmptyString2(deviceId)) {
|
|
1797
|
+
invalidBody(res);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const deliveriesCol = deps.db.collection("push_deliveries");
|
|
1801
|
+
const docs = await deliveriesCol.find({ deviceId }).sort({ createdAt: -1 }).limit(50).toArray();
|
|
1802
|
+
res.json({ success: true, data: docs });
|
|
1803
|
+
});
|
|
1804
|
+
return router;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/push/service.ts
|
|
1808
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
1809
|
+
var PERMANENT_DISABLE_CODES = /* @__PURE__ */ new Set([
|
|
1810
|
+
"DeviceNotRegistered",
|
|
1811
|
+
"InvalidCredentials"
|
|
1812
|
+
]);
|
|
1813
|
+
var RECEIPT_BATCH_SIZE = 100;
|
|
1814
|
+
var PushService = class {
|
|
1815
|
+
constructor(db, transport) {
|
|
1816
|
+
this.transport = transport;
|
|
1817
|
+
this.devicesCol = db.collection("push_devices");
|
|
1818
|
+
this.deliveriesCol = db.collection("push_deliveries");
|
|
1819
|
+
}
|
|
1820
|
+
transport;
|
|
1821
|
+
devicesCol;
|
|
1822
|
+
deliveriesCol;
|
|
1823
|
+
async send(input) {
|
|
1824
|
+
const deviceIds = Array.isArray(input.deviceId) ? [...new Set(input.deviceId)] : [input.deviceId];
|
|
1825
|
+
if (deviceIds.length === 0) return;
|
|
1826
|
+
const digest = computeDigest(
|
|
1827
|
+
input.title,
|
|
1828
|
+
input.body,
|
|
1829
|
+
input.idempotencyKey
|
|
1830
|
+
);
|
|
1831
|
+
const pending = [];
|
|
1832
|
+
for (const deviceId of deviceIds) {
|
|
1833
|
+
const devices = await this.devicesCol.find({ deviceId, enabled: true }).toArray();
|
|
1834
|
+
for (const device of devices) {
|
|
1835
|
+
const idempotencyKey = `${deviceId}:${device.token}:${digest}`;
|
|
1836
|
+
const existing = await this.deliveriesCol.findOne({ idempotencyKey });
|
|
1837
|
+
if (existing) continue;
|
|
1838
|
+
const deliveryId = randomUUID3();
|
|
1839
|
+
const data = input.data ? { ...input.data } : {};
|
|
1840
|
+
if (!("deliveryId" in data)) {
|
|
1841
|
+
data.deliveryId = deliveryId;
|
|
1842
|
+
}
|
|
1843
|
+
pending.push({
|
|
1844
|
+
token: device.token,
|
|
1845
|
+
deviceId,
|
|
1846
|
+
provider: device.provider,
|
|
1847
|
+
deliveryId,
|
|
1848
|
+
idempotencyKey,
|
|
1849
|
+
message: {
|
|
1850
|
+
token: device.token,
|
|
1851
|
+
title: input.title,
|
|
1852
|
+
body: input.body,
|
|
1853
|
+
data
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
if (pending.length === 0) return;
|
|
1859
|
+
const messages = pending.map((p) => p.message);
|
|
1860
|
+
const tickets = await this.transport.send(messages);
|
|
1861
|
+
for (let i = 0; i < pending.length; i++) {
|
|
1862
|
+
const item = pending[i];
|
|
1863
|
+
const ticket = tickets[i];
|
|
1864
|
+
if (ticket && ticket.status === "error" && ticket.errorCode && PERMANENT_DISABLE_CODES.has(ticket.errorCode)) {
|
|
1865
|
+
await this.devicesCol.updateOne(
|
|
1866
|
+
{ token: item.token },
|
|
1867
|
+
{ $set: { enabled: false, updatedAt: /* @__PURE__ */ new Date() } }
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
const doc = {
|
|
1871
|
+
_id: item.deliveryId,
|
|
1872
|
+
deviceId: item.deviceId,
|
|
1873
|
+
token: item.token,
|
|
1874
|
+
provider: item.provider,
|
|
1875
|
+
title: input.title,
|
|
1876
|
+
body: input.body,
|
|
1877
|
+
data: item.message.data,
|
|
1878
|
+
status: "failed",
|
|
1879
|
+
idempotencyKey: item.idempotencyKey,
|
|
1880
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1881
|
+
};
|
|
1882
|
+
if (ticket && ticket.status === "ok") {
|
|
1883
|
+
doc.status = "submitted";
|
|
1884
|
+
if (ticket.ticketId) doc.ticketId = ticket.ticketId;
|
|
1885
|
+
} else {
|
|
1886
|
+
doc.errorCode = ticket?.errorCode ?? "TRANSIENT";
|
|
1887
|
+
if (ticket?.ticketId) doc.ticketId = ticket.ticketId;
|
|
1888
|
+
}
|
|
1889
|
+
try {
|
|
1890
|
+
await this.deliveriesCol.insertOne(doc);
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
if (!isDuplicateKeyError2(err)) throw err;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
async sendTo(input) {
|
|
1897
|
+
const devices = await this.devicesCol.find({ $and: [input.filter, { enabled: true }] }).toArray();
|
|
1898
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1899
|
+
const deviceIds = [];
|
|
1900
|
+
for (const device of devices) {
|
|
1901
|
+
if (!seen.has(device.deviceId)) {
|
|
1902
|
+
seen.add(device.deviceId);
|
|
1903
|
+
deviceIds.push(device.deviceId);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (deviceIds.length === 0) return;
|
|
1907
|
+
await this.send({
|
|
1908
|
+
deviceId: deviceIds,
|
|
1909
|
+
title: input.title,
|
|
1910
|
+
body: input.body,
|
|
1911
|
+
data: input.data,
|
|
1912
|
+
idempotencyKey: input.idempotencyKey
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
async pollReceipts() {
|
|
1916
|
+
if (!this.transport.getReceipts) return;
|
|
1917
|
+
const deliveries = await this.deliveriesCol.find({
|
|
1918
|
+
status: "submitted",
|
|
1919
|
+
ticketId: { $exists: true, $ne: "" },
|
|
1920
|
+
receiptedAt: { $exists: false }
|
|
1921
|
+
}).toArray();
|
|
1922
|
+
if (deliveries.length === 0) return;
|
|
1923
|
+
const ticketIds = [
|
|
1924
|
+
...new Set(
|
|
1925
|
+
deliveries.map((d) => d.ticketId).filter((id) => !!id)
|
|
1926
|
+
)
|
|
1927
|
+
];
|
|
1928
|
+
const receipts = [];
|
|
1929
|
+
try {
|
|
1930
|
+
for (let i = 0; i < ticketIds.length; i += RECEIPT_BATCH_SIZE) {
|
|
1931
|
+
const chunk = ticketIds.slice(i, i + RECEIPT_BATCH_SIZE);
|
|
1932
|
+
const batch = await this.transport.getReceipts(chunk);
|
|
1933
|
+
receipts.push(...batch);
|
|
1934
|
+
}
|
|
1935
|
+
} catch {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const receiptMap = new Map(receipts.map((r) => [r.ticketId, r]));
|
|
1939
|
+
for (const delivery of deliveries) {
|
|
1940
|
+
if (!delivery.ticketId) continue;
|
|
1941
|
+
const receipt = receiptMap.get(delivery.ticketId);
|
|
1942
|
+
if (!receipt) continue;
|
|
1943
|
+
const now = /* @__PURE__ */ new Date();
|
|
1944
|
+
if (receipt.status === "ok") {
|
|
1945
|
+
await this.deliveriesCol.updateOne(
|
|
1946
|
+
{ _id: delivery._id },
|
|
1947
|
+
{ $set: { receiptedAt: now } }
|
|
1948
|
+
);
|
|
1949
|
+
} else {
|
|
1950
|
+
const errorCode = receipt.errorCode ?? "UNKNOWN";
|
|
1951
|
+
await this.deliveriesCol.updateOne(
|
|
1952
|
+
{ _id: delivery._id },
|
|
1953
|
+
{
|
|
1954
|
+
$set: {
|
|
1955
|
+
status: "failed",
|
|
1956
|
+
errorCode,
|
|
1957
|
+
receiptedAt: now
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
);
|
|
1961
|
+
if (PERMANENT_DISABLE_CODES.has(errorCode)) {
|
|
1962
|
+
await this.devicesCol.updateOne(
|
|
1963
|
+
{ token: delivery.token },
|
|
1964
|
+
{ $set: { enabled: false, updatedAt: now } }
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
async find(filter) {
|
|
1971
|
+
return this.devicesCol.find(filter).toArray();
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
function computeDigest(title, body, idempotencyKey) {
|
|
1975
|
+
if (idempotencyKey) return idempotencyKey;
|
|
1976
|
+
const utcDay = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1977
|
+
const input = title + "\0" + body + "\0" + utcDay;
|
|
1978
|
+
return createHash2("sha256").update(input, "utf8").digest("hex");
|
|
1979
|
+
}
|
|
1980
|
+
function isDuplicateKeyError2(err) {
|
|
1981
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === 11e3;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1263
1984
|
// src/codixus-server.ts
|
|
1264
1985
|
var CodixusServer = class {
|
|
1265
1986
|
constructor(config) {
|
|
@@ -1273,6 +1994,7 @@ var CodixusServer = class {
|
|
|
1273
1994
|
refreshTokenTtl: config.refreshTokenTtl ?? "30d"
|
|
1274
1995
|
});
|
|
1275
1996
|
}
|
|
1997
|
+
config;
|
|
1276
1998
|
connection;
|
|
1277
1999
|
jwt;
|
|
1278
2000
|
banService;
|
|
@@ -1282,6 +2004,8 @@ var CodixusServer = class {
|
|
|
1282
2004
|
_appSecret;
|
|
1283
2005
|
auth;
|
|
1284
2006
|
db;
|
|
2007
|
+
push;
|
|
2008
|
+
admin;
|
|
1285
2009
|
async connect() {
|
|
1286
2010
|
this._db = await this.connection.connect();
|
|
1287
2011
|
this.banService = new BanService(this._db);
|
|
@@ -1297,6 +2021,27 @@ var CodixusServer = class {
|
|
|
1297
2021
|
{ expiresAt: 1 },
|
|
1298
2022
|
{ expireAfterSeconds: 0 }
|
|
1299
2023
|
);
|
|
2024
|
+
if (this.config.push) {
|
|
2025
|
+
const pushDevicesCol = this._db.collection("push_devices");
|
|
2026
|
+
await pushDevicesCol.createIndex({ token: 1 }, { unique: true });
|
|
2027
|
+
await pushDevicesCol.createIndex({ deviceId: 1 });
|
|
2028
|
+
await pushDevicesCol.createIndex({ enabled: 1, lastSeenAt: -1 });
|
|
2029
|
+
const pushDeliveriesCol = this._db.collection("push_deliveries");
|
|
2030
|
+
await pushDeliveriesCol.createIndex(
|
|
2031
|
+
{ idempotencyKey: 1 },
|
|
2032
|
+
{ unique: true }
|
|
2033
|
+
);
|
|
2034
|
+
await pushDeliveriesCol.createIndex({ ticketId: 1 });
|
|
2035
|
+
await pushDeliveriesCol.createIndex({ deviceId: 1 });
|
|
2036
|
+
const pushService = new PushService(this._db, this.config.push.transport);
|
|
2037
|
+
this.push = {
|
|
2038
|
+
router: () => createPushRouter(this._db),
|
|
2039
|
+
send: (input) => pushService.send(input),
|
|
2040
|
+
sendTo: (input) => pushService.sendTo(input),
|
|
2041
|
+
pollReceipts: () => pushService.pollReceipts(),
|
|
2042
|
+
find: (filter) => pushService.find(filter)
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
1300
2045
|
this.auth = {
|
|
1301
2046
|
sign: (subject, claims) => this.jwt.sign(subject, claims),
|
|
1302
2047
|
verify: (token) => this.jwt.verify(token),
|
|
@@ -1316,6 +2061,16 @@ var CodixusServer = class {
|
|
|
1316
2061
|
transaction: (fn) => runTransaction(this.connection.getClient(), fn),
|
|
1317
2062
|
getDb: () => this._db
|
|
1318
2063
|
};
|
|
2064
|
+
if (this.config.admin) {
|
|
2065
|
+
this.admin = {
|
|
2066
|
+
router: () => createAdminRouter({
|
|
2067
|
+
db: this._db,
|
|
2068
|
+
token: this.config.admin.token,
|
|
2069
|
+
hasPush: !!this.config.push,
|
|
2070
|
+
pushSend: this.push?.send
|
|
2071
|
+
})
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
1319
2074
|
}
|
|
1320
2075
|
/**
|
|
1321
2076
|
* HMAC request signing middleware.
|
|
@@ -1368,6 +2123,149 @@ function validate(schema) {
|
|
|
1368
2123
|
};
|
|
1369
2124
|
}
|
|
1370
2125
|
|
|
2126
|
+
// src/push/expo-transport.ts
|
|
2127
|
+
var SEND_URL = "https://exp.host/--/api/v2/push/send";
|
|
2128
|
+
var RECEIPTS_URL = "https://exp.host/--/api/v2/push/getReceipts";
|
|
2129
|
+
var BATCH_SIZE = 100;
|
|
2130
|
+
function transientTickets(messages) {
|
|
2131
|
+
return messages.map((m) => ({
|
|
2132
|
+
token: m.token,
|
|
2133
|
+
status: "error",
|
|
2134
|
+
errorCode: "TRANSIENT"
|
|
2135
|
+
}));
|
|
2136
|
+
}
|
|
2137
|
+
function invalidPayloadTickets(messages) {
|
|
2138
|
+
return messages.map((m) => ({
|
|
2139
|
+
token: m.token,
|
|
2140
|
+
status: "error",
|
|
2141
|
+
errorCode: "INVALID_PAYLOAD"
|
|
2142
|
+
}));
|
|
2143
|
+
}
|
|
2144
|
+
function mapExpoTickets(messages, data) {
|
|
2145
|
+
const tickets = [];
|
|
2146
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2147
|
+
const msg = messages[i];
|
|
2148
|
+
const ticket = data[i];
|
|
2149
|
+
if (!ticket) {
|
|
2150
|
+
tickets.push({
|
|
2151
|
+
token: msg.token,
|
|
2152
|
+
status: "error",
|
|
2153
|
+
errorCode: "TRANSIENT"
|
|
2154
|
+
});
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (ticket.status === "ok") {
|
|
2158
|
+
tickets.push({
|
|
2159
|
+
token: msg.token,
|
|
2160
|
+
status: "ok",
|
|
2161
|
+
ticketId: ticket.id
|
|
2162
|
+
});
|
|
2163
|
+
} else {
|
|
2164
|
+
tickets.push({
|
|
2165
|
+
token: msg.token,
|
|
2166
|
+
status: "error",
|
|
2167
|
+
errorCode: ticket.details?.error ?? "UNKNOWN",
|
|
2168
|
+
errorMessage: ticket.message
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return tickets;
|
|
2173
|
+
}
|
|
2174
|
+
function createExpoTransport(opts) {
|
|
2175
|
+
const fetchImpl = opts?.fetchImpl ?? globalThis.fetch;
|
|
2176
|
+
const accessToken = opts?.accessToken;
|
|
2177
|
+
function headers() {
|
|
2178
|
+
const h = {
|
|
2179
|
+
"Content-Type": "application/json"
|
|
2180
|
+
};
|
|
2181
|
+
if (accessToken) {
|
|
2182
|
+
h["Authorization"] = `Bearer ${accessToken}`;
|
|
2183
|
+
}
|
|
2184
|
+
return h;
|
|
2185
|
+
}
|
|
2186
|
+
return {
|
|
2187
|
+
name: "expo",
|
|
2188
|
+
async send(messages) {
|
|
2189
|
+
const allTickets = [];
|
|
2190
|
+
for (let i = 0; i < messages.length; i += BATCH_SIZE) {
|
|
2191
|
+
const batch = messages.slice(i, i + BATCH_SIZE);
|
|
2192
|
+
const body = batch.map((m) => {
|
|
2193
|
+
const item = {
|
|
2194
|
+
to: m.token,
|
|
2195
|
+
title: m.title,
|
|
2196
|
+
body: m.body
|
|
2197
|
+
};
|
|
2198
|
+
if (m.data !== void 0) item.data = m.data;
|
|
2199
|
+
if (m.ttl !== void 0) item.ttl = m.ttl;
|
|
2200
|
+
if (m.badge !== void 0) item.badge = m.badge;
|
|
2201
|
+
if (m.sound !== void 0) item.sound = m.sound;
|
|
2202
|
+
if (m.channelId !== void 0) item.channelId = m.channelId;
|
|
2203
|
+
return item;
|
|
2204
|
+
});
|
|
2205
|
+
try {
|
|
2206
|
+
const res = await fetchImpl(SEND_URL, {
|
|
2207
|
+
method: "POST",
|
|
2208
|
+
headers: headers(),
|
|
2209
|
+
body: JSON.stringify(body)
|
|
2210
|
+
});
|
|
2211
|
+
if (res.status === 429 || res.status >= 500) {
|
|
2212
|
+
allTickets.push(...transientTickets(batch));
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
if (res.status >= 400 && res.status < 500) {
|
|
2216
|
+
allTickets.push(...invalidPayloadTickets(batch));
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
const json = await res.json();
|
|
2220
|
+
if (!Array.isArray(json.data)) {
|
|
2221
|
+
allTickets.push(...transientTickets(batch));
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
allTickets.push(
|
|
2225
|
+
...mapExpoTickets(batch, json.data)
|
|
2226
|
+
);
|
|
2227
|
+
} catch {
|
|
2228
|
+
allTickets.push(...transientTickets(batch));
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
return allTickets;
|
|
2232
|
+
},
|
|
2233
|
+
async getReceipts(ticketIds) {
|
|
2234
|
+
const allReceipts = [];
|
|
2235
|
+
for (let i = 0; i < ticketIds.length; i += BATCH_SIZE) {
|
|
2236
|
+
const batch = ticketIds.slice(i, i + BATCH_SIZE);
|
|
2237
|
+
const res = await fetchImpl(RECEIPTS_URL, {
|
|
2238
|
+
method: "POST",
|
|
2239
|
+
headers: headers(),
|
|
2240
|
+
body: JSON.stringify({ ids: batch })
|
|
2241
|
+
});
|
|
2242
|
+
if (!res.ok) {
|
|
2243
|
+
throw new Error(`Expo getReceipts HTTP ${res.status}`);
|
|
2244
|
+
}
|
|
2245
|
+
const json = await res.json();
|
|
2246
|
+
if (typeof json.data !== "object" || json.data === null || Array.isArray(json.data)) {
|
|
2247
|
+
throw new Error("Expo getReceipts invalid response");
|
|
2248
|
+
}
|
|
2249
|
+
const data = json.data;
|
|
2250
|
+
for (const id of batch) {
|
|
2251
|
+
const receipt = data[id];
|
|
2252
|
+
if (!receipt) continue;
|
|
2253
|
+
if (receipt.status === "ok") {
|
|
2254
|
+
allReceipts.push({ ticketId: id, status: "ok" });
|
|
2255
|
+
} else {
|
|
2256
|
+
allReceipts.push({
|
|
2257
|
+
ticketId: id,
|
|
2258
|
+
status: "error",
|
|
2259
|
+
errorCode: receipt.status === "error" ? receipt.details?.error ?? "UNKNOWN" : "UNKNOWN"
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return allReceipts;
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
|
|
1371
2269
|
// src/index.ts
|
|
1372
2270
|
import {
|
|
1373
2271
|
CodixusError as CodixusError3,
|
|
@@ -1381,6 +2279,7 @@ export {
|
|
|
1381
2279
|
Query,
|
|
1382
2280
|
RestErrorCode,
|
|
1383
2281
|
SubCollection,
|
|
2282
|
+
createExpoTransport,
|
|
1384
2283
|
model,
|
|
1385
2284
|
validate
|
|
1386
2285
|
};
|