@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.js
CHANGED
|
@@ -10,34 +10,34 @@ function calculateDistanceMeters(aLat, aLon, bLat, bLon) {
|
|
|
10
10
|
function mismatch(conditionType, reason, extra = {}) {
|
|
11
11
|
return { ok: false, error: { code: "CONDITION_MISMATCH", conditionType, reason, ...extra } };
|
|
12
12
|
}
|
|
13
|
-
function evaluateConditionDetailed(
|
|
14
|
-
switch (
|
|
13
|
+
function evaluateConditionDetailed(condition2, context) {
|
|
14
|
+
switch (condition2.type) {
|
|
15
15
|
case "qr":
|
|
16
|
-
return context.type === "qr" && context.token ===
|
|
16
|
+
return context.type === "qr" && context.token === condition2.secretToken ? { ok: true, value: { conditionType: "qr" } } : mismatch("qr", "INVALID_PROOF");
|
|
17
17
|
case "passcode":
|
|
18
|
-
return context.type === "passcode" && (
|
|
18
|
+
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");
|
|
19
19
|
case "nfc":
|
|
20
|
-
return context.type === "nfc" && context.tagId ===
|
|
20
|
+
return context.type === "nfc" && context.tagId === condition2.tagId ? { ok: true, value: { conditionType: "nfc" } } : mismatch("nfc", "INVALID_PROOF");
|
|
21
21
|
case "custom":
|
|
22
22
|
return mismatch("custom", "VALIDATOR_FAILED");
|
|
23
23
|
case "gps": {
|
|
24
|
-
if (!Number.isFinite(
|
|
24
|
+
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))
|
|
25
25
|
return mismatch("gps", "INVALID_GEO_INPUT");
|
|
26
26
|
const distanceMeters = calculateDistanceMeters(
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
condition2.latitude,
|
|
28
|
+
condition2.longitude,
|
|
29
29
|
context.latitude,
|
|
30
30
|
context.longitude
|
|
31
31
|
);
|
|
32
|
-
return distanceMeters <=
|
|
32
|
+
return distanceMeters <= condition2.radiusMeters ? { ok: true, value: { conditionType: "gps", distanceMeters } } : mismatch("gps", "OUTSIDE_RADIUS", {
|
|
33
33
|
distanceMeters,
|
|
34
|
-
radiusMeters:
|
|
34
|
+
radiusMeters: condition2.radiusMeters
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
function evaluateCondition(
|
|
40
|
-
return evaluateConditionDetailed(
|
|
39
|
+
function evaluateCondition(condition2, context) {
|
|
40
|
+
return evaluateConditionDetailed(condition2, context).ok;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// src/engine/order.ts
|
|
@@ -47,11 +47,11 @@ function getOrderedSpots(spots) {
|
|
|
47
47
|
|
|
48
48
|
// src/engine/progress.ts
|
|
49
49
|
function calculateProgress(state, config) {
|
|
50
|
-
const ids = new Set(config.spots.map((
|
|
50
|
+
const ids = new Set(config.spots.map((spot2) => spot2.id));
|
|
51
51
|
const acquired = new Set(
|
|
52
52
|
state.records.map((record) => record.stampId).filter((id2) => ids.has(id2))
|
|
53
53
|
);
|
|
54
|
-
const remaining = config.spots.filter((
|
|
54
|
+
const remaining = config.spots.filter((spot2) => !acquired.has(spot2.id));
|
|
55
55
|
return {
|
|
56
56
|
acquired: acquired.size,
|
|
57
57
|
total: config.spots.length,
|
|
@@ -91,9 +91,9 @@ function createUniqueClaimTicketNumber(rewardId, issuedAt) {
|
|
|
91
91
|
const timestampPart = Number.isNaN(timestamp) ? Date.now() : timestamp;
|
|
92
92
|
return `CLAIM-${rewardId}-${timestampPart}-${createRandomHash()}`;
|
|
93
93
|
}
|
|
94
|
-
function issueClaimTicketNumber(
|
|
94
|
+
function issueClaimTicketNumber(reward2, currentState, options = {}) {
|
|
95
95
|
if (currentState.claimTicketNumber !== void 0) return currentState;
|
|
96
|
-
const claimTicketNumber = createClaimTicketNumber(
|
|
96
|
+
const claimTicketNumber = createClaimTicketNumber(reward2.id, options);
|
|
97
97
|
return { ...currentState, claimTicketNumber };
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -301,8 +301,8 @@ function normalizePasscode(input, caseSensitive = false) {
|
|
|
301
301
|
const normalized = input.normalize("NFKC").trim();
|
|
302
302
|
return caseSensitive ? normalized : normalized.toUpperCase();
|
|
303
303
|
}
|
|
304
|
-
function verifyPasscode(inputCode,
|
|
305
|
-
return normalizePasscode(inputCode,
|
|
304
|
+
function verifyPasscode(inputCode, condition2) {
|
|
305
|
+
return normalizePasscode(inputCode, condition2.caseSensitive) === normalizePasscode(condition2.code, condition2.caseSensitive) ? { success: true } : { success: false, message: "The passcode is invalid." };
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
// src/detectors/qr.ts
|
|
@@ -429,71 +429,71 @@ async function readQrContext(videoElement, options = {}) {
|
|
|
429
429
|
// src/engine/transition.ts
|
|
430
430
|
function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now) {
|
|
431
431
|
const states = new Map(currentStates.map((state) => [state.rewardId, state]));
|
|
432
|
-
return rewards.map((
|
|
433
|
-
const current = states.get(
|
|
432
|
+
return rewards.map((reward2) => {
|
|
433
|
+
const current = states.get(reward2.id);
|
|
434
434
|
if (current?.status === "CONSUMED" || current?.status === "EXPIRED") return current;
|
|
435
|
-
if (
|
|
436
|
-
return { rewardId:
|
|
437
|
-
if (
|
|
438
|
-
return { rewardId:
|
|
439
|
-
if (acquiredStampCount >=
|
|
435
|
+
if (reward2.validUntil !== void 0 && Date.parse(reward2.validUntil) <= Date.parse(now))
|
|
436
|
+
return { rewardId: reward2.id, status: "EXPIRED" };
|
|
437
|
+
if (reward2.stockLimit !== void 0 && (current?.redeemedCount ?? 0) >= reward2.stockLimit)
|
|
438
|
+
return { rewardId: reward2.id, status: "EXPIRED" };
|
|
439
|
+
if (acquiredStampCount >= reward2.requiredStampCount)
|
|
440
440
|
return {
|
|
441
|
-
rewardId:
|
|
441
|
+
rewardId: reward2.id,
|
|
442
442
|
status: "AVAILABLE",
|
|
443
443
|
...current?.unlockedAt === void 0 ? { unlockedAt: now } : { unlockedAt: current.unlockedAt }
|
|
444
444
|
};
|
|
445
|
-
return { rewardId:
|
|
445
|
+
return { rewardId: reward2.id, status: "LOCKED" };
|
|
446
446
|
});
|
|
447
447
|
}
|
|
448
448
|
function consumeReward(params) {
|
|
449
|
-
const { reward, currentState } = params;
|
|
449
|
+
const { reward: reward2, currentState } = params;
|
|
450
450
|
if (currentState.status === "CONSUMED")
|
|
451
|
-
return { ok: false, error: { code: "ALREADY_CONSUMED", rewardId:
|
|
451
|
+
return { ok: false, error: { code: "ALREADY_CONSUMED", rewardId: reward2.id } };
|
|
452
452
|
if (currentState.status !== "AVAILABLE")
|
|
453
|
-
return { ok: false, error: { code: "NOT_AVAILABLE", rewardId:
|
|
454
|
-
if (
|
|
455
|
-
return { ok: false, error: { code: "EXPIRED", rewardId:
|
|
456
|
-
if (
|
|
457
|
-
return { ok: false, error: { code: "OUT_OF_STOCK", rewardId:
|
|
458
|
-
if (
|
|
459
|
-
return { ok: false, error: { code: "USER_LIMIT_REACHED", rewardId:
|
|
460
|
-
if (
|
|
461
|
-
if (
|
|
453
|
+
return { ok: false, error: { code: "NOT_AVAILABLE", rewardId: reward2.id } };
|
|
454
|
+
if (reward2.validUntil !== void 0 && Date.parse(reward2.validUntil) <= Date.parse(params.now))
|
|
455
|
+
return { ok: false, error: { code: "EXPIRED", rewardId: reward2.id } };
|
|
456
|
+
if (reward2.stockLimit !== void 0 && (currentState.redeemedCount ?? 0) >= reward2.stockLimit)
|
|
457
|
+
return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward2.id } };
|
|
458
|
+
if (reward2.userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= reward2.userClaimLimit)
|
|
459
|
+
return { ok: false, error: { code: "USER_LIMIT_REACHED", rewardId: reward2.id } };
|
|
460
|
+
if (reward2.redemptionMethod === "staff_passcode") {
|
|
461
|
+
if (reward2.staffPasscode === void 0 || !verifyPasscode(params.inputPasscode ?? "", { code: reward2.staffPasscode }).success)
|
|
462
462
|
return {
|
|
463
463
|
ok: false,
|
|
464
464
|
error: {
|
|
465
465
|
code: "INVALID_PASSCODE",
|
|
466
|
-
rewardId:
|
|
466
|
+
rewardId: reward2.id,
|
|
467
467
|
message: "The passcode is invalid."
|
|
468
468
|
}
|
|
469
469
|
};
|
|
470
470
|
}
|
|
471
|
-
if (
|
|
471
|
+
if (reward2.redemptionMethod === "view_only") return { ok: true, value: currentState };
|
|
472
472
|
return {
|
|
473
473
|
ok: true,
|
|
474
474
|
value: {
|
|
475
475
|
...currentState,
|
|
476
476
|
status: "CONSUMED",
|
|
477
477
|
consumedAt: params.now,
|
|
478
|
-
claimTicketNumber: createUniqueClaimTicketNumber(
|
|
478
|
+
claimTicketNumber: createUniqueClaimTicketNumber(reward2.id, params.now),
|
|
479
479
|
redeemedCount: (currentState.redeemedCount ?? 0) + 1,
|
|
480
480
|
...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
|
|
481
481
|
}
|
|
482
482
|
};
|
|
483
483
|
}
|
|
484
484
|
function processStamp(state, config, spotId, context, now) {
|
|
485
|
-
const
|
|
486
|
-
if (
|
|
485
|
+
const spot2 = config.spots.find((item) => item.id === spotId);
|
|
486
|
+
if (spot2 === void 0) return { ok: false, error: { code: "SPOT_NOT_FOUND", spotId } };
|
|
487
487
|
if (state.records.some((record2) => record2.stampId === spotId))
|
|
488
488
|
return { ok: false, error: { code: "STAMP_ALREADY_ACQUIRED", spotId } };
|
|
489
489
|
const acquired = new Set(state.records.map((record2) => record2.stampId));
|
|
490
|
-
if (
|
|
490
|
+
if (spot2.prerequisites?.some((id2) => !acquired.has(id2)))
|
|
491
491
|
return { ok: false, error: { code: "PREREQUISITES_NOT_MET", spotId } };
|
|
492
|
-
for (const
|
|
493
|
-
if (
|
|
492
|
+
for (const condition2 of spot2.conditions) {
|
|
493
|
+
if (condition2.type === "custom" || !evaluateConditionDetailed(condition2, context).ok)
|
|
494
494
|
return {
|
|
495
495
|
ok: false,
|
|
496
|
-
error:
|
|
496
|
+
error: condition2.type === "custom" ? {
|
|
497
497
|
code: "CUSTOM_VALIDATION_FAILED",
|
|
498
498
|
spotId,
|
|
499
499
|
message: "Custom validation requires an async validator."
|
|
@@ -957,18 +957,18 @@ function proof(value) {
|
|
|
957
957
|
}
|
|
958
958
|
return "";
|
|
959
959
|
}
|
|
960
|
-
function matches(
|
|
961
|
-
if (
|
|
960
|
+
function matches(condition2, value) {
|
|
961
|
+
if (condition2.type === "gps") {
|
|
962
962
|
if (typeof value !== "object" || value === null) return false;
|
|
963
963
|
const item = value;
|
|
964
964
|
const latitude = item.latitude;
|
|
965
965
|
const longitude = item.longitude;
|
|
966
966
|
if (typeof latitude !== "number" || typeof longitude !== "number") return false;
|
|
967
967
|
const radians = (v) => v * Math.PI / 180;
|
|
968
|
-
const dLat = radians(latitude -
|
|
969
|
-
const dLon = radians(longitude -
|
|
970
|
-
const h = Math.sin(dLat / 2) ** 2 + Math.cos(radians(
|
|
971
|
-
return 6371e3 * 2 * Math.asin(Math.sqrt(Math.min(1, h))) <=
|
|
968
|
+
const dLat = radians(latitude - condition2.latitude);
|
|
969
|
+
const dLon = radians(longitude - condition2.longitude);
|
|
970
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(radians(condition2.latitude)) * Math.cos(radians(latitude)) * Math.sin(dLon / 2) ** 2;
|
|
971
|
+
return 6371e3 * 2 * Math.asin(Math.sqrt(Math.min(1, h))) <= condition2.radiusMeters;
|
|
972
972
|
}
|
|
973
973
|
return proof(value).trim() !== "";
|
|
974
974
|
}
|
|
@@ -987,6 +987,7 @@ var StampRallyClient = class {
|
|
|
987
987
|
#storage;
|
|
988
988
|
#options;
|
|
989
989
|
#config;
|
|
990
|
+
#offlineQueue;
|
|
990
991
|
#userId;
|
|
991
992
|
#state = null;
|
|
992
993
|
#initialization = null;
|
|
@@ -996,6 +997,7 @@ var StampRallyClient = class {
|
|
|
996
997
|
this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions } : storageOrOptions;
|
|
997
998
|
this.#storage = this.#options.storage ?? new InMemoryStorage();
|
|
998
999
|
this.#userId = this.#options.userId ?? null;
|
|
1000
|
+
this.#offlineQueue = this.#options.offlineQueue;
|
|
999
1001
|
}
|
|
1000
1002
|
getConfig() {
|
|
1001
1003
|
return this.#config;
|
|
@@ -1006,6 +1008,12 @@ var StampRallyClient = class {
|
|
|
1006
1008
|
getUserId() {
|
|
1007
1009
|
return this.#userId;
|
|
1008
1010
|
}
|
|
1011
|
+
get syncState() {
|
|
1012
|
+
return this.#offlineQueue?.syncState ?? "idle";
|
|
1013
|
+
}
|
|
1014
|
+
get pendingCount() {
|
|
1015
|
+
return this.#offlineQueue?.pendingCount ?? 0;
|
|
1016
|
+
}
|
|
1009
1017
|
subscribe(listener) {
|
|
1010
1018
|
this.#listeners.add(listener);
|
|
1011
1019
|
return () => this.#listeners.delete(listener);
|
|
@@ -1055,8 +1063,8 @@ var StampRallyClient = class {
|
|
|
1055
1063
|
checkIn(spotId, proofData, options = {}) {
|
|
1056
1064
|
return this.#enqueue(async () => {
|
|
1057
1065
|
const current = await this.initialize();
|
|
1058
|
-
const
|
|
1059
|
-
if (
|
|
1066
|
+
const spot2 = this.#config.spots.find((item) => item.id === spotId);
|
|
1067
|
+
if (spot2 === void 0)
|
|
1060
1068
|
return this.#fail({ code: "SPOT_NOT_FOUND", spotId, message: "Spot was not found." });
|
|
1061
1069
|
if (current.records.some((record2) => record2.stampId === spotId))
|
|
1062
1070
|
return this.#fail({
|
|
@@ -1065,15 +1073,15 @@ var StampRallyClient = class {
|
|
|
1065
1073
|
message: "Spot was already claimed."
|
|
1066
1074
|
});
|
|
1067
1075
|
const acquired = new Set(current.records.map((record2) => record2.stampId));
|
|
1068
|
-
if (
|
|
1076
|
+
if (spot2.prerequisites?.some((id2) => !acquired.has(id2)))
|
|
1069
1077
|
return this.#fail({
|
|
1070
1078
|
code: "PREREQUISITES_NOT_MET",
|
|
1071
1079
|
spotId,
|
|
1072
1080
|
message: "Prerequisite spots are not complete."
|
|
1073
1081
|
});
|
|
1074
|
-
for (const
|
|
1075
|
-
if (
|
|
1076
|
-
const validator = this.#options.customValidators?.[
|
|
1082
|
+
for (const condition2 of spot2.conditions) {
|
|
1083
|
+
if (condition2.type === "custom") {
|
|
1084
|
+
const validator = this.#options.customValidators?.[condition2.validatorName] ?? this.#options.customValidator;
|
|
1077
1085
|
if (validator === void 0)
|
|
1078
1086
|
return this.#fail({
|
|
1079
1087
|
code: "CUSTOM_VALIDATION_FAILED",
|
|
@@ -1084,7 +1092,7 @@ var StampRallyClient = class {
|
|
|
1084
1092
|
rallyId: this.#config.id,
|
|
1085
1093
|
spotId,
|
|
1086
1094
|
proofData,
|
|
1087
|
-
condition: { type: "custom", validatorName:
|
|
1095
|
+
condition: { type: "custom", validatorName: condition2.validatorName },
|
|
1088
1096
|
userState: current
|
|
1089
1097
|
};
|
|
1090
1098
|
const result = typeof validator === "function" ? await validator(context) : await validator.validate(context);
|
|
@@ -1094,7 +1102,7 @@ var StampRallyClient = class {
|
|
|
1094
1102
|
spotId,
|
|
1095
1103
|
message: typeof result === "object" && result.message !== void 0 ? result.message : "Custom validation failed."
|
|
1096
1104
|
});
|
|
1097
|
-
} else if (!matches(
|
|
1105
|
+
} else if (!matches(condition2, proofData))
|
|
1098
1106
|
return this.#fail({ code: "INVALID_PROOF", spotId, message: "Verification failed." });
|
|
1099
1107
|
}
|
|
1100
1108
|
const now = options.now ?? this.#now();
|
|
@@ -1109,8 +1117,21 @@ var StampRallyClient = class {
|
|
|
1109
1117
|
};
|
|
1110
1118
|
const remote = this.#options.syncAdapter?.checkIn;
|
|
1111
1119
|
if (options.sync !== false && remote !== void 0) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1120
|
+
try {
|
|
1121
|
+
const result = await remote(request);
|
|
1122
|
+
return result.ok ? this.#commitCheckIn(result) : this.#fail(result.error);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
if (this.#offlineQueue === void 0) throw error;
|
|
1125
|
+
await this.#offlineQueue.enqueueCheckIn(request);
|
|
1126
|
+
const record2 = { stampId: spotId, acquiredAt: now };
|
|
1127
|
+
const next2 = this.#reconcile({
|
|
1128
|
+
...current,
|
|
1129
|
+
records: [...current.records, record2],
|
|
1130
|
+
updatedAt: now
|
|
1131
|
+
});
|
|
1132
|
+
await this.#storage.save(next2);
|
|
1133
|
+
return this.#commitCheckIn({ ok: true, value: { state: next2, record: record2 } });
|
|
1134
|
+
}
|
|
1114
1135
|
}
|
|
1115
1136
|
const record = { stampId: spotId, acquiredAt: now };
|
|
1116
1137
|
const next = this.#reconcile({
|
|
@@ -1152,8 +1173,22 @@ var StampRallyClient = class {
|
|
|
1152
1173
|
};
|
|
1153
1174
|
const remote = this.#options.syncAdapter?.claimReward;
|
|
1154
1175
|
if (options.sync !== false && remote !== void 0) {
|
|
1155
|
-
|
|
1156
|
-
|
|
1176
|
+
try {
|
|
1177
|
+
const result = await remote(request);
|
|
1178
|
+
return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
if (this.#offlineQueue === void 0) throw error;
|
|
1181
|
+
await this.#offlineQueue.enqueueClaimReward(request);
|
|
1182
|
+
const next2 = {
|
|
1183
|
+
...current,
|
|
1184
|
+
rewards: current.rewards.map(
|
|
1185
|
+
(item) => item.rewardId === rewardId ? local.value : item
|
|
1186
|
+
),
|
|
1187
|
+
updatedAt: now
|
|
1188
|
+
};
|
|
1189
|
+
await this.#storage.save(next2);
|
|
1190
|
+
return this.#commitClaim({ ok: true, value: { state: next2, reward: local.value } });
|
|
1191
|
+
}
|
|
1157
1192
|
}
|
|
1158
1193
|
const next = {
|
|
1159
1194
|
...current,
|
|
@@ -1167,6 +1202,18 @@ var StampRallyClient = class {
|
|
|
1167
1202
|
sync(adapter = this.#options.syncAdapter) {
|
|
1168
1203
|
return this.#enqueue(async () => {
|
|
1169
1204
|
const current = await this.initialize();
|
|
1205
|
+
if (this.#offlineQueue !== void 0 && adapter !== void 0) {
|
|
1206
|
+
await this.#offlineQueue.sync(async (operation) => {
|
|
1207
|
+
if (operation.kind === "checkIn") {
|
|
1208
|
+
if (adapter.checkIn === void 0)
|
|
1209
|
+
throw new Error("No check-in sync adapter is configured.");
|
|
1210
|
+
return adapter.checkIn(operation.request);
|
|
1211
|
+
}
|
|
1212
|
+
if (adapter.claimReward === void 0)
|
|
1213
|
+
throw new Error("No reward sync adapter is configured.");
|
|
1214
|
+
return adapter.claimReward(operation.request);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1170
1217
|
if (adapter?.sync === void 0) {
|
|
1171
1218
|
this.#emitEvent({ type: "sync", state: current });
|
|
1172
1219
|
return;
|
|
@@ -1180,6 +1227,9 @@ var StampRallyClient = class {
|
|
|
1180
1227
|
this.#emitEvent({ type: "sync", state: next });
|
|
1181
1228
|
});
|
|
1182
1229
|
}
|
|
1230
|
+
retrySync() {
|
|
1231
|
+
return this.sync();
|
|
1232
|
+
}
|
|
1183
1233
|
reset() {
|
|
1184
1234
|
return this.#enqueue(async () => {
|
|
1185
1235
|
await this.#storage.remove(this.#config.id, this.#userId);
|
|
@@ -1214,7 +1264,7 @@ var StampRallyClient = class {
|
|
|
1214
1264
|
return Promise.resolve(next);
|
|
1215
1265
|
}
|
|
1216
1266
|
#reconcile(state) {
|
|
1217
|
-
const ids = new Set(this.#config.spots.map((
|
|
1267
|
+
const ids = new Set(this.#config.spots.map((spot2) => spot2.id));
|
|
1218
1268
|
const records = state.records.filter(
|
|
1219
1269
|
(record, index, all) => ids.has(record.stampId) && all.findIndex((candidate) => candidate.stampId === record.stampId) === index
|
|
1220
1270
|
);
|
|
@@ -1261,6 +1311,205 @@ var StampRallyClient = class {
|
|
|
1261
1311
|
}
|
|
1262
1312
|
};
|
|
1263
1313
|
|
|
1314
|
+
// src/client/offlineQueue.ts
|
|
1315
|
+
var MemoryQueueStorage = class {
|
|
1316
|
+
#values = /* @__PURE__ */ new Map();
|
|
1317
|
+
async load(key) {
|
|
1318
|
+
return this.#values.get(key) ?? [];
|
|
1319
|
+
}
|
|
1320
|
+
async save(key, operations) {
|
|
1321
|
+
this.#values.set(key, structuredClone(operations));
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
var LocalStorageQueueStorage = class {
|
|
1325
|
+
constructor(storage) {
|
|
1326
|
+
this.storage = storage;
|
|
1327
|
+
}
|
|
1328
|
+
storage;
|
|
1329
|
+
async load(key) {
|
|
1330
|
+
const value = this.storage.getItem(key);
|
|
1331
|
+
if (value === null) return [];
|
|
1332
|
+
try {
|
|
1333
|
+
const parsed = JSON.parse(value);
|
|
1334
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1335
|
+
} catch {
|
|
1336
|
+
return [];
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
async save(key, operations) {
|
|
1340
|
+
this.storage.setItem(key, JSON.stringify(operations));
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
var IndexedDBOfflineQueueStorage = class {
|
|
1344
|
+
#providedFactory;
|
|
1345
|
+
#databaseName;
|
|
1346
|
+
#databasePromise = null;
|
|
1347
|
+
constructor(options = {}) {
|
|
1348
|
+
this.#providedFactory = options.indexedDB;
|
|
1349
|
+
this.#databaseName = options.databaseName ?? "stamprally-offline-queue";
|
|
1350
|
+
}
|
|
1351
|
+
async load(key) {
|
|
1352
|
+
const database = await this.#open();
|
|
1353
|
+
return new Promise((resolve, reject) => {
|
|
1354
|
+
const request = database.transaction("operations", "readonly").objectStore("operations").get(key);
|
|
1355
|
+
request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
|
|
1356
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to read offline queue."));
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
async save(key, operations) {
|
|
1360
|
+
const database = await this.#open();
|
|
1361
|
+
return new Promise((resolve, reject) => {
|
|
1362
|
+
const transaction = database.transaction("operations", "readwrite");
|
|
1363
|
+
transaction.objectStore("operations").put(structuredClone(operations), key);
|
|
1364
|
+
transaction.oncomplete = () => resolve();
|
|
1365
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to save offline queue."));
|
|
1366
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("Offline queue write aborted."));
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
#open() {
|
|
1370
|
+
if (this.#databasePromise !== null) return this.#databasePromise;
|
|
1371
|
+
let factory = this.#providedFactory;
|
|
1372
|
+
if (factory === void 0)
|
|
1373
|
+
factory = globalThis.indexedDB;
|
|
1374
|
+
if (factory === void 0 || factory === null)
|
|
1375
|
+
return Promise.reject(new Error("IndexedDB is unavailable in this environment."));
|
|
1376
|
+
this.#databasePromise = new Promise((resolve, reject) => {
|
|
1377
|
+
const request = factory.open(this.#databaseName, 1);
|
|
1378
|
+
request.onupgradeneeded = () => {
|
|
1379
|
+
if (!request.result.objectStoreNames.contains("operations"))
|
|
1380
|
+
request.result.createObjectStore("operations");
|
|
1381
|
+
};
|
|
1382
|
+
request.onsuccess = () => resolve(request.result);
|
|
1383
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to open offline queue."));
|
|
1384
|
+
}).catch((error) => {
|
|
1385
|
+
this.#databasePromise = null;
|
|
1386
|
+
throw error;
|
|
1387
|
+
});
|
|
1388
|
+
return this.#databasePromise;
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
function defaultStorage(databaseName) {
|
|
1392
|
+
try {
|
|
1393
|
+
const indexedDB = globalThis.indexedDB;
|
|
1394
|
+
if (indexedDB !== void 0)
|
|
1395
|
+
return new IndexedDBOfflineQueueStorage({
|
|
1396
|
+
indexedDB,
|
|
1397
|
+
...databaseName === void 0 ? {} : { databaseName }
|
|
1398
|
+
});
|
|
1399
|
+
const storage = globalThis.localStorage;
|
|
1400
|
+
if (storage !== void 0 && storage !== null) return new LocalStorageQueueStorage(storage);
|
|
1401
|
+
} catch {
|
|
1402
|
+
}
|
|
1403
|
+
return new MemoryQueueStorage();
|
|
1404
|
+
}
|
|
1405
|
+
function operationId(operation) {
|
|
1406
|
+
return operation.kind === "checkIn" ? `checkIn:${operation.request.rallyId}:${operation.request.userId ?? "anonymous"}:${operation.request.idempotencyKey}` : `claimReward:${operation.request.rallyId}:${operation.request.userId ?? "anonymous"}:${operation.request.idempotencyKey}`;
|
|
1407
|
+
}
|
|
1408
|
+
var OfflineQueue = class {
|
|
1409
|
+
#storage;
|
|
1410
|
+
#key;
|
|
1411
|
+
#conflictPolicy;
|
|
1412
|
+
#onSyncConflict;
|
|
1413
|
+
#operations = [];
|
|
1414
|
+
#loaded = false;
|
|
1415
|
+
#state = "idle";
|
|
1416
|
+
#error = null;
|
|
1417
|
+
#sender;
|
|
1418
|
+
#syncPromise = null;
|
|
1419
|
+
constructor(options = {}) {
|
|
1420
|
+
if (options.storage !== void 0) this.#storage = options.storage;
|
|
1421
|
+
else if (options.storageLike !== void 0 && options.storageLike !== null)
|
|
1422
|
+
this.#storage = new LocalStorageQueueStorage(options.storageLike);
|
|
1423
|
+
else this.#storage = defaultStorage(options.databaseName);
|
|
1424
|
+
this.#key = options.key ?? "stamprally:offline-queue";
|
|
1425
|
+
this.#conflictPolicy = options.conflictPolicy ?? "server_wins";
|
|
1426
|
+
this.#onSyncConflict = options.onSyncConflict;
|
|
1427
|
+
}
|
|
1428
|
+
get syncState() {
|
|
1429
|
+
return this.#state;
|
|
1430
|
+
}
|
|
1431
|
+
get pendingCount() {
|
|
1432
|
+
return this.#operations.length;
|
|
1433
|
+
}
|
|
1434
|
+
get error() {
|
|
1435
|
+
return this.#error;
|
|
1436
|
+
}
|
|
1437
|
+
get operations() {
|
|
1438
|
+
return this.#operations;
|
|
1439
|
+
}
|
|
1440
|
+
async initialize() {
|
|
1441
|
+
if (this.#loaded) return;
|
|
1442
|
+
this.#operations = [...await this.#storage.load(this.#key)];
|
|
1443
|
+
this.#loaded = true;
|
|
1444
|
+
}
|
|
1445
|
+
setSender(sender) {
|
|
1446
|
+
this.#sender = sender;
|
|
1447
|
+
}
|
|
1448
|
+
async enqueue(operation) {
|
|
1449
|
+
await this.initialize();
|
|
1450
|
+
const id2 = operationId(operation);
|
|
1451
|
+
if (this.#operations.some((item) => operationId(item) === id2)) return;
|
|
1452
|
+
this.#operations = [...this.#operations, operation];
|
|
1453
|
+
await this.#storage.save(this.#key, this.#operations);
|
|
1454
|
+
}
|
|
1455
|
+
async enqueueCheckIn(request) {
|
|
1456
|
+
return this.enqueue({ kind: "checkIn", request });
|
|
1457
|
+
}
|
|
1458
|
+
async enqueueClaimReward(request) {
|
|
1459
|
+
return this.enqueue({ kind: "claimReward", request });
|
|
1460
|
+
}
|
|
1461
|
+
async clear() {
|
|
1462
|
+
await this.initialize();
|
|
1463
|
+
this.#operations = [];
|
|
1464
|
+
await this.#storage.save(this.#key, this.#operations);
|
|
1465
|
+
}
|
|
1466
|
+
async sync(sender = this.#sender) {
|
|
1467
|
+
await this.initialize();
|
|
1468
|
+
if (sender === void 0) throw new Error("OfflineQueue.sync requires a sender.");
|
|
1469
|
+
if (this.#syncPromise !== null) return this.#syncPromise;
|
|
1470
|
+
this.#sender = sender;
|
|
1471
|
+
this.#syncPromise = this.#run(sender).finally(() => {
|
|
1472
|
+
this.#syncPromise = null;
|
|
1473
|
+
});
|
|
1474
|
+
return this.#syncPromise;
|
|
1475
|
+
}
|
|
1476
|
+
async retrySync(sender = this.#sender) {
|
|
1477
|
+
return this.sync(sender);
|
|
1478
|
+
}
|
|
1479
|
+
async #run(sender) {
|
|
1480
|
+
this.#state = "syncing";
|
|
1481
|
+
this.#error = null;
|
|
1482
|
+
try {
|
|
1483
|
+
while (this.#operations.length > 0) {
|
|
1484
|
+
const operation = this.#operations[0];
|
|
1485
|
+
if (operation === void 0) break;
|
|
1486
|
+
let result;
|
|
1487
|
+
try {
|
|
1488
|
+
result = await sender(operation);
|
|
1489
|
+
} catch (cause) {
|
|
1490
|
+
throw cause instanceof Error ? cause : new Error(String(cause));
|
|
1491
|
+
}
|
|
1492
|
+
if ("conflict" in result && result.conflict === true)
|
|
1493
|
+
await this.resolveConflict(operation, result.localState, result.serverState);
|
|
1494
|
+
this.#operations = this.#operations.slice(1);
|
|
1495
|
+
await this.#storage.save(this.#key, this.#operations);
|
|
1496
|
+
}
|
|
1497
|
+
this.#state = "idle";
|
|
1498
|
+
} catch (cause) {
|
|
1499
|
+
this.#state = "error";
|
|
1500
|
+
this.#error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1501
|
+
throw this.#error;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
async resolveConflict(operation, localState, serverState) {
|
|
1505
|
+
const configured = this.#onSyncConflict;
|
|
1506
|
+
const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
|
|
1507
|
+
if (policy === "merge") {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1264
1513
|
// src/crypto/token.ts
|
|
1265
1514
|
var encoder = new TextEncoder();
|
|
1266
1515
|
var decoder = new TextDecoder();
|
|
@@ -1393,22 +1642,6 @@ async function decryptPayload(body, secret) {
|
|
|
1393
1642
|
);
|
|
1394
1643
|
}
|
|
1395
1644
|
|
|
1396
|
-
// src/domain/i18n.ts
|
|
1397
|
-
function resolveLocalizedText(text, locale, fallbackLocale) {
|
|
1398
|
-
if (text === void 0 || text === "") return "";
|
|
1399
|
-
if (typeof text === "string") return text;
|
|
1400
|
-
const fallback = fallbackLocale === void 0 ? Object.values(text).find((value) => typeof value === "string") : text[fallbackLocale];
|
|
1401
|
-
return text[locale] || fallback || "";
|
|
1402
|
-
}
|
|
1403
|
-
function toLocalizedString(text) {
|
|
1404
|
-
if (text === void 0) return { ja: "", en: "" };
|
|
1405
|
-
return typeof text === "string" ? { ja: text, en: "" } : {
|
|
1406
|
-
ja: text["ja"] ?? "",
|
|
1407
|
-
en: text["en"] ?? "",
|
|
1408
|
-
...text
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
1645
|
// src/domain/models.ts
|
|
1413
1646
|
var DEFAULT_SHEET_THEME = {
|
|
1414
1647
|
primaryColor: "#9e551e",
|
|
@@ -1420,23 +1653,23 @@ var DEFAULT_SHEET_THEME = {
|
|
|
1420
1653
|
unclaimedOpacity: 1,
|
|
1421
1654
|
fontFamily: "serif"
|
|
1422
1655
|
};
|
|
1423
|
-
function publicCondition(
|
|
1424
|
-
switch (
|
|
1656
|
+
function publicCondition(condition2) {
|
|
1657
|
+
switch (condition2.type) {
|
|
1425
1658
|
case "qr":
|
|
1426
|
-
return
|
|
1659
|
+
return condition2.qrEntryUrl === void 0 ? { type: "qr" } : { type: "qr", qrEntryUrl: condition2.qrEntryUrl };
|
|
1427
1660
|
case "passcode":
|
|
1428
1661
|
return { type: "passcode" };
|
|
1429
1662
|
case "gps":
|
|
1430
1663
|
return {
|
|
1431
1664
|
type: "gps",
|
|
1432
|
-
latitude:
|
|
1433
|
-
longitude:
|
|
1434
|
-
radiusMeters:
|
|
1665
|
+
latitude: condition2.latitude,
|
|
1666
|
+
longitude: condition2.longitude,
|
|
1667
|
+
radiusMeters: condition2.radiusMeters
|
|
1435
1668
|
};
|
|
1436
1669
|
case "nfc":
|
|
1437
1670
|
return { type: "nfc" };
|
|
1438
1671
|
case "custom":
|
|
1439
|
-
return { type: "custom", validatorName:
|
|
1672
|
+
return { type: "custom", validatorName: condition2.validatorName };
|
|
1440
1673
|
}
|
|
1441
1674
|
}
|
|
1442
1675
|
function toPublicConfig(config) {
|
|
@@ -1446,14 +1679,14 @@ function toPublicConfig(config) {
|
|
|
1446
1679
|
title: config.title,
|
|
1447
1680
|
...config.description === void 0 ? {} : { description: config.description },
|
|
1448
1681
|
...config.theme === void 0 ? {} : { theme: config.theme },
|
|
1449
|
-
spots: config.spots.map((
|
|
1450
|
-
...
|
|
1451
|
-
conditions:
|
|
1682
|
+
spots: config.spots.map((spot2) => ({
|
|
1683
|
+
...spot2,
|
|
1684
|
+
conditions: spot2.conditions.map(publicCondition)
|
|
1452
1685
|
})),
|
|
1453
1686
|
rewards: config.rewards.map(
|
|
1454
|
-
({ digitalContentUrl: _content, staffPasscode: _passcode, ...
|
|
1687
|
+
({ digitalContentUrl: _content, staffPasscode: _passcode, ...reward2 }) => reward2
|
|
1455
1688
|
),
|
|
1456
|
-
...config.metadata === void 0 ? {} : { metadata: config.metadata },
|
|
1689
|
+
...config.publicMetadata !== void 0 ? { metadata: config.publicMetadata } : config.metadata === void 0 ? {} : { metadata: config.metadata },
|
|
1457
1690
|
...config.serverEndpoint === void 0 ? {} : { serverEndpoint: config.serverEndpoint }
|
|
1458
1691
|
};
|
|
1459
1692
|
}
|
|
@@ -1489,15 +1722,15 @@ function isPublicConfig(value) {
|
|
|
1489
1722
|
}
|
|
1490
1723
|
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))
|
|
1491
1724
|
return false;
|
|
1492
|
-
return candidate.spots.every((
|
|
1493
|
-
if (typeof
|
|
1494
|
-
const item =
|
|
1725
|
+
return candidate.spots.every((spot2) => {
|
|
1726
|
+
if (typeof spot2 !== "object" || spot2 === null || Array.isArray(spot2)) return false;
|
|
1727
|
+
const item = spot2;
|
|
1495
1728
|
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)
|
|
1496
1729
|
return false;
|
|
1497
|
-
return Array.isArray(item.conditions) && item.conditions.every((
|
|
1498
|
-
if (typeof
|
|
1730
|
+
return Array.isArray(item.conditions) && item.conditions.every((condition2) => {
|
|
1731
|
+
if (typeof condition2 !== "object" || condition2 === null || Array.isArray(condition2))
|
|
1499
1732
|
return false;
|
|
1500
|
-
const value2 =
|
|
1733
|
+
const value2 = condition2;
|
|
1501
1734
|
if ("secretToken" in value2 || "code" in value2 || "tagId" in value2 || "secretParams" in value2)
|
|
1502
1735
|
return false;
|
|
1503
1736
|
const type = value2.type;
|
|
@@ -1505,13 +1738,125 @@ function isPublicConfig(value) {
|
|
|
1505
1738
|
if (type === "custom") return typeof value2.validatorName === "string";
|
|
1506
1739
|
return type === "gps" && typeof value2.latitude === "number" && typeof value2.longitude === "number" && typeof value2.radiusMeters === "number";
|
|
1507
1740
|
});
|
|
1508
|
-
}) && candidate.rewards.every((
|
|
1509
|
-
if (typeof
|
|
1510
|
-
const item =
|
|
1741
|
+
}) && candidate.rewards.every((reward2) => {
|
|
1742
|
+
if (typeof reward2 !== "object" || reward2 === null || Array.isArray(reward2)) return false;
|
|
1743
|
+
const item = reward2;
|
|
1511
1744
|
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);
|
|
1512
1745
|
});
|
|
1513
1746
|
}
|
|
1514
1747
|
|
|
1748
|
+
// src/domain/configTransform.ts
|
|
1749
|
+
var PRIVATE_KEYS = /* @__PURE__ */ new Set([
|
|
1750
|
+
"staffPasscode",
|
|
1751
|
+
"serverMetadata",
|
|
1752
|
+
"inventory",
|
|
1753
|
+
"secretToken",
|
|
1754
|
+
"secretParams",
|
|
1755
|
+
"digitalContentUrl",
|
|
1756
|
+
"code",
|
|
1757
|
+
"tagId"
|
|
1758
|
+
]);
|
|
1759
|
+
var SENSITIVE_KEY_PATTERN = /^(?:api[_-]?key|access[_-]?token|auth[_-]?token|password|private[_-]?key|secret)$/i;
|
|
1760
|
+
function isRecord2(value) {
|
|
1761
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1762
|
+
}
|
|
1763
|
+
function isPrivateKey(key) {
|
|
1764
|
+
return PRIVATE_KEYS.has(key) || SENSITIVE_KEY_PATTERN.test(key);
|
|
1765
|
+
}
|
|
1766
|
+
function sanitizeValue(value, customFilter, seen) {
|
|
1767
|
+
if (Array.isArray(value)) {
|
|
1768
|
+
if (seen.has(value)) return void 0;
|
|
1769
|
+
seen.add(value);
|
|
1770
|
+
const items = value.map((item) => sanitizeValue(item, customFilter, seen)).filter((item) => item !== void 0);
|
|
1771
|
+
return items;
|
|
1772
|
+
}
|
|
1773
|
+
if (!isRecord2(value)) return value;
|
|
1774
|
+
if (seen.has(value)) return void 0;
|
|
1775
|
+
seen.add(value);
|
|
1776
|
+
const result = {};
|
|
1777
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1778
|
+
if (isPrivateKey(key) || customFilter?.(key, item) === false) continue;
|
|
1779
|
+
const sanitized = sanitizeValue(item, customFilter, seen);
|
|
1780
|
+
if (sanitized !== void 0) result[key] = sanitized;
|
|
1781
|
+
}
|
|
1782
|
+
return result;
|
|
1783
|
+
}
|
|
1784
|
+
function sanitizeSpot(spot2, customFilter) {
|
|
1785
|
+
return sanitizeValue(
|
|
1786
|
+
toPublicConfig({
|
|
1787
|
+
id: "__spot__",
|
|
1788
|
+
version: "1",
|
|
1789
|
+
title: "__spot__",
|
|
1790
|
+
spots: [spot2],
|
|
1791
|
+
rewards: []
|
|
1792
|
+
}).spots[0],
|
|
1793
|
+
customFilter,
|
|
1794
|
+
/* @__PURE__ */ new WeakSet()
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
function sanitizeReward(reward2, customFilter) {
|
|
1798
|
+
return sanitizeValue(
|
|
1799
|
+
toPublicConfig({
|
|
1800
|
+
id: "__reward__",
|
|
1801
|
+
version: "1",
|
|
1802
|
+
title: "__reward__",
|
|
1803
|
+
spots: [],
|
|
1804
|
+
rewards: [reward2]
|
|
1805
|
+
}).rewards[0],
|
|
1806
|
+
customFilter,
|
|
1807
|
+
/* @__PURE__ */ new WeakSet()
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
function sanitizeAdminConfig(admin, customFilter) {
|
|
1811
|
+
const publicConfig = toPublicConfig(admin);
|
|
1812
|
+
const sanitized = sanitizeValue(publicConfig, customFilter, /* @__PURE__ */ new WeakSet());
|
|
1813
|
+
sanitized.spots = admin.spots.map((spot2) => sanitizeSpot(spot2, customFilter));
|
|
1814
|
+
sanitized.rewards = admin.rewards.map((reward2) => sanitizeReward(reward2, customFilter));
|
|
1815
|
+
return sanitized;
|
|
1816
|
+
}
|
|
1817
|
+
function validatePublicConfigSafety(publicConfig) {
|
|
1818
|
+
const leakedKeys = [];
|
|
1819
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1820
|
+
const visit = (value, path) => {
|
|
1821
|
+
if (Array.isArray(value)) {
|
|
1822
|
+
value.forEach((item, index) => {
|
|
1823
|
+
visit(item, `${path}[${index}]`);
|
|
1824
|
+
});
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (!isRecord2(value) || seen.has(value)) return;
|
|
1828
|
+
seen.add(value);
|
|
1829
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1830
|
+
const nextPath = path === "$" ? key : `${path}.${key}`;
|
|
1831
|
+
if (isPrivateKey(key)) leakedKeys.push(nextPath);
|
|
1832
|
+
visit(item, nextPath);
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
visit(publicConfig, "$");
|
|
1836
|
+
return { safe: leakedKeys.length === 0, leakedKeys };
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/domain/i18n.ts
|
|
1840
|
+
function updateLocalizedField(current, locale, newValue) {
|
|
1841
|
+
if (typeof current === "string" || current === void 0)
|
|
1842
|
+
return { [locale]: newValue };
|
|
1843
|
+
return { ...current, [locale]: newValue };
|
|
1844
|
+
}
|
|
1845
|
+
function resolveLocalizedText(text, locale, fallbackLocale) {
|
|
1846
|
+
if (text === void 0 || text === "") return "";
|
|
1847
|
+
if (typeof text === "string") return text;
|
|
1848
|
+
const fallback = fallbackLocale === void 0 ? Object.values(text).find((value) => typeof value === "string") : text[fallbackLocale];
|
|
1849
|
+
return text[locale] || fallback || "";
|
|
1850
|
+
}
|
|
1851
|
+
function toLocalizedString(text) {
|
|
1852
|
+
if (text === void 0) return { ja: "", en: "" };
|
|
1853
|
+
return typeof text === "string" ? { ja: text, en: "" } : {
|
|
1854
|
+
ja: text["ja"] ?? "",
|
|
1855
|
+
en: text["en"] ?? "",
|
|
1856
|
+
...text
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1515
1860
|
// src/domain/themePresets.ts
|
|
1516
1861
|
var THEME_PRESETS = [
|
|
1517
1862
|
{
|
|
@@ -1611,6 +1956,326 @@ var THEME_PRESETS = [
|
|
|
1611
1956
|
}
|
|
1612
1957
|
];
|
|
1613
1958
|
|
|
1959
|
+
// src/domain/validation.ts
|
|
1960
|
+
var ConfigValidationError = class extends Error {
|
|
1961
|
+
constructor(errors) {
|
|
1962
|
+
super(errors.map((error) => `${error.path}: ${error.message}`).join("; "));
|
|
1963
|
+
this.errors = errors;
|
|
1964
|
+
}
|
|
1965
|
+
errors;
|
|
1966
|
+
name = "ConfigValidationError";
|
|
1967
|
+
};
|
|
1968
|
+
function isRecord3(value) {
|
|
1969
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1970
|
+
}
|
|
1971
|
+
function hasOwn(value, key) {
|
|
1972
|
+
return Object.hasOwn(value, key);
|
|
1973
|
+
}
|
|
1974
|
+
function add(errors, path, message, code) {
|
|
1975
|
+
errors.push({ path, message, code });
|
|
1976
|
+
}
|
|
1977
|
+
function requiredString(value, key, path, errors) {
|
|
1978
|
+
if (typeof value[key] !== "string" || value[key].length === 0) {
|
|
1979
|
+
add(errors, `${path}.${key}`, "A non-empty string is required.", "required_string");
|
|
1980
|
+
return false;
|
|
1981
|
+
}
|
|
1982
|
+
return true;
|
|
1983
|
+
}
|
|
1984
|
+
function optionalString(value, key, path, errors) {
|
|
1985
|
+
if (hasOwn(value, key) && value[key] !== void 0 && typeof value[key] !== "string")
|
|
1986
|
+
add(errors, `${path}.${key}`, "Expected a string.", "invalid_type");
|
|
1987
|
+
}
|
|
1988
|
+
function optionalBoolean(value, key, path, errors) {
|
|
1989
|
+
if (hasOwn(value, key) && value[key] !== void 0 && typeof value[key] !== "boolean")
|
|
1990
|
+
add(errors, `${path}.${key}`, "Expected a boolean.", "invalid_type");
|
|
1991
|
+
}
|
|
1992
|
+
function finiteNumber(value, key, path, errors, minimum) {
|
|
1993
|
+
const item = value[key];
|
|
1994
|
+
if (typeof item !== "number" || !Number.isFinite(item)) {
|
|
1995
|
+
add(errors, `${path}.${key}`, "Expected a finite number.", "invalid_number");
|
|
1996
|
+
} else if (minimum !== void 0 && item < minimum) {
|
|
1997
|
+
add(
|
|
1998
|
+
errors,
|
|
1999
|
+
`${path}.${key}`,
|
|
2000
|
+
`Expected a number greater than or equal to ${minimum}.`,
|
|
2001
|
+
"out_of_range"
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function localizedText(value, path, errors) {
|
|
2006
|
+
if (typeof value === "string") return;
|
|
2007
|
+
if (!isRecord3(value)) {
|
|
2008
|
+
add(errors, path, "Expected a string or a locale map.", "invalid_localized_text");
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
for (const [locale, text] of Object.entries(value)) {
|
|
2012
|
+
if (typeof text !== "string")
|
|
2013
|
+
add(errors, `${path}.${locale}`, "Expected a string.", "invalid_type");
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
function theme(value, path, errors) {
|
|
2017
|
+
if (!isRecord3(value)) {
|
|
2018
|
+
add(errors, path, "Expected a theme object.", "invalid_type");
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
for (const key of ["primaryColor", "cardBackgroundColor", "textColor"])
|
|
2022
|
+
requiredString(value, key, path, errors);
|
|
2023
|
+
optionalString(value, "backgroundColor", path, errors);
|
|
2024
|
+
optionalString(value, "backgroundImageUrl", path, errors);
|
|
2025
|
+
optionalString(value, "completedStampColor", path, errors);
|
|
2026
|
+
optionalString(value, "fontFamily", path, errors);
|
|
2027
|
+
const slotShape = value.slotShape;
|
|
2028
|
+
if (slotShape !== "circle" && slotShape !== "square" && slotShape !== "rounded")
|
|
2029
|
+
add(errors, `${path}.slotShape`, "Expected circle, square, or rounded.", "invalid_enum");
|
|
2030
|
+
finiteNumber(value, "gridColumns", path, errors, 1);
|
|
2031
|
+
if (typeof value.gridColumns === "number" && !Number.isInteger(value.gridColumns))
|
|
2032
|
+
add(errors, `${path}.gridColumns`, "Expected an integer.", "invalid_integer");
|
|
2033
|
+
if (hasOwn(value, "unclaimedOpacity")) finiteNumber(value, "unclaimedOpacity", path, errors, 0);
|
|
2034
|
+
}
|
|
2035
|
+
function externalReferences(value, path, errors) {
|
|
2036
|
+
if (!Array.isArray(value)) {
|
|
2037
|
+
add(errors, path, "Expected an array.", "invalid_type");
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
value.forEach((item, index) => {
|
|
2041
|
+
const itemPath = `${path}[${index}]`;
|
|
2042
|
+
if (!isRecord3(item)) {
|
|
2043
|
+
add(errors, itemPath, "Expected an object.", "invalid_type");
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
requiredString(item, "type", itemPath, errors);
|
|
2047
|
+
requiredString(item, "id", itemPath, errors);
|
|
2048
|
+
optionalString(item, "url", itemPath, errors);
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
function condition(value, path, errors, isPublic) {
|
|
2052
|
+
if (!isRecord3(value)) {
|
|
2053
|
+
add(errors, path, "Expected a condition object.", "invalid_type");
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
const type = value.type;
|
|
2057
|
+
if (type === "qr") {
|
|
2058
|
+
if (isPublic) {
|
|
2059
|
+
if (hasOwn(value, "secretToken"))
|
|
2060
|
+
add(errors, `${path}.secretToken`, "Private field is not allowed.", "private_field");
|
|
2061
|
+
} else requiredString(value, "secretToken", path, errors);
|
|
2062
|
+
optionalString(value, "qrEntryUrl", path, errors);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (type === "passcode") {
|
|
2066
|
+
if (isPublic) {
|
|
2067
|
+
for (const key of ["code", "caseSensitive"])
|
|
2068
|
+
if (hasOwn(value, key))
|
|
2069
|
+
add(errors, `${path}.${key}`, "Private field is not allowed.", "private_field");
|
|
2070
|
+
} else {
|
|
2071
|
+
requiredString(value, "code", path, errors);
|
|
2072
|
+
optionalBoolean(value, "caseSensitive", path, errors);
|
|
2073
|
+
}
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (type === "gps") {
|
|
2077
|
+
finiteNumber(value, "latitude", path, errors);
|
|
2078
|
+
finiteNumber(value, "longitude", path, errors);
|
|
2079
|
+
finiteNumber(value, "radiusMeters", path, errors, 0);
|
|
2080
|
+
if (typeof value.latitude === "number" && (value.latitude < -90 || value.latitude > 90))
|
|
2081
|
+
add(errors, `${path}.latitude`, "Expected a latitude between -90 and 90.", "out_of_range");
|
|
2082
|
+
if (typeof value.longitude === "number" && (value.longitude < -180 || value.longitude > 180))
|
|
2083
|
+
add(
|
|
2084
|
+
errors,
|
|
2085
|
+
`${path}.longitude`,
|
|
2086
|
+
"Expected a longitude between -180 and 180.",
|
|
2087
|
+
"out_of_range"
|
|
2088
|
+
);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
if (type === "nfc") {
|
|
2092
|
+
if (isPublic) {
|
|
2093
|
+
if (hasOwn(value, "tagId"))
|
|
2094
|
+
add(errors, `${path}.tagId`, "Private field is not allowed.", "private_field");
|
|
2095
|
+
} else requiredString(value, "tagId", path, errors);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (type === "custom") {
|
|
2099
|
+
requiredString(value, "validatorName", path, errors);
|
|
2100
|
+
if (isPublic && hasOwn(value, "secretParams"))
|
|
2101
|
+
add(errors, `${path}.secretParams`, "Private field is not allowed.", "private_field");
|
|
2102
|
+
if (!isPublic && hasOwn(value, "secretParams") && !isRecord3(value.secretParams))
|
|
2103
|
+
add(errors, `${path}.secretParams`, "Expected an object.", "invalid_type");
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
add(errors, `${path}.type`, "Unknown condition type.", "invalid_enum");
|
|
2107
|
+
}
|
|
2108
|
+
function unlockCondition(value, path, errors) {
|
|
2109
|
+
if (!isRecord3(value)) {
|
|
2110
|
+
add(errors, path, "Expected an unlock condition object.", "invalid_type");
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
if (value.type === "stamp_count") {
|
|
2114
|
+
finiteNumber(value, "count", path, errors, 0);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (value.type === "stamps") {
|
|
2118
|
+
if (!Array.isArray(value.stampIds))
|
|
2119
|
+
add(errors, `${path}.stampIds`, "Expected an array.", "invalid_type");
|
|
2120
|
+
else
|
|
2121
|
+
value.stampIds.forEach((item, index) => {
|
|
2122
|
+
if (typeof item !== "string" || item.length === 0)
|
|
2123
|
+
add(
|
|
2124
|
+
errors,
|
|
2125
|
+
`${path}.stampIds[${index}]`,
|
|
2126
|
+
"Expected a non-empty string.",
|
|
2127
|
+
"required_string"
|
|
2128
|
+
);
|
|
2129
|
+
});
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
if (value.type === "all" || value.type === "any") {
|
|
2133
|
+
if (!Array.isArray(value.conditions))
|
|
2134
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
2135
|
+
else
|
|
2136
|
+
value.conditions.forEach((item, index) => {
|
|
2137
|
+
unlockCondition(item, `${path}.conditions[${index}]`, errors);
|
|
2138
|
+
});
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
add(errors, `${path}.type`, "Unknown unlock condition type.", "invalid_enum");
|
|
2142
|
+
}
|
|
2143
|
+
function spot(value, path, errors, isPublic) {
|
|
2144
|
+
if (!isRecord3(value)) {
|
|
2145
|
+
add(errors, path, "Expected a spot object.", "invalid_type");
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
requiredString(value, "id", path, errors);
|
|
2149
|
+
finiteNumber(value, "orderIndex", path, errors, 0);
|
|
2150
|
+
if (typeof value.orderIndex === "number" && !Number.isInteger(value.orderIndex))
|
|
2151
|
+
add(errors, `${path}.orderIndex`, "Expected an integer.", "invalid_integer");
|
|
2152
|
+
localizedText(value.name, `${path}.name`, errors);
|
|
2153
|
+
for (const key of ["description", "hint"])
|
|
2154
|
+
if (hasOwn(value, key)) localizedText(value[key], `${path}.${key}`, errors);
|
|
2155
|
+
for (const key of ["imageUrl", "iconUrl", "redirectUrlAfterClaim"])
|
|
2156
|
+
optionalString(value, key, path, errors);
|
|
2157
|
+
if (hasOwn(value, "externalReferences") && value.externalReferences !== void 0)
|
|
2158
|
+
externalReferences(value.externalReferences, `${path}.externalReferences`, errors);
|
|
2159
|
+
if (hasOwn(value, "prerequisites") && value.prerequisites !== void 0) {
|
|
2160
|
+
if (!Array.isArray(value.prerequisites))
|
|
2161
|
+
add(errors, `${path}.prerequisites`, "Expected an array.", "invalid_type");
|
|
2162
|
+
else
|
|
2163
|
+
value.prerequisites.forEach((item, index) => {
|
|
2164
|
+
if (typeof item !== "string" || item.length === 0)
|
|
2165
|
+
add(
|
|
2166
|
+
errors,
|
|
2167
|
+
`${path}.prerequisites[${index}]`,
|
|
2168
|
+
"Expected a non-empty string.",
|
|
2169
|
+
"required_string"
|
|
2170
|
+
);
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
if (!Array.isArray(value.conditions))
|
|
2174
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
2175
|
+
else
|
|
2176
|
+
value.conditions.forEach((item, index) => {
|
|
2177
|
+
condition(item, `${path}.conditions[${index}]`, errors, isPublic);
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
function reward(value, path, errors, isPublic) {
|
|
2181
|
+
if (!isRecord3(value)) {
|
|
2182
|
+
add(errors, path, "Expected a reward object.", "invalid_type");
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
requiredString(value, "id", path, errors);
|
|
2186
|
+
localizedText(value.title, `${path}.title`, errors);
|
|
2187
|
+
if (hasOwn(value, "description")) localizedText(value.description, `${path}.description`, errors);
|
|
2188
|
+
if (value.type !== "digital" && value.type !== "in_person")
|
|
2189
|
+
add(errors, `${path}.type`, "Unknown reward type.", "invalid_enum");
|
|
2190
|
+
if (!["manual_slide", "staff_passcode", "view_only", "server_claim"].includes(
|
|
2191
|
+
String(value.redemptionMethod)
|
|
2192
|
+
))
|
|
2193
|
+
add(errors, `${path}.redemptionMethod`, "Unknown redemption method.", "invalid_enum");
|
|
2194
|
+
finiteNumber(value, "requiredStampCount", path, errors, 0);
|
|
2195
|
+
for (const key of ["stockLimit", "userClaimLimit"]) {
|
|
2196
|
+
if (hasOwn(value, key) && value[key] !== void 0) {
|
|
2197
|
+
finiteNumber(value, key, path, errors, 0);
|
|
2198
|
+
if (typeof value[key] === "number" && !Number.isInteger(value[key]))
|
|
2199
|
+
add(errors, `${path}.${key}`, "Expected an integer.", "invalid_integer");
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
optionalString(value, "validUntil", path, errors);
|
|
2203
|
+
if (typeof value.validUntil === "string" && Number.isNaN(Date.parse(value.validUntil)))
|
|
2204
|
+
add(errors, `${path}.validUntil`, "Expected a valid date string.", "invalid_date");
|
|
2205
|
+
if (isPublic) {
|
|
2206
|
+
for (const key of ["staffPasscode", "digitalContentUrl"])
|
|
2207
|
+
if (hasOwn(value, key))
|
|
2208
|
+
add(errors, `${path}.${key}`, "Private field is not allowed.", "private_field");
|
|
2209
|
+
} else {
|
|
2210
|
+
optionalString(value, "staffPasscode", path, errors);
|
|
2211
|
+
optionalString(value, "digitalContentUrl", path, errors);
|
|
2212
|
+
}
|
|
2213
|
+
if (hasOwn(value, "conditions") && value.conditions !== void 0) {
|
|
2214
|
+
if (!Array.isArray(value.conditions))
|
|
2215
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
2216
|
+
else
|
|
2217
|
+
value.conditions.forEach((item, index) => {
|
|
2218
|
+
unlockCondition(item, `${path}.conditions[${index}]`, errors);
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
function validate(value, isPublic) {
|
|
2223
|
+
const errors = [];
|
|
2224
|
+
if (!isRecord3(value)) {
|
|
2225
|
+
add(errors, "$", "Expected a configuration object.", "invalid_type");
|
|
2226
|
+
return errors;
|
|
2227
|
+
}
|
|
2228
|
+
requiredString(value, "id", "$", errors);
|
|
2229
|
+
requiredString(value, "version", "$", errors);
|
|
2230
|
+
localizedText(value.title, "$.title", errors);
|
|
2231
|
+
if (hasOwn(value, "description")) localizedText(value.description, "$.description", errors);
|
|
2232
|
+
if (hasOwn(value, "theme") && value.theme !== void 0) theme(value.theme, "$.theme", errors);
|
|
2233
|
+
if (!Array.isArray(value.spots)) add(errors, "spots", "Expected an array.", "invalid_type");
|
|
2234
|
+
else
|
|
2235
|
+
value.spots.forEach((item, index) => {
|
|
2236
|
+
spot(item, `spots[${index}]`, errors, isPublic);
|
|
2237
|
+
});
|
|
2238
|
+
if (!Array.isArray(value.rewards)) add(errors, "rewards", "Expected an array.", "invalid_type");
|
|
2239
|
+
else
|
|
2240
|
+
value.rewards.forEach((item, index) => {
|
|
2241
|
+
reward(item, `rewards[${index}]`, errors, isPublic);
|
|
2242
|
+
});
|
|
2243
|
+
if (!isPublic) {
|
|
2244
|
+
optionalString(value, "staffPasscode", "$", errors);
|
|
2245
|
+
if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord3(value.inventory))
|
|
2246
|
+
add(errors, "$.inventory", "Expected an object.", "invalid_type");
|
|
2247
|
+
if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord3(value.serverMetadata))
|
|
2248
|
+
add(errors, "$.serverMetadata", "Expected an object.", "invalid_type");
|
|
2249
|
+
if (hasOwn(value, "publicMetadata") && value.publicMetadata !== void 0 && !isRecord3(value.publicMetadata))
|
|
2250
|
+
add(errors, "$.publicMetadata", "Expected an object.", "invalid_type");
|
|
2251
|
+
optionalString(value, "serverEndpoint", "$", errors);
|
|
2252
|
+
} else {
|
|
2253
|
+
for (const key of ["staffPasscode", "serverMetadata", "inventory"])
|
|
2254
|
+
if (hasOwn(value, key))
|
|
2255
|
+
add(errors, `$.${key}`, "Private field is not allowed.", "private_field");
|
|
2256
|
+
optionalString(value, "serverEndpoint", "$", errors);
|
|
2257
|
+
}
|
|
2258
|
+
return errors;
|
|
2259
|
+
}
|
|
2260
|
+
function safeParseAdminConfig(input) {
|
|
2261
|
+
const errors = validate(input, false);
|
|
2262
|
+
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2263
|
+
}
|
|
2264
|
+
function parseAdminConfig(input) {
|
|
2265
|
+
const result = safeParseAdminConfig(input);
|
|
2266
|
+
if (!result.success) throw new ConfigValidationError(result.errors);
|
|
2267
|
+
return result.data;
|
|
2268
|
+
}
|
|
2269
|
+
function safeParsePublicConfig(input) {
|
|
2270
|
+
const errors = validate(input, true);
|
|
2271
|
+
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2272
|
+
}
|
|
2273
|
+
function parsePublicConfig(input) {
|
|
2274
|
+
const result = safeParsePublicConfig(input);
|
|
2275
|
+
if (!result.success) throw new ConfigValidationError(result.errors);
|
|
2276
|
+
return result.data;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
1614
2279
|
// src/security/snapshotToken.ts
|
|
1615
2280
|
var encoder2 = new TextEncoder();
|
|
1616
2281
|
function cryptoApi2() {
|
|
@@ -1730,6 +2395,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1730
2395
|
}
|
|
1731
2396
|
}
|
|
1732
2397
|
|
|
1733
|
-
export { 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, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, storageKey, toLocalizedString, toPublicConfig, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
2398
|
+
export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, exportProgressToken, getCurrentGeoContext, getOrderedSpots, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, safeParseAdminConfig, safeParsePublicConfig, sanitizeAdminConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, validatePublicConfigSafety, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
1734
2399
|
//# sourceMappingURL=index.js.map
|
|
1735
2400
|
//# sourceMappingURL=index.js.map
|