@stamprally/core 0.10.0 → 0.12.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 +458 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +453 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -99,6 +99,47 @@ function issueClaimTicketNumber(reward2, currentState, options = {}) {
|
|
|
99
99
|
return { ...currentState, claimTicketNumber };
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// src/engine/sync.ts
|
|
103
|
+
function latestTimestamp(serverTimestamp, localTimestamp) {
|
|
104
|
+
const serverTime = Date.parse(serverTimestamp);
|
|
105
|
+
const localTime = Date.parse(localTimestamp);
|
|
106
|
+
if (!Number.isNaN(serverTime) && !Number.isNaN(localTime))
|
|
107
|
+
return serverTime >= localTime ? serverTimestamp : localTimestamp;
|
|
108
|
+
if (!Number.isNaN(serverTime)) return serverTimestamp;
|
|
109
|
+
if (!Number.isNaN(localTime)) return localTimestamp;
|
|
110
|
+
return serverTimestamp >= localTimestamp ? serverTimestamp : localTimestamp;
|
|
111
|
+
}
|
|
112
|
+
function mergeRewardStates(serverRewards, localRewards) {
|
|
113
|
+
const merged = new Map(serverRewards.map((reward2) => [reward2.rewardId, reward2]));
|
|
114
|
+
for (const localReward of localRewards) {
|
|
115
|
+
const serverReward = merged.get(localReward.rewardId);
|
|
116
|
+
if (serverReward === void 0) {
|
|
117
|
+
merged.set(localReward.rewardId, localReward);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (localReward.status === "CONSUMED" && serverReward.status !== "CONSUMED")
|
|
121
|
+
merged.set(localReward.rewardId, localReward);
|
|
122
|
+
}
|
|
123
|
+
return [...merged.values()];
|
|
124
|
+
}
|
|
125
|
+
function resolveRallyStateConflict(serverState, localState, options = { policy: "merge" }) {
|
|
126
|
+
if (options.policy === "server_wins") return serverState;
|
|
127
|
+
const records = [...serverState.records];
|
|
128
|
+
const knownStamps = new Set(records.map((record) => record.stampId));
|
|
129
|
+
for (const record of localState.records) {
|
|
130
|
+
if (!knownStamps.has(record.stampId)) {
|
|
131
|
+
records.push(record);
|
|
132
|
+
knownStamps.add(record.stampId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
...serverState,
|
|
137
|
+
records,
|
|
138
|
+
rewards: mergeRewardStates(serverState.rewards, localState.rewards),
|
|
139
|
+
updatedAt: latestTimestamp(serverState.updatedAt, localState.updatedAt)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
102
143
|
// src/detectors/types.ts
|
|
103
144
|
function createDetectorError(detector, code, message, cause) {
|
|
104
145
|
return cause === void 0 ? { detector, code, message } : { detector, code, message, cause };
|
|
@@ -989,6 +1030,7 @@ var StampRallyClient = class {
|
|
|
989
1030
|
#storage;
|
|
990
1031
|
#options;
|
|
991
1032
|
#config;
|
|
1033
|
+
#offlineQueue;
|
|
992
1034
|
#userId;
|
|
993
1035
|
#state = null;
|
|
994
1036
|
#initialization = null;
|
|
@@ -998,6 +1040,8 @@ var StampRallyClient = class {
|
|
|
998
1040
|
this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions } : storageOrOptions;
|
|
999
1041
|
this.#storage = this.#options.storage ?? new InMemoryStorage();
|
|
1000
1042
|
this.#userId = this.#options.userId ?? null;
|
|
1043
|
+
this.#offlineQueue = this.#options.offlineQueue;
|
|
1044
|
+
this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
|
|
1001
1045
|
}
|
|
1002
1046
|
getConfig() {
|
|
1003
1047
|
return this.#config;
|
|
@@ -1008,6 +1052,12 @@ var StampRallyClient = class {
|
|
|
1008
1052
|
getUserId() {
|
|
1009
1053
|
return this.#userId;
|
|
1010
1054
|
}
|
|
1055
|
+
get syncState() {
|
|
1056
|
+
return this.#offlineQueue?.syncState ?? "idle";
|
|
1057
|
+
}
|
|
1058
|
+
get pendingCount() {
|
|
1059
|
+
return this.#offlineQueue?.pendingCount ?? 0;
|
|
1060
|
+
}
|
|
1011
1061
|
subscribe(listener) {
|
|
1012
1062
|
this.#listeners.add(listener);
|
|
1013
1063
|
return () => this.#listeners.delete(listener);
|
|
@@ -1111,8 +1161,21 @@ var StampRallyClient = class {
|
|
|
1111
1161
|
};
|
|
1112
1162
|
const remote = this.#options.syncAdapter?.checkIn;
|
|
1113
1163
|
if (options.sync !== false && remote !== void 0) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1164
|
+
try {
|
|
1165
|
+
const result = await remote(request);
|
|
1166
|
+
return result.ok ? this.#commitCheckIn(result) : this.#fail(result.error);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (this.#offlineQueue === void 0) throw error;
|
|
1169
|
+
await this.#offlineQueue.enqueueCheckIn(request);
|
|
1170
|
+
const record2 = { stampId: spotId, acquiredAt: now };
|
|
1171
|
+
const next2 = this.#reconcile({
|
|
1172
|
+
...current,
|
|
1173
|
+
records: [...current.records, record2],
|
|
1174
|
+
updatedAt: now
|
|
1175
|
+
});
|
|
1176
|
+
await this.#storage.save(next2);
|
|
1177
|
+
return this.#commitCheckIn({ ok: true, value: { state: next2, record: record2 } });
|
|
1178
|
+
}
|
|
1116
1179
|
}
|
|
1117
1180
|
const record = { stampId: spotId, acquiredAt: now };
|
|
1118
1181
|
const next = this.#reconcile({
|
|
@@ -1154,8 +1217,22 @@ var StampRallyClient = class {
|
|
|
1154
1217
|
};
|
|
1155
1218
|
const remote = this.#options.syncAdapter?.claimReward;
|
|
1156
1219
|
if (options.sync !== false && remote !== void 0) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1220
|
+
try {
|
|
1221
|
+
const result = await remote(request);
|
|
1222
|
+
return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
if (this.#offlineQueue === void 0) throw error;
|
|
1225
|
+
await this.#offlineQueue.enqueueClaimReward(request);
|
|
1226
|
+
const next2 = {
|
|
1227
|
+
...current,
|
|
1228
|
+
rewards: current.rewards.map(
|
|
1229
|
+
(item) => item.rewardId === rewardId ? local.value : item
|
|
1230
|
+
),
|
|
1231
|
+
updatedAt: now
|
|
1232
|
+
};
|
|
1233
|
+
await this.#storage.save(next2);
|
|
1234
|
+
return this.#commitClaim({ ok: true, value: { state: next2, reward: local.value } });
|
|
1235
|
+
}
|
|
1159
1236
|
}
|
|
1160
1237
|
const next = {
|
|
1161
1238
|
...current,
|
|
@@ -1169,19 +1246,41 @@ var StampRallyClient = class {
|
|
|
1169
1246
|
sync(adapter = this.#options.syncAdapter) {
|
|
1170
1247
|
return this.#enqueue(async () => {
|
|
1171
1248
|
const current = await this.initialize();
|
|
1249
|
+
if (this.#offlineQueue !== void 0 && adapter !== void 0) {
|
|
1250
|
+
await this.#offlineQueue.sync(async (operation) => {
|
|
1251
|
+
if (operation.kind === "checkIn") {
|
|
1252
|
+
if (adapter.checkIn === void 0)
|
|
1253
|
+
throw new Error("No check-in sync adapter is configured.");
|
|
1254
|
+
return adapter.checkIn(operation.request);
|
|
1255
|
+
}
|
|
1256
|
+
if (adapter.claimReward === void 0)
|
|
1257
|
+
throw new Error("No reward sync adapter is configured.");
|
|
1258
|
+
return adapter.claimReward(operation.request);
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1172
1261
|
if (adapter?.sync === void 0) {
|
|
1173
|
-
this.#emitEvent({ type: "sync", state: current });
|
|
1262
|
+
this.#emitEvent({ type: "sync", state: this.#state ?? current });
|
|
1174
1263
|
return;
|
|
1175
1264
|
}
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1265
|
+
const serverState = await adapter.sync({
|
|
1266
|
+
rallyId: this.#config.id,
|
|
1267
|
+
userId: this.#userId,
|
|
1268
|
+
state: this.#state ?? current
|
|
1269
|
+
});
|
|
1270
|
+
const localState = this.#state ?? current;
|
|
1271
|
+
const merged = this.#offlineQueue === void 0 ? serverState : resolveRallyStateConflict(serverState, localState, {
|
|
1272
|
+
policy: this.#offlineQueue.conflictPolicy
|
|
1273
|
+
});
|
|
1274
|
+
const next = this.#reconcile(merged);
|
|
1179
1275
|
await this.#storage.save(next);
|
|
1180
1276
|
this.#state = next;
|
|
1181
1277
|
this.#emit(next);
|
|
1182
1278
|
this.#emitEvent({ type: "sync", state: next });
|
|
1183
1279
|
});
|
|
1184
1280
|
}
|
|
1281
|
+
retrySync() {
|
|
1282
|
+
return this.sync();
|
|
1283
|
+
}
|
|
1185
1284
|
reset() {
|
|
1186
1285
|
return this.#enqueue(async () => {
|
|
1187
1286
|
await this.#storage.remove(this.#config.id, this.#userId);
|
|
@@ -1232,6 +1331,16 @@ var StampRallyClient = class {
|
|
|
1232
1331
|
)
|
|
1233
1332
|
};
|
|
1234
1333
|
}
|
|
1334
|
+
async #handleOfflineSyncResult(event) {
|
|
1335
|
+
if (event.state !== void 0) {
|
|
1336
|
+
const next = this.#reconcile(event.state);
|
|
1337
|
+
await this.#storage.save(next);
|
|
1338
|
+
this.#state = next;
|
|
1339
|
+
this.#emit(next);
|
|
1340
|
+
}
|
|
1341
|
+
if ("ok" in event.result && !event.result.ok)
|
|
1342
|
+
this.#emitEvent({ type: "error", error: event.result.error });
|
|
1343
|
+
}
|
|
1235
1344
|
#now() {
|
|
1236
1345
|
return this.#options.clock?.() ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1237
1346
|
}
|
|
@@ -1263,6 +1372,214 @@ var StampRallyClient = class {
|
|
|
1263
1372
|
}
|
|
1264
1373
|
};
|
|
1265
1374
|
|
|
1375
|
+
// src/client/offlineQueue.ts
|
|
1376
|
+
var MemoryQueueStorage = class {
|
|
1377
|
+
#values = /* @__PURE__ */ new Map();
|
|
1378
|
+
async load(key) {
|
|
1379
|
+
return this.#values.get(key) ?? [];
|
|
1380
|
+
}
|
|
1381
|
+
async save(key, operations) {
|
|
1382
|
+
this.#values.set(key, structuredClone(operations));
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
var LocalStorageQueueStorage = class {
|
|
1386
|
+
constructor(storage) {
|
|
1387
|
+
this.storage = storage;
|
|
1388
|
+
}
|
|
1389
|
+
storage;
|
|
1390
|
+
async load(key) {
|
|
1391
|
+
const value = this.storage.getItem(key);
|
|
1392
|
+
if (value === null) return [];
|
|
1393
|
+
try {
|
|
1394
|
+
const parsed = JSON.parse(value);
|
|
1395
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1396
|
+
} catch {
|
|
1397
|
+
return [];
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
async save(key, operations) {
|
|
1401
|
+
this.storage.setItem(key, JSON.stringify(operations));
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
var IndexedDBOfflineQueueStorage = class {
|
|
1405
|
+
#providedFactory;
|
|
1406
|
+
#databaseName;
|
|
1407
|
+
#databasePromise = null;
|
|
1408
|
+
constructor(options = {}) {
|
|
1409
|
+
this.#providedFactory = options.indexedDB;
|
|
1410
|
+
this.#databaseName = options.databaseName ?? "stamprally-offline-queue";
|
|
1411
|
+
}
|
|
1412
|
+
async load(key) {
|
|
1413
|
+
const database = await this.#open();
|
|
1414
|
+
return new Promise((resolve, reject) => {
|
|
1415
|
+
const request = database.transaction("operations", "readonly").objectStore("operations").get(key);
|
|
1416
|
+
request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
|
|
1417
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to read offline queue."));
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
async save(key, operations) {
|
|
1421
|
+
const database = await this.#open();
|
|
1422
|
+
return new Promise((resolve, reject) => {
|
|
1423
|
+
const transaction = database.transaction("operations", "readwrite");
|
|
1424
|
+
transaction.objectStore("operations").put(structuredClone(operations), key);
|
|
1425
|
+
transaction.oncomplete = () => resolve();
|
|
1426
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to save offline queue."));
|
|
1427
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("Offline queue write aborted."));
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
#open() {
|
|
1431
|
+
if (this.#databasePromise !== null) return this.#databasePromise;
|
|
1432
|
+
let factory = this.#providedFactory;
|
|
1433
|
+
if (factory === void 0)
|
|
1434
|
+
factory = globalThis.indexedDB;
|
|
1435
|
+
if (factory === void 0 || factory === null)
|
|
1436
|
+
return Promise.reject(new Error("IndexedDB is unavailable in this environment."));
|
|
1437
|
+
this.#databasePromise = new Promise((resolve, reject) => {
|
|
1438
|
+
const request = factory.open(this.#databaseName, 1);
|
|
1439
|
+
request.onupgradeneeded = () => {
|
|
1440
|
+
if (!request.result.objectStoreNames.contains("operations"))
|
|
1441
|
+
request.result.createObjectStore("operations");
|
|
1442
|
+
};
|
|
1443
|
+
request.onsuccess = () => resolve(request.result);
|
|
1444
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to open offline queue."));
|
|
1445
|
+
}).catch((error) => {
|
|
1446
|
+
this.#databasePromise = null;
|
|
1447
|
+
throw error;
|
|
1448
|
+
});
|
|
1449
|
+
return this.#databasePromise;
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
function defaultStorage(databaseName) {
|
|
1453
|
+
try {
|
|
1454
|
+
const indexedDB = globalThis.indexedDB;
|
|
1455
|
+
if (indexedDB !== void 0)
|
|
1456
|
+
return new IndexedDBOfflineQueueStorage({
|
|
1457
|
+
indexedDB,
|
|
1458
|
+
...databaseName === void 0 ? {} : { databaseName }
|
|
1459
|
+
});
|
|
1460
|
+
const storage = globalThis.localStorage;
|
|
1461
|
+
if (storage !== void 0 && storage !== null) return new LocalStorageQueueStorage(storage);
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
return new MemoryQueueStorage();
|
|
1465
|
+
}
|
|
1466
|
+
function operationId(operation) {
|
|
1467
|
+
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}`;
|
|
1468
|
+
}
|
|
1469
|
+
var OfflineQueue = class {
|
|
1470
|
+
#storage;
|
|
1471
|
+
#key;
|
|
1472
|
+
#conflictPolicy;
|
|
1473
|
+
#onSyncConflict;
|
|
1474
|
+
#operations = [];
|
|
1475
|
+
#loaded = false;
|
|
1476
|
+
#state = "idle";
|
|
1477
|
+
#error = null;
|
|
1478
|
+
#sender;
|
|
1479
|
+
#syncPromise = null;
|
|
1480
|
+
#syncResultListener;
|
|
1481
|
+
constructor(options = {}) {
|
|
1482
|
+
if (options.storage !== void 0) this.#storage = options.storage;
|
|
1483
|
+
else if (options.storageLike !== void 0 && options.storageLike !== null)
|
|
1484
|
+
this.#storage = new LocalStorageQueueStorage(options.storageLike);
|
|
1485
|
+
else this.#storage = defaultStorage(options.databaseName);
|
|
1486
|
+
this.#key = options.key ?? "stamprally:offline-queue";
|
|
1487
|
+
this.#conflictPolicy = options.conflictPolicy ?? "server_wins";
|
|
1488
|
+
this.#onSyncConflict = options.onSyncConflict;
|
|
1489
|
+
}
|
|
1490
|
+
get syncState() {
|
|
1491
|
+
return this.#state;
|
|
1492
|
+
}
|
|
1493
|
+
get pendingCount() {
|
|
1494
|
+
return this.#operations.length;
|
|
1495
|
+
}
|
|
1496
|
+
get error() {
|
|
1497
|
+
return this.#error;
|
|
1498
|
+
}
|
|
1499
|
+
get operations() {
|
|
1500
|
+
return this.#operations;
|
|
1501
|
+
}
|
|
1502
|
+
get conflictPolicy() {
|
|
1503
|
+
return this.#conflictPolicy;
|
|
1504
|
+
}
|
|
1505
|
+
setSyncResultListener(listener) {
|
|
1506
|
+
this.#syncResultListener = listener;
|
|
1507
|
+
}
|
|
1508
|
+
async initialize() {
|
|
1509
|
+
if (this.#loaded) return;
|
|
1510
|
+
this.#operations = [...await this.#storage.load(this.#key)];
|
|
1511
|
+
this.#loaded = true;
|
|
1512
|
+
}
|
|
1513
|
+
setSender(sender) {
|
|
1514
|
+
this.#sender = sender;
|
|
1515
|
+
}
|
|
1516
|
+
async enqueue(operation) {
|
|
1517
|
+
await this.initialize();
|
|
1518
|
+
const id2 = operationId(operation);
|
|
1519
|
+
if (this.#operations.some((item) => operationId(item) === id2)) return;
|
|
1520
|
+
this.#operations = [...this.#operations, operation];
|
|
1521
|
+
await this.#storage.save(this.#key, this.#operations);
|
|
1522
|
+
}
|
|
1523
|
+
async enqueueCheckIn(request) {
|
|
1524
|
+
return this.enqueue({ kind: "checkIn", request });
|
|
1525
|
+
}
|
|
1526
|
+
async enqueueClaimReward(request) {
|
|
1527
|
+
return this.enqueue({ kind: "claimReward", request });
|
|
1528
|
+
}
|
|
1529
|
+
async clear() {
|
|
1530
|
+
await this.initialize();
|
|
1531
|
+
this.#operations = [];
|
|
1532
|
+
await this.#storage.save(this.#key, this.#operations);
|
|
1533
|
+
}
|
|
1534
|
+
async sync(sender = this.#sender) {
|
|
1535
|
+
await this.initialize();
|
|
1536
|
+
if (sender === void 0) throw new Error("OfflineQueue.sync requires a sender.");
|
|
1537
|
+
if (this.#syncPromise !== null) return this.#syncPromise;
|
|
1538
|
+
this.#sender = sender;
|
|
1539
|
+
this.#syncPromise = this.#run(sender).finally(() => {
|
|
1540
|
+
this.#syncPromise = null;
|
|
1541
|
+
});
|
|
1542
|
+
return this.#syncPromise;
|
|
1543
|
+
}
|
|
1544
|
+
async retrySync(sender = this.#sender) {
|
|
1545
|
+
return this.sync(sender);
|
|
1546
|
+
}
|
|
1547
|
+
async #run(sender) {
|
|
1548
|
+
this.#state = "syncing";
|
|
1549
|
+
this.#error = null;
|
|
1550
|
+
try {
|
|
1551
|
+
while (this.#operations.length > 0) {
|
|
1552
|
+
const operation = this.#operations[0];
|
|
1553
|
+
if (operation === void 0) break;
|
|
1554
|
+
let result;
|
|
1555
|
+
try {
|
|
1556
|
+
result = await sender(operation);
|
|
1557
|
+
} catch (cause) {
|
|
1558
|
+
throw cause instanceof Error ? cause : new Error(String(cause));
|
|
1559
|
+
}
|
|
1560
|
+
const state = "conflict" in result && result.conflict === true ? await this.resolveConflict(operation, result.localState, result.serverState) : "ok" in result && result.ok ? result.value.state : void 0;
|
|
1561
|
+
await this.#syncResultListener?.({
|
|
1562
|
+
operation,
|
|
1563
|
+
result,
|
|
1564
|
+
...state === void 0 ? {} : { state }
|
|
1565
|
+
});
|
|
1566
|
+
this.#operations = this.#operations.slice(1);
|
|
1567
|
+
await this.#storage.save(this.#key, this.#operations);
|
|
1568
|
+
}
|
|
1569
|
+
this.#state = "idle";
|
|
1570
|
+
} catch (cause) {
|
|
1571
|
+
this.#state = "error";
|
|
1572
|
+
this.#error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1573
|
+
throw this.#error;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
async resolveConflict(operation, localState, serverState) {
|
|
1577
|
+
const configured = this.#onSyncConflict;
|
|
1578
|
+
const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
|
|
1579
|
+
return resolveRallyStateConflict(serverState, localState, { policy });
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1266
1583
|
// src/crypto/token.ts
|
|
1267
1584
|
var encoder = new TextEncoder();
|
|
1268
1585
|
var decoder = new TextDecoder();
|
|
@@ -1395,27 +1712,6 @@ async function decryptPayload(body, secret) {
|
|
|
1395
1712
|
);
|
|
1396
1713
|
}
|
|
1397
1714
|
|
|
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
1715
|
// src/domain/models.ts
|
|
1420
1716
|
var DEFAULT_SHEET_THEME = {
|
|
1421
1717
|
primaryColor: "#9e551e",
|
|
@@ -1460,7 +1756,7 @@ function toPublicConfig(config) {
|
|
|
1460
1756
|
rewards: config.rewards.map(
|
|
1461
1757
|
({ digitalContentUrl: _content, staffPasscode: _passcode, ...reward2 }) => reward2
|
|
1462
1758
|
),
|
|
1463
|
-
...config.metadata === void 0 ? {} : { metadata: config.metadata },
|
|
1759
|
+
...config.publicMetadata !== void 0 ? { metadata: config.publicMetadata } : config.metadata === void 0 ? {} : { metadata: config.metadata },
|
|
1464
1760
|
...config.serverEndpoint === void 0 ? {} : { serverEndpoint: config.serverEndpoint }
|
|
1465
1761
|
};
|
|
1466
1762
|
}
|
|
@@ -1519,6 +1815,118 @@ function isPublicConfig(value) {
|
|
|
1519
1815
|
});
|
|
1520
1816
|
}
|
|
1521
1817
|
|
|
1818
|
+
// src/domain/configTransform.ts
|
|
1819
|
+
var PRIVATE_KEYS = /* @__PURE__ */ new Set([
|
|
1820
|
+
"staffPasscode",
|
|
1821
|
+
"serverMetadata",
|
|
1822
|
+
"inventory",
|
|
1823
|
+
"secretToken",
|
|
1824
|
+
"secretParams",
|
|
1825
|
+
"digitalContentUrl",
|
|
1826
|
+
"code",
|
|
1827
|
+
"tagId"
|
|
1828
|
+
]);
|
|
1829
|
+
var SENSITIVE_KEY_PATTERN = /^(?:api[_-]?key|access[_-]?token|auth[_-]?token|password|private[_-]?key|secret)$/i;
|
|
1830
|
+
function isRecord2(value) {
|
|
1831
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1832
|
+
}
|
|
1833
|
+
function isPrivateKey(key) {
|
|
1834
|
+
return PRIVATE_KEYS.has(key) || SENSITIVE_KEY_PATTERN.test(key);
|
|
1835
|
+
}
|
|
1836
|
+
function sanitizeValue(value, customFilter, seen) {
|
|
1837
|
+
if (Array.isArray(value)) {
|
|
1838
|
+
if (seen.has(value)) return void 0;
|
|
1839
|
+
seen.add(value);
|
|
1840
|
+
const items = value.map((item) => sanitizeValue(item, customFilter, seen)).filter((item) => item !== void 0);
|
|
1841
|
+
return items;
|
|
1842
|
+
}
|
|
1843
|
+
if (!isRecord2(value)) return value;
|
|
1844
|
+
if (seen.has(value)) return void 0;
|
|
1845
|
+
seen.add(value);
|
|
1846
|
+
const result = {};
|
|
1847
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1848
|
+
if (isPrivateKey(key) || customFilter?.(key, item) === false) continue;
|
|
1849
|
+
const sanitized = sanitizeValue(item, customFilter, seen);
|
|
1850
|
+
if (sanitized !== void 0) result[key] = sanitized;
|
|
1851
|
+
}
|
|
1852
|
+
return result;
|
|
1853
|
+
}
|
|
1854
|
+
function sanitizeSpot(spot2, customFilter) {
|
|
1855
|
+
return sanitizeValue(
|
|
1856
|
+
toPublicConfig({
|
|
1857
|
+
id: "__spot__",
|
|
1858
|
+
version: "1",
|
|
1859
|
+
title: "__spot__",
|
|
1860
|
+
spots: [spot2],
|
|
1861
|
+
rewards: []
|
|
1862
|
+
}).spots[0],
|
|
1863
|
+
customFilter,
|
|
1864
|
+
/* @__PURE__ */ new WeakSet()
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
function sanitizeReward(reward2, customFilter) {
|
|
1868
|
+
return sanitizeValue(
|
|
1869
|
+
toPublicConfig({
|
|
1870
|
+
id: "__reward__",
|
|
1871
|
+
version: "1",
|
|
1872
|
+
title: "__reward__",
|
|
1873
|
+
spots: [],
|
|
1874
|
+
rewards: [reward2]
|
|
1875
|
+
}).rewards[0],
|
|
1876
|
+
customFilter,
|
|
1877
|
+
/* @__PURE__ */ new WeakSet()
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
function sanitizeAdminConfig(admin, customFilter) {
|
|
1881
|
+
const publicConfig = toPublicConfig(admin);
|
|
1882
|
+
const sanitized = sanitizeValue(publicConfig, customFilter, /* @__PURE__ */ new WeakSet());
|
|
1883
|
+
sanitized.spots = admin.spots.map((spot2) => sanitizeSpot(spot2, customFilter));
|
|
1884
|
+
sanitized.rewards = admin.rewards.map((reward2) => sanitizeReward(reward2, customFilter));
|
|
1885
|
+
return sanitized;
|
|
1886
|
+
}
|
|
1887
|
+
function validatePublicConfigSafety(publicConfig) {
|
|
1888
|
+
const leakedKeys = [];
|
|
1889
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1890
|
+
const visit = (value, path) => {
|
|
1891
|
+
if (Array.isArray(value)) {
|
|
1892
|
+
value.forEach((item, index) => {
|
|
1893
|
+
visit(item, `${path}[${index}]`);
|
|
1894
|
+
});
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
if (!isRecord2(value) || seen.has(value)) return;
|
|
1898
|
+
seen.add(value);
|
|
1899
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1900
|
+
const nextPath = path === "$" ? key : `${path}.${key}`;
|
|
1901
|
+
if (isPrivateKey(key)) leakedKeys.push(nextPath);
|
|
1902
|
+
visit(item, nextPath);
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
visit(publicConfig, "$");
|
|
1906
|
+
return { safe: leakedKeys.length === 0, leakedKeys };
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// src/domain/i18n.ts
|
|
1910
|
+
function updateLocalizedField(current, locale, newValue) {
|
|
1911
|
+
if (typeof current === "string" || current === void 0)
|
|
1912
|
+
return { [locale]: newValue };
|
|
1913
|
+
return { ...current, [locale]: newValue };
|
|
1914
|
+
}
|
|
1915
|
+
function resolveLocalizedText(text, locale, fallbackLocale) {
|
|
1916
|
+
if (text === void 0 || text === "") return "";
|
|
1917
|
+
if (typeof text === "string") return text;
|
|
1918
|
+
const fallback = fallbackLocale === void 0 ? Object.values(text).find((value) => typeof value === "string") : text[fallbackLocale];
|
|
1919
|
+
return text[locale] || fallback || "";
|
|
1920
|
+
}
|
|
1921
|
+
function toLocalizedString(text) {
|
|
1922
|
+
if (text === void 0) return { ja: "", en: "" };
|
|
1923
|
+
return typeof text === "string" ? { ja: text, en: "" } : {
|
|
1924
|
+
ja: text["ja"] ?? "",
|
|
1925
|
+
en: text["en"] ?? "",
|
|
1926
|
+
...text
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1522
1930
|
// src/domain/themePresets.ts
|
|
1523
1931
|
var THEME_PRESETS = [
|
|
1524
1932
|
{
|
|
@@ -1627,7 +2035,7 @@ var ConfigValidationError = class extends Error {
|
|
|
1627
2035
|
errors;
|
|
1628
2036
|
name = "ConfigValidationError";
|
|
1629
2037
|
};
|
|
1630
|
-
function
|
|
2038
|
+
function isRecord3(value) {
|
|
1631
2039
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1632
2040
|
}
|
|
1633
2041
|
function hasOwn(value, key) {
|
|
@@ -1666,7 +2074,7 @@ function finiteNumber(value, key, path, errors, minimum) {
|
|
|
1666
2074
|
}
|
|
1667
2075
|
function localizedText(value, path, errors) {
|
|
1668
2076
|
if (typeof value === "string") return;
|
|
1669
|
-
if (!
|
|
2077
|
+
if (!isRecord3(value)) {
|
|
1670
2078
|
add(errors, path, "Expected a string or a locale map.", "invalid_localized_text");
|
|
1671
2079
|
return;
|
|
1672
2080
|
}
|
|
@@ -1676,7 +2084,7 @@ function localizedText(value, path, errors) {
|
|
|
1676
2084
|
}
|
|
1677
2085
|
}
|
|
1678
2086
|
function theme(value, path, errors) {
|
|
1679
|
-
if (!
|
|
2087
|
+
if (!isRecord3(value)) {
|
|
1680
2088
|
add(errors, path, "Expected a theme object.", "invalid_type");
|
|
1681
2089
|
return;
|
|
1682
2090
|
}
|
|
@@ -1701,7 +2109,7 @@ function externalReferences(value, path, errors) {
|
|
|
1701
2109
|
}
|
|
1702
2110
|
value.forEach((item, index) => {
|
|
1703
2111
|
const itemPath = `${path}[${index}]`;
|
|
1704
|
-
if (!
|
|
2112
|
+
if (!isRecord3(item)) {
|
|
1705
2113
|
add(errors, itemPath, "Expected an object.", "invalid_type");
|
|
1706
2114
|
return;
|
|
1707
2115
|
}
|
|
@@ -1711,7 +2119,7 @@ function externalReferences(value, path, errors) {
|
|
|
1711
2119
|
});
|
|
1712
2120
|
}
|
|
1713
2121
|
function condition(value, path, errors, isPublic) {
|
|
1714
|
-
if (!
|
|
2122
|
+
if (!isRecord3(value)) {
|
|
1715
2123
|
add(errors, path, "Expected a condition object.", "invalid_type");
|
|
1716
2124
|
return;
|
|
1717
2125
|
}
|
|
@@ -1761,14 +2169,14 @@ function condition(value, path, errors, isPublic) {
|
|
|
1761
2169
|
requiredString(value, "validatorName", path, errors);
|
|
1762
2170
|
if (isPublic && hasOwn(value, "secretParams"))
|
|
1763
2171
|
add(errors, `${path}.secretParams`, "Private field is not allowed.", "private_field");
|
|
1764
|
-
if (!isPublic && hasOwn(value, "secretParams") && !
|
|
2172
|
+
if (!isPublic && hasOwn(value, "secretParams") && !isRecord3(value.secretParams))
|
|
1765
2173
|
add(errors, `${path}.secretParams`, "Expected an object.", "invalid_type");
|
|
1766
2174
|
return;
|
|
1767
2175
|
}
|
|
1768
2176
|
add(errors, `${path}.type`, "Unknown condition type.", "invalid_enum");
|
|
1769
2177
|
}
|
|
1770
2178
|
function unlockCondition(value, path, errors) {
|
|
1771
|
-
if (!
|
|
2179
|
+
if (!isRecord3(value)) {
|
|
1772
2180
|
add(errors, path, "Expected an unlock condition object.", "invalid_type");
|
|
1773
2181
|
return;
|
|
1774
2182
|
}
|
|
@@ -1803,7 +2211,7 @@ function unlockCondition(value, path, errors) {
|
|
|
1803
2211
|
add(errors, `${path}.type`, "Unknown unlock condition type.", "invalid_enum");
|
|
1804
2212
|
}
|
|
1805
2213
|
function spot(value, path, errors, isPublic) {
|
|
1806
|
-
if (!
|
|
2214
|
+
if (!isRecord3(value)) {
|
|
1807
2215
|
add(errors, path, "Expected a spot object.", "invalid_type");
|
|
1808
2216
|
return;
|
|
1809
2217
|
}
|
|
@@ -1840,7 +2248,7 @@ function spot(value, path, errors, isPublic) {
|
|
|
1840
2248
|
});
|
|
1841
2249
|
}
|
|
1842
2250
|
function reward(value, path, errors, isPublic) {
|
|
1843
|
-
if (!
|
|
2251
|
+
if (!isRecord3(value)) {
|
|
1844
2252
|
add(errors, path, "Expected a reward object.", "invalid_type");
|
|
1845
2253
|
return;
|
|
1846
2254
|
}
|
|
@@ -1883,7 +2291,7 @@ function reward(value, path, errors, isPublic) {
|
|
|
1883
2291
|
}
|
|
1884
2292
|
function validate(value, isPublic) {
|
|
1885
2293
|
const errors = [];
|
|
1886
|
-
if (!
|
|
2294
|
+
if (!isRecord3(value)) {
|
|
1887
2295
|
add(errors, "$", "Expected a configuration object.", "invalid_type");
|
|
1888
2296
|
return errors;
|
|
1889
2297
|
}
|
|
@@ -1904,10 +2312,12 @@ function validate(value, isPublic) {
|
|
|
1904
2312
|
});
|
|
1905
2313
|
if (!isPublic) {
|
|
1906
2314
|
optionalString(value, "staffPasscode", "$", errors);
|
|
1907
|
-
if (hasOwn(value, "inventory") && value.inventory !== void 0 && !
|
|
2315
|
+
if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord3(value.inventory))
|
|
1908
2316
|
add(errors, "$.inventory", "Expected an object.", "invalid_type");
|
|
1909
|
-
if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !
|
|
2317
|
+
if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord3(value.serverMetadata))
|
|
1910
2318
|
add(errors, "$.serverMetadata", "Expected an object.", "invalid_type");
|
|
2319
|
+
if (hasOwn(value, "publicMetadata") && value.publicMetadata !== void 0 && !isRecord3(value.publicMetadata))
|
|
2320
|
+
add(errors, "$.publicMetadata", "Expected an object.", "invalid_type");
|
|
1911
2321
|
optionalString(value, "serverEndpoint", "$", errors);
|
|
1912
2322
|
} else {
|
|
1913
2323
|
for (const key of ["staffPasscode", "serverMetadata", "inventory"])
|
|
@@ -2057,9 +2467,12 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
2057
2467
|
|
|
2058
2468
|
exports.ConfigValidationError = ConfigValidationError;
|
|
2059
2469
|
exports.DEFAULT_SHEET_THEME = DEFAULT_SHEET_THEME;
|
|
2470
|
+
exports.InMemoryOfflineQueueStorage = MemoryQueueStorage;
|
|
2060
2471
|
exports.InMemoryStorage = InMemoryStorage;
|
|
2061
2472
|
exports.IndexedDBAdapter = IndexedDBAdapter;
|
|
2473
|
+
exports.IndexedDBOfflineQueueStorage = IndexedDBOfflineQueueStorage;
|
|
2062
2474
|
exports.LocalStorageAdapter = LocalStorageAdapter;
|
|
2475
|
+
exports.OfflineQueue = OfflineQueue;
|
|
2063
2476
|
exports.StampRallyClient = StampRallyClient;
|
|
2064
2477
|
exports.StorageAdapterError = StorageAdapterError;
|
|
2065
2478
|
exports.THEME_PRESETS = THEME_PRESETS;
|
|
@@ -2092,12 +2505,15 @@ exports.readNfcContext = readNfcContext;
|
|
|
2092
2505
|
exports.readQrContext = readQrContext;
|
|
2093
2506
|
exports.reconcileRewardStates = reconcileRewardStates;
|
|
2094
2507
|
exports.resolveLocalizedText = resolveLocalizedText;
|
|
2508
|
+
exports.resolveRallyStateConflict = resolveRallyStateConflict;
|
|
2095
2509
|
exports.safeParseAdminConfig = safeParseAdminConfig;
|
|
2096
2510
|
exports.safeParsePublicConfig = safeParsePublicConfig;
|
|
2511
|
+
exports.sanitizeAdminConfig = sanitizeAdminConfig;
|
|
2097
2512
|
exports.storageKey = storageKey;
|
|
2098
2513
|
exports.toLocalizedString = toLocalizedString;
|
|
2099
2514
|
exports.toPublicConfig = toPublicConfig;
|
|
2100
2515
|
exports.updateLocalizedField = updateLocalizedField;
|
|
2516
|
+
exports.validatePublicConfigSafety = validatePublicConfigSafety;
|
|
2101
2517
|
exports.verifyPasscode = verifyPasscode;
|
|
2102
2518
|
exports.verifySecureToken = verifySecureToken;
|
|
2103
2519
|
exports.verifySnapshotToken = verifySnapshotToken;
|