@stamprally/core 0.4.0 → 0.5.1

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 CHANGED
@@ -225,7 +225,7 @@ function evaluateCheckIn(spot, input) {
225
225
  // src/engine/order.ts
226
226
  function getOrderedStamps(config) {
227
227
  return config.stamps.map((stamp, index) => ({ stamp, index })).sort((left, right) => {
228
- const orderDifference = (left.stamp.order ?? Number.POSITIVE_INFINITY) - (right.stamp.order ?? Number.POSITIVE_INFINITY);
228
+ const orderDifference = (left.stamp.orderIndex ?? left.stamp.order ?? Number.POSITIVE_INFINITY) - (right.stamp.orderIndex ?? right.stamp.order ?? Number.POSITIVE_INFINITY);
229
229
  return orderDifference === 0 ? left.index - right.index : orderDifference;
230
230
  }).map(({ stamp }) => stamp);
231
231
  }
@@ -261,10 +261,10 @@ function hash(value) {
261
261
  return result.toString(36).toUpperCase().padStart(7, "0");
262
262
  }
263
263
  function createRandomHash() {
264
- const cryptoApi2 = globalThis.crypto;
265
- if (cryptoApi2 !== void 0 && typeof cryptoApi2.getRandomValues === "function") {
264
+ const cryptoApi3 = globalThis.crypto;
265
+ if (cryptoApi3 !== void 0 && typeof cryptoApi3.getRandomValues === "function") {
266
266
  const values = new Uint32Array(2);
267
- cryptoApi2.getRandomValues(values);
267
+ cryptoApi3.getRandomValues(values);
268
268
  return Array.from(values, (value) => value.toString(36).toUpperCase().padStart(7, "0")).join(
269
269
  ""
270
270
  );
@@ -633,7 +633,8 @@ function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now)
633
633
  if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(now)) {
634
634
  return { rewardId: reward.id, status: "EXPIRED" };
635
635
  }
636
- if (reward.maxStock !== void 0 && (current?.redeemedCount ?? 0) >= reward.maxStock) {
636
+ const stockLimit = reward.stockLimit ?? reward.maxStock;
637
+ if (stockLimit !== void 0 && (current?.redeemedCount ?? 0) >= stockLimit) {
637
638
  return {
638
639
  rewardId: reward.id,
639
640
  status: "EXPIRED",
@@ -670,10 +671,12 @@ function consumeReward(params) {
670
671
  if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(params.now)) {
671
672
  return { ok: false, error: { code: "EXPIRED", reason: "EXPIRED", rewardId: reward.id } };
672
673
  }
673
- if (reward.maxStock !== void 0 && (currentState.redeemedCount ?? 0) >= reward.maxStock) {
674
+ const stockLimit = reward.stockLimit ?? reward.maxStock;
675
+ const userClaimLimit = reward.userClaimLimit ?? reward.limitPerUser;
676
+ if (stockLimit !== void 0 && (currentState.redeemedCount ?? 0) >= stockLimit) {
674
677
  return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward.id } };
675
678
  }
676
- if (reward.limitPerUser !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= reward.limitPerUser) {
679
+ if (userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= userClaimLimit) {
677
680
  return {
678
681
  ok: false,
679
682
  error: { code: "USER_LIMIT_REACHED", reason: "LIMIT_EXCEEDED", rewardId: reward.id }
@@ -702,7 +705,7 @@ function consumeReward(params) {
702
705
  status: "CONSUMED",
703
706
  consumedAt: params.now,
704
707
  claimTicketNumber: createUniqueClaimTicketNumber(reward.id, params.now),
705
- ...reward.maxStock !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
708
+ ...stockLimit !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
706
709
  ...params.userId === void 0 ? {} : { userRedemptionCount: (currentState.userRedemptionCount ?? 0) + 1 },
707
710
  ...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
708
711
  }
@@ -1199,6 +1202,7 @@ var IndexedDBAdapter = class {
1199
1202
  var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
1200
1203
  var StampRallyClient = class {
1201
1204
  #listeners = /* @__PURE__ */ new Set();
1205
+ #eventListeners = /* @__PURE__ */ new Set();
1202
1206
  #config;
1203
1207
  #storage;
1204
1208
  #clock;
@@ -1216,12 +1220,38 @@ var StampRallyClient = class {
1216
1220
  getConfig() {
1217
1221
  return this.#config;
1218
1222
  }
1219
- subscribe(listener) {
1223
+ subscribe(listener, options = {}) {
1224
+ if (options.events === true) {
1225
+ this.#eventListeners.add(listener);
1226
+ return () => this.#eventListeners.delete(listener);
1227
+ }
1220
1228
  this.#listeners.add(listener);
1221
1229
  return () => {
1222
1230
  this.#listeners.delete(listener);
1223
1231
  };
1224
1232
  }
1233
+ subscribeEvents(listener) {
1234
+ this.#eventListeners.add(listener);
1235
+ return () => this.#eventListeners.delete(listener);
1236
+ }
1237
+ async updateConfig(newConfig) {
1238
+ return this.#enqueue(async () => {
1239
+ const current = await this.initialize();
1240
+ this.#config = newConfig;
1241
+ const next = this.#reconcileState(cloneState(current), this.#clock());
1242
+ await this.#storage.save(next);
1243
+ this.#currentState = next;
1244
+ this.#initialization = Promise.resolve(next);
1245
+ this.#emit(next);
1246
+ return next;
1247
+ });
1248
+ }
1249
+ notifyRewardClaimed(rewardId, state = this.#currentState) {
1250
+ if (state !== null) this.#emitEvent({ type: "rewardClaimed", rewardId, state });
1251
+ }
1252
+ notifySyncCompleted(state = this.#currentState) {
1253
+ if (state !== null) this.#emitEvent({ type: "syncCompleted", state });
1254
+ }
1225
1255
  init() {
1226
1256
  return this.initialize();
1227
1257
  }
@@ -1247,11 +1277,13 @@ var StampRallyClient = class {
1247
1277
  const currentState = await this.initialize();
1248
1278
  const result = processStamp(currentState, this.#config, stampId, context, now);
1249
1279
  if (!result.ok) {
1280
+ this.#emitEvent({ type: "error", error: result.error });
1250
1281
  return result;
1251
1282
  }
1252
1283
  await this.#storage.save(result.value.nextState);
1253
1284
  this.#currentState = result.value.nextState;
1254
1285
  this.#emit(result.value.nextState);
1286
+ this.#emitEvent({ type: "checkIn", stampId, state: result.value.nextState });
1255
1287
  return result;
1256
1288
  });
1257
1289
  }
@@ -1301,6 +1333,9 @@ var StampRallyClient = class {
1301
1333
  listener(state);
1302
1334
  }
1303
1335
  }
1336
+ #emitEvent(event) {
1337
+ for (const listener of this.#eventListeners) listener(event);
1338
+ }
1304
1339
  #createEmptyState(now) {
1305
1340
  const state = {
1306
1341
  rallyId: this.#config.id,
@@ -1336,12 +1371,144 @@ var StampRallyClient = class {
1336
1371
  }
1337
1372
  };
1338
1373
 
1374
+ // src/crypto/token.ts
1375
+ var encoder = new TextEncoder();
1376
+ var decoder = new TextDecoder();
1377
+ function cryptoApi() {
1378
+ if (globalThis.crypto?.subtle === void 0) {
1379
+ throw new Error("Web Crypto API is unavailable in this environment.");
1380
+ }
1381
+ return globalThis.crypto;
1382
+ }
1383
+ function bytes(value) {
1384
+ return typeof value === "string" ? encoder.encode(value) : new Uint8Array(value);
1385
+ }
1386
+ function source(value) {
1387
+ return value.buffer;
1388
+ }
1389
+ function encode(value) {
1390
+ let binary = "";
1391
+ for (const byte of value) binary += String.fromCharCode(byte);
1392
+ return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
1393
+ }
1394
+ function decode(value) {
1395
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
1396
+ const padded = `${base64}${"=".repeat((4 - base64.length % 4) % 4)}`;
1397
+ return Uint8Array.from(globalThis.atob(padded), (character) => character.charCodeAt(0));
1398
+ }
1399
+ async function hmacKey(secret) {
1400
+ return cryptoApi().subtle.importKey(
1401
+ "raw",
1402
+ source(secret),
1403
+ { name: "HMAC", hash: "SHA-256" },
1404
+ false,
1405
+ ["sign", "verify"]
1406
+ );
1407
+ }
1408
+ async function aesKey(secret) {
1409
+ const digest = await cryptoApi().subtle.digest("SHA-256", source(secret));
1410
+ return cryptoApi().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
1411
+ }
1412
+ async function createSecureToken(payload, secretKey, options = {}) {
1413
+ const api = cryptoApi();
1414
+ const expiresInSeconds = options.expiresInSeconds;
1415
+ const effectivePayload = expiresInSeconds === void 0 || payload.exp !== void 0 ? payload : { ...payload, exp: Math.floor(Date.now() / 1e3) + expiresInSeconds };
1416
+ const secret = bytes(secretKey);
1417
+ const plaintext = encoder.encode(JSON.stringify(effectivePayload));
1418
+ const actualBody = options.encrypt === true ? await encryptPayload(plaintext, secret, api) : encode(plaintext);
1419
+ const signature = new Uint8Array(
1420
+ await api.subtle.sign("HMAC", await hmacKey(secret), encoder.encode(actualBody))
1421
+ );
1422
+ return `sr3.${options.encrypt === true ? "e" : "p"}.${actualBody}.${encode(signature)}`;
1423
+ }
1424
+ async function encryptPayload(plaintext, secret, api) {
1425
+ const iv = api.getRandomValues(new Uint8Array(12));
1426
+ const encrypted = new Uint8Array(
1427
+ await api.subtle.encrypt(
1428
+ { name: "AES-GCM", iv: source(iv) },
1429
+ await aesKey(secret),
1430
+ source(plaintext)
1431
+ )
1432
+ );
1433
+ return encode(new Uint8Array([...iv, ...encrypted]));
1434
+ }
1435
+ async function verifySecureToken(token, secretKey, now = Date.now()) {
1436
+ try {
1437
+ const parts = token.split(".");
1438
+ if (parts.length !== 4 || parts[0] !== "sr3" || parts[1] !== "e" && parts[1] !== "p") {
1439
+ return {
1440
+ ok: false,
1441
+ valid: false,
1442
+ error: { code: "MALFORMED", message: "Secure token is malformed." }
1443
+ };
1444
+ }
1445
+ const mode = parts[1];
1446
+ const body = parts[2];
1447
+ const encodedSignature = parts[3];
1448
+ if (body === void 0 || encodedSignature === void 0 || mode === void 0) {
1449
+ return {
1450
+ ok: false,
1451
+ valid: false,
1452
+ error: { code: "MALFORMED", message: "Secure token is malformed." }
1453
+ };
1454
+ }
1455
+ const secret = bytes(secretKey);
1456
+ const valid = await cryptoApi().subtle.verify(
1457
+ "HMAC",
1458
+ await hmacKey(secret),
1459
+ source(decode(encodedSignature)),
1460
+ source(encoder.encode(body))
1461
+ );
1462
+ if (!valid)
1463
+ return {
1464
+ ok: false,
1465
+ valid: false,
1466
+ error: { code: "INVALID_SIGNATURE", message: "Secure token signature is invalid." }
1467
+ };
1468
+ const plaintext = mode === "e" ? await decryptPayload(body, secret) : decode(body);
1469
+ const parsed = JSON.parse(decoder.decode(plaintext));
1470
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1471
+ return {
1472
+ ok: false,
1473
+ valid: false,
1474
+ error: { code: "INVALID_PAYLOAD", message: "Secure token payload is invalid." }
1475
+ };
1476
+ }
1477
+ const payload = parsed;
1478
+ if (typeof payload.exp === "number" && now >= payload.exp * 1e3) {
1479
+ return {
1480
+ ok: false,
1481
+ valid: false,
1482
+ error: { code: "EXPIRED", message: "Secure token has expired." }
1483
+ };
1484
+ }
1485
+ return { ok: true, valid: true, payload };
1486
+ } catch {
1487
+ return {
1488
+ ok: false,
1489
+ valid: false,
1490
+ error: { code: "DECRYPTION_FAILED", message: "Secure token could not be verified." }
1491
+ };
1492
+ }
1493
+ }
1494
+ async function decryptPayload(body, secret) {
1495
+ const encrypted = decode(body);
1496
+ if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
1497
+ return new Uint8Array(
1498
+ await cryptoApi().subtle.decrypt(
1499
+ { name: "AES-GCM", iv: source(encrypted.slice(0, 12)) },
1500
+ await aesKey(secret),
1501
+ source(encrypted.slice(12))
1502
+ )
1503
+ );
1504
+ }
1505
+
1339
1506
  // src/domain/i18n.ts
1340
1507
  function resolveLocalizedText(text2, locale, fallbackLocale) {
1341
1508
  if (text2 === void 0 || text2 === "") return "";
1342
1509
  if (typeof text2 === "string") return text2;
1343
- const fallback = fallbackLocale ?? "ja";
1344
- return text2[locale] || text2[fallback] || "";
1510
+ const fallback = fallbackLocale === void 0 ? Object.values(text2).find((value) => typeof value === "string") : text2[fallbackLocale];
1511
+ return text2[locale] || fallback || "";
1345
1512
  }
1346
1513
  function toLocalizedString(text2) {
1347
1514
  if (text2 === void 0) return { ja: "", en: "" };
@@ -1443,6 +1610,11 @@ function validateSpot(value, index, errors) {
1443
1610
  add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be an array of IDs.`);
1444
1611
  }
1445
1612
  }
1613
+ for (const field of ["order", "orderIndex"]) {
1614
+ if (value[field] !== void 0 && (typeof value[field] !== "number" || !Number.isFinite(value[field]))) {
1615
+ add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be a finite number.`);
1616
+ }
1617
+ }
1446
1618
  }
1447
1619
  function validateReward(value, index, stampCount, errors) {
1448
1620
  const path = `rewards[${index}]`;
@@ -1471,7 +1643,7 @@ function validateReward(value, index, stampCount, errors) {
1471
1643
  if (value.validUntil !== void 0 && (typeof value.validUntil !== "string" || Number.isNaN(Date.parse(value.validUntil)))) {
1472
1644
  add(errors, `${path}.validUntil`, "INVALID_DATE", "Reward expiry must be a valid ISO date.");
1473
1645
  }
1474
- for (const field of ["maxStock", "limitPerUser"]) {
1646
+ for (const field of ["maxStock", "limitPerUser", "stockLimit", "userClaimLimit"]) {
1475
1647
  const count = value[field];
1476
1648
  if (count !== void 0 && (typeof count !== "number" || !Number.isInteger(count) || count <= 0)) {
1477
1649
  add(errors, `${path}.${field}`, "INVALID_REWARD", `${field} must be a positive integer.`);
@@ -1631,66 +1803,68 @@ function condition(value) {
1631
1803
  }
1632
1804
  }
1633
1805
  function migrateSpot(value, index) {
1634
- const source = isObject2(value) ? value : {};
1635
- const id = typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : `spot-${index + 1}`;
1636
- const description = text(source.description);
1637
- const hint = text(source.hint);
1806
+ const source2 = isObject2(value) ? value : {};
1807
+ const id = typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : `spot-${index + 1}`;
1808
+ const description = text(source2.description);
1809
+ const hint = text(source2.hint);
1638
1810
  return {
1639
1811
  id,
1640
- name: text(source.name) ?? `Spot ${index + 1}`,
1812
+ name: text(source2.name) ?? `Spot ${index + 1}`,
1641
1813
  ...description === void 0 ? {} : { description },
1642
1814
  ...hint === void 0 ? {} : { hint },
1643
1815
  condition: condition(
1644
- source.condition ?? (typeof source.token === "string" ? { type: "token", token: source.token } : void 0)
1816
+ source2.condition ?? (typeof source2.token === "string" ? { type: "token", token: source2.token } : void 0)
1645
1817
  ),
1646
- ...typeof source.order === "number" ? { order: source.order } : {},
1647
- ...typeof source.deckId === "string" ? { deckId: source.deckId } : {},
1648
- ...typeof source.groupId === "string" ? { groupId: source.groupId } : {},
1649
- ...typeof source.guideId === "string" ? { guideId: source.guideId } : {},
1650
- ...typeof source.iconUrl === "string" ? { iconUrl: source.iconUrl } : {},
1651
- ...typeof source.imageUrl === "string" ? { imageUrl: source.imageUrl } : {},
1652
- ...typeof source.externalUrl === "string" ? { externalUrl: source.externalUrl } : {},
1653
- ...typeof source.redirectUrlAfterClaim === "string" ? { redirectUrlAfterClaim: source.redirectUrlAfterClaim } : {},
1654
- ...isObject2(source.metadata) ? { metadata: source.metadata } : {},
1655
- ...Array.isArray(source.dependsOn) ? { dependsOn: source.dependsOn.filter((item) => typeof item === "string") } : {}
1818
+ ...typeof source2.orderIndex === "number" ? { orderIndex: source2.orderIndex } : typeof source2.order === "number" ? { order: source2.order } : {},
1819
+ ...typeof source2.deckId === "string" ? { deckId: source2.deckId } : {},
1820
+ ...typeof source2.groupId === "string" ? { groupId: source2.groupId } : {},
1821
+ ...typeof source2.guideId === "string" ? { guideId: source2.guideId } : {},
1822
+ ...typeof source2.iconUrl === "string" ? { iconUrl: source2.iconUrl } : {},
1823
+ ...typeof source2.imageUrl === "string" ? { imageUrl: source2.imageUrl } : {},
1824
+ ...typeof source2.externalUrl === "string" ? { externalUrl: source2.externalUrl } : {},
1825
+ ...typeof source2.redirectUrlAfterClaim === "string" ? { redirectUrlAfterClaim: source2.redirectUrlAfterClaim } : {},
1826
+ ...isObject2(source2.metadata) ? { metadata: source2.metadata } : {},
1827
+ ...Array.isArray(source2.dependsOn) ? { dependsOn: source2.dependsOn.filter((item) => typeof item === "string") } : {}
1656
1828
  };
1657
1829
  }
1658
1830
  function migrateReward(value, index) {
1659
- const source = isObject2(value) ? value : {};
1831
+ const source2 = isObject2(value) ? value : {};
1660
1832
  return {
1661
- id: typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : `reward-${index + 1}`,
1662
- title: text(source.title) ?? `Reward ${index + 1}`,
1663
- description: text(source.description) ?? "",
1664
- type: source.type === "digital" ? "digital" : "in_person",
1665
- redemptionMethod: source.redemptionMethod === "staff_passcode" || source.redemptionMethod === "view_only" ? source.redemptionMethod : "manual_slide",
1666
- requiredStampCount: typeof source.requiredStampCount === "number" && Number.isFinite(source.requiredStampCount) ? Math.max(0, Math.trunc(source.requiredStampCount)) : 0,
1667
- ...typeof source.digitalContentUrl === "string" ? { digitalContentUrl: source.digitalContentUrl } : {},
1668
- ...typeof source.staffPasscode === "string" ? { staffPasscode: source.staffPasscode } : {},
1669
- ...typeof source.validUntil === "string" ? { validUntil: source.validUntil } : {},
1670
- ...typeof source.maxStock === "number" ? { maxStock: source.maxStock } : {},
1671
- ...typeof source.limitPerUser === "number" ? { limitPerUser: source.limitPerUser } : {},
1672
- ...typeof source.claimTicketNumber === "string" ? { claimTicketNumber: source.claimTicketNumber } : {}
1833
+ id: typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : `reward-${index + 1}`,
1834
+ title: text(source2.title) ?? `Reward ${index + 1}`,
1835
+ description: text(source2.description) ?? "",
1836
+ type: source2.type === "digital" ? "digital" : "in_person",
1837
+ redemptionMethod: source2.redemptionMethod === "staff_passcode" || source2.redemptionMethod === "view_only" || source2.redemptionMethod === "server_claim" ? source2.redemptionMethod : "manual_slide",
1838
+ requiredStampCount: typeof source2.requiredStampCount === "number" && Number.isFinite(source2.requiredStampCount) ? Math.max(0, Math.trunc(source2.requiredStampCount)) : 0,
1839
+ ...typeof source2.digitalContentUrl === "string" ? { digitalContentUrl: source2.digitalContentUrl } : {},
1840
+ ...typeof source2.staffPasscode === "string" ? { staffPasscode: source2.staffPasscode } : {},
1841
+ ...typeof source2.validUntil === "string" ? { validUntil: source2.validUntil } : {},
1842
+ ...typeof source2.maxStock === "number" ? { maxStock: source2.maxStock } : {},
1843
+ ...typeof source2.limitPerUser === "number" ? { limitPerUser: source2.limitPerUser } : {},
1844
+ ...typeof source2.stockLimit === "number" ? { stockLimit: source2.stockLimit } : {},
1845
+ ...typeof source2.userClaimLimit === "number" ? { userClaimLimit: source2.userClaimLimit } : {},
1846
+ ...typeof source2.claimTicketNumber === "string" ? { claimTicketNumber: source2.claimTicketNumber } : {}
1673
1847
  };
1674
1848
  }
1675
1849
  function migrateRallyConfig(raw) {
1676
- const source = isObject2(raw) ? raw : {};
1677
- const rawStamps = Array.isArray(source.stamps) ? source.stamps : Array.isArray(source.spots) ? source.spots : [];
1678
- const rawRewards = Array.isArray(source.rewards) ? source.rewards : [];
1679
- const id = typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : "migrated-rally";
1680
- const title = text(source.title);
1681
- const description = text(source.description);
1682
- const theme = isObject2(source.theme) ? source.theme : void 0;
1850
+ const source2 = isObject2(raw) ? raw : {};
1851
+ const rawStamps = Array.isArray(source2.stamps) ? source2.stamps : Array.isArray(source2.spots) ? source2.spots : [];
1852
+ const rawRewards = Array.isArray(source2.rewards) ? source2.rewards : [];
1853
+ const id = typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : "migrated-rally";
1854
+ const title = text(source2.title);
1855
+ const description = text(source2.description);
1856
+ const theme = isObject2(source2.theme) ? source2.theme : void 0;
1683
1857
  return {
1684
1858
  id,
1685
1859
  ...title === void 0 ? {} : { title },
1686
1860
  ...description === void 0 ? {} : { description },
1687
1861
  stamps: rawStamps.map(migrateSpot),
1688
1862
  ...rawRewards.length === 0 ? {} : { rewards: rawRewards.map(migrateReward) },
1689
- ...typeof source.isSequential === "boolean" ? { isSequential: source.isSequential } : {},
1863
+ ...typeof source2.isSequential === "boolean" ? { isSequential: source2.isSequential } : {},
1690
1864
  ...theme === void 0 ? {} : { theme },
1691
1865
  version: CURRENT_RALLY_CONFIG_VERSION,
1692
- ...typeof source.startDate === "string" ? { startDate: source.startDate } : typeof source.startsAt === "string" ? { startDate: source.startsAt } : {},
1693
- ...typeof source.endDate === "string" ? { endDate: source.endDate } : typeof source.endsAt === "string" ? { endDate: source.endsAt } : {}
1866
+ ...typeof source2.startDate === "string" ? { startDate: source2.startDate } : typeof source2.startsAt === "string" ? { startDate: source2.startsAt } : {},
1867
+ ...typeof source2.endDate === "string" ? { endDate: source2.endDate } : typeof source2.endsAt === "string" ? { endDate: source2.endsAt } : {}
1694
1868
  };
1695
1869
  }
1696
1870
 
@@ -1815,22 +1989,22 @@ var THEME_PRESETS = [
1815
1989
  ];
1816
1990
 
1817
1991
  // src/security/snapshotToken.ts
1818
- var encoder = new TextEncoder();
1819
- function cryptoApi() {
1992
+ var encoder2 = new TextEncoder();
1993
+ function cryptoApi2() {
1820
1994
  if (globalThis.crypto === void 0 || globalThis.crypto.subtle === void 0) {
1821
1995
  throw new Error("Web Crypto API is unavailable in this environment.");
1822
1996
  }
1823
1997
  return globalThis.crypto;
1824
1998
  }
1825
1999
  function secretBytes(secretKey) {
1826
- return typeof secretKey === "string" ? encoder.encode(secretKey) : new Uint8Array(secretKey);
2000
+ return typeof secretKey === "string" ? encoder2.encode(secretKey) : new Uint8Array(secretKey);
1827
2001
  }
1828
- function webCryptoBytes(bytes) {
1829
- return bytes.buffer;
2002
+ function webCryptoBytes(bytes2) {
2003
+ return bytes2.buffer;
1830
2004
  }
1831
- function base64Url(bytes) {
2005
+ function base64Url(bytes2) {
1832
2006
  let binary = "";
1833
- for (const byte of bytes) binary += String.fromCharCode(byte);
2007
+ for (const byte of bytes2) binary += String.fromCharCode(byte);
1834
2008
  return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
1835
2009
  }
1836
2010
  function fromBase64Url(value) {
@@ -1840,7 +2014,7 @@ function fromBase64Url(value) {
1840
2014
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
1841
2015
  }
1842
2016
  async function importHmacKey(secret) {
1843
- return cryptoApi().subtle.importKey(
2017
+ return cryptoApi2().subtle.importKey(
1844
2018
  "raw",
1845
2019
  webCryptoBytes(secret),
1846
2020
  { name: "HMAC", hash: "SHA-256" },
@@ -1849,18 +2023,18 @@ async function importHmacKey(secret) {
1849
2023
  );
1850
2024
  }
1851
2025
  async function importAesKey(secret) {
1852
- const digest = await cryptoApi().subtle.digest("SHA-256", webCryptoBytes(secret));
1853
- return cryptoApi().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
2026
+ const digest = await cryptoApi2().subtle.digest("SHA-256", webCryptoBytes(secret));
2027
+ return cryptoApi2().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
1854
2028
  }
1855
2029
  function isExpired(payload, now) {
1856
2030
  if (typeof payload.exp === "number" && now >= payload.exp * 1e3) return true;
1857
2031
  return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now;
1858
2032
  }
1859
2033
  async function createSignedSnapshotToken(payload, secretKey) {
1860
- const api = cryptoApi();
2034
+ const api = cryptoApi2();
1861
2035
  const secret = secretBytes(secretKey);
1862
2036
  const iv = api.getRandomValues(new Uint8Array(12));
1863
- const plaintext = encoder.encode(JSON.stringify(payload));
2037
+ const plaintext = encoder2.encode(JSON.stringify(payload));
1864
2038
  const encrypted = new Uint8Array(
1865
2039
  await api.subtle.encrypt(
1866
2040
  { name: "AES-GCM", iv: webCryptoBytes(iv) },
@@ -1870,7 +2044,7 @@ async function createSignedSnapshotToken(payload, secretKey) {
1870
2044
  );
1871
2045
  const body = base64Url(new Uint8Array([...iv, ...encrypted]));
1872
2046
  const signature = new Uint8Array(
1873
- await api.subtle.sign("HMAC", await importHmacKey(secret), encoder.encode(body))
2047
+ await api.subtle.sign("HMAC", await importHmacKey(secret), encoder2.encode(body))
1874
2048
  );
1875
2049
  return `sr2.${body}.${base64Url(signature)}`;
1876
2050
  }
@@ -1893,11 +2067,11 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
1893
2067
  };
1894
2068
  }
1895
2069
  const secret = secretBytes(secretKey);
1896
- const validSignature = await cryptoApi().subtle.verify(
2070
+ const validSignature = await cryptoApi2().subtle.verify(
1897
2071
  "HMAC",
1898
2072
  await importHmacKey(secret),
1899
2073
  webCryptoBytes(fromBase64Url(encodedSignature)),
1900
- webCryptoBytes(encoder.encode(body))
2074
+ webCryptoBytes(encoder2.encode(body))
1901
2075
  );
1902
2076
  if (!validSignature) {
1903
2077
  return {
@@ -1908,7 +2082,7 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
1908
2082
  }
1909
2083
  const encrypted = fromBase64Url(body);
1910
2084
  if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
1911
- const payloadText = await cryptoApi().subtle.decrypt(
2085
+ const payloadText = await cryptoApi2().subtle.decrypt(
1912
2086
  { name: "AES-GCM", iv: webCryptoBytes(encrypted.slice(0, 12)) },
1913
2087
  await importAesKey(secret),
1914
2088
  webCryptoBytes(encrypted.slice(12))
@@ -1945,6 +2119,7 @@ exports.calculateDistanceMeters = calculateDistanceMeters;
1945
2119
  exports.calculateProgress = calculateProgress;
1946
2120
  exports.consumeReward = consumeReward;
1947
2121
  exports.createClaimTicketNumber = createClaimTicketNumber;
2122
+ exports.createSecureToken = createSecureToken;
1948
2123
  exports.createSignedSnapshotToken = createSignedSnapshotToken;
1949
2124
  exports.createUniqueClaimTicketNumber = createUniqueClaimTicketNumber;
1950
2125
  exports.evaluateCheckIn = evaluateCheckIn;
@@ -1970,6 +2145,7 @@ exports.stripSensitiveConfig = stripSensitiveConfig;
1970
2145
  exports.toLocalizedString = toLocalizedString;
1971
2146
  exports.validateRallyConfig = validateRallyConfig;
1972
2147
  exports.verifyPasscode = verifyPasscode;
2148
+ exports.verifySecureToken = verifySecureToken;
1973
2149
  exports.verifySnapshotToken = verifySnapshotToken;
1974
2150
  //# sourceMappingURL=index.cjs.map
1975
2151
  //# sourceMappingURL=index.cjs.map