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