@stamprally/core 0.9.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/README.md +10 -143
- package/dist/index.cjs +779 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -1
- package/dist/index.d.ts +114 -1
- package/dist/index.js +769 -104
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -12,34 +12,34 @@ function calculateDistanceMeters(aLat, aLon, bLat, bLon) {
|
|
|
12
12
|
function mismatch(conditionType, reason, extra = {}) {
|
|
13
13
|
return { ok: false, error: { code: "CONDITION_MISMATCH", conditionType, reason, ...extra } };
|
|
14
14
|
}
|
|
15
|
-
function evaluateConditionDetailed(
|
|
16
|
-
switch (
|
|
15
|
+
function evaluateConditionDetailed(condition2, context) {
|
|
16
|
+
switch (condition2.type) {
|
|
17
17
|
case "qr":
|
|
18
|
-
return context.type === "qr" && context.token ===
|
|
18
|
+
return context.type === "qr" && context.token === condition2.secretToken ? { ok: true, value: { conditionType: "qr" } } : mismatch("qr", "INVALID_PROOF");
|
|
19
19
|
case "passcode":
|
|
20
|
-
return context.type === "passcode" && (
|
|
20
|
+
return context.type === "passcode" && (condition2.caseSensitive === false ? context.code.toLocaleLowerCase() === condition2.code.toLocaleLowerCase() : context.code === condition2.code) ? { ok: true, value: { conditionType: "passcode" } } : mismatch("passcode", "INVALID_PROOF");
|
|
21
21
|
case "nfc":
|
|
22
|
-
return context.type === "nfc" && context.tagId ===
|
|
22
|
+
return context.type === "nfc" && context.tagId === condition2.tagId ? { ok: true, value: { conditionType: "nfc" } } : mismatch("nfc", "INVALID_PROOF");
|
|
23
23
|
case "custom":
|
|
24
24
|
return mismatch("custom", "VALIDATOR_FAILED");
|
|
25
25
|
case "gps": {
|
|
26
|
-
if (!Number.isFinite(
|
|
26
|
+
if (!Number.isFinite(condition2.latitude) || !Number.isFinite(condition2.longitude) || !Number.isFinite(condition2.radiusMeters) || condition2.radiusMeters < 0 || context.type !== "gps" || !Number.isFinite(context.latitude) || !Number.isFinite(context.longitude))
|
|
27
27
|
return mismatch("gps", "INVALID_GEO_INPUT");
|
|
28
28
|
const distanceMeters = calculateDistanceMeters(
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
condition2.latitude,
|
|
30
|
+
condition2.longitude,
|
|
31
31
|
context.latitude,
|
|
32
32
|
context.longitude
|
|
33
33
|
);
|
|
34
|
-
return distanceMeters <=
|
|
34
|
+
return distanceMeters <= condition2.radiusMeters ? { ok: true, value: { conditionType: "gps", distanceMeters } } : mismatch("gps", "OUTSIDE_RADIUS", {
|
|
35
35
|
distanceMeters,
|
|
36
|
-
radiusMeters:
|
|
36
|
+
radiusMeters: condition2.radiusMeters
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
function evaluateCondition(
|
|
42
|
-
return evaluateConditionDetailed(
|
|
41
|
+
function evaluateCondition(condition2, context) {
|
|
42
|
+
return evaluateConditionDetailed(condition2, context).ok;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// src/engine/order.ts
|
|
@@ -49,11 +49,11 @@ function getOrderedSpots(spots) {
|
|
|
49
49
|
|
|
50
50
|
// src/engine/progress.ts
|
|
51
51
|
function calculateProgress(state, config) {
|
|
52
|
-
const ids = new Set(config.spots.map((
|
|
52
|
+
const ids = new Set(config.spots.map((spot2) => spot2.id));
|
|
53
53
|
const acquired = new Set(
|
|
54
54
|
state.records.map((record) => record.stampId).filter((id2) => ids.has(id2))
|
|
55
55
|
);
|
|
56
|
-
const remaining = config.spots.filter((
|
|
56
|
+
const remaining = config.spots.filter((spot2) => !acquired.has(spot2.id));
|
|
57
57
|
return {
|
|
58
58
|
acquired: acquired.size,
|
|
59
59
|
total: config.spots.length,
|
|
@@ -93,9 +93,9 @@ function createUniqueClaimTicketNumber(rewardId, issuedAt) {
|
|
|
93
93
|
const timestampPart = Number.isNaN(timestamp) ? Date.now() : timestamp;
|
|
94
94
|
return `CLAIM-${rewardId}-${timestampPart}-${createRandomHash()}`;
|
|
95
95
|
}
|
|
96
|
-
function issueClaimTicketNumber(
|
|
96
|
+
function issueClaimTicketNumber(reward2, currentState, options = {}) {
|
|
97
97
|
if (currentState.claimTicketNumber !== void 0) return currentState;
|
|
98
|
-
const claimTicketNumber = createClaimTicketNumber(
|
|
98
|
+
const claimTicketNumber = createClaimTicketNumber(reward2.id, options);
|
|
99
99
|
return { ...currentState, claimTicketNumber };
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -303,8 +303,8 @@ function normalizePasscode(input, caseSensitive = false) {
|
|
|
303
303
|
const normalized = input.normalize("NFKC").trim();
|
|
304
304
|
return caseSensitive ? normalized : normalized.toUpperCase();
|
|
305
305
|
}
|
|
306
|
-
function verifyPasscode(inputCode,
|
|
307
|
-
return normalizePasscode(inputCode,
|
|
306
|
+
function verifyPasscode(inputCode, condition2) {
|
|
307
|
+
return normalizePasscode(inputCode, condition2.caseSensitive) === normalizePasscode(condition2.code, condition2.caseSensitive) ? { success: true } : { success: false, message: "The passcode is invalid." };
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
// src/detectors/qr.ts
|
|
@@ -431,71 +431,71 @@ async function readQrContext(videoElement, options = {}) {
|
|
|
431
431
|
// src/engine/transition.ts
|
|
432
432
|
function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now) {
|
|
433
433
|
const states = new Map(currentStates.map((state) => [state.rewardId, state]));
|
|
434
|
-
return rewards.map((
|
|
435
|
-
const current = states.get(
|
|
434
|
+
return rewards.map((reward2) => {
|
|
435
|
+
const current = states.get(reward2.id);
|
|
436
436
|
if (current?.status === "CONSUMED" || current?.status === "EXPIRED") return current;
|
|
437
|
-
if (
|
|
438
|
-
return { rewardId:
|
|
439
|
-
if (
|
|
440
|
-
return { rewardId:
|
|
441
|
-
if (acquiredStampCount >=
|
|
437
|
+
if (reward2.validUntil !== void 0 && Date.parse(reward2.validUntil) <= Date.parse(now))
|
|
438
|
+
return { rewardId: reward2.id, status: "EXPIRED" };
|
|
439
|
+
if (reward2.stockLimit !== void 0 && (current?.redeemedCount ?? 0) >= reward2.stockLimit)
|
|
440
|
+
return { rewardId: reward2.id, status: "EXPIRED" };
|
|
441
|
+
if (acquiredStampCount >= reward2.requiredStampCount)
|
|
442
442
|
return {
|
|
443
|
-
rewardId:
|
|
443
|
+
rewardId: reward2.id,
|
|
444
444
|
status: "AVAILABLE",
|
|
445
445
|
...current?.unlockedAt === void 0 ? { unlockedAt: now } : { unlockedAt: current.unlockedAt }
|
|
446
446
|
};
|
|
447
|
-
return { rewardId:
|
|
447
|
+
return { rewardId: reward2.id, status: "LOCKED" };
|
|
448
448
|
});
|
|
449
449
|
}
|
|
450
450
|
function consumeReward(params) {
|
|
451
|
-
const { reward, currentState } = params;
|
|
451
|
+
const { reward: reward2, currentState } = params;
|
|
452
452
|
if (currentState.status === "CONSUMED")
|
|
453
|
-
return { ok: false, error: { code: "ALREADY_CONSUMED", rewardId:
|
|
453
|
+
return { ok: false, error: { code: "ALREADY_CONSUMED", rewardId: reward2.id } };
|
|
454
454
|
if (currentState.status !== "AVAILABLE")
|
|
455
|
-
return { ok: false, error: { code: "NOT_AVAILABLE", rewardId:
|
|
456
|
-
if (
|
|
457
|
-
return { ok: false, error: { code: "EXPIRED", rewardId:
|
|
458
|
-
if (
|
|
459
|
-
return { ok: false, error: { code: "OUT_OF_STOCK", rewardId:
|
|
460
|
-
if (
|
|
461
|
-
return { ok: false, error: { code: "USER_LIMIT_REACHED", rewardId:
|
|
462
|
-
if (
|
|
463
|
-
if (
|
|
455
|
+
return { ok: false, error: { code: "NOT_AVAILABLE", rewardId: reward2.id } };
|
|
456
|
+
if (reward2.validUntil !== void 0 && Date.parse(reward2.validUntil) <= Date.parse(params.now))
|
|
457
|
+
return { ok: false, error: { code: "EXPIRED", rewardId: reward2.id } };
|
|
458
|
+
if (reward2.stockLimit !== void 0 && (currentState.redeemedCount ?? 0) >= reward2.stockLimit)
|
|
459
|
+
return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward2.id } };
|
|
460
|
+
if (reward2.userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= reward2.userClaimLimit)
|
|
461
|
+
return { ok: false, error: { code: "USER_LIMIT_REACHED", rewardId: reward2.id } };
|
|
462
|
+
if (reward2.redemptionMethod === "staff_passcode") {
|
|
463
|
+
if (reward2.staffPasscode === void 0 || !verifyPasscode(params.inputPasscode ?? "", { code: reward2.staffPasscode }).success)
|
|
464
464
|
return {
|
|
465
465
|
ok: false,
|
|
466
466
|
error: {
|
|
467
467
|
code: "INVALID_PASSCODE",
|
|
468
|
-
rewardId:
|
|
468
|
+
rewardId: reward2.id,
|
|
469
469
|
message: "The passcode is invalid."
|
|
470
470
|
}
|
|
471
471
|
};
|
|
472
472
|
}
|
|
473
|
-
if (
|
|
473
|
+
if (reward2.redemptionMethod === "view_only") return { ok: true, value: currentState };
|
|
474
474
|
return {
|
|
475
475
|
ok: true,
|
|
476
476
|
value: {
|
|
477
477
|
...currentState,
|
|
478
478
|
status: "CONSUMED",
|
|
479
479
|
consumedAt: params.now,
|
|
480
|
-
claimTicketNumber: createUniqueClaimTicketNumber(
|
|
480
|
+
claimTicketNumber: createUniqueClaimTicketNumber(reward2.id, params.now),
|
|
481
481
|
redeemedCount: (currentState.redeemedCount ?? 0) + 1,
|
|
482
482
|
...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
|
|
483
483
|
}
|
|
484
484
|
};
|
|
485
485
|
}
|
|
486
486
|
function processStamp(state, config, spotId, context, now) {
|
|
487
|
-
const
|
|
488
|
-
if (
|
|
487
|
+
const spot2 = config.spots.find((item) => item.id === spotId);
|
|
488
|
+
if (spot2 === void 0) return { ok: false, error: { code: "SPOT_NOT_FOUND", spotId } };
|
|
489
489
|
if (state.records.some((record2) => record2.stampId === spotId))
|
|
490
490
|
return { ok: false, error: { code: "STAMP_ALREADY_ACQUIRED", spotId } };
|
|
491
491
|
const acquired = new Set(state.records.map((record2) => record2.stampId));
|
|
492
|
-
if (
|
|
492
|
+
if (spot2.prerequisites?.some((id2) => !acquired.has(id2)))
|
|
493
493
|
return { ok: false, error: { code: "PREREQUISITES_NOT_MET", spotId } };
|
|
494
|
-
for (const
|
|
495
|
-
if (
|
|
494
|
+
for (const condition2 of spot2.conditions) {
|
|
495
|
+
if (condition2.type === "custom" || !evaluateConditionDetailed(condition2, context).ok)
|
|
496
496
|
return {
|
|
497
497
|
ok: false,
|
|
498
|
-
error:
|
|
498
|
+
error: condition2.type === "custom" ? {
|
|
499
499
|
code: "CUSTOM_VALIDATION_FAILED",
|
|
500
500
|
spotId,
|
|
501
501
|
message: "Custom validation requires an async validator."
|
|
@@ -959,18 +959,18 @@ function proof(value) {
|
|
|
959
959
|
}
|
|
960
960
|
return "";
|
|
961
961
|
}
|
|
962
|
-
function matches(
|
|
963
|
-
if (
|
|
962
|
+
function matches(condition2, value) {
|
|
963
|
+
if (condition2.type === "gps") {
|
|
964
964
|
if (typeof value !== "object" || value === null) return false;
|
|
965
965
|
const item = value;
|
|
966
966
|
const latitude = item.latitude;
|
|
967
967
|
const longitude = item.longitude;
|
|
968
968
|
if (typeof latitude !== "number" || typeof longitude !== "number") return false;
|
|
969
969
|
const radians = (v) => v * Math.PI / 180;
|
|
970
|
-
const dLat = radians(latitude -
|
|
971
|
-
const dLon = radians(longitude -
|
|
972
|
-
const h = Math.sin(dLat / 2) ** 2 + Math.cos(radians(
|
|
973
|
-
return 6371e3 * 2 * Math.asin(Math.sqrt(Math.min(1, h))) <=
|
|
970
|
+
const dLat = radians(latitude - condition2.latitude);
|
|
971
|
+
const dLon = radians(longitude - condition2.longitude);
|
|
972
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(radians(condition2.latitude)) * Math.cos(radians(latitude)) * Math.sin(dLon / 2) ** 2;
|
|
973
|
+
return 6371e3 * 2 * Math.asin(Math.sqrt(Math.min(1, h))) <= condition2.radiusMeters;
|
|
974
974
|
}
|
|
975
975
|
return proof(value).trim() !== "";
|
|
976
976
|
}
|
|
@@ -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);
|
|
@@ -1057,8 +1065,8 @@ var StampRallyClient = class {
|
|
|
1057
1065
|
checkIn(spotId, proofData, options = {}) {
|
|
1058
1066
|
return this.#enqueue(async () => {
|
|
1059
1067
|
const current = await this.initialize();
|
|
1060
|
-
const
|
|
1061
|
-
if (
|
|
1068
|
+
const spot2 = this.#config.spots.find((item) => item.id === spotId);
|
|
1069
|
+
if (spot2 === void 0)
|
|
1062
1070
|
return this.#fail({ code: "SPOT_NOT_FOUND", spotId, message: "Spot was not found." });
|
|
1063
1071
|
if (current.records.some((record2) => record2.stampId === spotId))
|
|
1064
1072
|
return this.#fail({
|
|
@@ -1067,15 +1075,15 @@ var StampRallyClient = class {
|
|
|
1067
1075
|
message: "Spot was already claimed."
|
|
1068
1076
|
});
|
|
1069
1077
|
const acquired = new Set(current.records.map((record2) => record2.stampId));
|
|
1070
|
-
if (
|
|
1078
|
+
if (spot2.prerequisites?.some((id2) => !acquired.has(id2)))
|
|
1071
1079
|
return this.#fail({
|
|
1072
1080
|
code: "PREREQUISITES_NOT_MET",
|
|
1073
1081
|
spotId,
|
|
1074
1082
|
message: "Prerequisite spots are not complete."
|
|
1075
1083
|
});
|
|
1076
|
-
for (const
|
|
1077
|
-
if (
|
|
1078
|
-
const validator = this.#options.customValidators?.[
|
|
1084
|
+
for (const condition2 of spot2.conditions) {
|
|
1085
|
+
if (condition2.type === "custom") {
|
|
1086
|
+
const validator = this.#options.customValidators?.[condition2.validatorName] ?? this.#options.customValidator;
|
|
1079
1087
|
if (validator === void 0)
|
|
1080
1088
|
return this.#fail({
|
|
1081
1089
|
code: "CUSTOM_VALIDATION_FAILED",
|
|
@@ -1086,7 +1094,7 @@ var StampRallyClient = class {
|
|
|
1086
1094
|
rallyId: this.#config.id,
|
|
1087
1095
|
spotId,
|
|
1088
1096
|
proofData,
|
|
1089
|
-
condition: { type: "custom", validatorName:
|
|
1097
|
+
condition: { type: "custom", validatorName: condition2.validatorName },
|
|
1090
1098
|
userState: current
|
|
1091
1099
|
};
|
|
1092
1100
|
const result = typeof validator === "function" ? await validator(context) : await validator.validate(context);
|
|
@@ -1096,7 +1104,7 @@ var StampRallyClient = class {
|
|
|
1096
1104
|
spotId,
|
|
1097
1105
|
message: typeof result === "object" && result.message !== void 0 ? result.message : "Custom validation failed."
|
|
1098
1106
|
});
|
|
1099
|
-
} else if (!matches(
|
|
1107
|
+
} else if (!matches(condition2, proofData))
|
|
1100
1108
|
return this.#fail({ code: "INVALID_PROOF", spotId, message: "Verification failed." });
|
|
1101
1109
|
}
|
|
1102
1110
|
const now = options.now ?? this.#now();
|
|
@@ -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
|
-
|
|
1115
|
-
|
|
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
|
-
|
|
1158
|
-
|
|
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);
|
|
@@ -1216,7 +1266,7 @@ var StampRallyClient = class {
|
|
|
1216
1266
|
return Promise.resolve(next);
|
|
1217
1267
|
}
|
|
1218
1268
|
#reconcile(state) {
|
|
1219
|
-
const ids = new Set(this.#config.spots.map((
|
|
1269
|
+
const ids = new Set(this.#config.spots.map((spot2) => spot2.id));
|
|
1220
1270
|
const records = state.records.filter(
|
|
1221
1271
|
(record, index, all) => ids.has(record.stampId) && all.findIndex((candidate) => candidate.stampId === record.stampId) === index
|
|
1222
1272
|
);
|
|
@@ -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,22 +1644,6 @@ async function decryptPayload(body, secret) {
|
|
|
1395
1644
|
);
|
|
1396
1645
|
}
|
|
1397
1646
|
|
|
1398
|
-
// src/domain/i18n.ts
|
|
1399
|
-
function resolveLocalizedText(text, locale, fallbackLocale) {
|
|
1400
|
-
if (text === void 0 || text === "") return "";
|
|
1401
|
-
if (typeof text === "string") return text;
|
|
1402
|
-
const fallback = fallbackLocale === void 0 ? Object.values(text).find((value) => typeof value === "string") : text[fallbackLocale];
|
|
1403
|
-
return text[locale] || fallback || "";
|
|
1404
|
-
}
|
|
1405
|
-
function toLocalizedString(text) {
|
|
1406
|
-
if (text === void 0) return { ja: "", en: "" };
|
|
1407
|
-
return typeof text === "string" ? { ja: text, en: "" } : {
|
|
1408
|
-
ja: text["ja"] ?? "",
|
|
1409
|
-
en: text["en"] ?? "",
|
|
1410
|
-
...text
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
1647
|
// src/domain/models.ts
|
|
1415
1648
|
var DEFAULT_SHEET_THEME = {
|
|
1416
1649
|
primaryColor: "#9e551e",
|
|
@@ -1422,23 +1655,23 @@ var DEFAULT_SHEET_THEME = {
|
|
|
1422
1655
|
unclaimedOpacity: 1,
|
|
1423
1656
|
fontFamily: "serif"
|
|
1424
1657
|
};
|
|
1425
|
-
function publicCondition(
|
|
1426
|
-
switch (
|
|
1658
|
+
function publicCondition(condition2) {
|
|
1659
|
+
switch (condition2.type) {
|
|
1427
1660
|
case "qr":
|
|
1428
|
-
return
|
|
1661
|
+
return condition2.qrEntryUrl === void 0 ? { type: "qr" } : { type: "qr", qrEntryUrl: condition2.qrEntryUrl };
|
|
1429
1662
|
case "passcode":
|
|
1430
1663
|
return { type: "passcode" };
|
|
1431
1664
|
case "gps":
|
|
1432
1665
|
return {
|
|
1433
1666
|
type: "gps",
|
|
1434
|
-
latitude:
|
|
1435
|
-
longitude:
|
|
1436
|
-
radiusMeters:
|
|
1667
|
+
latitude: condition2.latitude,
|
|
1668
|
+
longitude: condition2.longitude,
|
|
1669
|
+
radiusMeters: condition2.radiusMeters
|
|
1437
1670
|
};
|
|
1438
1671
|
case "nfc":
|
|
1439
1672
|
return { type: "nfc" };
|
|
1440
1673
|
case "custom":
|
|
1441
|
-
return { type: "custom", validatorName:
|
|
1674
|
+
return { type: "custom", validatorName: condition2.validatorName };
|
|
1442
1675
|
}
|
|
1443
1676
|
}
|
|
1444
1677
|
function toPublicConfig(config) {
|
|
@@ -1448,14 +1681,14 @@ function toPublicConfig(config) {
|
|
|
1448
1681
|
title: config.title,
|
|
1449
1682
|
...config.description === void 0 ? {} : { description: config.description },
|
|
1450
1683
|
...config.theme === void 0 ? {} : { theme: config.theme },
|
|
1451
|
-
spots: config.spots.map((
|
|
1452
|
-
...
|
|
1453
|
-
conditions:
|
|
1684
|
+
spots: config.spots.map((spot2) => ({
|
|
1685
|
+
...spot2,
|
|
1686
|
+
conditions: spot2.conditions.map(publicCondition)
|
|
1454
1687
|
})),
|
|
1455
1688
|
rewards: config.rewards.map(
|
|
1456
|
-
({ digitalContentUrl: _content, staffPasscode: _passcode, ...
|
|
1689
|
+
({ digitalContentUrl: _content, staffPasscode: _passcode, ...reward2 }) => reward2
|
|
1457
1690
|
),
|
|
1458
|
-
...config.metadata === void 0 ? {} : { metadata: config.metadata },
|
|
1691
|
+
...config.publicMetadata !== void 0 ? { metadata: config.publicMetadata } : config.metadata === void 0 ? {} : { metadata: config.metadata },
|
|
1459
1692
|
...config.serverEndpoint === void 0 ? {} : { serverEndpoint: config.serverEndpoint }
|
|
1460
1693
|
};
|
|
1461
1694
|
}
|
|
@@ -1491,15 +1724,15 @@ function isPublicConfig(value) {
|
|
|
1491
1724
|
}
|
|
1492
1725
|
if (typeof candidate.id !== "string" || typeof candidate.version !== "string" || typeof candidate.title !== "string" && (typeof candidate.title !== "object" || candidate.title === null || Array.isArray(candidate.title)) || !Array.isArray(candidate.spots) || !Array.isArray(candidate.rewards))
|
|
1493
1726
|
return false;
|
|
1494
|
-
return candidate.spots.every((
|
|
1495
|
-
if (typeof
|
|
1496
|
-
const item =
|
|
1727
|
+
return candidate.spots.every((spot2) => {
|
|
1728
|
+
if (typeof spot2 !== "object" || spot2 === null || Array.isArray(spot2)) return false;
|
|
1729
|
+
const item = spot2;
|
|
1497
1730
|
if (typeof item.id !== "string" || typeof item.orderIndex !== "number" || item.name === void 0 || "secretToken" in item || "code" in item || "tagId" in item || "secretParams" in item)
|
|
1498
1731
|
return false;
|
|
1499
|
-
return Array.isArray(item.conditions) && item.conditions.every((
|
|
1500
|
-
if (typeof
|
|
1732
|
+
return Array.isArray(item.conditions) && item.conditions.every((condition2) => {
|
|
1733
|
+
if (typeof condition2 !== "object" || condition2 === null || Array.isArray(condition2))
|
|
1501
1734
|
return false;
|
|
1502
|
-
const value2 =
|
|
1735
|
+
const value2 = condition2;
|
|
1503
1736
|
if ("secretToken" in value2 || "code" in value2 || "tagId" in value2 || "secretParams" in value2)
|
|
1504
1737
|
return false;
|
|
1505
1738
|
const type = value2.type;
|
|
@@ -1507,13 +1740,125 @@ function isPublicConfig(value) {
|
|
|
1507
1740
|
if (type === "custom") return typeof value2.validatorName === "string";
|
|
1508
1741
|
return type === "gps" && typeof value2.latitude === "number" && typeof value2.longitude === "number" && typeof value2.radiusMeters === "number";
|
|
1509
1742
|
});
|
|
1510
|
-
}) && candidate.rewards.every((
|
|
1511
|
-
if (typeof
|
|
1512
|
-
const item =
|
|
1743
|
+
}) && candidate.rewards.every((reward2) => {
|
|
1744
|
+
if (typeof reward2 !== "object" || reward2 === null || Array.isArray(reward2)) return false;
|
|
1745
|
+
const item = reward2;
|
|
1513
1746
|
return typeof item.id === "string" && typeof item.requiredStampCount === "number" && (item.type === "digital" || item.type === "in_person") && (item.redemptionMethod === "manual_slide" || item.redemptionMethod === "staff_passcode" || item.redemptionMethod === "view_only" || item.redemptionMethod === "server_claim") && !("staffPasscode" in item || "digitalContentUrl" in item);
|
|
1514
1747
|
});
|
|
1515
1748
|
}
|
|
1516
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
|
+
|
|
1517
1862
|
// src/domain/themePresets.ts
|
|
1518
1863
|
var THEME_PRESETS = [
|
|
1519
1864
|
{
|
|
@@ -1613,6 +1958,326 @@ var THEME_PRESETS = [
|
|
|
1613
1958
|
}
|
|
1614
1959
|
];
|
|
1615
1960
|
|
|
1961
|
+
// src/domain/validation.ts
|
|
1962
|
+
var ConfigValidationError = class extends Error {
|
|
1963
|
+
constructor(errors) {
|
|
1964
|
+
super(errors.map((error) => `${error.path}: ${error.message}`).join("; "));
|
|
1965
|
+
this.errors = errors;
|
|
1966
|
+
}
|
|
1967
|
+
errors;
|
|
1968
|
+
name = "ConfigValidationError";
|
|
1969
|
+
};
|
|
1970
|
+
function isRecord3(value) {
|
|
1971
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1972
|
+
}
|
|
1973
|
+
function hasOwn(value, key) {
|
|
1974
|
+
return Object.hasOwn(value, key);
|
|
1975
|
+
}
|
|
1976
|
+
function add(errors, path, message, code) {
|
|
1977
|
+
errors.push({ path, message, code });
|
|
1978
|
+
}
|
|
1979
|
+
function requiredString(value, key, path, errors) {
|
|
1980
|
+
if (typeof value[key] !== "string" || value[key].length === 0) {
|
|
1981
|
+
add(errors, `${path}.${key}`, "A non-empty string is required.", "required_string");
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1984
|
+
return true;
|
|
1985
|
+
}
|
|
1986
|
+
function optionalString(value, key, path, errors) {
|
|
1987
|
+
if (hasOwn(value, key) && value[key] !== void 0 && typeof value[key] !== "string")
|
|
1988
|
+
add(errors, `${path}.${key}`, "Expected a string.", "invalid_type");
|
|
1989
|
+
}
|
|
1990
|
+
function optionalBoolean(value, key, path, errors) {
|
|
1991
|
+
if (hasOwn(value, key) && value[key] !== void 0 && typeof value[key] !== "boolean")
|
|
1992
|
+
add(errors, `${path}.${key}`, "Expected a boolean.", "invalid_type");
|
|
1993
|
+
}
|
|
1994
|
+
function finiteNumber(value, key, path, errors, minimum) {
|
|
1995
|
+
const item = value[key];
|
|
1996
|
+
if (typeof item !== "number" || !Number.isFinite(item)) {
|
|
1997
|
+
add(errors, `${path}.${key}`, "Expected a finite number.", "invalid_number");
|
|
1998
|
+
} else if (minimum !== void 0 && item < minimum) {
|
|
1999
|
+
add(
|
|
2000
|
+
errors,
|
|
2001
|
+
`${path}.${key}`,
|
|
2002
|
+
`Expected a number greater than or equal to ${minimum}.`,
|
|
2003
|
+
"out_of_range"
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
function localizedText(value, path, errors) {
|
|
2008
|
+
if (typeof value === "string") return;
|
|
2009
|
+
if (!isRecord3(value)) {
|
|
2010
|
+
add(errors, path, "Expected a string or a locale map.", "invalid_localized_text");
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
for (const [locale, text] of Object.entries(value)) {
|
|
2014
|
+
if (typeof text !== "string")
|
|
2015
|
+
add(errors, `${path}.${locale}`, "Expected a string.", "invalid_type");
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
function theme(value, path, errors) {
|
|
2019
|
+
if (!isRecord3(value)) {
|
|
2020
|
+
add(errors, path, "Expected a theme object.", "invalid_type");
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
for (const key of ["primaryColor", "cardBackgroundColor", "textColor"])
|
|
2024
|
+
requiredString(value, key, path, errors);
|
|
2025
|
+
optionalString(value, "backgroundColor", path, errors);
|
|
2026
|
+
optionalString(value, "backgroundImageUrl", path, errors);
|
|
2027
|
+
optionalString(value, "completedStampColor", path, errors);
|
|
2028
|
+
optionalString(value, "fontFamily", path, errors);
|
|
2029
|
+
const slotShape = value.slotShape;
|
|
2030
|
+
if (slotShape !== "circle" && slotShape !== "square" && slotShape !== "rounded")
|
|
2031
|
+
add(errors, `${path}.slotShape`, "Expected circle, square, or rounded.", "invalid_enum");
|
|
2032
|
+
finiteNumber(value, "gridColumns", path, errors, 1);
|
|
2033
|
+
if (typeof value.gridColumns === "number" && !Number.isInteger(value.gridColumns))
|
|
2034
|
+
add(errors, `${path}.gridColumns`, "Expected an integer.", "invalid_integer");
|
|
2035
|
+
if (hasOwn(value, "unclaimedOpacity")) finiteNumber(value, "unclaimedOpacity", path, errors, 0);
|
|
2036
|
+
}
|
|
2037
|
+
function externalReferences(value, path, errors) {
|
|
2038
|
+
if (!Array.isArray(value)) {
|
|
2039
|
+
add(errors, path, "Expected an array.", "invalid_type");
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
value.forEach((item, index) => {
|
|
2043
|
+
const itemPath = `${path}[${index}]`;
|
|
2044
|
+
if (!isRecord3(item)) {
|
|
2045
|
+
add(errors, itemPath, "Expected an object.", "invalid_type");
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
requiredString(item, "type", itemPath, errors);
|
|
2049
|
+
requiredString(item, "id", itemPath, errors);
|
|
2050
|
+
optionalString(item, "url", itemPath, errors);
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
function condition(value, path, errors, isPublic) {
|
|
2054
|
+
if (!isRecord3(value)) {
|
|
2055
|
+
add(errors, path, "Expected a condition object.", "invalid_type");
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
const type = value.type;
|
|
2059
|
+
if (type === "qr") {
|
|
2060
|
+
if (isPublic) {
|
|
2061
|
+
if (hasOwn(value, "secretToken"))
|
|
2062
|
+
add(errors, `${path}.secretToken`, "Private field is not allowed.", "private_field");
|
|
2063
|
+
} else requiredString(value, "secretToken", path, errors);
|
|
2064
|
+
optionalString(value, "qrEntryUrl", path, errors);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
if (type === "passcode") {
|
|
2068
|
+
if (isPublic) {
|
|
2069
|
+
for (const key of ["code", "caseSensitive"])
|
|
2070
|
+
if (hasOwn(value, key))
|
|
2071
|
+
add(errors, `${path}.${key}`, "Private field is not allowed.", "private_field");
|
|
2072
|
+
} else {
|
|
2073
|
+
requiredString(value, "code", path, errors);
|
|
2074
|
+
optionalBoolean(value, "caseSensitive", path, errors);
|
|
2075
|
+
}
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
if (type === "gps") {
|
|
2079
|
+
finiteNumber(value, "latitude", path, errors);
|
|
2080
|
+
finiteNumber(value, "longitude", path, errors);
|
|
2081
|
+
finiteNumber(value, "radiusMeters", path, errors, 0);
|
|
2082
|
+
if (typeof value.latitude === "number" && (value.latitude < -90 || value.latitude > 90))
|
|
2083
|
+
add(errors, `${path}.latitude`, "Expected a latitude between -90 and 90.", "out_of_range");
|
|
2084
|
+
if (typeof value.longitude === "number" && (value.longitude < -180 || value.longitude > 180))
|
|
2085
|
+
add(
|
|
2086
|
+
errors,
|
|
2087
|
+
`${path}.longitude`,
|
|
2088
|
+
"Expected a longitude between -180 and 180.",
|
|
2089
|
+
"out_of_range"
|
|
2090
|
+
);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
if (type === "nfc") {
|
|
2094
|
+
if (isPublic) {
|
|
2095
|
+
if (hasOwn(value, "tagId"))
|
|
2096
|
+
add(errors, `${path}.tagId`, "Private field is not allowed.", "private_field");
|
|
2097
|
+
} else requiredString(value, "tagId", path, errors);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (type === "custom") {
|
|
2101
|
+
requiredString(value, "validatorName", path, errors);
|
|
2102
|
+
if (isPublic && hasOwn(value, "secretParams"))
|
|
2103
|
+
add(errors, `${path}.secretParams`, "Private field is not allowed.", "private_field");
|
|
2104
|
+
if (!isPublic && hasOwn(value, "secretParams") && !isRecord3(value.secretParams))
|
|
2105
|
+
add(errors, `${path}.secretParams`, "Expected an object.", "invalid_type");
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
add(errors, `${path}.type`, "Unknown condition type.", "invalid_enum");
|
|
2109
|
+
}
|
|
2110
|
+
function unlockCondition(value, path, errors) {
|
|
2111
|
+
if (!isRecord3(value)) {
|
|
2112
|
+
add(errors, path, "Expected an unlock condition object.", "invalid_type");
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
if (value.type === "stamp_count") {
|
|
2116
|
+
finiteNumber(value, "count", path, errors, 0);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (value.type === "stamps") {
|
|
2120
|
+
if (!Array.isArray(value.stampIds))
|
|
2121
|
+
add(errors, `${path}.stampIds`, "Expected an array.", "invalid_type");
|
|
2122
|
+
else
|
|
2123
|
+
value.stampIds.forEach((item, index) => {
|
|
2124
|
+
if (typeof item !== "string" || item.length === 0)
|
|
2125
|
+
add(
|
|
2126
|
+
errors,
|
|
2127
|
+
`${path}.stampIds[${index}]`,
|
|
2128
|
+
"Expected a non-empty string.",
|
|
2129
|
+
"required_string"
|
|
2130
|
+
);
|
|
2131
|
+
});
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
if (value.type === "all" || value.type === "any") {
|
|
2135
|
+
if (!Array.isArray(value.conditions))
|
|
2136
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
2137
|
+
else
|
|
2138
|
+
value.conditions.forEach((item, index) => {
|
|
2139
|
+
unlockCondition(item, `${path}.conditions[${index}]`, errors);
|
|
2140
|
+
});
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
add(errors, `${path}.type`, "Unknown unlock condition type.", "invalid_enum");
|
|
2144
|
+
}
|
|
2145
|
+
function spot(value, path, errors, isPublic) {
|
|
2146
|
+
if (!isRecord3(value)) {
|
|
2147
|
+
add(errors, path, "Expected a spot object.", "invalid_type");
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
requiredString(value, "id", path, errors);
|
|
2151
|
+
finiteNumber(value, "orderIndex", path, errors, 0);
|
|
2152
|
+
if (typeof value.orderIndex === "number" && !Number.isInteger(value.orderIndex))
|
|
2153
|
+
add(errors, `${path}.orderIndex`, "Expected an integer.", "invalid_integer");
|
|
2154
|
+
localizedText(value.name, `${path}.name`, errors);
|
|
2155
|
+
for (const key of ["description", "hint"])
|
|
2156
|
+
if (hasOwn(value, key)) localizedText(value[key], `${path}.${key}`, errors);
|
|
2157
|
+
for (const key of ["imageUrl", "iconUrl", "redirectUrlAfterClaim"])
|
|
2158
|
+
optionalString(value, key, path, errors);
|
|
2159
|
+
if (hasOwn(value, "externalReferences") && value.externalReferences !== void 0)
|
|
2160
|
+
externalReferences(value.externalReferences, `${path}.externalReferences`, errors);
|
|
2161
|
+
if (hasOwn(value, "prerequisites") && value.prerequisites !== void 0) {
|
|
2162
|
+
if (!Array.isArray(value.prerequisites))
|
|
2163
|
+
add(errors, `${path}.prerequisites`, "Expected an array.", "invalid_type");
|
|
2164
|
+
else
|
|
2165
|
+
value.prerequisites.forEach((item, index) => {
|
|
2166
|
+
if (typeof item !== "string" || item.length === 0)
|
|
2167
|
+
add(
|
|
2168
|
+
errors,
|
|
2169
|
+
`${path}.prerequisites[${index}]`,
|
|
2170
|
+
"Expected a non-empty string.",
|
|
2171
|
+
"required_string"
|
|
2172
|
+
);
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
if (!Array.isArray(value.conditions))
|
|
2176
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
2177
|
+
else
|
|
2178
|
+
value.conditions.forEach((item, index) => {
|
|
2179
|
+
condition(item, `${path}.conditions[${index}]`, errors, isPublic);
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
function reward(value, path, errors, isPublic) {
|
|
2183
|
+
if (!isRecord3(value)) {
|
|
2184
|
+
add(errors, path, "Expected a reward object.", "invalid_type");
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
requiredString(value, "id", path, errors);
|
|
2188
|
+
localizedText(value.title, `${path}.title`, errors);
|
|
2189
|
+
if (hasOwn(value, "description")) localizedText(value.description, `${path}.description`, errors);
|
|
2190
|
+
if (value.type !== "digital" && value.type !== "in_person")
|
|
2191
|
+
add(errors, `${path}.type`, "Unknown reward type.", "invalid_enum");
|
|
2192
|
+
if (!["manual_slide", "staff_passcode", "view_only", "server_claim"].includes(
|
|
2193
|
+
String(value.redemptionMethod)
|
|
2194
|
+
))
|
|
2195
|
+
add(errors, `${path}.redemptionMethod`, "Unknown redemption method.", "invalid_enum");
|
|
2196
|
+
finiteNumber(value, "requiredStampCount", path, errors, 0);
|
|
2197
|
+
for (const key of ["stockLimit", "userClaimLimit"]) {
|
|
2198
|
+
if (hasOwn(value, key) && value[key] !== void 0) {
|
|
2199
|
+
finiteNumber(value, key, path, errors, 0);
|
|
2200
|
+
if (typeof value[key] === "number" && !Number.isInteger(value[key]))
|
|
2201
|
+
add(errors, `${path}.${key}`, "Expected an integer.", "invalid_integer");
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
optionalString(value, "validUntil", path, errors);
|
|
2205
|
+
if (typeof value.validUntil === "string" && Number.isNaN(Date.parse(value.validUntil)))
|
|
2206
|
+
add(errors, `${path}.validUntil`, "Expected a valid date string.", "invalid_date");
|
|
2207
|
+
if (isPublic) {
|
|
2208
|
+
for (const key of ["staffPasscode", "digitalContentUrl"])
|
|
2209
|
+
if (hasOwn(value, key))
|
|
2210
|
+
add(errors, `${path}.${key}`, "Private field is not allowed.", "private_field");
|
|
2211
|
+
} else {
|
|
2212
|
+
optionalString(value, "staffPasscode", path, errors);
|
|
2213
|
+
optionalString(value, "digitalContentUrl", path, errors);
|
|
2214
|
+
}
|
|
2215
|
+
if (hasOwn(value, "conditions") && value.conditions !== void 0) {
|
|
2216
|
+
if (!Array.isArray(value.conditions))
|
|
2217
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
2218
|
+
else
|
|
2219
|
+
value.conditions.forEach((item, index) => {
|
|
2220
|
+
unlockCondition(item, `${path}.conditions[${index}]`, errors);
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function validate(value, isPublic) {
|
|
2225
|
+
const errors = [];
|
|
2226
|
+
if (!isRecord3(value)) {
|
|
2227
|
+
add(errors, "$", "Expected a configuration object.", "invalid_type");
|
|
2228
|
+
return errors;
|
|
2229
|
+
}
|
|
2230
|
+
requiredString(value, "id", "$", errors);
|
|
2231
|
+
requiredString(value, "version", "$", errors);
|
|
2232
|
+
localizedText(value.title, "$.title", errors);
|
|
2233
|
+
if (hasOwn(value, "description")) localizedText(value.description, "$.description", errors);
|
|
2234
|
+
if (hasOwn(value, "theme") && value.theme !== void 0) theme(value.theme, "$.theme", errors);
|
|
2235
|
+
if (!Array.isArray(value.spots)) add(errors, "spots", "Expected an array.", "invalid_type");
|
|
2236
|
+
else
|
|
2237
|
+
value.spots.forEach((item, index) => {
|
|
2238
|
+
spot(item, `spots[${index}]`, errors, isPublic);
|
|
2239
|
+
});
|
|
2240
|
+
if (!Array.isArray(value.rewards)) add(errors, "rewards", "Expected an array.", "invalid_type");
|
|
2241
|
+
else
|
|
2242
|
+
value.rewards.forEach((item, index) => {
|
|
2243
|
+
reward(item, `rewards[${index}]`, errors, isPublic);
|
|
2244
|
+
});
|
|
2245
|
+
if (!isPublic) {
|
|
2246
|
+
optionalString(value, "staffPasscode", "$", errors);
|
|
2247
|
+
if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord3(value.inventory))
|
|
2248
|
+
add(errors, "$.inventory", "Expected an object.", "invalid_type");
|
|
2249
|
+
if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord3(value.serverMetadata))
|
|
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");
|
|
2253
|
+
optionalString(value, "serverEndpoint", "$", errors);
|
|
2254
|
+
} else {
|
|
2255
|
+
for (const key of ["staffPasscode", "serverMetadata", "inventory"])
|
|
2256
|
+
if (hasOwn(value, key))
|
|
2257
|
+
add(errors, `$.${key}`, "Private field is not allowed.", "private_field");
|
|
2258
|
+
optionalString(value, "serverEndpoint", "$", errors);
|
|
2259
|
+
}
|
|
2260
|
+
return errors;
|
|
2261
|
+
}
|
|
2262
|
+
function safeParseAdminConfig(input) {
|
|
2263
|
+
const errors = validate(input, false);
|
|
2264
|
+
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2265
|
+
}
|
|
2266
|
+
function parseAdminConfig(input) {
|
|
2267
|
+
const result = safeParseAdminConfig(input);
|
|
2268
|
+
if (!result.success) throw new ConfigValidationError(result.errors);
|
|
2269
|
+
return result.data;
|
|
2270
|
+
}
|
|
2271
|
+
function safeParsePublicConfig(input) {
|
|
2272
|
+
const errors = validate(input, true);
|
|
2273
|
+
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2274
|
+
}
|
|
2275
|
+
function parsePublicConfig(input) {
|
|
2276
|
+
const result = safeParsePublicConfig(input);
|
|
2277
|
+
if (!result.success) throw new ConfigValidationError(result.errors);
|
|
2278
|
+
return result.data;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
1616
2281
|
// src/security/snapshotToken.ts
|
|
1617
2282
|
var encoder2 = new TextEncoder();
|
|
1618
2283
|
function cryptoApi2() {
|
|
@@ -1732,10 +2397,14 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1732
2397
|
}
|
|
1733
2398
|
}
|
|
1734
2399
|
|
|
2400
|
+
exports.ConfigValidationError = ConfigValidationError;
|
|
1735
2401
|
exports.DEFAULT_SHEET_THEME = DEFAULT_SHEET_THEME;
|
|
2402
|
+
exports.InMemoryOfflineQueueStorage = MemoryQueueStorage;
|
|
1736
2403
|
exports.InMemoryStorage = InMemoryStorage;
|
|
1737
2404
|
exports.IndexedDBAdapter = IndexedDBAdapter;
|
|
2405
|
+
exports.IndexedDBOfflineQueueStorage = IndexedDBOfflineQueueStorage;
|
|
1738
2406
|
exports.LocalStorageAdapter = LocalStorageAdapter;
|
|
2407
|
+
exports.OfflineQueue = OfflineQueue;
|
|
1739
2408
|
exports.StampRallyClient = StampRallyClient;
|
|
1740
2409
|
exports.StorageAdapterError = StorageAdapterError;
|
|
1741
2410
|
exports.THEME_PRESETS = THEME_PRESETS;
|
|
@@ -1761,14 +2430,21 @@ exports.isRewardState = isRewardState;
|
|
|
1761
2430
|
exports.isStampRallyState = isStampRallyState;
|
|
1762
2431
|
exports.issueClaimTicketNumber = issueClaimTicketNumber;
|
|
1763
2432
|
exports.normalizePasscode = normalizePasscode;
|
|
2433
|
+
exports.parseAdminConfig = parseAdminConfig;
|
|
2434
|
+
exports.parsePublicConfig = parsePublicConfig;
|
|
1764
2435
|
exports.processStamp = processStamp;
|
|
1765
2436
|
exports.readNfcContext = readNfcContext;
|
|
1766
2437
|
exports.readQrContext = readQrContext;
|
|
1767
2438
|
exports.reconcileRewardStates = reconcileRewardStates;
|
|
1768
2439
|
exports.resolveLocalizedText = resolveLocalizedText;
|
|
2440
|
+
exports.safeParseAdminConfig = safeParseAdminConfig;
|
|
2441
|
+
exports.safeParsePublicConfig = safeParsePublicConfig;
|
|
2442
|
+
exports.sanitizeAdminConfig = sanitizeAdminConfig;
|
|
1769
2443
|
exports.storageKey = storageKey;
|
|
1770
2444
|
exports.toLocalizedString = toLocalizedString;
|
|
1771
2445
|
exports.toPublicConfig = toPublicConfig;
|
|
2446
|
+
exports.updateLocalizedField = updateLocalizedField;
|
|
2447
|
+
exports.validatePublicConfigSafety = validatePublicConfigSafety;
|
|
1772
2448
|
exports.verifyPasscode = verifyPasscode;
|
|
1773
2449
|
exports.verifySecureToken = verifySecureToken;
|
|
1774
2450
|
exports.verifySnapshotToken = verifySnapshotToken;
|