@stamprally/core 0.3.0 → 0.5.0

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
  }
@@ -260,11 +260,27 @@ function hash(value) {
260
260
  }
261
261
  return result.toString(36).toUpperCase().padStart(7, "0");
262
262
  }
263
+ function createRandomHash() {
264
+ const cryptoApi3 = globalThis.crypto;
265
+ if (cryptoApi3 !== void 0 && typeof cryptoApi3.getRandomValues === "function") {
266
+ const values = new Uint32Array(2);
267
+ cryptoApi3.getRandomValues(values);
268
+ return Array.from(values, (value) => value.toString(36).toUpperCase().padStart(7, "0")).join(
269
+ ""
270
+ );
271
+ }
272
+ return `${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).slice(2).toUpperCase()}`;
273
+ }
263
274
  function createClaimTicketNumber(rewardId, options = {}) {
264
275
  const issuedAt = options.issuedAt ?? "";
265
276
  const sequence = options.sequence ?? 0;
266
277
  return `SR-${hash(`${rewardId}|${issuedAt}|${sequence}`)}`;
267
278
  }
279
+ function createUniqueClaimTicketNumber(rewardId, issuedAt) {
280
+ const timestamp = Date.parse(issuedAt);
281
+ const timestampPart = Number.isNaN(timestamp) ? Date.now() : timestamp;
282
+ return `CLAIM-${rewardId}-${timestampPart}-${createRandomHash()}`;
283
+ }
268
284
  function issueClaimTicketNumber(reward, currentState, options = {}) {
269
285
  if (currentState.claimTicketNumber !== void 0) return currentState;
270
286
  const claimTicketNumber = reward.claimTicketNumber ?? createClaimTicketNumber(reward.id, options);
@@ -617,7 +633,8 @@ function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now)
617
633
  if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(now)) {
618
634
  return { rewardId: reward.id, status: "EXPIRED" };
619
635
  }
620
- 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) {
621
638
  return {
622
639
  rewardId: reward.id,
623
640
  status: "EXPIRED",
@@ -652,13 +669,18 @@ function consumeReward(params) {
652
669
  };
653
670
  }
654
671
  if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(params.now)) {
655
- return { ok: false, error: { code: "EXPIRED", rewardId: reward.id } };
672
+ return { ok: false, error: { code: "EXPIRED", reason: "EXPIRED", rewardId: reward.id } };
656
673
  }
657
- 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) {
658
677
  return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward.id } };
659
678
  }
660
- if (reward.limitPerUser !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= reward.limitPerUser) {
661
- return { ok: false, error: { code: "USER_LIMIT_REACHED", rewardId: reward.id } };
679
+ if (userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= userClaimLimit) {
680
+ return {
681
+ ok: false,
682
+ error: { code: "USER_LIMIT_REACHED", reason: "LIMIT_EXCEEDED", rewardId: reward.id }
683
+ };
662
684
  }
663
685
  if (reward.redemptionMethod === "staff_passcode") {
664
686
  const passcodeResult = reward.staffPasscode === void 0 ? null : verifyPasscode(params.inputPasscode ?? "", { passcode: reward.staffPasscode });
@@ -682,7 +704,8 @@ function consumeReward(params) {
682
704
  ...currentState,
683
705
  status: "CONSUMED",
684
706
  consumedAt: params.now,
685
- ...reward.maxStock !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
707
+ claimTicketNumber: createUniqueClaimTicketNumber(reward.id, params.now),
708
+ ...stockLimit !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
686
709
  ...params.userId === void 0 ? {} : { userRedemptionCount: (currentState.userRedemptionCount ?? 0) + 1 },
687
710
  ...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
688
711
  }
@@ -1179,6 +1202,7 @@ var IndexedDBAdapter = class {
1179
1202
  var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
1180
1203
  var StampRallyClient = class {
1181
1204
  #listeners = /* @__PURE__ */ new Set();
1205
+ #eventListeners = /* @__PURE__ */ new Set();
1182
1206
  #config;
1183
1207
  #storage;
1184
1208
  #clock;
@@ -1196,12 +1220,38 @@ var StampRallyClient = class {
1196
1220
  getConfig() {
1197
1221
  return this.#config;
1198
1222
  }
1199
- subscribe(listener) {
1223
+ subscribe(listener, options = {}) {
1224
+ if (options.events === true) {
1225
+ this.#eventListeners.add(listener);
1226
+ return () => this.#eventListeners.delete(listener);
1227
+ }
1200
1228
  this.#listeners.add(listener);
1201
1229
  return () => {
1202
1230
  this.#listeners.delete(listener);
1203
1231
  };
1204
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
+ }
1205
1255
  init() {
1206
1256
  return this.initialize();
1207
1257
  }
@@ -1227,11 +1277,13 @@ var StampRallyClient = class {
1227
1277
  const currentState = await this.initialize();
1228
1278
  const result = processStamp(currentState, this.#config, stampId, context, now);
1229
1279
  if (!result.ok) {
1280
+ this.#emitEvent({ type: "error", error: result.error });
1230
1281
  return result;
1231
1282
  }
1232
1283
  await this.#storage.save(result.value.nextState);
1233
1284
  this.#currentState = result.value.nextState;
1234
1285
  this.#emit(result.value.nextState);
1286
+ this.#emitEvent({ type: "checkIn", stampId, state: result.value.nextState });
1235
1287
  return result;
1236
1288
  });
1237
1289
  }
@@ -1281,6 +1333,9 @@ var StampRallyClient = class {
1281
1333
  listener(state);
1282
1334
  }
1283
1335
  }
1336
+ #emitEvent(event) {
1337
+ for (const listener of this.#eventListeners) listener(event);
1338
+ }
1284
1339
  #createEmptyState(now) {
1285
1340
  const state = {
1286
1341
  rallyId: this.#config.id,
@@ -1316,12 +1371,144 @@ var StampRallyClient = class {
1316
1371
  }
1317
1372
  };
1318
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
+
1319
1506
  // src/domain/i18n.ts
1320
1507
  function resolveLocalizedText(text2, locale, fallbackLocale) {
1321
1508
  if (text2 === void 0 || text2 === "") return "";
1322
1509
  if (typeof text2 === "string") return text2;
1323
- const fallback = fallbackLocale ?? "ja";
1324
- 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 || "";
1325
1512
  }
1326
1513
  function toLocalizedString(text2) {
1327
1514
  if (text2 === void 0) return { ja: "", en: "" };
@@ -1423,6 +1610,11 @@ function validateSpot(value, index, errors) {
1423
1610
  add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be an array of IDs.`);
1424
1611
  }
1425
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
+ }
1426
1618
  }
1427
1619
  function validateReward(value, index, stampCount, errors) {
1428
1620
  const path = `rewards[${index}]`;
@@ -1451,7 +1643,7 @@ function validateReward(value, index, stampCount, errors) {
1451
1643
  if (value.validUntil !== void 0 && (typeof value.validUntil !== "string" || Number.isNaN(Date.parse(value.validUntil)))) {
1452
1644
  add(errors, `${path}.validUntil`, "INVALID_DATE", "Reward expiry must be a valid ISO date.");
1453
1645
  }
1454
- for (const field of ["maxStock", "limitPerUser"]) {
1646
+ for (const field of ["maxStock", "limitPerUser", "stockLimit", "userClaimLimit"]) {
1455
1647
  const count = value[field];
1456
1648
  if (count !== void 0 && (typeof count !== "number" || !Number.isInteger(count) || count <= 0)) {
1457
1649
  add(errors, `${path}.${field}`, "INVALID_REWARD", `${field} must be a positive integer.`);
@@ -1611,66 +1803,68 @@ function condition(value) {
1611
1803
  }
1612
1804
  }
1613
1805
  function migrateSpot(value, index) {
1614
- const source = isObject2(value) ? value : {};
1615
- const id = typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : `spot-${index + 1}`;
1616
- const description = text(source.description);
1617
- 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);
1618
1810
  return {
1619
1811
  id,
1620
- name: text(source.name) ?? `Spot ${index + 1}`,
1812
+ name: text(source2.name) ?? `Spot ${index + 1}`,
1621
1813
  ...description === void 0 ? {} : { description },
1622
1814
  ...hint === void 0 ? {} : { hint },
1623
1815
  condition: condition(
1624
- 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)
1625
1817
  ),
1626
- ...typeof source.order === "number" ? { order: source.order } : {},
1627
- ...typeof source.deckId === "string" ? { deckId: source.deckId } : {},
1628
- ...typeof source.groupId === "string" ? { groupId: source.groupId } : {},
1629
- ...typeof source.guideId === "string" ? { guideId: source.guideId } : {},
1630
- ...typeof source.iconUrl === "string" ? { iconUrl: source.iconUrl } : {},
1631
- ...typeof source.imageUrl === "string" ? { imageUrl: source.imageUrl } : {},
1632
- ...typeof source.externalUrl === "string" ? { externalUrl: source.externalUrl } : {},
1633
- ...typeof source.redirectUrlAfterClaim === "string" ? { redirectUrlAfterClaim: source.redirectUrlAfterClaim } : {},
1634
- ...isObject2(source.metadata) ? { metadata: source.metadata } : {},
1635
- ...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") } : {}
1636
1828
  };
1637
1829
  }
1638
1830
  function migrateReward(value, index) {
1639
- const source = isObject2(value) ? value : {};
1831
+ const source2 = isObject2(value) ? value : {};
1640
1832
  return {
1641
- id: typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : `reward-${index + 1}`,
1642
- title: text(source.title) ?? `Reward ${index + 1}`,
1643
- description: text(source.description) ?? "",
1644
- type: source.type === "digital" ? "digital" : "in_person",
1645
- redemptionMethod: source.redemptionMethod === "staff_passcode" || source.redemptionMethod === "view_only" ? source.redemptionMethod : "manual_slide",
1646
- requiredStampCount: typeof source.requiredStampCount === "number" && Number.isFinite(source.requiredStampCount) ? Math.max(0, Math.trunc(source.requiredStampCount)) : 0,
1647
- ...typeof source.digitalContentUrl === "string" ? { digitalContentUrl: source.digitalContentUrl } : {},
1648
- ...typeof source.staffPasscode === "string" ? { staffPasscode: source.staffPasscode } : {},
1649
- ...typeof source.validUntil === "string" ? { validUntil: source.validUntil } : {},
1650
- ...typeof source.maxStock === "number" ? { maxStock: source.maxStock } : {},
1651
- ...typeof source.limitPerUser === "number" ? { limitPerUser: source.limitPerUser } : {},
1652
- ...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 } : {}
1653
1847
  };
1654
1848
  }
1655
1849
  function migrateRallyConfig(raw) {
1656
- const source = isObject2(raw) ? raw : {};
1657
- const rawStamps = Array.isArray(source.stamps) ? source.stamps : Array.isArray(source.spots) ? source.spots : [];
1658
- const rawRewards = Array.isArray(source.rewards) ? source.rewards : [];
1659
- const id = typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : "migrated-rally";
1660
- const title = text(source.title);
1661
- const description = text(source.description);
1662
- 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;
1663
1857
  return {
1664
1858
  id,
1665
1859
  ...title === void 0 ? {} : { title },
1666
1860
  ...description === void 0 ? {} : { description },
1667
1861
  stamps: rawStamps.map(migrateSpot),
1668
1862
  ...rawRewards.length === 0 ? {} : { rewards: rawRewards.map(migrateReward) },
1669
- ...typeof source.isSequential === "boolean" ? { isSequential: source.isSequential } : {},
1863
+ ...typeof source2.isSequential === "boolean" ? { isSequential: source2.isSequential } : {},
1670
1864
  ...theme === void 0 ? {} : { theme },
1671
1865
  version: CURRENT_RALLY_CONFIG_VERSION,
1672
- ...typeof source.startDate === "string" ? { startDate: source.startDate } : typeof source.startsAt === "string" ? { startDate: source.startsAt } : {},
1673
- ...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 } : {}
1674
1868
  };
1675
1869
  }
1676
1870
 
@@ -1795,22 +1989,22 @@ var THEME_PRESETS = [
1795
1989
  ];
1796
1990
 
1797
1991
  // src/security/snapshotToken.ts
1798
- var encoder = new TextEncoder();
1799
- function cryptoApi() {
1992
+ var encoder2 = new TextEncoder();
1993
+ function cryptoApi2() {
1800
1994
  if (globalThis.crypto === void 0 || globalThis.crypto.subtle === void 0) {
1801
1995
  throw new Error("Web Crypto API is unavailable in this environment.");
1802
1996
  }
1803
1997
  return globalThis.crypto;
1804
1998
  }
1805
1999
  function secretBytes(secretKey) {
1806
- return typeof secretKey === "string" ? encoder.encode(secretKey) : new Uint8Array(secretKey);
2000
+ return typeof secretKey === "string" ? encoder2.encode(secretKey) : new Uint8Array(secretKey);
1807
2001
  }
1808
- function webCryptoBytes(bytes) {
1809
- return bytes.buffer;
2002
+ function webCryptoBytes(bytes2) {
2003
+ return bytes2.buffer;
1810
2004
  }
1811
- function base64Url(bytes) {
2005
+ function base64Url(bytes2) {
1812
2006
  let binary = "";
1813
- for (const byte of bytes) binary += String.fromCharCode(byte);
2007
+ for (const byte of bytes2) binary += String.fromCharCode(byte);
1814
2008
  return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
1815
2009
  }
1816
2010
  function fromBase64Url(value) {
@@ -1820,7 +2014,7 @@ function fromBase64Url(value) {
1820
2014
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
1821
2015
  }
1822
2016
  async function importHmacKey(secret) {
1823
- return cryptoApi().subtle.importKey(
2017
+ return cryptoApi2().subtle.importKey(
1824
2018
  "raw",
1825
2019
  webCryptoBytes(secret),
1826
2020
  { name: "HMAC", hash: "SHA-256" },
@@ -1829,18 +2023,18 @@ async function importHmacKey(secret) {
1829
2023
  );
1830
2024
  }
1831
2025
  async function importAesKey(secret) {
1832
- const digest = await cryptoApi().subtle.digest("SHA-256", webCryptoBytes(secret));
1833
- 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"]);
1834
2028
  }
1835
2029
  function isExpired(payload, now) {
1836
2030
  if (typeof payload.exp === "number" && now >= payload.exp * 1e3) return true;
1837
2031
  return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now;
1838
2032
  }
1839
2033
  async function createSignedSnapshotToken(payload, secretKey) {
1840
- const api = cryptoApi();
2034
+ const api = cryptoApi2();
1841
2035
  const secret = secretBytes(secretKey);
1842
2036
  const iv = api.getRandomValues(new Uint8Array(12));
1843
- const plaintext = encoder.encode(JSON.stringify(payload));
2037
+ const plaintext = encoder2.encode(JSON.stringify(payload));
1844
2038
  const encrypted = new Uint8Array(
1845
2039
  await api.subtle.encrypt(
1846
2040
  { name: "AES-GCM", iv: webCryptoBytes(iv) },
@@ -1850,7 +2044,7 @@ async function createSignedSnapshotToken(payload, secretKey) {
1850
2044
  );
1851
2045
  const body = base64Url(new Uint8Array([...iv, ...encrypted]));
1852
2046
  const signature = new Uint8Array(
1853
- await api.subtle.sign("HMAC", await importHmacKey(secret), encoder.encode(body))
2047
+ await api.subtle.sign("HMAC", await importHmacKey(secret), encoder2.encode(body))
1854
2048
  );
1855
2049
  return `sr2.${body}.${base64Url(signature)}`;
1856
2050
  }
@@ -1873,11 +2067,11 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
1873
2067
  };
1874
2068
  }
1875
2069
  const secret = secretBytes(secretKey);
1876
- const validSignature = await cryptoApi().subtle.verify(
2070
+ const validSignature = await cryptoApi2().subtle.verify(
1877
2071
  "HMAC",
1878
2072
  await importHmacKey(secret),
1879
2073
  webCryptoBytes(fromBase64Url(encodedSignature)),
1880
- webCryptoBytes(encoder.encode(body))
2074
+ webCryptoBytes(encoder2.encode(body))
1881
2075
  );
1882
2076
  if (!validSignature) {
1883
2077
  return {
@@ -1888,7 +2082,7 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
1888
2082
  }
1889
2083
  const encrypted = fromBase64Url(body);
1890
2084
  if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
1891
- const payloadText = await cryptoApi().subtle.decrypt(
2085
+ const payloadText = await cryptoApi2().subtle.decrypt(
1892
2086
  { name: "AES-GCM", iv: webCryptoBytes(encrypted.slice(0, 12)) },
1893
2087
  await importAesKey(secret),
1894
2088
  webCryptoBytes(encrypted.slice(12))
@@ -1925,7 +2119,9 @@ exports.calculateDistanceMeters = calculateDistanceMeters;
1925
2119
  exports.calculateProgress = calculateProgress;
1926
2120
  exports.consumeReward = consumeReward;
1927
2121
  exports.createClaimTicketNumber = createClaimTicketNumber;
2122
+ exports.createSecureToken = createSecureToken;
1928
2123
  exports.createSignedSnapshotToken = createSignedSnapshotToken;
2124
+ exports.createUniqueClaimTicketNumber = createUniqueClaimTicketNumber;
1929
2125
  exports.evaluateCheckIn = evaluateCheckIn;
1930
2126
  exports.evaluateCondition = evaluateCondition;
1931
2127
  exports.evaluateConditionDetailed = evaluateConditionDetailed;
@@ -1949,6 +2145,7 @@ exports.stripSensitiveConfig = stripSensitiveConfig;
1949
2145
  exports.toLocalizedString = toLocalizedString;
1950
2146
  exports.validateRallyConfig = validateRallyConfig;
1951
2147
  exports.verifyPasscode = verifyPasscode;
2148
+ exports.verifySecureToken = verifySecureToken;
1952
2149
  exports.verifySnapshotToken = verifySnapshotToken;
1953
2150
  //# sourceMappingURL=index.cjs.map
1954
2151
  //# sourceMappingURL=index.cjs.map