@stamprally/core 0.7.0 → 0.8.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
@@ -29,7 +29,7 @@ function contextTypeMismatch(conditionType, expectedContextType, actualContextTy
29
29
  function assertNever(value) {
30
30
  throw new Error(`Unexpected condition: ${JSON.stringify(value)}`);
31
31
  }
32
- function evaluateConditionDetailed(condition2, context, now) {
32
+ function evaluateConditionDetailed(condition2, context, now2) {
33
33
  switch (condition2.type) {
34
34
  case "instant":
35
35
  return context.type === "instant" ? { ok: true, value: { conditionType: "instant" } } : contextTypeMismatch("instant", "instant", context.type);
@@ -59,14 +59,14 @@ function evaluateConditionDetailed(condition2, context, now) {
59
59
  }
60
60
  };
61
61
  }
62
- const distanceMeters = calculateDistanceMeters(
62
+ const distanceMeters2 = calculateDistanceMeters(
63
63
  condition2.latitude,
64
64
  condition2.longitude,
65
65
  context.currentLatitude,
66
66
  context.currentLongitude
67
67
  );
68
- if (distanceMeters <= condition2.radiusMeters) {
69
- return { ok: true, value: { conditionType: "geo", distanceMeters } };
68
+ if (distanceMeters2 <= condition2.radiusMeters) {
69
+ return { ok: true, value: { conditionType: "geo", distanceMeters: distanceMeters2 } };
70
70
  }
71
71
  return {
72
72
  ok: false,
@@ -74,9 +74,9 @@ function evaluateConditionDetailed(condition2, context, now) {
74
74
  code: "CONDITION_MISMATCH",
75
75
  conditionType: "geo",
76
76
  reason: "OUTSIDE_RADIUS",
77
- distanceMeters,
77
+ distanceMeters: distanceMeters2,
78
78
  radiusMeters: condition2.radiusMeters,
79
- differenceMeters: distanceMeters - condition2.radiusMeters
79
+ differenceMeters: distanceMeters2 - condition2.radiusMeters
80
80
  }
81
81
  };
82
82
  }
@@ -101,7 +101,7 @@ function evaluateConditionDetailed(condition2, context, now) {
101
101
  for (const [index, childCondition] of condition2.conditions.entries()) {
102
102
  const childContext = context.contexts[index];
103
103
  if (childContext === void 0) continue;
104
- const result = evaluateConditionDetailed(childCondition, childContext, now);
104
+ const result = evaluateConditionDetailed(childCondition, childContext, now2);
105
105
  if (result.ok) matchedCount += 1;
106
106
  else failures.push({ index, error: result.error });
107
107
  }
@@ -124,7 +124,7 @@ function evaluateConditionDetailed(condition2, context, now) {
124
124
  case "time_window": {
125
125
  const startsAt = Date.parse(condition2.startsAt);
126
126
  const endsAt = Date.parse(condition2.endsAt);
127
- const currentTime = Date.parse(now);
127
+ const currentTime = Date.parse(now2);
128
128
  if (!Number.isFinite(currentTime)) {
129
129
  return {
130
130
  ok: false,
@@ -132,7 +132,7 @@ function evaluateConditionDetailed(condition2, context, now) {
132
132
  code: "CONDITION_MISMATCH",
133
133
  conditionType: "time_window",
134
134
  reason: "INVALID_NOW",
135
- now,
135
+ now: now2,
136
136
  startsAt: condition2.startsAt,
137
137
  endsAt: condition2.endsAt
138
138
  }
@@ -145,7 +145,7 @@ function evaluateConditionDetailed(condition2, context, now) {
145
145
  code: "CONDITION_MISMATCH",
146
146
  conditionType: "time_window",
147
147
  reason: "INVALID_TIME_WINDOW",
148
- now,
148
+ now: now2,
149
149
  startsAt: condition2.startsAt,
150
150
  endsAt: condition2.endsAt
151
151
  }
@@ -158,21 +158,21 @@ function evaluateConditionDetailed(condition2, context, now) {
158
158
  code: "CONDITION_MISMATCH",
159
159
  conditionType: "time_window",
160
160
  reason: currentTime < startsAt ? "BEFORE_START" : "AFTER_END",
161
- now,
161
+ now: now2,
162
162
  startsAt: condition2.startsAt,
163
163
  endsAt: condition2.endsAt
164
164
  }
165
165
  };
166
166
  }
167
- const childResult = evaluateConditionDetailed(condition2.condition, context, now);
167
+ const childResult = evaluateConditionDetailed(condition2.condition, context, now2);
168
168
  return childResult.ok ? { ok: true, value: { conditionType: "time_window" } } : childResult;
169
169
  }
170
170
  default:
171
171
  return assertNever(condition2);
172
172
  }
173
173
  }
174
- function evaluateCondition(condition2, context, now) {
175
- return evaluateConditionDetailed(condition2, context, now ?? "").ok;
174
+ function evaluateCondition(condition2, context, now2) {
175
+ return evaluateConditionDetailed(condition2, context, now2 ?? "").ok;
176
176
  }
177
177
 
178
178
  // src/engine/checkIn.ts
@@ -621,14 +621,14 @@ async function readQrContext(videoElement, options = {}) {
621
621
  }
622
622
 
623
623
  // src/engine/transition.ts
624
- function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now) {
624
+ function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now2) {
625
625
  const statesById = new Map(currentStates.map((state) => [state.rewardId, state]));
626
626
  return rewards.map((reward) => {
627
627
  const current = statesById.get(reward.id);
628
628
  if (current?.status === "CONSUMED" || current?.status === "EXPIRED") {
629
629
  return current;
630
630
  }
631
- if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(now)) {
631
+ if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(now2)) {
632
632
  return { rewardId: reward.id, status: "EXPIRED" };
633
633
  }
634
634
  const stockLimit = reward.stockLimit ?? reward.maxStock;
@@ -644,7 +644,7 @@ function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now)
644
644
  return {
645
645
  rewardId: reward.id,
646
646
  status: "AVAILABLE",
647
- unlockedAt: current?.unlockedAt ?? now,
647
+ unlockedAt: current?.unlockedAt ?? now2,
648
648
  ...current?.claimTicketNumber === void 0 ? {} : { claimTicketNumber: current.claimTicketNumber }
649
649
  };
650
650
  }
@@ -709,7 +709,7 @@ function consumeReward(params) {
709
709
  }
710
710
  };
711
711
  }
712
- function processStamp(state, config, targetStampId, context, now) {
712
+ function processStamp(state, config, targetStampId, context, now2) {
713
713
  const targetStamp = config.stamps.find((stamp) => stamp.id === targetStampId);
714
714
  if (targetStamp === void 0) {
715
715
  return { ok: false, error: { code: "STAMP_NOT_FOUND", stampId: targetStampId } };
@@ -734,7 +734,7 @@ function processStamp(state, config, targetStampId, context, now) {
734
734
  };
735
735
  }
736
736
  }
737
- const conditionResult = evaluateConditionDetailed(targetStamp.condition, context, now);
737
+ const conditionResult = evaluateConditionDetailed(targetStamp.condition, context, now2);
738
738
  if (!conditionResult.ok) {
739
739
  return {
740
740
  ok: false,
@@ -745,27 +745,27 @@ function processStamp(state, config, targetStampId, context, now) {
745
745
  }
746
746
  };
747
747
  }
748
- const record = { stampId: targetStampId, acquiredAt: now };
748
+ const record = { stampId: targetStampId, acquiredAt: now2 };
749
749
  const nextRecords = [...state.records, record];
750
- const nextRewards = config.rewards === void 0 && state.rewards === void 0 ? void 0 : reconcileRewardStates(config.rewards ?? [], state.rewards ?? [], nextRecords.length, now);
750
+ const nextRewards = config.rewards === void 0 && state.rewards === void 0 ? void 0 : reconcileRewardStates(config.rewards ?? [], state.rewards ?? [], nextRecords.length, now2);
751
751
  const nextState = {
752
752
  ...state,
753
753
  records: nextRecords,
754
754
  ...nextRewards === void 0 ? {} : { rewards: nextRewards },
755
- updatedAt: now
755
+ updatedAt: now2
756
756
  };
757
757
  const events = [{ type: "stampAcquired", record }];
758
758
  if (nextRewards !== void 0) {
759
759
  for (const rewardState of nextRewards) {
760
760
  const previous = state.rewards?.find((item) => item.rewardId === rewardState.rewardId);
761
761
  if (rewardState.status === "AVAILABLE" && previous?.status !== "AVAILABLE") {
762
- events.push({ type: "rewardUnlocked", rewardId: rewardState.rewardId, unlockedAt: now });
762
+ events.push({ type: "rewardUnlocked", rewardId: rewardState.rewardId, unlockedAt: now2 });
763
763
  }
764
764
  }
765
765
  }
766
766
  const completed = config.stamps.length > 0 && config.stamps.every((stamp) => nextState.records.some((item) => item.stampId === stamp.id));
767
767
  if (completed) {
768
- events.push({ type: "rallyCompleted", rallyId: config.id, completedAt: now });
768
+ events.push({ type: "rallyCompleted", rallyId: config.id, completedAt: now2 });
769
769
  }
770
770
  return { ok: true, value: { nextState, events } };
771
771
  }
@@ -1270,10 +1270,10 @@ var StampRallyClient = class {
1270
1270
  }
1271
1271
  return this.#initialization;
1272
1272
  }
1273
- acquire(stampId, context, now = this.#clock()) {
1273
+ acquire(stampId, context, now2 = this.#clock()) {
1274
1274
  return this.#enqueue(async () => {
1275
1275
  const currentState = await this.initialize();
1276
- const result = processStamp(currentState, this.#config, stampId, context, now);
1276
+ const result = processStamp(currentState, this.#config, stampId, context, now2);
1277
1277
  if (!result.ok) {
1278
1278
  this.#emitEvent({ type: "error", error: result.error });
1279
1279
  return result;
@@ -1285,14 +1285,14 @@ var StampRallyClient = class {
1285
1285
  return result;
1286
1286
  });
1287
1287
  }
1288
- reset(now = this.#clock()) {
1288
+ reset(now2 = this.#clock()) {
1289
1289
  return this.#enqueue(async () => {
1290
1290
  const initialization = this.#initialization;
1291
1291
  if (initialization !== null) {
1292
1292
  await initialization.catch(() => void 0);
1293
1293
  }
1294
1294
  await this.#storage.remove(this.#config.id);
1295
- const nextState = this.#createEmptyState(now);
1295
+ const nextState = this.#createEmptyState(now2);
1296
1296
  this.#currentState = nextState;
1297
1297
  this.#initialization = Promise.resolve(nextState);
1298
1298
  this.#emit(nextState);
@@ -1334,18 +1334,18 @@ var StampRallyClient = class {
1334
1334
  #emitEvent(event) {
1335
1335
  for (const listener of this.#eventListeners) listener(event);
1336
1336
  }
1337
- #createEmptyState(now) {
1337
+ #createEmptyState(now2) {
1338
1338
  const state = {
1339
1339
  rallyId: this.#config.id,
1340
1340
  records: [],
1341
1341
  ...this.#config.rewards === void 0 ? {} : {
1342
- rewards: reconcileRewardStates(this.#config.rewards, [], 0, now)
1342
+ rewards: reconcileRewardStates(this.#config.rewards, [], 0, now2)
1343
1343
  },
1344
- updatedAt: now
1344
+ updatedAt: now2
1345
1345
  };
1346
1346
  return state;
1347
1347
  }
1348
- #reconcileState(state, now) {
1348
+ #reconcileState(state, now2) {
1349
1349
  const configuredStampIds = new Set(this.#config.stamps.map((stamp) => stamp.id));
1350
1350
  const seenStampIds = /* @__PURE__ */ new Set();
1351
1351
  const records = state.records.filter((record) => {
@@ -1363,12 +1363,319 @@ var StampRallyClient = class {
1363
1363
  this.#config.rewards ?? [],
1364
1364
  state.rewards ?? [],
1365
1365
  records.length,
1366
- now
1366
+ now2
1367
1367
  )
1368
1368
  };
1369
1369
  }
1370
1370
  };
1371
1371
 
1372
+ // src/client/universalClient.ts
1373
+ function isStorage(value) {
1374
+ return "load" in value && "save" in value && "remove" in value;
1375
+ }
1376
+ function randomId(prefix) {
1377
+ return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
1378
+ }
1379
+ function now() {
1380
+ return (/* @__PURE__ */ new Date()).toISOString();
1381
+ }
1382
+ function text(value) {
1383
+ if (typeof value === "string") return value;
1384
+ if (typeof value === "object" && value !== null) {
1385
+ const first = Object.values(value).find((item) => typeof item === "string");
1386
+ if (typeof first === "string") return first;
1387
+ }
1388
+ return "";
1389
+ }
1390
+ function proofString(value) {
1391
+ if (typeof value === "string") return value;
1392
+ if (typeof value === "object" && value !== null) {
1393
+ const record = value;
1394
+ for (const key of ["token", "code", "passcode", "value"]) {
1395
+ if (typeof record[key] === "string") return record[key];
1396
+ }
1397
+ }
1398
+ return "";
1399
+ }
1400
+ function distanceMeters(aLat, aLon, bLat, bLon) {
1401
+ const radians = (degrees) => degrees * Math.PI / 180;
1402
+ const dLat = radians(bLat - aLat);
1403
+ const dLon = radians(bLon - aLon);
1404
+ const value = Math.sin(dLat / 2) ** 2 + Math.cos(radians(aLat)) * Math.cos(radians(bLat)) * Math.sin(dLon / 2) ** 2;
1405
+ return 6371e3 * 2 * Math.asin(Math.sqrt(Math.min(1, value)));
1406
+ }
1407
+ function conditionMatches(condition2, proofData) {
1408
+ switch (condition2.type) {
1409
+ case "qr":
1410
+ return proofString(proofData).trim() !== "";
1411
+ case "passcode":
1412
+ return proofString(proofData).trim() !== "";
1413
+ case "gps": {
1414
+ if (typeof proofData !== "object" || proofData === null) return false;
1415
+ const value = proofData;
1416
+ const latitude = typeof value.latitude === "number" ? value.latitude : Number.NaN;
1417
+ const longitude = typeof value.longitude === "number" ? value.longitude : Number.NaN;
1418
+ return Number.isFinite(latitude) && Number.isFinite(longitude) && distanceMeters(condition2.latitude, condition2.longitude, latitude, longitude) <= condition2.radiusMeters;
1419
+ }
1420
+ case "custom":
1421
+ return true;
1422
+ }
1423
+ }
1424
+ function asReward(reward) {
1425
+ return { ...reward, description: text(reward.description) };
1426
+ }
1427
+ function initialState(config, timestamp) {
1428
+ const rewards = config.rewards.map(asReward);
1429
+ return {
1430
+ rallyId: config.id,
1431
+ records: [],
1432
+ rewards: reconcileRewardStates(rewards, [], 0, timestamp),
1433
+ updatedAt: timestamp
1434
+ };
1435
+ }
1436
+ var UniversalStampRallyClient = class {
1437
+ #listeners = /* @__PURE__ */ new Set();
1438
+ #eventListeners = /* @__PURE__ */ new Set();
1439
+ #config;
1440
+ #storage;
1441
+ #options;
1442
+ #state = null;
1443
+ #initialization = null;
1444
+ #queue = Promise.resolve();
1445
+ constructor(config, storageOrOptions = {}, clock) {
1446
+ this.#config = config;
1447
+ this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions, ...clock === void 0 ? {} : { clock } } : storageOrOptions;
1448
+ this.#storage = this.#options.storage ?? new InMemoryStorage();
1449
+ }
1450
+ getConfig() {
1451
+ return this.#config;
1452
+ }
1453
+ getState() {
1454
+ return this.#state;
1455
+ }
1456
+ subscribe(listener) {
1457
+ this.#listeners.add(listener);
1458
+ return () => this.#listeners.delete(listener);
1459
+ }
1460
+ subscribeEvents(listener) {
1461
+ this.#eventListeners.add(listener);
1462
+ return () => this.#eventListeners.delete(listener);
1463
+ }
1464
+ init() {
1465
+ return this.initialize();
1466
+ }
1467
+ initialize() {
1468
+ if (this.#state !== null) return Promise.resolve(this.#state);
1469
+ if (this.#initialization === null) {
1470
+ this.#initialization = this.#storage.load(this.#config.id).then((stored) => {
1471
+ const state = stored === null ? initialState(this.#config, this.#now()) : this.#reconcile(stored);
1472
+ this.#state = state;
1473
+ this.#emit(state);
1474
+ return state;
1475
+ }).catch((error) => {
1476
+ this.#initialization = null;
1477
+ throw error;
1478
+ });
1479
+ }
1480
+ return this.#initialization;
1481
+ }
1482
+ checkIn(spotId, proofData, options = {}) {
1483
+ return this.#enqueue(async () => {
1484
+ const current = await this.initialize();
1485
+ const spot = this.#config.spots.find((item) => item.id === spotId);
1486
+ if (spot === void 0)
1487
+ return this.#fail({ code: "SPOT_NOT_FOUND", spotId, message: "Spot was not found." });
1488
+ if (current.records.some((record2) => record2.stampId === spotId))
1489
+ return this.#fail({
1490
+ code: "STAMP_ALREADY_ACQUIRED",
1491
+ spotId,
1492
+ message: "Spot was already claimed."
1493
+ });
1494
+ const acquired = new Set(current.records.map((record2) => record2.stampId));
1495
+ if (spot.prerequisites?.some((id) => !acquired.has(id)))
1496
+ return this.#fail({
1497
+ code: "PREREQUISITES_NOT_MET",
1498
+ spotId,
1499
+ message: "Prerequisite spots are not complete."
1500
+ });
1501
+ for (const condition2 of spot.conditions) {
1502
+ if (condition2.type === "custom") {
1503
+ const validator = this.#options.customValidators?.[condition2.validatorName] ?? this.#options.customValidator;
1504
+ if (validator !== void 0) {
1505
+ try {
1506
+ const validation = await validator({
1507
+ spotId,
1508
+ proofData,
1509
+ config: this.#config,
1510
+ userState: current
1511
+ });
1512
+ if (!validation.success)
1513
+ return this.#fail({
1514
+ code: "CUSTOM_VALIDATION_FAILED",
1515
+ spotId,
1516
+ message: validation.error ?? "Custom validation failed."
1517
+ });
1518
+ } catch (error) {
1519
+ return this.#fail({
1520
+ code: "CUSTOM_VALIDATION_FAILED",
1521
+ spotId,
1522
+ message: error instanceof Error ? error.message : "Custom validation failed."
1523
+ });
1524
+ }
1525
+ } else {
1526
+ return this.#fail({
1527
+ code: "CUSTOM_VALIDATION_FAILED",
1528
+ spotId,
1529
+ message: "No custom validator is registered."
1530
+ });
1531
+ }
1532
+ } else if (!conditionMatches(condition2, proofData)) {
1533
+ return this.#fail({ code: "INVALID_PROOF", spotId, message: "Verification failed." });
1534
+ }
1535
+ }
1536
+ const timestamp = options.now ?? this.#now();
1537
+ const request = {
1538
+ rallyId: this.#config.id,
1539
+ spotId,
1540
+ proofData,
1541
+ idempotencyKey: options.idempotencyKey ?? randomId("check-in"),
1542
+ now: timestamp,
1543
+ state: current
1544
+ };
1545
+ const remote = this.#options.syncAdapter?.checkIn;
1546
+ if (options.sync !== false && remote !== void 0) {
1547
+ const result = await remote(request);
1548
+ if (!result.ok) return this.#fail(result.error);
1549
+ return this.#commitCheckIn(result.value.state, result);
1550
+ }
1551
+ const record = { stampId: spotId, acquiredAt: timestamp };
1552
+ const next = this.#reconcile({
1553
+ ...current,
1554
+ records: [...current.records, record],
1555
+ updatedAt: timestamp
1556
+ });
1557
+ await this.#storage.save(next);
1558
+ return this.#commitCheckIn(next, { ok: true, value: { state: next, record } });
1559
+ });
1560
+ }
1561
+ claimReward(rewardId, options = {}) {
1562
+ return this.#enqueue(async () => {
1563
+ const current = await this.initialize();
1564
+ const configured = this.#config.rewards.find((item) => item.id === rewardId);
1565
+ if (configured === void 0)
1566
+ return this.#fail({ code: "REWARD_NOT_FOUND", rewardId, message: "Reward was not found." });
1567
+ const timestamp = options.now ?? this.#now();
1568
+ const state = current.rewards?.find((item) => item.rewardId === rewardId) ?? {
1569
+ rewardId,
1570
+ status: "LOCKED"
1571
+ };
1572
+ const local = consumeReward({
1573
+ reward: asReward(configured),
1574
+ currentState: state,
1575
+ now: timestamp,
1576
+ ...options.staffPasscode === void 0 ? {} : { inputPasscode: options.staffPasscode },
1577
+ ...options.staffId === void 0 ? {} : { staffId: options.staffId }
1578
+ });
1579
+ if (!local.ok) return this.#fail(local.error);
1580
+ const request = {
1581
+ rallyId: this.#config.id,
1582
+ rewardId,
1583
+ idempotencyKey: options.idempotencyKey ?? randomId("claim"),
1584
+ now: timestamp,
1585
+ options,
1586
+ state: current
1587
+ };
1588
+ const remote = this.#options.syncAdapter?.claimReward;
1589
+ if (options.sync !== false && remote !== void 0) {
1590
+ const result = await remote(request);
1591
+ if (!result.ok) return this.#fail(result.error);
1592
+ return this.#commitClaim(result.value.state, result);
1593
+ }
1594
+ const nextRewards = (current.rewards ?? []).map(
1595
+ (item) => item.rewardId === rewardId ? local.value : item
1596
+ );
1597
+ const next = { ...current, rewards: nextRewards, updatedAt: timestamp };
1598
+ await this.#storage.save(next);
1599
+ return this.#commitClaim(next, { ok: true, value: { state: next, reward: local.value } });
1600
+ });
1601
+ }
1602
+ sync(adapter = this.#options.syncAdapter ?? {}) {
1603
+ return this.#enqueue(async () => {
1604
+ const current = await this.initialize();
1605
+ if (adapter.sync === void 0) {
1606
+ this.#emitEvent({ type: "sync", state: current });
1607
+ return;
1608
+ }
1609
+ try {
1610
+ const next = this.#reconcile(
1611
+ await adapter.sync({ rallyId: this.#config.id, state: current })
1612
+ );
1613
+ await this.#storage.save(next);
1614
+ this.#state = next;
1615
+ this.#emit(next);
1616
+ this.#emitEvent({ type: "sync", state: next });
1617
+ } catch (error) {
1618
+ const failure = {
1619
+ code: "SYNC_FAILED",
1620
+ message: error instanceof Error ? error.message : String(error)
1621
+ };
1622
+ this.#emitEvent({ type: "error", error: failure });
1623
+ throw error;
1624
+ }
1625
+ });
1626
+ }
1627
+ #now() {
1628
+ return this.#options.clock?.() ?? now();
1629
+ }
1630
+ #reconcile(state) {
1631
+ const ids = new Set(this.#config.spots.map((spot) => spot.id));
1632
+ const seen = /* @__PURE__ */ new Set();
1633
+ const records = state.records.filter(
1634
+ (record) => ids.has(record.stampId) && !seen.has(record.stampId) && seen.add(record.stampId)
1635
+ );
1636
+ return {
1637
+ ...cloneState(state),
1638
+ records,
1639
+ rewards: reconcileRewardStates(
1640
+ this.#config.rewards.map(asReward),
1641
+ state.rewards ?? [],
1642
+ records.length,
1643
+ state.updatedAt
1644
+ )
1645
+ };
1646
+ }
1647
+ #enqueue(operation) {
1648
+ const next = this.#queue.then(operation, operation);
1649
+ this.#queue = next.then(
1650
+ () => void 0,
1651
+ () => void 0
1652
+ );
1653
+ return next;
1654
+ }
1655
+ #fail(error) {
1656
+ this.#emitEvent({ type: "error", error });
1657
+ return { ok: false, error };
1658
+ }
1659
+ #commitCheckIn(state, result) {
1660
+ this.#state = state;
1661
+ this.#emit(state);
1662
+ this.#emitEvent({ type: "checkIn", result });
1663
+ return result;
1664
+ }
1665
+ #commitClaim(state, result) {
1666
+ this.#state = state;
1667
+ this.#emit(state);
1668
+ this.#emitEvent({ type: "rewardClaimed", result });
1669
+ return result;
1670
+ }
1671
+ #emit(state) {
1672
+ for (const listener of this.#listeners) listener(state);
1673
+ }
1674
+ #emitEvent(event) {
1675
+ for (const listener of this.#eventListeners) listener(event);
1676
+ }
1677
+ };
1678
+
1372
1679
  // src/crypto/token.ts
1373
1680
  var encoder = new TextEncoder();
1374
1681
  var decoder = new TextDecoder();
@@ -1430,7 +1737,7 @@ async function encryptPayload(plaintext, secret, api) {
1430
1737
  );
1431
1738
  return encode(new Uint8Array([...iv, ...encrypted]));
1432
1739
  }
1433
- async function verifySecureToken(token, secretKey, now = Date.now()) {
1740
+ async function verifySecureToken(token, secretKey, now2 = Date.now()) {
1434
1741
  try {
1435
1742
  const parts = token.split(".");
1436
1743
  if (parts.length !== 4 || parts[0] !== "sr3" || parts[1] !== "e" && parts[1] !== "p") {
@@ -1473,7 +1780,7 @@ async function verifySecureToken(token, secretKey, now = Date.now()) {
1473
1780
  };
1474
1781
  }
1475
1782
  const payload = parsed;
1476
- if (typeof payload.exp === "number" && now >= payload.exp * 1e3) {
1783
+ if (typeof payload.exp === "number" && now2 >= payload.exp * 1e3) {
1477
1784
  return {
1478
1785
  ok: false,
1479
1786
  valid: false,
@@ -1502,18 +1809,18 @@ async function decryptPayload(body, secret) {
1502
1809
  }
1503
1810
 
1504
1811
  // src/domain/i18n.ts
1505
- function resolveLocalizedText(text2, locale, fallbackLocale) {
1506
- if (text2 === void 0 || text2 === "") return "";
1507
- if (typeof text2 === "string") return text2;
1508
- const fallback = fallbackLocale === void 0 ? Object.values(text2).find((value) => typeof value === "string") : text2[fallbackLocale];
1509
- return text2[locale] || fallback || "";
1510
- }
1511
- function toLocalizedString(text2) {
1512
- if (text2 === void 0) return { ja: "", en: "" };
1513
- return typeof text2 === "string" ? { ja: text2, en: "" } : {
1514
- ja: text2["ja"] ?? "",
1515
- en: text2["en"] ?? "",
1516
- ...text2
1812
+ function resolveLocalizedText(text3, locale, fallbackLocale) {
1813
+ if (text3 === void 0 || text3 === "") return "";
1814
+ if (typeof text3 === "string") return text3;
1815
+ const fallback = fallbackLocale === void 0 ? Object.values(text3).find((value) => typeof value === "string") : text3[fallbackLocale];
1816
+ return text3[locale] || fallback || "";
1817
+ }
1818
+ function toLocalizedString(text3) {
1819
+ if (text3 === void 0) return { ja: "", en: "" };
1820
+ return typeof text3 === "string" ? { ja: text3, en: "" } : {
1821
+ ja: text3["ja"] ?? "",
1822
+ en: text3["en"] ?? "",
1823
+ ...text3
1517
1824
  };
1518
1825
  }
1519
1826
 
@@ -1759,7 +2066,7 @@ function validateRallyConfig(config) {
1759
2066
  function isObject2(value) {
1760
2067
  return typeof value === "object" && value !== null && !Array.isArray(value);
1761
2068
  }
1762
- function text(value) {
2069
+ function text2(value) {
1763
2070
  if (typeof value === "string") return value;
1764
2071
  if (!isObject2(value)) return void 0;
1765
2072
  const entries = Object.entries(value).filter(([, item]) => typeof item === "string");
@@ -1803,11 +2110,11 @@ function condition(value) {
1803
2110
  function migrateSpot(value, index) {
1804
2111
  const source2 = isObject2(value) ? value : {};
1805
2112
  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);
2113
+ const description = text2(source2.description);
2114
+ const hint = text2(source2.hint);
1808
2115
  return {
1809
2116
  id,
1810
- name: text(source2.name) ?? `Spot ${index + 1}`,
2117
+ name: text2(source2.name) ?? `Spot ${index + 1}`,
1811
2118
  ...description === void 0 ? {} : { description },
1812
2119
  ...hint === void 0 ? {} : { hint },
1813
2120
  condition: condition(
@@ -1829,8 +2136,8 @@ function migrateReward(value, index) {
1829
2136
  const source2 = isObject2(value) ? value : {};
1830
2137
  return {
1831
2138
  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) ?? "",
2139
+ title: text2(source2.title) ?? `Reward ${index + 1}`,
2140
+ description: text2(source2.description) ?? "",
1834
2141
  type: source2.type === "digital" ? "digital" : "in_person",
1835
2142
  redemptionMethod: source2.redemptionMethod === "staff_passcode" || source2.redemptionMethod === "view_only" || source2.redemptionMethod === "server_claim" ? source2.redemptionMethod : "manual_slide",
1836
2143
  requiredStampCount: typeof source2.requiredStampCount === "number" && Number.isFinite(source2.requiredStampCount) ? Math.max(0, Math.trunc(source2.requiredStampCount)) : 0,
@@ -1849,8 +2156,8 @@ function migrateRallyConfig(raw) {
1849
2156
  const rawStamps = Array.isArray(source2.stamps) ? source2.stamps : Array.isArray(source2.spots) ? source2.spots : [];
1850
2157
  const rawRewards = Array.isArray(source2.rewards) ? source2.rewards : [];
1851
2158
  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);
2159
+ const title = text2(source2.title);
2160
+ const description = text2(source2.description);
1854
2161
  const theme = isObject2(source2.theme) ? source2.theme : void 0;
1855
2162
  return {
1856
2163
  id,
@@ -2251,9 +2558,9 @@ async function importAesKey(secret) {
2251
2558
  const digest = await cryptoApi2().subtle.digest("SHA-256", webCryptoBytes(secret));
2252
2559
  return cryptoApi2().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
2253
2560
  }
2254
- function isExpired(payload, now) {
2255
- if (typeof payload.exp === "number" && now >= payload.exp * 1e3) return true;
2256
- return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now;
2561
+ function isExpired(payload, now2) {
2562
+ if (typeof payload.exp === "number" && now2 >= payload.exp * 1e3) return true;
2563
+ return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now2;
2257
2564
  }
2258
2565
  async function createSignedSnapshotToken(payload, secretKey) {
2259
2566
  const api = cryptoApi2();
@@ -2273,7 +2580,7 @@ async function createSignedSnapshotToken(payload, secretKey) {
2273
2580
  );
2274
2581
  return `sr2.${body}.${base64Url(signature)}`;
2275
2582
  }
2276
- async function verifySnapshotToken(token, secretKey, now = Date.now()) {
2583
+ async function verifySnapshotToken(token, secretKey, now2 = Date.now()) {
2277
2584
  try {
2278
2585
  const parts = token.split(".");
2279
2586
  if (parts.length !== 3 || parts[0] !== "sr2") {
@@ -2315,7 +2622,7 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
2315
2622
  const payload = JSON.parse(new TextDecoder().decode(payloadText));
2316
2623
  if (typeof payload !== "object" || payload === null || Array.isArray(payload))
2317
2624
  throw new Error("Invalid payload.");
2318
- if (isExpired(payload, now)) {
2625
+ if (isExpired(payload, now2)) {
2319
2626
  return {
2320
2627
  ok: false,
2321
2628
  valid: false,
@@ -2332,6 +2639,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
2332
2639
  }
2333
2640
  }
2334
2641
 
2335
- 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, isAdminRallyConfig, isGeolocationSupported, isNfcSupported, isPublicRallyConfig, isPublicRallyConfigShape, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, migrateRallyConfig, normalizePasscode, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, stripSensitiveConfig, toLocalizedString, toPublicRallyConfig, validateAdminRallyConfig, validatePublicRallyConfig, validateRallyConfig, verifyPasscode, verifySecureToken, verifySnapshotToken };
2642
+ export { CURRENT_RALLY_CONFIG_VERSION, DEFAULT_SHEET_THEME, InMemoryStorage, IndexedDBAdapter, LocalStorageAdapter, StampRallyClient, StorageAdapterError, THEME_PRESETS, UniversalStampRallyClient, calculateDistanceMeters, calculateProgress, consumeReward, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCheckIn, evaluateCondition, evaluateConditionDetailed, exportProgressToken, getCurrentGeoContext, importProgressToken, isAdminRallyConfig, isGeolocationSupported, isNfcSupported, isPublicRallyConfig, isPublicRallyConfigShape, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, migrateRallyConfig, normalizePasscode, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, stripSensitiveConfig, toLocalizedString, toPublicRallyConfig, validateAdminRallyConfig, validatePublicRallyConfig, validateRallyConfig, verifyPasscode, verifySecureToken, verifySnapshotToken };
2336
2643
  //# sourceMappingURL=index.js.map
2337
2644
  //# sourceMappingURL=index.js.map