@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.js CHANGED
@@ -223,7 +223,7 @@ function evaluateCheckIn(spot, input) {
223
223
  // src/engine/order.ts
224
224
  function getOrderedStamps(config) {
225
225
  return config.stamps.map((stamp, index) => ({ stamp, index })).sort((left, right) => {
226
- const orderDifference = (left.stamp.order ?? Number.POSITIVE_INFINITY) - (right.stamp.order ?? Number.POSITIVE_INFINITY);
226
+ const orderDifference = (left.stamp.orderIndex ?? left.stamp.order ?? Number.POSITIVE_INFINITY) - (right.stamp.orderIndex ?? right.stamp.order ?? Number.POSITIVE_INFINITY);
227
227
  return orderDifference === 0 ? left.index - right.index : orderDifference;
228
228
  }).map(({ stamp }) => stamp);
229
229
  }
@@ -258,11 +258,27 @@ function hash(value) {
258
258
  }
259
259
  return result.toString(36).toUpperCase().padStart(7, "0");
260
260
  }
261
+ function createRandomHash() {
262
+ const cryptoApi3 = globalThis.crypto;
263
+ if (cryptoApi3 !== void 0 && typeof cryptoApi3.getRandomValues === "function") {
264
+ const values = new Uint32Array(2);
265
+ cryptoApi3.getRandomValues(values);
266
+ return Array.from(values, (value) => value.toString(36).toUpperCase().padStart(7, "0")).join(
267
+ ""
268
+ );
269
+ }
270
+ return `${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).slice(2).toUpperCase()}`;
271
+ }
261
272
  function createClaimTicketNumber(rewardId, options = {}) {
262
273
  const issuedAt = options.issuedAt ?? "";
263
274
  const sequence = options.sequence ?? 0;
264
275
  return `SR-${hash(`${rewardId}|${issuedAt}|${sequence}`)}`;
265
276
  }
277
+ function createUniqueClaimTicketNumber(rewardId, issuedAt) {
278
+ const timestamp = Date.parse(issuedAt);
279
+ const timestampPart = Number.isNaN(timestamp) ? Date.now() : timestamp;
280
+ return `CLAIM-${rewardId}-${timestampPart}-${createRandomHash()}`;
281
+ }
266
282
  function issueClaimTicketNumber(reward, currentState, options = {}) {
267
283
  if (currentState.claimTicketNumber !== void 0) return currentState;
268
284
  const claimTicketNumber = reward.claimTicketNumber ?? createClaimTicketNumber(reward.id, options);
@@ -615,7 +631,8 @@ function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now)
615
631
  if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(now)) {
616
632
  return { rewardId: reward.id, status: "EXPIRED" };
617
633
  }
618
- if (reward.maxStock !== void 0 && (current?.redeemedCount ?? 0) >= reward.maxStock) {
634
+ const stockLimit = reward.stockLimit ?? reward.maxStock;
635
+ if (stockLimit !== void 0 && (current?.redeemedCount ?? 0) >= stockLimit) {
619
636
  return {
620
637
  rewardId: reward.id,
621
638
  status: "EXPIRED",
@@ -650,13 +667,18 @@ function consumeReward(params) {
650
667
  };
651
668
  }
652
669
  if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(params.now)) {
653
- return { ok: false, error: { code: "EXPIRED", rewardId: reward.id } };
670
+ return { ok: false, error: { code: "EXPIRED", reason: "EXPIRED", rewardId: reward.id } };
654
671
  }
655
- if (reward.maxStock !== void 0 && (currentState.redeemedCount ?? 0) >= reward.maxStock) {
672
+ const stockLimit = reward.stockLimit ?? reward.maxStock;
673
+ const userClaimLimit = reward.userClaimLimit ?? reward.limitPerUser;
674
+ if (stockLimit !== void 0 && (currentState.redeemedCount ?? 0) >= stockLimit) {
656
675
  return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward.id } };
657
676
  }
658
- if (reward.limitPerUser !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= reward.limitPerUser) {
659
- return { ok: false, error: { code: "USER_LIMIT_REACHED", rewardId: reward.id } };
677
+ if (userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= userClaimLimit) {
678
+ return {
679
+ ok: false,
680
+ error: { code: "USER_LIMIT_REACHED", reason: "LIMIT_EXCEEDED", rewardId: reward.id }
681
+ };
660
682
  }
661
683
  if (reward.redemptionMethod === "staff_passcode") {
662
684
  const passcodeResult = reward.staffPasscode === void 0 ? null : verifyPasscode(params.inputPasscode ?? "", { passcode: reward.staffPasscode });
@@ -680,7 +702,8 @@ function consumeReward(params) {
680
702
  ...currentState,
681
703
  status: "CONSUMED",
682
704
  consumedAt: params.now,
683
- ...reward.maxStock !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
705
+ claimTicketNumber: createUniqueClaimTicketNumber(reward.id, params.now),
706
+ ...stockLimit !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
684
707
  ...params.userId === void 0 ? {} : { userRedemptionCount: (currentState.userRedemptionCount ?? 0) + 1 },
685
708
  ...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
686
709
  }
@@ -1177,6 +1200,7 @@ var IndexedDBAdapter = class {
1177
1200
  var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
1178
1201
  var StampRallyClient = class {
1179
1202
  #listeners = /* @__PURE__ */ new Set();
1203
+ #eventListeners = /* @__PURE__ */ new Set();
1180
1204
  #config;
1181
1205
  #storage;
1182
1206
  #clock;
@@ -1194,12 +1218,38 @@ var StampRallyClient = class {
1194
1218
  getConfig() {
1195
1219
  return this.#config;
1196
1220
  }
1197
- subscribe(listener) {
1221
+ subscribe(listener, options = {}) {
1222
+ if (options.events === true) {
1223
+ this.#eventListeners.add(listener);
1224
+ return () => this.#eventListeners.delete(listener);
1225
+ }
1198
1226
  this.#listeners.add(listener);
1199
1227
  return () => {
1200
1228
  this.#listeners.delete(listener);
1201
1229
  };
1202
1230
  }
1231
+ subscribeEvents(listener) {
1232
+ this.#eventListeners.add(listener);
1233
+ return () => this.#eventListeners.delete(listener);
1234
+ }
1235
+ async updateConfig(newConfig) {
1236
+ return this.#enqueue(async () => {
1237
+ const current = await this.initialize();
1238
+ this.#config = newConfig;
1239
+ const next = this.#reconcileState(cloneState(current), this.#clock());
1240
+ await this.#storage.save(next);
1241
+ this.#currentState = next;
1242
+ this.#initialization = Promise.resolve(next);
1243
+ this.#emit(next);
1244
+ return next;
1245
+ });
1246
+ }
1247
+ notifyRewardClaimed(rewardId, state = this.#currentState) {
1248
+ if (state !== null) this.#emitEvent({ type: "rewardClaimed", rewardId, state });
1249
+ }
1250
+ notifySyncCompleted(state = this.#currentState) {
1251
+ if (state !== null) this.#emitEvent({ type: "syncCompleted", state });
1252
+ }
1203
1253
  init() {
1204
1254
  return this.initialize();
1205
1255
  }
@@ -1225,11 +1275,13 @@ var StampRallyClient = class {
1225
1275
  const currentState = await this.initialize();
1226
1276
  const result = processStamp(currentState, this.#config, stampId, context, now);
1227
1277
  if (!result.ok) {
1278
+ this.#emitEvent({ type: "error", error: result.error });
1228
1279
  return result;
1229
1280
  }
1230
1281
  await this.#storage.save(result.value.nextState);
1231
1282
  this.#currentState = result.value.nextState;
1232
1283
  this.#emit(result.value.nextState);
1284
+ this.#emitEvent({ type: "checkIn", stampId, state: result.value.nextState });
1233
1285
  return result;
1234
1286
  });
1235
1287
  }
@@ -1279,6 +1331,9 @@ var StampRallyClient = class {
1279
1331
  listener(state);
1280
1332
  }
1281
1333
  }
1334
+ #emitEvent(event) {
1335
+ for (const listener of this.#eventListeners) listener(event);
1336
+ }
1282
1337
  #createEmptyState(now) {
1283
1338
  const state = {
1284
1339
  rallyId: this.#config.id,
@@ -1314,12 +1369,144 @@ var StampRallyClient = class {
1314
1369
  }
1315
1370
  };
1316
1371
 
1372
+ // src/crypto/token.ts
1373
+ var encoder = new TextEncoder();
1374
+ var decoder = new TextDecoder();
1375
+ function cryptoApi() {
1376
+ if (globalThis.crypto?.subtle === void 0) {
1377
+ throw new Error("Web Crypto API is unavailable in this environment.");
1378
+ }
1379
+ return globalThis.crypto;
1380
+ }
1381
+ function bytes(value) {
1382
+ return typeof value === "string" ? encoder.encode(value) : new Uint8Array(value);
1383
+ }
1384
+ function source(value) {
1385
+ return value.buffer;
1386
+ }
1387
+ function encode(value) {
1388
+ let binary = "";
1389
+ for (const byte of value) binary += String.fromCharCode(byte);
1390
+ return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
1391
+ }
1392
+ function decode(value) {
1393
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
1394
+ const padded = `${base64}${"=".repeat((4 - base64.length % 4) % 4)}`;
1395
+ return Uint8Array.from(globalThis.atob(padded), (character) => character.charCodeAt(0));
1396
+ }
1397
+ async function hmacKey(secret) {
1398
+ return cryptoApi().subtle.importKey(
1399
+ "raw",
1400
+ source(secret),
1401
+ { name: "HMAC", hash: "SHA-256" },
1402
+ false,
1403
+ ["sign", "verify"]
1404
+ );
1405
+ }
1406
+ async function aesKey(secret) {
1407
+ const digest = await cryptoApi().subtle.digest("SHA-256", source(secret));
1408
+ return cryptoApi().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
1409
+ }
1410
+ async function createSecureToken(payload, secretKey, options = {}) {
1411
+ const api = cryptoApi();
1412
+ const expiresInSeconds = options.expiresInSeconds;
1413
+ const effectivePayload = expiresInSeconds === void 0 || payload.exp !== void 0 ? payload : { ...payload, exp: Math.floor(Date.now() / 1e3) + expiresInSeconds };
1414
+ const secret = bytes(secretKey);
1415
+ const plaintext = encoder.encode(JSON.stringify(effectivePayload));
1416
+ const actualBody = options.encrypt === true ? await encryptPayload(plaintext, secret, api) : encode(plaintext);
1417
+ const signature = new Uint8Array(
1418
+ await api.subtle.sign("HMAC", await hmacKey(secret), encoder.encode(actualBody))
1419
+ );
1420
+ return `sr3.${options.encrypt === true ? "e" : "p"}.${actualBody}.${encode(signature)}`;
1421
+ }
1422
+ async function encryptPayload(plaintext, secret, api) {
1423
+ const iv = api.getRandomValues(new Uint8Array(12));
1424
+ const encrypted = new Uint8Array(
1425
+ await api.subtle.encrypt(
1426
+ { name: "AES-GCM", iv: source(iv) },
1427
+ await aesKey(secret),
1428
+ source(plaintext)
1429
+ )
1430
+ );
1431
+ return encode(new Uint8Array([...iv, ...encrypted]));
1432
+ }
1433
+ async function verifySecureToken(token, secretKey, now = Date.now()) {
1434
+ try {
1435
+ const parts = token.split(".");
1436
+ if (parts.length !== 4 || parts[0] !== "sr3" || parts[1] !== "e" && parts[1] !== "p") {
1437
+ return {
1438
+ ok: false,
1439
+ valid: false,
1440
+ error: { code: "MALFORMED", message: "Secure token is malformed." }
1441
+ };
1442
+ }
1443
+ const mode = parts[1];
1444
+ const body = parts[2];
1445
+ const encodedSignature = parts[3];
1446
+ if (body === void 0 || encodedSignature === void 0 || mode === void 0) {
1447
+ return {
1448
+ ok: false,
1449
+ valid: false,
1450
+ error: { code: "MALFORMED", message: "Secure token is malformed." }
1451
+ };
1452
+ }
1453
+ const secret = bytes(secretKey);
1454
+ const valid = await cryptoApi().subtle.verify(
1455
+ "HMAC",
1456
+ await hmacKey(secret),
1457
+ source(decode(encodedSignature)),
1458
+ source(encoder.encode(body))
1459
+ );
1460
+ if (!valid)
1461
+ return {
1462
+ ok: false,
1463
+ valid: false,
1464
+ error: { code: "INVALID_SIGNATURE", message: "Secure token signature is invalid." }
1465
+ };
1466
+ const plaintext = mode === "e" ? await decryptPayload(body, secret) : decode(body);
1467
+ const parsed = JSON.parse(decoder.decode(plaintext));
1468
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1469
+ return {
1470
+ ok: false,
1471
+ valid: false,
1472
+ error: { code: "INVALID_PAYLOAD", message: "Secure token payload is invalid." }
1473
+ };
1474
+ }
1475
+ const payload = parsed;
1476
+ if (typeof payload.exp === "number" && now >= payload.exp * 1e3) {
1477
+ return {
1478
+ ok: false,
1479
+ valid: false,
1480
+ error: { code: "EXPIRED", message: "Secure token has expired." }
1481
+ };
1482
+ }
1483
+ return { ok: true, valid: true, payload };
1484
+ } catch {
1485
+ return {
1486
+ ok: false,
1487
+ valid: false,
1488
+ error: { code: "DECRYPTION_FAILED", message: "Secure token could not be verified." }
1489
+ };
1490
+ }
1491
+ }
1492
+ async function decryptPayload(body, secret) {
1493
+ const encrypted = decode(body);
1494
+ if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
1495
+ return new Uint8Array(
1496
+ await cryptoApi().subtle.decrypt(
1497
+ { name: "AES-GCM", iv: source(encrypted.slice(0, 12)) },
1498
+ await aesKey(secret),
1499
+ source(encrypted.slice(12))
1500
+ )
1501
+ );
1502
+ }
1503
+
1317
1504
  // src/domain/i18n.ts
1318
1505
  function resolveLocalizedText(text2, locale, fallbackLocale) {
1319
1506
  if (text2 === void 0 || text2 === "") return "";
1320
1507
  if (typeof text2 === "string") return text2;
1321
- const fallback = fallbackLocale ?? "ja";
1322
- return text2[locale] || text2[fallback] || "";
1508
+ const fallback = fallbackLocale === void 0 ? Object.values(text2).find((value) => typeof value === "string") : text2[fallbackLocale];
1509
+ return text2[locale] || fallback || "";
1323
1510
  }
1324
1511
  function toLocalizedString(text2) {
1325
1512
  if (text2 === void 0) return { ja: "", en: "" };
@@ -1421,6 +1608,11 @@ function validateSpot(value, index, errors) {
1421
1608
  add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be an array of IDs.`);
1422
1609
  }
1423
1610
  }
1611
+ for (const field of ["order", "orderIndex"]) {
1612
+ if (value[field] !== void 0 && (typeof value[field] !== "number" || !Number.isFinite(value[field]))) {
1613
+ add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be a finite number.`);
1614
+ }
1615
+ }
1424
1616
  }
1425
1617
  function validateReward(value, index, stampCount, errors) {
1426
1618
  const path = `rewards[${index}]`;
@@ -1449,7 +1641,7 @@ function validateReward(value, index, stampCount, errors) {
1449
1641
  if (value.validUntil !== void 0 && (typeof value.validUntil !== "string" || Number.isNaN(Date.parse(value.validUntil)))) {
1450
1642
  add(errors, `${path}.validUntil`, "INVALID_DATE", "Reward expiry must be a valid ISO date.");
1451
1643
  }
1452
- for (const field of ["maxStock", "limitPerUser"]) {
1644
+ for (const field of ["maxStock", "limitPerUser", "stockLimit", "userClaimLimit"]) {
1453
1645
  const count = value[field];
1454
1646
  if (count !== void 0 && (typeof count !== "number" || !Number.isInteger(count) || count <= 0)) {
1455
1647
  add(errors, `${path}.${field}`, "INVALID_REWARD", `${field} must be a positive integer.`);
@@ -1609,66 +1801,68 @@ function condition(value) {
1609
1801
  }
1610
1802
  }
1611
1803
  function migrateSpot(value, index) {
1612
- const source = isObject2(value) ? value : {};
1613
- const id = typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : `spot-${index + 1}`;
1614
- const description = text(source.description);
1615
- const hint = text(source.hint);
1804
+ const source2 = isObject2(value) ? value : {};
1805
+ const id = typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : `spot-${index + 1}`;
1806
+ const description = text(source2.description);
1807
+ const hint = text(source2.hint);
1616
1808
  return {
1617
1809
  id,
1618
- name: text(source.name) ?? `Spot ${index + 1}`,
1810
+ name: text(source2.name) ?? `Spot ${index + 1}`,
1619
1811
  ...description === void 0 ? {} : { description },
1620
1812
  ...hint === void 0 ? {} : { hint },
1621
1813
  condition: condition(
1622
- source.condition ?? (typeof source.token === "string" ? { type: "token", token: source.token } : void 0)
1814
+ source2.condition ?? (typeof source2.token === "string" ? { type: "token", token: source2.token } : void 0)
1623
1815
  ),
1624
- ...typeof source.order === "number" ? { order: source.order } : {},
1625
- ...typeof source.deckId === "string" ? { deckId: source.deckId } : {},
1626
- ...typeof source.groupId === "string" ? { groupId: source.groupId } : {},
1627
- ...typeof source.guideId === "string" ? { guideId: source.guideId } : {},
1628
- ...typeof source.iconUrl === "string" ? { iconUrl: source.iconUrl } : {},
1629
- ...typeof source.imageUrl === "string" ? { imageUrl: source.imageUrl } : {},
1630
- ...typeof source.externalUrl === "string" ? { externalUrl: source.externalUrl } : {},
1631
- ...typeof source.redirectUrlAfterClaim === "string" ? { redirectUrlAfterClaim: source.redirectUrlAfterClaim } : {},
1632
- ...isObject2(source.metadata) ? { metadata: source.metadata } : {},
1633
- ...Array.isArray(source.dependsOn) ? { dependsOn: source.dependsOn.filter((item) => typeof item === "string") } : {}
1816
+ ...typeof source2.orderIndex === "number" ? { orderIndex: source2.orderIndex } : typeof source2.order === "number" ? { order: source2.order } : {},
1817
+ ...typeof source2.deckId === "string" ? { deckId: source2.deckId } : {},
1818
+ ...typeof source2.groupId === "string" ? { groupId: source2.groupId } : {},
1819
+ ...typeof source2.guideId === "string" ? { guideId: source2.guideId } : {},
1820
+ ...typeof source2.iconUrl === "string" ? { iconUrl: source2.iconUrl } : {},
1821
+ ...typeof source2.imageUrl === "string" ? { imageUrl: source2.imageUrl } : {},
1822
+ ...typeof source2.externalUrl === "string" ? { externalUrl: source2.externalUrl } : {},
1823
+ ...typeof source2.redirectUrlAfterClaim === "string" ? { redirectUrlAfterClaim: source2.redirectUrlAfterClaim } : {},
1824
+ ...isObject2(source2.metadata) ? { metadata: source2.metadata } : {},
1825
+ ...Array.isArray(source2.dependsOn) ? { dependsOn: source2.dependsOn.filter((item) => typeof item === "string") } : {}
1634
1826
  };
1635
1827
  }
1636
1828
  function migrateReward(value, index) {
1637
- const source = isObject2(value) ? value : {};
1829
+ const source2 = isObject2(value) ? value : {};
1638
1830
  return {
1639
- id: typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : `reward-${index + 1}`,
1640
- title: text(source.title) ?? `Reward ${index + 1}`,
1641
- description: text(source.description) ?? "",
1642
- type: source.type === "digital" ? "digital" : "in_person",
1643
- redemptionMethod: source.redemptionMethod === "staff_passcode" || source.redemptionMethod === "view_only" ? source.redemptionMethod : "manual_slide",
1644
- requiredStampCount: typeof source.requiredStampCount === "number" && Number.isFinite(source.requiredStampCount) ? Math.max(0, Math.trunc(source.requiredStampCount)) : 0,
1645
- ...typeof source.digitalContentUrl === "string" ? { digitalContentUrl: source.digitalContentUrl } : {},
1646
- ...typeof source.staffPasscode === "string" ? { staffPasscode: source.staffPasscode } : {},
1647
- ...typeof source.validUntil === "string" ? { validUntil: source.validUntil } : {},
1648
- ...typeof source.maxStock === "number" ? { maxStock: source.maxStock } : {},
1649
- ...typeof source.limitPerUser === "number" ? { limitPerUser: source.limitPerUser } : {},
1650
- ...typeof source.claimTicketNumber === "string" ? { claimTicketNumber: source.claimTicketNumber } : {}
1831
+ id: typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : `reward-${index + 1}`,
1832
+ title: text(source2.title) ?? `Reward ${index + 1}`,
1833
+ description: text(source2.description) ?? "",
1834
+ type: source2.type === "digital" ? "digital" : "in_person",
1835
+ redemptionMethod: source2.redemptionMethod === "staff_passcode" || source2.redemptionMethod === "view_only" || source2.redemptionMethod === "server_claim" ? source2.redemptionMethod : "manual_slide",
1836
+ requiredStampCount: typeof source2.requiredStampCount === "number" && Number.isFinite(source2.requiredStampCount) ? Math.max(0, Math.trunc(source2.requiredStampCount)) : 0,
1837
+ ...typeof source2.digitalContentUrl === "string" ? { digitalContentUrl: source2.digitalContentUrl } : {},
1838
+ ...typeof source2.staffPasscode === "string" ? { staffPasscode: source2.staffPasscode } : {},
1839
+ ...typeof source2.validUntil === "string" ? { validUntil: source2.validUntil } : {},
1840
+ ...typeof source2.maxStock === "number" ? { maxStock: source2.maxStock } : {},
1841
+ ...typeof source2.limitPerUser === "number" ? { limitPerUser: source2.limitPerUser } : {},
1842
+ ...typeof source2.stockLimit === "number" ? { stockLimit: source2.stockLimit } : {},
1843
+ ...typeof source2.userClaimLimit === "number" ? { userClaimLimit: source2.userClaimLimit } : {},
1844
+ ...typeof source2.claimTicketNumber === "string" ? { claimTicketNumber: source2.claimTicketNumber } : {}
1651
1845
  };
1652
1846
  }
1653
1847
  function migrateRallyConfig(raw) {
1654
- const source = isObject2(raw) ? raw : {};
1655
- const rawStamps = Array.isArray(source.stamps) ? source.stamps : Array.isArray(source.spots) ? source.spots : [];
1656
- const rawRewards = Array.isArray(source.rewards) ? source.rewards : [];
1657
- const id = typeof source.id === "string" && source.id.trim() !== "" ? source.id.trim() : "migrated-rally";
1658
- const title = text(source.title);
1659
- const description = text(source.description);
1660
- const theme = isObject2(source.theme) ? source.theme : void 0;
1848
+ const source2 = isObject2(raw) ? raw : {};
1849
+ const rawStamps = Array.isArray(source2.stamps) ? source2.stamps : Array.isArray(source2.spots) ? source2.spots : [];
1850
+ const rawRewards = Array.isArray(source2.rewards) ? source2.rewards : [];
1851
+ const id = typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : "migrated-rally";
1852
+ const title = text(source2.title);
1853
+ const description = text(source2.description);
1854
+ const theme = isObject2(source2.theme) ? source2.theme : void 0;
1661
1855
  return {
1662
1856
  id,
1663
1857
  ...title === void 0 ? {} : { title },
1664
1858
  ...description === void 0 ? {} : { description },
1665
1859
  stamps: rawStamps.map(migrateSpot),
1666
1860
  ...rawRewards.length === 0 ? {} : { rewards: rawRewards.map(migrateReward) },
1667
- ...typeof source.isSequential === "boolean" ? { isSequential: source.isSequential } : {},
1861
+ ...typeof source2.isSequential === "boolean" ? { isSequential: source2.isSequential } : {},
1668
1862
  ...theme === void 0 ? {} : { theme },
1669
1863
  version: CURRENT_RALLY_CONFIG_VERSION,
1670
- ...typeof source.startDate === "string" ? { startDate: source.startDate } : typeof source.startsAt === "string" ? { startDate: source.startsAt } : {},
1671
- ...typeof source.endDate === "string" ? { endDate: source.endDate } : typeof source.endsAt === "string" ? { endDate: source.endsAt } : {}
1864
+ ...typeof source2.startDate === "string" ? { startDate: source2.startDate } : typeof source2.startsAt === "string" ? { startDate: source2.startsAt } : {},
1865
+ ...typeof source2.endDate === "string" ? { endDate: source2.endDate } : typeof source2.endsAt === "string" ? { endDate: source2.endsAt } : {}
1672
1866
  };
1673
1867
  }
1674
1868
 
@@ -1793,22 +1987,22 @@ var THEME_PRESETS = [
1793
1987
  ];
1794
1988
 
1795
1989
  // src/security/snapshotToken.ts
1796
- var encoder = new TextEncoder();
1797
- function cryptoApi() {
1990
+ var encoder2 = new TextEncoder();
1991
+ function cryptoApi2() {
1798
1992
  if (globalThis.crypto === void 0 || globalThis.crypto.subtle === void 0) {
1799
1993
  throw new Error("Web Crypto API is unavailable in this environment.");
1800
1994
  }
1801
1995
  return globalThis.crypto;
1802
1996
  }
1803
1997
  function secretBytes(secretKey) {
1804
- return typeof secretKey === "string" ? encoder.encode(secretKey) : new Uint8Array(secretKey);
1998
+ return typeof secretKey === "string" ? encoder2.encode(secretKey) : new Uint8Array(secretKey);
1805
1999
  }
1806
- function webCryptoBytes(bytes) {
1807
- return bytes.buffer;
2000
+ function webCryptoBytes(bytes2) {
2001
+ return bytes2.buffer;
1808
2002
  }
1809
- function base64Url(bytes) {
2003
+ function base64Url(bytes2) {
1810
2004
  let binary = "";
1811
- for (const byte of bytes) binary += String.fromCharCode(byte);
2005
+ for (const byte of bytes2) binary += String.fromCharCode(byte);
1812
2006
  return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
1813
2007
  }
1814
2008
  function fromBase64Url(value) {
@@ -1818,7 +2012,7 @@ function fromBase64Url(value) {
1818
2012
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
1819
2013
  }
1820
2014
  async function importHmacKey(secret) {
1821
- return cryptoApi().subtle.importKey(
2015
+ return cryptoApi2().subtle.importKey(
1822
2016
  "raw",
1823
2017
  webCryptoBytes(secret),
1824
2018
  { name: "HMAC", hash: "SHA-256" },
@@ -1827,18 +2021,18 @@ async function importHmacKey(secret) {
1827
2021
  );
1828
2022
  }
1829
2023
  async function importAesKey(secret) {
1830
- const digest = await cryptoApi().subtle.digest("SHA-256", webCryptoBytes(secret));
1831
- return cryptoApi().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
2024
+ const digest = await cryptoApi2().subtle.digest("SHA-256", webCryptoBytes(secret));
2025
+ return cryptoApi2().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
1832
2026
  }
1833
2027
  function isExpired(payload, now) {
1834
2028
  if (typeof payload.exp === "number" && now >= payload.exp * 1e3) return true;
1835
2029
  return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now;
1836
2030
  }
1837
2031
  async function createSignedSnapshotToken(payload, secretKey) {
1838
- const api = cryptoApi();
2032
+ const api = cryptoApi2();
1839
2033
  const secret = secretBytes(secretKey);
1840
2034
  const iv = api.getRandomValues(new Uint8Array(12));
1841
- const plaintext = encoder.encode(JSON.stringify(payload));
2035
+ const plaintext = encoder2.encode(JSON.stringify(payload));
1842
2036
  const encrypted = new Uint8Array(
1843
2037
  await api.subtle.encrypt(
1844
2038
  { name: "AES-GCM", iv: webCryptoBytes(iv) },
@@ -1848,7 +2042,7 @@ async function createSignedSnapshotToken(payload, secretKey) {
1848
2042
  );
1849
2043
  const body = base64Url(new Uint8Array([...iv, ...encrypted]));
1850
2044
  const signature = new Uint8Array(
1851
- await api.subtle.sign("HMAC", await importHmacKey(secret), encoder.encode(body))
2045
+ await api.subtle.sign("HMAC", await importHmacKey(secret), encoder2.encode(body))
1852
2046
  );
1853
2047
  return `sr2.${body}.${base64Url(signature)}`;
1854
2048
  }
@@ -1871,11 +2065,11 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
1871
2065
  };
1872
2066
  }
1873
2067
  const secret = secretBytes(secretKey);
1874
- const validSignature = await cryptoApi().subtle.verify(
2068
+ const validSignature = await cryptoApi2().subtle.verify(
1875
2069
  "HMAC",
1876
2070
  await importHmacKey(secret),
1877
2071
  webCryptoBytes(fromBase64Url(encodedSignature)),
1878
- webCryptoBytes(encoder.encode(body))
2072
+ webCryptoBytes(encoder2.encode(body))
1879
2073
  );
1880
2074
  if (!validSignature) {
1881
2075
  return {
@@ -1886,7 +2080,7 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
1886
2080
  }
1887
2081
  const encrypted = fromBase64Url(body);
1888
2082
  if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
1889
- const payloadText = await cryptoApi().subtle.decrypt(
2083
+ const payloadText = await cryptoApi2().subtle.decrypt(
1890
2084
  { name: "AES-GCM", iv: webCryptoBytes(encrypted.slice(0, 12)) },
1891
2085
  await importAesKey(secret),
1892
2086
  webCryptoBytes(encrypted.slice(12))
@@ -1911,6 +2105,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
1911
2105
  }
1912
2106
  }
1913
2107
 
1914
- export { CURRENT_RALLY_CONFIG_VERSION, DEFAULT_SHEET_THEME, InMemoryStorage, IndexedDBAdapter, LocalStorageAdapter, StampRallyClient, StorageAdapterError, THEME_PRESETS, calculateDistanceMeters, calculateProgress, consumeReward, createClaimTicketNumber, createSignedSnapshotToken, evaluateCheckIn, evaluateCondition, evaluateConditionDetailed, exportProgressToken, getCurrentGeoContext, importProgressToken, isGeolocationSupported, isNfcSupported, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, migrateRallyConfig, normalizePasscode, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, stripSensitiveConfig, toLocalizedString, validateRallyConfig, verifyPasscode, verifySnapshotToken };
2108
+ export { CURRENT_RALLY_CONFIG_VERSION, DEFAULT_SHEET_THEME, InMemoryStorage, IndexedDBAdapter, LocalStorageAdapter, StampRallyClient, StorageAdapterError, THEME_PRESETS, calculateDistanceMeters, calculateProgress, consumeReward, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCheckIn, evaluateCondition, evaluateConditionDetailed, exportProgressToken, getCurrentGeoContext, importProgressToken, isGeolocationSupported, isNfcSupported, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, migrateRallyConfig, normalizePasscode, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, stripSensitiveConfig, toLocalizedString, validateRallyConfig, verifyPasscode, verifySecureToken, verifySnapshotToken };
1915
2109
  //# sourceMappingURL=index.js.map
1916
2110
  //# sourceMappingURL=index.js.map