@stamprally/core 0.10.0 → 0.11.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
@@ -987,6 +987,7 @@ var StampRallyClient = class {
987
987
  #storage;
988
988
  #options;
989
989
  #config;
990
+ #offlineQueue;
990
991
  #userId;
991
992
  #state = null;
992
993
  #initialization = null;
@@ -996,6 +997,7 @@ var StampRallyClient = class {
996
997
  this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions } : storageOrOptions;
997
998
  this.#storage = this.#options.storage ?? new InMemoryStorage();
998
999
  this.#userId = this.#options.userId ?? null;
1000
+ this.#offlineQueue = this.#options.offlineQueue;
999
1001
  }
1000
1002
  getConfig() {
1001
1003
  return this.#config;
@@ -1006,6 +1008,12 @@ var StampRallyClient = class {
1006
1008
  getUserId() {
1007
1009
  return this.#userId;
1008
1010
  }
1011
+ get syncState() {
1012
+ return this.#offlineQueue?.syncState ?? "idle";
1013
+ }
1014
+ get pendingCount() {
1015
+ return this.#offlineQueue?.pendingCount ?? 0;
1016
+ }
1009
1017
  subscribe(listener) {
1010
1018
  this.#listeners.add(listener);
1011
1019
  return () => this.#listeners.delete(listener);
@@ -1109,8 +1117,21 @@ var StampRallyClient = class {
1109
1117
  };
1110
1118
  const remote = this.#options.syncAdapter?.checkIn;
1111
1119
  if (options.sync !== false && remote !== void 0) {
1112
- const result = await remote(request);
1113
- return result.ok ? this.#commitCheckIn(result) : this.#fail(result.error);
1120
+ try {
1121
+ const result = await remote(request);
1122
+ return result.ok ? this.#commitCheckIn(result) : this.#fail(result.error);
1123
+ } catch (error) {
1124
+ if (this.#offlineQueue === void 0) throw error;
1125
+ await this.#offlineQueue.enqueueCheckIn(request);
1126
+ const record2 = { stampId: spotId, acquiredAt: now };
1127
+ const next2 = this.#reconcile({
1128
+ ...current,
1129
+ records: [...current.records, record2],
1130
+ updatedAt: now
1131
+ });
1132
+ await this.#storage.save(next2);
1133
+ return this.#commitCheckIn({ ok: true, value: { state: next2, record: record2 } });
1134
+ }
1114
1135
  }
1115
1136
  const record = { stampId: spotId, acquiredAt: now };
1116
1137
  const next = this.#reconcile({
@@ -1152,8 +1173,22 @@ var StampRallyClient = class {
1152
1173
  };
1153
1174
  const remote = this.#options.syncAdapter?.claimReward;
1154
1175
  if (options.sync !== false && remote !== void 0) {
1155
- const result = await remote(request);
1156
- return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
1176
+ try {
1177
+ const result = await remote(request);
1178
+ return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
1179
+ } catch (error) {
1180
+ if (this.#offlineQueue === void 0) throw error;
1181
+ await this.#offlineQueue.enqueueClaimReward(request);
1182
+ const next2 = {
1183
+ ...current,
1184
+ rewards: current.rewards.map(
1185
+ (item) => item.rewardId === rewardId ? local.value : item
1186
+ ),
1187
+ updatedAt: now
1188
+ };
1189
+ await this.#storage.save(next2);
1190
+ return this.#commitClaim({ ok: true, value: { state: next2, reward: local.value } });
1191
+ }
1157
1192
  }
1158
1193
  const next = {
1159
1194
  ...current,
@@ -1167,6 +1202,18 @@ var StampRallyClient = class {
1167
1202
  sync(adapter = this.#options.syncAdapter) {
1168
1203
  return this.#enqueue(async () => {
1169
1204
  const current = await this.initialize();
1205
+ if (this.#offlineQueue !== void 0 && adapter !== void 0) {
1206
+ await this.#offlineQueue.sync(async (operation) => {
1207
+ if (operation.kind === "checkIn") {
1208
+ if (adapter.checkIn === void 0)
1209
+ throw new Error("No check-in sync adapter is configured.");
1210
+ return adapter.checkIn(operation.request);
1211
+ }
1212
+ if (adapter.claimReward === void 0)
1213
+ throw new Error("No reward sync adapter is configured.");
1214
+ return adapter.claimReward(operation.request);
1215
+ });
1216
+ }
1170
1217
  if (adapter?.sync === void 0) {
1171
1218
  this.#emitEvent({ type: "sync", state: current });
1172
1219
  return;
@@ -1180,6 +1227,9 @@ var StampRallyClient = class {
1180
1227
  this.#emitEvent({ type: "sync", state: next });
1181
1228
  });
1182
1229
  }
1230
+ retrySync() {
1231
+ return this.sync();
1232
+ }
1183
1233
  reset() {
1184
1234
  return this.#enqueue(async () => {
1185
1235
  await this.#storage.remove(this.#config.id, this.#userId);
@@ -1261,6 +1311,205 @@ var StampRallyClient = class {
1261
1311
  }
1262
1312
  };
1263
1313
 
1314
+ // src/client/offlineQueue.ts
1315
+ var MemoryQueueStorage = class {
1316
+ #values = /* @__PURE__ */ new Map();
1317
+ async load(key) {
1318
+ return this.#values.get(key) ?? [];
1319
+ }
1320
+ async save(key, operations) {
1321
+ this.#values.set(key, structuredClone(operations));
1322
+ }
1323
+ };
1324
+ var LocalStorageQueueStorage = class {
1325
+ constructor(storage) {
1326
+ this.storage = storage;
1327
+ }
1328
+ storage;
1329
+ async load(key) {
1330
+ const value = this.storage.getItem(key);
1331
+ if (value === null) return [];
1332
+ try {
1333
+ const parsed = JSON.parse(value);
1334
+ return Array.isArray(parsed) ? parsed : [];
1335
+ } catch {
1336
+ return [];
1337
+ }
1338
+ }
1339
+ async save(key, operations) {
1340
+ this.storage.setItem(key, JSON.stringify(operations));
1341
+ }
1342
+ };
1343
+ var IndexedDBOfflineQueueStorage = class {
1344
+ #providedFactory;
1345
+ #databaseName;
1346
+ #databasePromise = null;
1347
+ constructor(options = {}) {
1348
+ this.#providedFactory = options.indexedDB;
1349
+ this.#databaseName = options.databaseName ?? "stamprally-offline-queue";
1350
+ }
1351
+ async load(key) {
1352
+ const database = await this.#open();
1353
+ return new Promise((resolve, reject) => {
1354
+ const request = database.transaction("operations", "readonly").objectStore("operations").get(key);
1355
+ request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
1356
+ request.onerror = () => reject(request.error ?? new Error("Failed to read offline queue."));
1357
+ });
1358
+ }
1359
+ async save(key, operations) {
1360
+ const database = await this.#open();
1361
+ return new Promise((resolve, reject) => {
1362
+ const transaction = database.transaction("operations", "readwrite");
1363
+ transaction.objectStore("operations").put(structuredClone(operations), key);
1364
+ transaction.oncomplete = () => resolve();
1365
+ transaction.onerror = () => reject(transaction.error ?? new Error("Failed to save offline queue."));
1366
+ transaction.onabort = () => reject(transaction.error ?? new Error("Offline queue write aborted."));
1367
+ });
1368
+ }
1369
+ #open() {
1370
+ if (this.#databasePromise !== null) return this.#databasePromise;
1371
+ let factory = this.#providedFactory;
1372
+ if (factory === void 0)
1373
+ factory = globalThis.indexedDB;
1374
+ if (factory === void 0 || factory === null)
1375
+ return Promise.reject(new Error("IndexedDB is unavailable in this environment."));
1376
+ this.#databasePromise = new Promise((resolve, reject) => {
1377
+ const request = factory.open(this.#databaseName, 1);
1378
+ request.onupgradeneeded = () => {
1379
+ if (!request.result.objectStoreNames.contains("operations"))
1380
+ request.result.createObjectStore("operations");
1381
+ };
1382
+ request.onsuccess = () => resolve(request.result);
1383
+ request.onerror = () => reject(request.error ?? new Error("Failed to open offline queue."));
1384
+ }).catch((error) => {
1385
+ this.#databasePromise = null;
1386
+ throw error;
1387
+ });
1388
+ return this.#databasePromise;
1389
+ }
1390
+ };
1391
+ function defaultStorage(databaseName) {
1392
+ try {
1393
+ const indexedDB = globalThis.indexedDB;
1394
+ if (indexedDB !== void 0)
1395
+ return new IndexedDBOfflineQueueStorage({
1396
+ indexedDB,
1397
+ ...databaseName === void 0 ? {} : { databaseName }
1398
+ });
1399
+ const storage = globalThis.localStorage;
1400
+ if (storage !== void 0 && storage !== null) return new LocalStorageQueueStorage(storage);
1401
+ } catch {
1402
+ }
1403
+ return new MemoryQueueStorage();
1404
+ }
1405
+ function operationId(operation) {
1406
+ return operation.kind === "checkIn" ? `checkIn:${operation.request.rallyId}:${operation.request.userId ?? "anonymous"}:${operation.request.idempotencyKey}` : `claimReward:${operation.request.rallyId}:${operation.request.userId ?? "anonymous"}:${operation.request.idempotencyKey}`;
1407
+ }
1408
+ var OfflineQueue = class {
1409
+ #storage;
1410
+ #key;
1411
+ #conflictPolicy;
1412
+ #onSyncConflict;
1413
+ #operations = [];
1414
+ #loaded = false;
1415
+ #state = "idle";
1416
+ #error = null;
1417
+ #sender;
1418
+ #syncPromise = null;
1419
+ constructor(options = {}) {
1420
+ if (options.storage !== void 0) this.#storage = options.storage;
1421
+ else if (options.storageLike !== void 0 && options.storageLike !== null)
1422
+ this.#storage = new LocalStorageQueueStorage(options.storageLike);
1423
+ else this.#storage = defaultStorage(options.databaseName);
1424
+ this.#key = options.key ?? "stamprally:offline-queue";
1425
+ this.#conflictPolicy = options.conflictPolicy ?? "server_wins";
1426
+ this.#onSyncConflict = options.onSyncConflict;
1427
+ }
1428
+ get syncState() {
1429
+ return this.#state;
1430
+ }
1431
+ get pendingCount() {
1432
+ return this.#operations.length;
1433
+ }
1434
+ get error() {
1435
+ return this.#error;
1436
+ }
1437
+ get operations() {
1438
+ return this.#operations;
1439
+ }
1440
+ async initialize() {
1441
+ if (this.#loaded) return;
1442
+ this.#operations = [...await this.#storage.load(this.#key)];
1443
+ this.#loaded = true;
1444
+ }
1445
+ setSender(sender) {
1446
+ this.#sender = sender;
1447
+ }
1448
+ async enqueue(operation) {
1449
+ await this.initialize();
1450
+ const id2 = operationId(operation);
1451
+ if (this.#operations.some((item) => operationId(item) === id2)) return;
1452
+ this.#operations = [...this.#operations, operation];
1453
+ await this.#storage.save(this.#key, this.#operations);
1454
+ }
1455
+ async enqueueCheckIn(request) {
1456
+ return this.enqueue({ kind: "checkIn", request });
1457
+ }
1458
+ async enqueueClaimReward(request) {
1459
+ return this.enqueue({ kind: "claimReward", request });
1460
+ }
1461
+ async clear() {
1462
+ await this.initialize();
1463
+ this.#operations = [];
1464
+ await this.#storage.save(this.#key, this.#operations);
1465
+ }
1466
+ async sync(sender = this.#sender) {
1467
+ await this.initialize();
1468
+ if (sender === void 0) throw new Error("OfflineQueue.sync requires a sender.");
1469
+ if (this.#syncPromise !== null) return this.#syncPromise;
1470
+ this.#sender = sender;
1471
+ this.#syncPromise = this.#run(sender).finally(() => {
1472
+ this.#syncPromise = null;
1473
+ });
1474
+ return this.#syncPromise;
1475
+ }
1476
+ async retrySync(sender = this.#sender) {
1477
+ return this.sync(sender);
1478
+ }
1479
+ async #run(sender) {
1480
+ this.#state = "syncing";
1481
+ this.#error = null;
1482
+ try {
1483
+ while (this.#operations.length > 0) {
1484
+ const operation = this.#operations[0];
1485
+ if (operation === void 0) break;
1486
+ let result;
1487
+ try {
1488
+ result = await sender(operation);
1489
+ } catch (cause) {
1490
+ throw cause instanceof Error ? cause : new Error(String(cause));
1491
+ }
1492
+ if ("conflict" in result && result.conflict === true)
1493
+ await this.resolveConflict(operation, result.localState, result.serverState);
1494
+ this.#operations = this.#operations.slice(1);
1495
+ await this.#storage.save(this.#key, this.#operations);
1496
+ }
1497
+ this.#state = "idle";
1498
+ } catch (cause) {
1499
+ this.#state = "error";
1500
+ this.#error = cause instanceof Error ? cause : new Error(String(cause));
1501
+ throw this.#error;
1502
+ }
1503
+ }
1504
+ async resolveConflict(operation, localState, serverState) {
1505
+ const configured = this.#onSyncConflict;
1506
+ const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
1507
+ if (policy === "merge") {
1508
+ return;
1509
+ }
1510
+ }
1511
+ };
1512
+
1264
1513
  // src/crypto/token.ts
1265
1514
  var encoder = new TextEncoder();
1266
1515
  var decoder = new TextDecoder();
@@ -1393,27 +1642,6 @@ async function decryptPayload(body, secret) {
1393
1642
  );
1394
1643
  }
1395
1644
 
1396
- // src/domain/i18n.ts
1397
- function updateLocalizedField(current, locale, newValue) {
1398
- if (typeof current === "string" || current === void 0)
1399
- return { [locale]: newValue };
1400
- return { ...current, [locale]: newValue };
1401
- }
1402
- function resolveLocalizedText(text, locale, fallbackLocale) {
1403
- if (text === void 0 || text === "") return "";
1404
- if (typeof text === "string") return text;
1405
- const fallback = fallbackLocale === void 0 ? Object.values(text).find((value) => typeof value === "string") : text[fallbackLocale];
1406
- return text[locale] || fallback || "";
1407
- }
1408
- function toLocalizedString(text) {
1409
- if (text === void 0) return { ja: "", en: "" };
1410
- return typeof text === "string" ? { ja: text, en: "" } : {
1411
- ja: text["ja"] ?? "",
1412
- en: text["en"] ?? "",
1413
- ...text
1414
- };
1415
- }
1416
-
1417
1645
  // src/domain/models.ts
1418
1646
  var DEFAULT_SHEET_THEME = {
1419
1647
  primaryColor: "#9e551e",
@@ -1458,7 +1686,7 @@ function toPublicConfig(config) {
1458
1686
  rewards: config.rewards.map(
1459
1687
  ({ digitalContentUrl: _content, staffPasscode: _passcode, ...reward2 }) => reward2
1460
1688
  ),
1461
- ...config.metadata === void 0 ? {} : { metadata: config.metadata },
1689
+ ...config.publicMetadata !== void 0 ? { metadata: config.publicMetadata } : config.metadata === void 0 ? {} : { metadata: config.metadata },
1462
1690
  ...config.serverEndpoint === void 0 ? {} : { serverEndpoint: config.serverEndpoint }
1463
1691
  };
1464
1692
  }
@@ -1517,6 +1745,118 @@ function isPublicConfig(value) {
1517
1745
  });
1518
1746
  }
1519
1747
 
1748
+ // src/domain/configTransform.ts
1749
+ var PRIVATE_KEYS = /* @__PURE__ */ new Set([
1750
+ "staffPasscode",
1751
+ "serverMetadata",
1752
+ "inventory",
1753
+ "secretToken",
1754
+ "secretParams",
1755
+ "digitalContentUrl",
1756
+ "code",
1757
+ "tagId"
1758
+ ]);
1759
+ var SENSITIVE_KEY_PATTERN = /^(?:api[_-]?key|access[_-]?token|auth[_-]?token|password|private[_-]?key|secret)$/i;
1760
+ function isRecord2(value) {
1761
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1762
+ }
1763
+ function isPrivateKey(key) {
1764
+ return PRIVATE_KEYS.has(key) || SENSITIVE_KEY_PATTERN.test(key);
1765
+ }
1766
+ function sanitizeValue(value, customFilter, seen) {
1767
+ if (Array.isArray(value)) {
1768
+ if (seen.has(value)) return void 0;
1769
+ seen.add(value);
1770
+ const items = value.map((item) => sanitizeValue(item, customFilter, seen)).filter((item) => item !== void 0);
1771
+ return items;
1772
+ }
1773
+ if (!isRecord2(value)) return value;
1774
+ if (seen.has(value)) return void 0;
1775
+ seen.add(value);
1776
+ const result = {};
1777
+ for (const [key, item] of Object.entries(value)) {
1778
+ if (isPrivateKey(key) || customFilter?.(key, item) === false) continue;
1779
+ const sanitized = sanitizeValue(item, customFilter, seen);
1780
+ if (sanitized !== void 0) result[key] = sanitized;
1781
+ }
1782
+ return result;
1783
+ }
1784
+ function sanitizeSpot(spot2, customFilter) {
1785
+ return sanitizeValue(
1786
+ toPublicConfig({
1787
+ id: "__spot__",
1788
+ version: "1",
1789
+ title: "__spot__",
1790
+ spots: [spot2],
1791
+ rewards: []
1792
+ }).spots[0],
1793
+ customFilter,
1794
+ /* @__PURE__ */ new WeakSet()
1795
+ );
1796
+ }
1797
+ function sanitizeReward(reward2, customFilter) {
1798
+ return sanitizeValue(
1799
+ toPublicConfig({
1800
+ id: "__reward__",
1801
+ version: "1",
1802
+ title: "__reward__",
1803
+ spots: [],
1804
+ rewards: [reward2]
1805
+ }).rewards[0],
1806
+ customFilter,
1807
+ /* @__PURE__ */ new WeakSet()
1808
+ );
1809
+ }
1810
+ function sanitizeAdminConfig(admin, customFilter) {
1811
+ const publicConfig = toPublicConfig(admin);
1812
+ const sanitized = sanitizeValue(publicConfig, customFilter, /* @__PURE__ */ new WeakSet());
1813
+ sanitized.spots = admin.spots.map((spot2) => sanitizeSpot(spot2, customFilter));
1814
+ sanitized.rewards = admin.rewards.map((reward2) => sanitizeReward(reward2, customFilter));
1815
+ return sanitized;
1816
+ }
1817
+ function validatePublicConfigSafety(publicConfig) {
1818
+ const leakedKeys = [];
1819
+ const seen = /* @__PURE__ */ new WeakSet();
1820
+ const visit = (value, path) => {
1821
+ if (Array.isArray(value)) {
1822
+ value.forEach((item, index) => {
1823
+ visit(item, `${path}[${index}]`);
1824
+ });
1825
+ return;
1826
+ }
1827
+ if (!isRecord2(value) || seen.has(value)) return;
1828
+ seen.add(value);
1829
+ for (const [key, item] of Object.entries(value)) {
1830
+ const nextPath = path === "$" ? key : `${path}.${key}`;
1831
+ if (isPrivateKey(key)) leakedKeys.push(nextPath);
1832
+ visit(item, nextPath);
1833
+ }
1834
+ };
1835
+ visit(publicConfig, "$");
1836
+ return { safe: leakedKeys.length === 0, leakedKeys };
1837
+ }
1838
+
1839
+ // src/domain/i18n.ts
1840
+ function updateLocalizedField(current, locale, newValue) {
1841
+ if (typeof current === "string" || current === void 0)
1842
+ return { [locale]: newValue };
1843
+ return { ...current, [locale]: newValue };
1844
+ }
1845
+ function resolveLocalizedText(text, locale, fallbackLocale) {
1846
+ if (text === void 0 || text === "") return "";
1847
+ if (typeof text === "string") return text;
1848
+ const fallback = fallbackLocale === void 0 ? Object.values(text).find((value) => typeof value === "string") : text[fallbackLocale];
1849
+ return text[locale] || fallback || "";
1850
+ }
1851
+ function toLocalizedString(text) {
1852
+ if (text === void 0) return { ja: "", en: "" };
1853
+ return typeof text === "string" ? { ja: text, en: "" } : {
1854
+ ja: text["ja"] ?? "",
1855
+ en: text["en"] ?? "",
1856
+ ...text
1857
+ };
1858
+ }
1859
+
1520
1860
  // src/domain/themePresets.ts
1521
1861
  var THEME_PRESETS = [
1522
1862
  {
@@ -1625,7 +1965,7 @@ var ConfigValidationError = class extends Error {
1625
1965
  errors;
1626
1966
  name = "ConfigValidationError";
1627
1967
  };
1628
- function isRecord2(value) {
1968
+ function isRecord3(value) {
1629
1969
  return typeof value === "object" && value !== null && !Array.isArray(value);
1630
1970
  }
1631
1971
  function hasOwn(value, key) {
@@ -1664,7 +2004,7 @@ function finiteNumber(value, key, path, errors, minimum) {
1664
2004
  }
1665
2005
  function localizedText(value, path, errors) {
1666
2006
  if (typeof value === "string") return;
1667
- if (!isRecord2(value)) {
2007
+ if (!isRecord3(value)) {
1668
2008
  add(errors, path, "Expected a string or a locale map.", "invalid_localized_text");
1669
2009
  return;
1670
2010
  }
@@ -1674,7 +2014,7 @@ function localizedText(value, path, errors) {
1674
2014
  }
1675
2015
  }
1676
2016
  function theme(value, path, errors) {
1677
- if (!isRecord2(value)) {
2017
+ if (!isRecord3(value)) {
1678
2018
  add(errors, path, "Expected a theme object.", "invalid_type");
1679
2019
  return;
1680
2020
  }
@@ -1699,7 +2039,7 @@ function externalReferences(value, path, errors) {
1699
2039
  }
1700
2040
  value.forEach((item, index) => {
1701
2041
  const itemPath = `${path}[${index}]`;
1702
- if (!isRecord2(item)) {
2042
+ if (!isRecord3(item)) {
1703
2043
  add(errors, itemPath, "Expected an object.", "invalid_type");
1704
2044
  return;
1705
2045
  }
@@ -1709,7 +2049,7 @@ function externalReferences(value, path, errors) {
1709
2049
  });
1710
2050
  }
1711
2051
  function condition(value, path, errors, isPublic) {
1712
- if (!isRecord2(value)) {
2052
+ if (!isRecord3(value)) {
1713
2053
  add(errors, path, "Expected a condition object.", "invalid_type");
1714
2054
  return;
1715
2055
  }
@@ -1759,14 +2099,14 @@ function condition(value, path, errors, isPublic) {
1759
2099
  requiredString(value, "validatorName", path, errors);
1760
2100
  if (isPublic && hasOwn(value, "secretParams"))
1761
2101
  add(errors, `${path}.secretParams`, "Private field is not allowed.", "private_field");
1762
- if (!isPublic && hasOwn(value, "secretParams") && !isRecord2(value.secretParams))
2102
+ if (!isPublic && hasOwn(value, "secretParams") && !isRecord3(value.secretParams))
1763
2103
  add(errors, `${path}.secretParams`, "Expected an object.", "invalid_type");
1764
2104
  return;
1765
2105
  }
1766
2106
  add(errors, `${path}.type`, "Unknown condition type.", "invalid_enum");
1767
2107
  }
1768
2108
  function unlockCondition(value, path, errors) {
1769
- if (!isRecord2(value)) {
2109
+ if (!isRecord3(value)) {
1770
2110
  add(errors, path, "Expected an unlock condition object.", "invalid_type");
1771
2111
  return;
1772
2112
  }
@@ -1801,7 +2141,7 @@ function unlockCondition(value, path, errors) {
1801
2141
  add(errors, `${path}.type`, "Unknown unlock condition type.", "invalid_enum");
1802
2142
  }
1803
2143
  function spot(value, path, errors, isPublic) {
1804
- if (!isRecord2(value)) {
2144
+ if (!isRecord3(value)) {
1805
2145
  add(errors, path, "Expected a spot object.", "invalid_type");
1806
2146
  return;
1807
2147
  }
@@ -1838,7 +2178,7 @@ function spot(value, path, errors, isPublic) {
1838
2178
  });
1839
2179
  }
1840
2180
  function reward(value, path, errors, isPublic) {
1841
- if (!isRecord2(value)) {
2181
+ if (!isRecord3(value)) {
1842
2182
  add(errors, path, "Expected a reward object.", "invalid_type");
1843
2183
  return;
1844
2184
  }
@@ -1881,7 +2221,7 @@ function reward(value, path, errors, isPublic) {
1881
2221
  }
1882
2222
  function validate(value, isPublic) {
1883
2223
  const errors = [];
1884
- if (!isRecord2(value)) {
2224
+ if (!isRecord3(value)) {
1885
2225
  add(errors, "$", "Expected a configuration object.", "invalid_type");
1886
2226
  return errors;
1887
2227
  }
@@ -1902,10 +2242,12 @@ function validate(value, isPublic) {
1902
2242
  });
1903
2243
  if (!isPublic) {
1904
2244
  optionalString(value, "staffPasscode", "$", errors);
1905
- if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord2(value.inventory))
2245
+ if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord3(value.inventory))
1906
2246
  add(errors, "$.inventory", "Expected an object.", "invalid_type");
1907
- if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord2(value.serverMetadata))
2247
+ if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord3(value.serverMetadata))
1908
2248
  add(errors, "$.serverMetadata", "Expected an object.", "invalid_type");
2249
+ if (hasOwn(value, "publicMetadata") && value.publicMetadata !== void 0 && !isRecord3(value.publicMetadata))
2250
+ add(errors, "$.publicMetadata", "Expected an object.", "invalid_type");
1909
2251
  optionalString(value, "serverEndpoint", "$", errors);
1910
2252
  } else {
1911
2253
  for (const key of ["staffPasscode", "serverMetadata", "inventory"])
@@ -2053,6 +2395,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
2053
2395
  }
2054
2396
  }
2055
2397
 
2056
- export { ConfigValidationError, DEFAULT_SHEET_THEME, InMemoryStorage, IndexedDBAdapter, LocalStorageAdapter, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, exportProgressToken, getCurrentGeoContext, getOrderedSpots, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, safeParseAdminConfig, safeParsePublicConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, verifyPasscode, verifySecureToken, verifySnapshotToken };
2398
+ export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, exportProgressToken, getCurrentGeoContext, getOrderedSpots, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, safeParseAdminConfig, safeParsePublicConfig, sanitizeAdminConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, validatePublicConfigSafety, verifyPasscode, verifySecureToken, verifySnapshotToken };
2057
2399
  //# sourceMappingURL=index.js.map
2058
2400
  //# sourceMappingURL=index.js.map