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