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