@stamprally/core 0.8.0 → 0.10.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 +687 -1274
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +280 -670
- package/dist/index.d.ts +280 -670
- package/dist/index.js +677 -1263
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2,252 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
// src/engine/evaluate.ts
|
|
4
4
|
var EARTH_RADIUS_METERS = 6371e3;
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const latitudeARadians = toRadians(latitudeA);
|
|
12
|
-
const latitudeBRadians = toRadians(latitudeB);
|
|
13
|
-
const haversine = Math.sin(latitudeDelta / 2) ** 2 + Math.cos(latitudeARadians) * Math.cos(latitudeBRadians) * Math.sin(longitudeDelta / 2) ** 2;
|
|
14
|
-
return 2 * EARTH_RADIUS_METERS * Math.asin(Math.sqrt(Math.min(1, haversine)));
|
|
15
|
-
}
|
|
16
|
-
function isValidCoordinate(latitude, longitude) {
|
|
17
|
-
return Number.isFinite(latitude) && latitude >= -90 && latitude <= 90 && Number.isFinite(longitude) && longitude >= -180 && longitude <= 180;
|
|
18
|
-
}
|
|
19
|
-
function contextTypeMismatch(conditionType, expectedContextType, actualContextType) {
|
|
20
|
-
return {
|
|
21
|
-
ok: false,
|
|
22
|
-
error: {
|
|
23
|
-
code: "CONDITION_MISMATCH",
|
|
24
|
-
conditionType,
|
|
25
|
-
reason: "CONTEXT_TYPE_MISMATCH",
|
|
26
|
-
expectedContextType,
|
|
27
|
-
actualContextType
|
|
28
|
-
}
|
|
29
|
-
};
|
|
5
|
+
function calculateDistanceMeters(aLat, aLon, bLat, bLon) {
|
|
6
|
+
const radians = (value2) => value2 * Math.PI / 180;
|
|
7
|
+
const dLat = radians(bLat - aLat);
|
|
8
|
+
const dLon = radians(bLon - aLon);
|
|
9
|
+
const value = Math.sin(dLat / 2) ** 2 + Math.cos(radians(aLat)) * Math.cos(radians(bLat)) * Math.sin(dLon / 2) ** 2;
|
|
10
|
+
return 2 * EARTH_RADIUS_METERS * Math.asin(Math.sqrt(Math.min(1, value)));
|
|
30
11
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
12
|
+
function mismatch(conditionType, reason, extra = {}) {
|
|
13
|
+
return { ok: false, error: { code: "CONDITION_MISMATCH", conditionType, reason, ...extra } };
|
|
33
14
|
}
|
|
34
|
-
function evaluateConditionDetailed(condition2, context
|
|
15
|
+
function evaluateConditionDetailed(condition2, context) {
|
|
35
16
|
switch (condition2.type) {
|
|
36
|
-
case "
|
|
37
|
-
return context.type === "
|
|
38
|
-
case "
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
case "geo": {
|
|
51
|
-
if (context.type !== "geo") {
|
|
52
|
-
return contextTypeMismatch("geo", "geo", context.type);
|
|
53
|
-
}
|
|
54
|
-
if (!isValidCoordinate(condition2.latitude, condition2.longitude) || !isValidCoordinate(context.currentLatitude, context.currentLongitude) || !Number.isFinite(condition2.radiusMeters) || condition2.radiusMeters < 0) {
|
|
55
|
-
return {
|
|
56
|
-
ok: false,
|
|
57
|
-
error: {
|
|
58
|
-
code: "CONDITION_MISMATCH",
|
|
59
|
-
conditionType: "geo",
|
|
60
|
-
reason: "INVALID_GEO_INPUT"
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
const distanceMeters2 = calculateDistanceMeters(
|
|
17
|
+
case "qr":
|
|
18
|
+
return context.type === "qr" && context.token === condition2.secretToken ? { ok: true, value: { conditionType: "qr" } } : mismatch("qr", "INVALID_PROOF");
|
|
19
|
+
case "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
|
+
case "nfc":
|
|
22
|
+
return context.type === "nfc" && context.tagId === condition2.tagId ? { ok: true, value: { conditionType: "nfc" } } : mismatch("nfc", "INVALID_PROOF");
|
|
23
|
+
case "custom":
|
|
24
|
+
return mismatch("custom", "VALIDATOR_FAILED");
|
|
25
|
+
case "gps": {
|
|
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
|
+
return mismatch("gps", "INVALID_GEO_INPUT");
|
|
28
|
+
const distanceMeters = calculateDistanceMeters(
|
|
65
29
|
condition2.latitude,
|
|
66
30
|
condition2.longitude,
|
|
67
|
-
context.
|
|
68
|
-
context.
|
|
31
|
+
context.latitude,
|
|
32
|
+
context.longitude
|
|
69
33
|
);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
ok: false,
|
|
75
|
-
error: {
|
|
76
|
-
code: "CONDITION_MISMATCH",
|
|
77
|
-
conditionType: "geo",
|
|
78
|
-
reason: "OUTSIDE_RADIUS",
|
|
79
|
-
distanceMeters: distanceMeters2,
|
|
80
|
-
radiusMeters: condition2.radiusMeters,
|
|
81
|
-
differenceMeters: distanceMeters2 - condition2.radiusMeters
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
case "composite": {
|
|
86
|
-
if (context.type !== "composite") {
|
|
87
|
-
return contextTypeMismatch("composite", "composite", context.type);
|
|
88
|
-
}
|
|
89
|
-
if (condition2.conditions.length !== context.contexts.length) {
|
|
90
|
-
return {
|
|
91
|
-
ok: false,
|
|
92
|
-
error: {
|
|
93
|
-
code: "CONDITION_MISMATCH",
|
|
94
|
-
conditionType: "composite",
|
|
95
|
-
reason: "CONTEXT_LENGTH_MISMATCH",
|
|
96
|
-
expectedCount: condition2.conditions.length,
|
|
97
|
-
actualCount: context.contexts.length
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
const failures = [];
|
|
102
|
-
let matchedCount = 0;
|
|
103
|
-
for (const [index, childCondition] of condition2.conditions.entries()) {
|
|
104
|
-
const childContext = context.contexts[index];
|
|
105
|
-
if (childContext === void 0) continue;
|
|
106
|
-
const result = evaluateConditionDetailed(childCondition, childContext, now2);
|
|
107
|
-
if (result.ok) matchedCount += 1;
|
|
108
|
-
else failures.push({ index, error: result.error });
|
|
109
|
-
}
|
|
110
|
-
if (condition2.operator === "AND" && failures.length === 0) {
|
|
111
|
-
return { ok: true, value: { conditionType: "composite" } };
|
|
112
|
-
}
|
|
113
|
-
if (condition2.operator === "OR" && matchedCount > 0) {
|
|
114
|
-
return { ok: true, value: { conditionType: "composite" } };
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
ok: false,
|
|
118
|
-
error: {
|
|
119
|
-
code: "CONDITION_MISMATCH",
|
|
120
|
-
conditionType: "composite",
|
|
121
|
-
reason: condition2.operator === "AND" ? "AND_CHILD_FAILED" : "OR_ALL_FAILED",
|
|
122
|
-
failures
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
case "time_window": {
|
|
127
|
-
const startsAt = Date.parse(condition2.startsAt);
|
|
128
|
-
const endsAt = Date.parse(condition2.endsAt);
|
|
129
|
-
const currentTime = Date.parse(now2);
|
|
130
|
-
if (!Number.isFinite(currentTime)) {
|
|
131
|
-
return {
|
|
132
|
-
ok: false,
|
|
133
|
-
error: {
|
|
134
|
-
code: "CONDITION_MISMATCH",
|
|
135
|
-
conditionType: "time_window",
|
|
136
|
-
reason: "INVALID_NOW",
|
|
137
|
-
now: now2,
|
|
138
|
-
startsAt: condition2.startsAt,
|
|
139
|
-
endsAt: condition2.endsAt
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
if (!Number.isFinite(startsAt) || !Number.isFinite(endsAt) || startsAt > endsAt) {
|
|
144
|
-
return {
|
|
145
|
-
ok: false,
|
|
146
|
-
error: {
|
|
147
|
-
code: "CONDITION_MISMATCH",
|
|
148
|
-
conditionType: "time_window",
|
|
149
|
-
reason: "INVALID_TIME_WINDOW",
|
|
150
|
-
now: now2,
|
|
151
|
-
startsAt: condition2.startsAt,
|
|
152
|
-
endsAt: condition2.endsAt
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
if (currentTime < startsAt || currentTime > endsAt) {
|
|
157
|
-
return {
|
|
158
|
-
ok: false,
|
|
159
|
-
error: {
|
|
160
|
-
code: "CONDITION_MISMATCH",
|
|
161
|
-
conditionType: "time_window",
|
|
162
|
-
reason: currentTime < startsAt ? "BEFORE_START" : "AFTER_END",
|
|
163
|
-
now: now2,
|
|
164
|
-
startsAt: condition2.startsAt,
|
|
165
|
-
endsAt: condition2.endsAt
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
const childResult = evaluateConditionDetailed(condition2.condition, context, now2);
|
|
170
|
-
return childResult.ok ? { ok: true, value: { conditionType: "time_window" } } : childResult;
|
|
34
|
+
return distanceMeters <= condition2.radiusMeters ? { ok: true, value: { conditionType: "gps", distanceMeters } } : mismatch("gps", "OUTSIDE_RADIUS", {
|
|
35
|
+
distanceMeters,
|
|
36
|
+
radiusMeters: condition2.radiusMeters
|
|
37
|
+
});
|
|
171
38
|
}
|
|
172
|
-
default:
|
|
173
|
-
return assertNever(condition2);
|
|
174
39
|
}
|
|
175
40
|
}
|
|
176
|
-
function evaluateCondition(condition2, context
|
|
177
|
-
return evaluateConditionDetailed(condition2, context
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// src/engine/checkIn.ts
|
|
181
|
-
function unwrapContext(input) {
|
|
182
|
-
if ("verificationContext" in input) {
|
|
183
|
-
return {
|
|
184
|
-
context: input.verificationContext,
|
|
185
|
-
now: input.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
186
|
-
...input.expectedStampId === void 0 ? {} : { expectedStampId: input.expectedStampId },
|
|
187
|
-
alreadyClaimed: input.alreadyClaimed ?? false
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
return {
|
|
191
|
-
context: input,
|
|
192
|
-
now: "now" in input && typeof input.now === "string" ? input.now : (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
-
..."expectedStampId" in input && typeof input.expectedStampId === "string" ? { expectedStampId: input.expectedStampId } : {},
|
|
194
|
-
alreadyClaimed: "alreadyClaimed" in input && input.alreadyClaimed === true
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
function evaluateCheckIn(spot, input) {
|
|
198
|
-
const context = unwrapContext(input);
|
|
199
|
-
if (context.alreadyClaimed) {
|
|
200
|
-
return {
|
|
201
|
-
ok: false,
|
|
202
|
-
success: false,
|
|
203
|
-
code: "ALREADY_CLAIMED",
|
|
204
|
-
stampId: spot.id,
|
|
205
|
-
message: "This spot has already been claimed."
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
if (context.expectedStampId !== void 0 && context.expectedStampId !== spot.id) {
|
|
209
|
-
return {
|
|
210
|
-
ok: false,
|
|
211
|
-
success: false,
|
|
212
|
-
code: "ORDER_VIOLATION",
|
|
213
|
-
stampId: spot.id,
|
|
214
|
-
message: `The next required spot is '${context.expectedStampId}'.`
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
const evaluated = evaluateConditionDetailed(spot.condition, context.context, context.now);
|
|
218
|
-
if (evaluated.ok) {
|
|
219
|
-
return { ok: true, success: true, stampId: spot.id, checkedAt: context.now };
|
|
220
|
-
}
|
|
221
|
-
const code = evaluated.error.reason === "OUTSIDE_RADIUS" ? "OUT_OF_RANGE" : evaluated.error.reason === "BEFORE_START" || evaluated.error.reason === "AFTER_END" ? "EXPIRED" : evaluated.error.reason === "TOKEN_MISMATCH" ? "INVALID_PROOF" : "INVALID_CONTEXT";
|
|
222
|
-
return { ok: false, success: false, code, stampId: spot.id, message: evaluated.error.reason };
|
|
41
|
+
function evaluateCondition(condition2, context) {
|
|
42
|
+
return evaluateConditionDetailed(condition2, context).ok;
|
|
223
43
|
}
|
|
224
44
|
|
|
225
45
|
// src/engine/order.ts
|
|
226
|
-
function
|
|
227
|
-
return
|
|
228
|
-
const orderDifference = (left.stamp.orderIndex ?? left.stamp.order ?? Number.POSITIVE_INFINITY) - (right.stamp.orderIndex ?? right.stamp.order ?? Number.POSITIVE_INFINITY);
|
|
229
|
-
return orderDifference === 0 ? left.index - right.index : orderDifference;
|
|
230
|
-
}).map(({ stamp }) => stamp);
|
|
46
|
+
function getOrderedSpots(spots) {
|
|
47
|
+
return [...spots].sort((left, right) => left.orderIndex - right.orderIndex);
|
|
231
48
|
}
|
|
232
49
|
|
|
233
50
|
// src/engine/progress.ts
|
|
234
51
|
function calculateProgress(state, config) {
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
state.records.map((record) => record.stampId).filter((
|
|
52
|
+
const ids = new Set(config.spots.map((spot2) => spot2.id));
|
|
53
|
+
const acquired = new Set(
|
|
54
|
+
state.records.map((record) => record.stampId).filter((id2) => ids.has(id2))
|
|
238
55
|
);
|
|
239
|
-
const
|
|
240
|
-
const acquired = acquiredStampIds.size;
|
|
241
|
-
const isCompleted = total > 0 && acquired === total;
|
|
242
|
-
const remainingStamps = config.stamps.filter((stamp) => !acquiredStampIds.has(stamp.id));
|
|
243
|
-
const nextAvailableStamps = config.isSequential === true ? getOrderedStamps(config).filter((stamp) => !acquiredStampIds.has(stamp.id)).slice(0, 1) : remainingStamps;
|
|
56
|
+
const remaining = config.spots.filter((spot2) => !acquired.has(spot2.id));
|
|
244
57
|
return {
|
|
245
|
-
acquired,
|
|
246
|
-
total,
|
|
247
|
-
percentage:
|
|
248
|
-
isCompleted,
|
|
249
|
-
|
|
250
|
-
nextAvailableStamps
|
|
58
|
+
acquired: acquired.size,
|
|
59
|
+
total: config.spots.length,
|
|
60
|
+
percentage: config.spots.length === 0 ? 0 : acquired.size / config.spots.length * 100,
|
|
61
|
+
isCompleted: config.spots.length > 0 && acquired.size === config.spots.length,
|
|
62
|
+
nextAvailableSpots: [...remaining].sort((left, right) => left.orderIndex - right.orderIndex)
|
|
251
63
|
};
|
|
252
64
|
}
|
|
253
65
|
|
|
@@ -281,9 +93,9 @@ function createUniqueClaimTicketNumber(rewardId, issuedAt) {
|
|
|
281
93
|
const timestampPart = Number.isNaN(timestamp) ? Date.now() : timestamp;
|
|
282
94
|
return `CLAIM-${rewardId}-${timestampPart}-${createRandomHash()}`;
|
|
283
95
|
}
|
|
284
|
-
function issueClaimTicketNumber(
|
|
96
|
+
function issueClaimTicketNumber(reward2, currentState, options = {}) {
|
|
285
97
|
if (currentState.claimTicketNumber !== void 0) return currentState;
|
|
286
|
-
const claimTicketNumber =
|
|
98
|
+
const claimTicketNumber = createClaimTicketNumber(reward2.id, options);
|
|
287
99
|
return { ...currentState, claimTicketNumber };
|
|
288
100
|
}
|
|
289
101
|
|
|
@@ -352,9 +164,9 @@ function getCurrentGeoContext(options = {}) {
|
|
|
352
164
|
try {
|
|
353
165
|
navigator.geolocation.getCurrentPosition(
|
|
354
166
|
(position) => {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
if (!Number.isFinite(
|
|
167
|
+
const latitude = position.coords.latitude;
|
|
168
|
+
const longitude = position.coords.longitude;
|
|
169
|
+
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90 || !Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
|
|
358
170
|
resolve({
|
|
359
171
|
ok: false,
|
|
360
172
|
error: createDetectorError(
|
|
@@ -367,7 +179,7 @@ function getCurrentGeoContext(options = {}) {
|
|
|
367
179
|
}
|
|
368
180
|
resolve({
|
|
369
181
|
ok: true,
|
|
370
|
-
value: { type: "
|
|
182
|
+
value: { type: "gps", latitude, longitude }
|
|
371
183
|
});
|
|
372
184
|
},
|
|
373
185
|
(error) => {
|
|
@@ -475,7 +287,7 @@ function readNfcContext(options = {}) {
|
|
|
475
287
|
fail(createDetectorError("nfc", "NO_TOKEN", "The NFC tag has no text token."));
|
|
476
288
|
return;
|
|
477
289
|
}
|
|
478
|
-
finish({ ok: true, value: { type: "
|
|
290
|
+
finish({ ok: true, value: { type: "nfc", tagId: token } });
|
|
479
291
|
};
|
|
480
292
|
reader.onreadingerror = (event) => fail(createDetectorError("nfc", "READ_FAILED", "The NFC tag could not be read.", event));
|
|
481
293
|
try {
|
|
@@ -492,13 +304,7 @@ function normalizePasscode(input, caseSensitive = false) {
|
|
|
492
304
|
return caseSensitive ? normalized : normalized.toUpperCase();
|
|
493
305
|
}
|
|
494
306
|
function verifyPasscode(inputCode, condition2) {
|
|
495
|
-
|
|
496
|
-
const expected = normalizePasscode(condition2.passcode, condition2.caseSensitive);
|
|
497
|
-
return input === expected ? { success: true } : {
|
|
498
|
-
success: false,
|
|
499
|
-
reason: "INVALID_PASSCODE",
|
|
500
|
-
message: "The passcode is invalid."
|
|
501
|
-
};
|
|
307
|
+
return normalizePasscode(inputCode, condition2.caseSensitive) === normalizePasscode(condition2.code, condition2.caseSensitive) ? { success: true } : { success: false, message: "The passcode is invalid." };
|
|
502
308
|
}
|
|
503
309
|
|
|
504
310
|
// src/detectors/qr.ts
|
|
@@ -605,7 +411,7 @@ async function readQrContext(videoElement, options = {}) {
|
|
|
605
411
|
if (!detection.ok) return detection;
|
|
606
412
|
const token = detection.value.find((barcode) => barcode.rawValue.length > 0)?.rawValue;
|
|
607
413
|
if (token !== void 0) {
|
|
608
|
-
return { ok: true, value: { type: "
|
|
414
|
+
return { ok: true, value: { type: "qr", token } };
|
|
609
415
|
}
|
|
610
416
|
const interval = await raceWithTermination(waitForNextScan(), termination.promise);
|
|
611
417
|
if (!interval.ok) return interval;
|
|
@@ -623,153 +429,89 @@ async function readQrContext(videoElement, options = {}) {
|
|
|
623
429
|
}
|
|
624
430
|
|
|
625
431
|
// src/engine/transition.ts
|
|
626
|
-
function reconcileRewardStates(rewards, currentStates, acquiredStampCount,
|
|
627
|
-
const
|
|
628
|
-
return rewards.map((
|
|
629
|
-
const current =
|
|
630
|
-
if (current?.status === "CONSUMED" || current?.status === "EXPIRED")
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (
|
|
634
|
-
return { rewardId:
|
|
635
|
-
|
|
636
|
-
const stockLimit = reward.stockLimit ?? reward.maxStock;
|
|
637
|
-
if (stockLimit !== void 0 && (current?.redeemedCount ?? 0) >= stockLimit) {
|
|
638
|
-
return {
|
|
639
|
-
rewardId: reward.id,
|
|
640
|
-
status: "EXPIRED",
|
|
641
|
-
...current?.claimTicketNumber === void 0 ? {} : { claimTicketNumber: current.claimTicketNumber }
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
if (acquiredStampCount >= reward.requiredStampCount) {
|
|
645
|
-
if (current?.status === "AVAILABLE") return current;
|
|
432
|
+
function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now) {
|
|
433
|
+
const states = new Map(currentStates.map((state) => [state.rewardId, state]));
|
|
434
|
+
return rewards.map((reward2) => {
|
|
435
|
+
const current = states.get(reward2.id);
|
|
436
|
+
if (current?.status === "CONSUMED" || current?.status === "EXPIRED") return current;
|
|
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)
|
|
646
442
|
return {
|
|
647
|
-
rewardId:
|
|
443
|
+
rewardId: reward2.id,
|
|
648
444
|
status: "AVAILABLE",
|
|
649
|
-
unlockedAt: current
|
|
650
|
-
...current?.claimTicketNumber === void 0 ? {} : { claimTicketNumber: current.claimTicketNumber }
|
|
445
|
+
...current?.unlockedAt === void 0 ? { unlockedAt: now } : { unlockedAt: current.unlockedAt }
|
|
651
446
|
};
|
|
652
|
-
}
|
|
653
|
-
if (current?.status === "LOCKED" && current.unlockedAt === void 0) return current;
|
|
654
|
-
return { rewardId: reward.id, status: "LOCKED" };
|
|
447
|
+
return { rewardId: reward2.id, status: "LOCKED" };
|
|
655
448
|
});
|
|
656
449
|
}
|
|
657
450
|
function consumeReward(params) {
|
|
658
|
-
const { reward, currentState } = params;
|
|
659
|
-
if (currentState.status === "CONSUMED")
|
|
660
|
-
return {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
return {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(params.now)) {
|
|
672
|
-
return { ok: false, error: { code: "EXPIRED", reason: "EXPIRED", rewardId: reward.id } };
|
|
673
|
-
}
|
|
674
|
-
const stockLimit = reward.stockLimit ?? reward.maxStock;
|
|
675
|
-
const userClaimLimit = reward.userClaimLimit ?? reward.limitPerUser;
|
|
676
|
-
if (stockLimit !== void 0 && (currentState.redeemedCount ?? 0) >= stockLimit) {
|
|
677
|
-
return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward.id } };
|
|
678
|
-
}
|
|
679
|
-
if (userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= userClaimLimit) {
|
|
680
|
-
return {
|
|
681
|
-
ok: false,
|
|
682
|
-
error: { code: "USER_LIMIT_REACHED", reason: "LIMIT_EXCEEDED", rewardId: reward.id }
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
if (reward.redemptionMethod === "staff_passcode") {
|
|
686
|
-
const passcodeResult = reward.staffPasscode === void 0 ? null : verifyPasscode(params.inputPasscode ?? "", { passcode: reward.staffPasscode });
|
|
687
|
-
if (passcodeResult === null || !passcodeResult.success) {
|
|
451
|
+
const { reward: reward2, currentState } = params;
|
|
452
|
+
if (currentState.status === "CONSUMED")
|
|
453
|
+
return { ok: false, error: { code: "ALREADY_CONSUMED", rewardId: reward2.id } };
|
|
454
|
+
if (currentState.status !== "AVAILABLE")
|
|
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)
|
|
688
464
|
return {
|
|
689
465
|
ok: false,
|
|
690
466
|
error: {
|
|
691
467
|
code: "INVALID_PASSCODE",
|
|
692
|
-
rewardId:
|
|
693
|
-
message:
|
|
468
|
+
rewardId: reward2.id,
|
|
469
|
+
message: "The passcode is invalid."
|
|
694
470
|
}
|
|
695
471
|
};
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
if (reward.redemptionMethod === "view_only") {
|
|
699
|
-
return { ok: true, value: currentState };
|
|
700
472
|
}
|
|
473
|
+
if (reward2.redemptionMethod === "view_only") return { ok: true, value: currentState };
|
|
701
474
|
return {
|
|
702
475
|
ok: true,
|
|
703
476
|
value: {
|
|
704
477
|
...currentState,
|
|
705
478
|
status: "CONSUMED",
|
|
706
479
|
consumedAt: params.now,
|
|
707
|
-
claimTicketNumber: createUniqueClaimTicketNumber(
|
|
708
|
-
|
|
709
|
-
...params.userId === void 0 ? {} : { userRedemptionCount: (currentState.userRedemptionCount ?? 0) + 1 },
|
|
480
|
+
claimTicketNumber: createUniqueClaimTicketNumber(reward2.id, params.now),
|
|
481
|
+
redeemedCount: (currentState.redeemedCount ?? 0) + 1,
|
|
710
482
|
...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
|
|
711
483
|
}
|
|
712
484
|
};
|
|
713
485
|
}
|
|
714
|
-
function processStamp(state, config,
|
|
715
|
-
const
|
|
716
|
-
if (
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
if (
|
|
721
|
-
return {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
if (config.isSequential === true) {
|
|
727
|
-
const expectedStamp = getOrderedStamps(config).find((stamp) => !acquiredStampIds.has(stamp.id));
|
|
728
|
-
if (expectedStamp !== void 0 && expectedStamp.id !== targetStampId) {
|
|
486
|
+
function processStamp(state, config, spotId, context, now) {
|
|
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
|
+
if (state.records.some((record2) => record2.stampId === spotId))
|
|
490
|
+
return { ok: false, error: { code: "STAMP_ALREADY_ACQUIRED", spotId } };
|
|
491
|
+
const acquired = new Set(state.records.map((record2) => record2.stampId));
|
|
492
|
+
if (spot2.prerequisites?.some((id2) => !acquired.has(id2)))
|
|
493
|
+
return { ok: false, error: { code: "PREREQUISITES_NOT_MET", spotId } };
|
|
494
|
+
for (const condition2 of spot2.conditions) {
|
|
495
|
+
if (condition2.type === "custom" || !evaluateConditionDetailed(condition2, context).ok)
|
|
729
496
|
return {
|
|
730
497
|
ok: false,
|
|
731
|
-
error: {
|
|
732
|
-
code: "
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
498
|
+
error: condition2.type === "custom" ? {
|
|
499
|
+
code: "CUSTOM_VALIDATION_FAILED",
|
|
500
|
+
spotId,
|
|
501
|
+
message: "Custom validation requires an async validator."
|
|
502
|
+
} : { code: "INVALID_PROOF", spotId }
|
|
736
503
|
};
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
const conditionResult = evaluateConditionDetailed(targetStamp.condition, context, now2);
|
|
740
|
-
if (!conditionResult.ok) {
|
|
741
|
-
return {
|
|
742
|
-
ok: false,
|
|
743
|
-
error: {
|
|
744
|
-
code: "CONDITION_MISMATCH",
|
|
745
|
-
stampId: targetStampId,
|
|
746
|
-
mismatch: conditionResult.error
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
504
|
}
|
|
750
|
-
const record = { stampId:
|
|
751
|
-
const
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
};
|
|
759
|
-
const events = [{ type: "stampAcquired", record }];
|
|
760
|
-
if (nextRewards !== void 0) {
|
|
761
|
-
for (const rewardState of nextRewards) {
|
|
762
|
-
const previous = state.rewards?.find((item) => item.rewardId === rewardState.rewardId);
|
|
763
|
-
if (rewardState.status === "AVAILABLE" && previous?.status !== "AVAILABLE") {
|
|
764
|
-
events.push({ type: "rewardUnlocked", rewardId: rewardState.rewardId, unlockedAt: now2 });
|
|
765
|
-
}
|
|
505
|
+
const record = { stampId: spotId, acquiredAt: now };
|
|
506
|
+
const records = [...state.records, record];
|
|
507
|
+
const rewards = reconcileRewardStates(config.rewards, state.rewards, records.length, now);
|
|
508
|
+
return {
|
|
509
|
+
ok: true,
|
|
510
|
+
value: {
|
|
511
|
+
nextState: { ...state, records, rewards, updatedAt: now },
|
|
512
|
+
events: [{ type: "stampAcquired", record }]
|
|
766
513
|
}
|
|
767
|
-
}
|
|
768
|
-
const completed = config.stamps.length > 0 && config.stamps.every((stamp) => nextState.records.some((item) => item.stampId === stamp.id));
|
|
769
|
-
if (completed) {
|
|
770
|
-
events.push({ type: "rallyCompleted", rallyId: config.id, completedAt: now2 });
|
|
771
|
-
}
|
|
772
|
-
return { ok: true, value: { nextState, events } };
|
|
514
|
+
};
|
|
773
515
|
}
|
|
774
516
|
|
|
775
517
|
// src/client/storage.ts
|
|
@@ -815,7 +557,7 @@ function isRewardState(value) {
|
|
|
815
557
|
function isStampRallyState(value) {
|
|
816
558
|
if (typeof value !== "object" || value === null) return false;
|
|
817
559
|
const state = value;
|
|
818
|
-
return typeof state.rallyId === "string" && typeof state.updatedAt === "string" && Array.isArray(state.records) && state.records.every(isRecord) && (state.rewards === void 0 || Array.isArray(state.rewards) && state.rewards.every(isRewardState));
|
|
560
|
+
return typeof state.rallyId === "string" && (typeof state.userId === "string" || state.userId === null) && typeof state.updatedAt === "string" && Array.isArray(state.records) && state.records.every(isRecord) && (state.rewards === void 0 || Array.isArray(state.rewards) && state.rewards.every(isRewardState));
|
|
819
561
|
}
|
|
820
562
|
function isValidDate(value) {
|
|
821
563
|
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
@@ -826,7 +568,7 @@ function isSnapshotRecord(value) {
|
|
|
826
568
|
function isRallySnapshot(value) {
|
|
827
569
|
if (typeof value !== "object" || value === null) return false;
|
|
828
570
|
const snapshot = value;
|
|
829
|
-
return snapshot.version === 1 && typeof snapshot.rallyId === "string" && Array.isArray(snapshot.
|
|
571
|
+
return snapshot.version === 1 && typeof snapshot.rallyId === "string" && (typeof snapshot.userId === "string" || snapshot.userId === null) && Array.isArray(snapshot.records) && snapshot.records.every(isSnapshotRecord) && Array.isArray(snapshot.rewards) && snapshot.rewards.every(isRewardState) && isValidDate(snapshot.exportedAt);
|
|
830
572
|
}
|
|
831
573
|
function exportProgressToken(snapshot) {
|
|
832
574
|
return globalThis.btoa(encodeURIComponent(JSON.stringify(snapshot)));
|
|
@@ -837,7 +579,7 @@ function importProgressToken(token, currentRallyId) {
|
|
|
837
579
|
if (!isRallySnapshot(parsed) || parsed.rallyId !== currentRallyId) return null;
|
|
838
580
|
return {
|
|
839
581
|
...parsed,
|
|
840
|
-
|
|
582
|
+
records: parsed.records.map(cloneRecord),
|
|
841
583
|
rewards: parsed.rewards.map(cloneRewardState)
|
|
842
584
|
};
|
|
843
585
|
} catch {
|
|
@@ -846,17 +588,20 @@ function importProgressToken(token, currentRallyId) {
|
|
|
846
588
|
}
|
|
847
589
|
var InMemoryStorage = class {
|
|
848
590
|
#states = /* @__PURE__ */ new Map();
|
|
849
|
-
async load(rallyId) {
|
|
850
|
-
const state = this.#states.get(rallyId);
|
|
591
|
+
async load(rallyId, userId) {
|
|
592
|
+
const state = this.#states.get(storageKey(rallyId, userId));
|
|
851
593
|
return state === void 0 ? null : cloneState(state);
|
|
852
594
|
}
|
|
853
595
|
async save(state) {
|
|
854
|
-
this.#states.set(state.rallyId, cloneState(state));
|
|
596
|
+
this.#states.set(storageKey(state.rallyId, state.userId), cloneState(state));
|
|
855
597
|
}
|
|
856
|
-
async remove(rallyId) {
|
|
857
|
-
this.#states.delete(rallyId);
|
|
598
|
+
async remove(rallyId, userId) {
|
|
599
|
+
this.#states.delete(storageKey(rallyId, userId));
|
|
858
600
|
}
|
|
859
601
|
};
|
|
602
|
+
function storageKey(rallyId, userId) {
|
|
603
|
+
return `stamprally:${rallyId}:${userId ?? "anonymous"}`;
|
|
604
|
+
}
|
|
860
605
|
var defaultStorageWarningHandler = (error) => {
|
|
861
606
|
console.warn(`[@stamprally/core] ${error.message}`, error);
|
|
862
607
|
};
|
|
@@ -873,10 +618,10 @@ var LocalStorageAdapter = class {
|
|
|
873
618
|
this.#failureMode = options.failureMode ?? "fallback";
|
|
874
619
|
this.#onWarning = options.onWarning ?? defaultStorageWarningHandler;
|
|
875
620
|
}
|
|
876
|
-
async load(rallyId) {
|
|
877
|
-
if (this.#isFallbackActive) return this.#fallbackStorage.load(rallyId);
|
|
621
|
+
async load(rallyId, userId) {
|
|
622
|
+
if (this.#isFallbackActive) return this.#fallbackStorage.load(rallyId, userId);
|
|
878
623
|
try {
|
|
879
|
-
const serialized = this.#getStorage("load", rallyId).getItem(this.#key(rallyId));
|
|
624
|
+
const serialized = this.#getStorage("load", rallyId).getItem(this.#key(rallyId, userId));
|
|
880
625
|
if (serialized === null) return null;
|
|
881
626
|
let parsed;
|
|
882
627
|
try {
|
|
@@ -907,7 +652,7 @@ var LocalStorageAdapter = class {
|
|
|
907
652
|
`Failed to read rally '${rallyId}' from localStorage.`,
|
|
908
653
|
rallyId
|
|
909
654
|
),
|
|
910
|
-
() => this.#fallbackStorage.load(rallyId)
|
|
655
|
+
() => this.#fallbackStorage.load(rallyId, userId)
|
|
911
656
|
);
|
|
912
657
|
}
|
|
913
658
|
}
|
|
@@ -915,7 +660,7 @@ var LocalStorageAdapter = class {
|
|
|
915
660
|
if (this.#isFallbackActive) return this.#fallbackStorage.save(state);
|
|
916
661
|
try {
|
|
917
662
|
this.#getStorage("save", state.rallyId).setItem(
|
|
918
|
-
this.#key(state.rallyId),
|
|
663
|
+
this.#key(state.rallyId, state.userId),
|
|
919
664
|
JSON.stringify(state)
|
|
920
665
|
);
|
|
921
666
|
} catch (cause) {
|
|
@@ -931,10 +676,10 @@ var LocalStorageAdapter = class {
|
|
|
931
676
|
);
|
|
932
677
|
}
|
|
933
678
|
}
|
|
934
|
-
async remove(rallyId) {
|
|
935
|
-
if (this.#isFallbackActive) return this.#fallbackStorage.remove(rallyId);
|
|
679
|
+
async remove(rallyId, userId) {
|
|
680
|
+
if (this.#isFallbackActive) return this.#fallbackStorage.remove(rallyId, userId);
|
|
936
681
|
try {
|
|
937
|
-
this.#getStorage("remove", rallyId).removeItem(this.#key(rallyId));
|
|
682
|
+
this.#getStorage("remove", rallyId).removeItem(this.#key(rallyId, userId));
|
|
938
683
|
} catch (cause) {
|
|
939
684
|
return this.#handleFailure(
|
|
940
685
|
this.#normalizeError(
|
|
@@ -944,12 +689,12 @@ var LocalStorageAdapter = class {
|
|
|
944
689
|
`Failed to remove rally '${rallyId}' from localStorage.`,
|
|
945
690
|
rallyId
|
|
946
691
|
),
|
|
947
|
-
() => this.#fallbackStorage.remove(rallyId)
|
|
692
|
+
() => this.#fallbackStorage.remove(rallyId, userId)
|
|
948
693
|
);
|
|
949
694
|
}
|
|
950
695
|
}
|
|
951
|
-
#key(rallyId) {
|
|
952
|
-
return `${this.#keyPrefix}${rallyId}`;
|
|
696
|
+
#key(rallyId, userId) {
|
|
697
|
+
return `${this.#keyPrefix}${rallyId}:${userId ?? "anonymous"}`;
|
|
953
698
|
}
|
|
954
699
|
#getStorage(operation, rallyId) {
|
|
955
700
|
if (this.#providedStorage === null) {
|
|
@@ -1009,12 +754,12 @@ var IndexedDBAdapter = class {
|
|
|
1009
754
|
this.#providedFactory = options.indexedDB;
|
|
1010
755
|
this.#databaseName = options.databaseName ?? "stamprally";
|
|
1011
756
|
}
|
|
1012
|
-
async load(rallyId) {
|
|
757
|
+
async load(rallyId, userId) {
|
|
1013
758
|
const database = await this.#openDatabase(rallyId);
|
|
1014
759
|
return new Promise((resolve, reject) => {
|
|
1015
760
|
try {
|
|
1016
761
|
const transaction = database.transaction(INDEXED_DB_STORE_NAME, "readonly");
|
|
1017
|
-
const request = transaction.objectStore(INDEXED_DB_STORE_NAME).get(rallyId);
|
|
762
|
+
const request = transaction.objectStore(INDEXED_DB_STORE_NAME).get(storageKey(rallyId, userId));
|
|
1018
763
|
request.onsuccess = () => {
|
|
1019
764
|
const value = request.result;
|
|
1020
765
|
if (value === void 0) {
|
|
@@ -1061,7 +806,7 @@ var IndexedDBAdapter = class {
|
|
|
1061
806
|
return new Promise((resolve, reject) => {
|
|
1062
807
|
try {
|
|
1063
808
|
const transaction = database.transaction(INDEXED_DB_STORE_NAME, "readwrite");
|
|
1064
|
-
transaction.objectStore(INDEXED_DB_STORE_NAME).put(cloneState(state), state.rallyId);
|
|
809
|
+
transaction.objectStore(INDEXED_DB_STORE_NAME).put(cloneState(state), storageKey(state.rallyId, state.userId));
|
|
1065
810
|
transaction.oncomplete = () => resolve();
|
|
1066
811
|
transaction.onerror = () => {
|
|
1067
812
|
reject(
|
|
@@ -1086,12 +831,12 @@ var IndexedDBAdapter = class {
|
|
|
1086
831
|
}
|
|
1087
832
|
});
|
|
1088
833
|
}
|
|
1089
|
-
async remove(rallyId) {
|
|
834
|
+
async remove(rallyId, userId) {
|
|
1090
835
|
const database = await this.#openDatabase(rallyId);
|
|
1091
836
|
return new Promise((resolve, reject) => {
|
|
1092
837
|
try {
|
|
1093
838
|
const transaction = database.transaction(INDEXED_DB_STORE_NAME, "readwrite");
|
|
1094
|
-
transaction.objectStore(INDEXED_DB_STORE_NAME).delete(rallyId);
|
|
839
|
+
transaction.objectStore(INDEXED_DB_STORE_NAME).delete(storageKey(rallyId, userId));
|
|
1095
840
|
transaction.oncomplete = () => resolve();
|
|
1096
841
|
transaction.onerror = () => {
|
|
1097
842
|
reject(
|
|
@@ -1199,255 +944,60 @@ var IndexedDBAdapter = class {
|
|
|
1199
944
|
};
|
|
1200
945
|
|
|
1201
946
|
// src/client/client.ts
|
|
1202
|
-
var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1203
|
-
var StampRallyClient = class {
|
|
1204
|
-
#listeners = /* @__PURE__ */ new Set();
|
|
1205
|
-
#eventListeners = /* @__PURE__ */ new Set();
|
|
1206
|
-
#config;
|
|
1207
|
-
#storage;
|
|
1208
|
-
#clock;
|
|
1209
|
-
#currentState = null;
|
|
1210
|
-
#initialization = null;
|
|
1211
|
-
#operationQueue = Promise.resolve();
|
|
1212
|
-
constructor(config, storage, clock = systemClock) {
|
|
1213
|
-
this.#config = config;
|
|
1214
|
-
this.#storage = storage;
|
|
1215
|
-
this.#clock = clock;
|
|
1216
|
-
}
|
|
1217
|
-
getState() {
|
|
1218
|
-
return this.#currentState;
|
|
1219
|
-
}
|
|
1220
|
-
getConfig() {
|
|
1221
|
-
return this.#config;
|
|
1222
|
-
}
|
|
1223
|
-
subscribe(listener, options = {}) {
|
|
1224
|
-
if (options.events === true) {
|
|
1225
|
-
this.#eventListeners.add(listener);
|
|
1226
|
-
return () => this.#eventListeners.delete(listener);
|
|
1227
|
-
}
|
|
1228
|
-
this.#listeners.add(listener);
|
|
1229
|
-
return () => {
|
|
1230
|
-
this.#listeners.delete(listener);
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
subscribeEvents(listener) {
|
|
1234
|
-
this.#eventListeners.add(listener);
|
|
1235
|
-
return () => this.#eventListeners.delete(listener);
|
|
1236
|
-
}
|
|
1237
|
-
async updateConfig(newConfig) {
|
|
1238
|
-
return this.#enqueue(async () => {
|
|
1239
|
-
const current = await this.initialize();
|
|
1240
|
-
this.#config = newConfig;
|
|
1241
|
-
const next = this.#reconcileState(cloneState(current), this.#clock());
|
|
1242
|
-
await this.#storage.save(next);
|
|
1243
|
-
this.#currentState = next;
|
|
1244
|
-
this.#initialization = Promise.resolve(next);
|
|
1245
|
-
this.#emit(next);
|
|
1246
|
-
return next;
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
1249
|
-
notifyRewardClaimed(rewardId, state = this.#currentState) {
|
|
1250
|
-
if (state !== null) this.#emitEvent({ type: "rewardClaimed", rewardId, state });
|
|
1251
|
-
}
|
|
1252
|
-
notifySyncCompleted(state = this.#currentState) {
|
|
1253
|
-
if (state !== null) this.#emitEvent({ type: "syncCompleted", state });
|
|
1254
|
-
}
|
|
1255
|
-
init() {
|
|
1256
|
-
return this.initialize();
|
|
1257
|
-
}
|
|
1258
|
-
initialize() {
|
|
1259
|
-
if (this.#currentState !== null) {
|
|
1260
|
-
return Promise.resolve(this.#currentState);
|
|
1261
|
-
}
|
|
1262
|
-
if (this.#initialization === null) {
|
|
1263
|
-
this.#initialization = this.#storage.load(this.#config.id).then((storedState) => {
|
|
1264
|
-
const state = storedState === null ? this.#createEmptyState(this.#clock()) : this.#reconcileState(cloneState(storedState), storedState.updatedAt);
|
|
1265
|
-
this.#currentState = state;
|
|
1266
|
-
this.#emit(state);
|
|
1267
|
-
return state;
|
|
1268
|
-
}).catch((error) => {
|
|
1269
|
-
this.#initialization = null;
|
|
1270
|
-
throw error;
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
return this.#initialization;
|
|
1274
|
-
}
|
|
1275
|
-
acquire(stampId, context, now2 = this.#clock()) {
|
|
1276
|
-
return this.#enqueue(async () => {
|
|
1277
|
-
const currentState = await this.initialize();
|
|
1278
|
-
const result = processStamp(currentState, this.#config, stampId, context, now2);
|
|
1279
|
-
if (!result.ok) {
|
|
1280
|
-
this.#emitEvent({ type: "error", error: result.error });
|
|
1281
|
-
return result;
|
|
1282
|
-
}
|
|
1283
|
-
await this.#storage.save(result.value.nextState);
|
|
1284
|
-
this.#currentState = result.value.nextState;
|
|
1285
|
-
this.#emit(result.value.nextState);
|
|
1286
|
-
this.#emitEvent({ type: "checkIn", stampId, state: result.value.nextState });
|
|
1287
|
-
return result;
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
reset(now2 = this.#clock()) {
|
|
1291
|
-
return this.#enqueue(async () => {
|
|
1292
|
-
const initialization = this.#initialization;
|
|
1293
|
-
if (initialization !== null) {
|
|
1294
|
-
await initialization.catch(() => void 0);
|
|
1295
|
-
}
|
|
1296
|
-
await this.#storage.remove(this.#config.id);
|
|
1297
|
-
const nextState = this.#createEmptyState(now2);
|
|
1298
|
-
this.#currentState = nextState;
|
|
1299
|
-
this.#initialization = Promise.resolve(nextState);
|
|
1300
|
-
this.#emit(nextState);
|
|
1301
|
-
return nextState;
|
|
1302
|
-
});
|
|
1303
|
-
}
|
|
1304
|
-
restore(state) {
|
|
1305
|
-
return this.#enqueue(async () => {
|
|
1306
|
-
const initialization = this.#initialization;
|
|
1307
|
-
if (initialization !== null) {
|
|
1308
|
-
await initialization.catch(() => void 0);
|
|
1309
|
-
}
|
|
1310
|
-
if (state.rallyId !== this.#config.id) {
|
|
1311
|
-
throw new Error(
|
|
1312
|
-
`Cannot restore rally '${state.rallyId}' into client '${this.#config.id}'.`
|
|
1313
|
-
);
|
|
1314
|
-
}
|
|
1315
|
-
const nextState = this.#reconcileState(cloneState(state), state.updatedAt);
|
|
1316
|
-
await this.#storage.save(nextState);
|
|
1317
|
-
this.#currentState = nextState;
|
|
1318
|
-
this.#initialization = Promise.resolve(nextState);
|
|
1319
|
-
this.#emit(nextState);
|
|
1320
|
-
return nextState;
|
|
1321
|
-
});
|
|
1322
|
-
}
|
|
1323
|
-
#enqueue(operation) {
|
|
1324
|
-
const next = this.#operationQueue.then(operation, operation);
|
|
1325
|
-
this.#operationQueue = next.then(
|
|
1326
|
-
() => void 0,
|
|
1327
|
-
() => void 0
|
|
1328
|
-
);
|
|
1329
|
-
return next;
|
|
1330
|
-
}
|
|
1331
|
-
#emit(state) {
|
|
1332
|
-
for (const listener of this.#listeners) {
|
|
1333
|
-
listener(state);
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
#emitEvent(event) {
|
|
1337
|
-
for (const listener of this.#eventListeners) listener(event);
|
|
1338
|
-
}
|
|
1339
|
-
#createEmptyState(now2) {
|
|
1340
|
-
const state = {
|
|
1341
|
-
rallyId: this.#config.id,
|
|
1342
|
-
records: [],
|
|
1343
|
-
...this.#config.rewards === void 0 ? {} : {
|
|
1344
|
-
rewards: reconcileRewardStates(this.#config.rewards, [], 0, now2)
|
|
1345
|
-
},
|
|
1346
|
-
updatedAt: now2
|
|
1347
|
-
};
|
|
1348
|
-
return state;
|
|
1349
|
-
}
|
|
1350
|
-
#reconcileState(state, now2) {
|
|
1351
|
-
const configuredStampIds = new Set(this.#config.stamps.map((stamp) => stamp.id));
|
|
1352
|
-
const seenStampIds = /* @__PURE__ */ new Set();
|
|
1353
|
-
const records = state.records.filter((record) => {
|
|
1354
|
-
if (!configuredStampIds.has(record.stampId) || seenStampIds.has(record.stampId)) return false;
|
|
1355
|
-
seenStampIds.add(record.stampId);
|
|
1356
|
-
return true;
|
|
1357
|
-
});
|
|
1358
|
-
if (this.#config.rewards === void 0 && state.rewards === void 0) {
|
|
1359
|
-
return records.length === state.records.length ? state : { ...state, records };
|
|
1360
|
-
}
|
|
1361
|
-
return {
|
|
1362
|
-
...state,
|
|
1363
|
-
records,
|
|
1364
|
-
rewards: reconcileRewardStates(
|
|
1365
|
-
this.#config.rewards ?? [],
|
|
1366
|
-
state.rewards ?? [],
|
|
1367
|
-
records.length,
|
|
1368
|
-
now2
|
|
1369
|
-
)
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
};
|
|
1373
|
-
|
|
1374
|
-
// src/client/universalClient.ts
|
|
1375
947
|
function isStorage(value) {
|
|
1376
948
|
return "load" in value && "save" in value && "remove" in value;
|
|
1377
949
|
}
|
|
1378
|
-
function
|
|
950
|
+
function id(prefix) {
|
|
1379
951
|
return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
|
|
1380
952
|
}
|
|
1381
|
-
function
|
|
1382
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1383
|
-
}
|
|
1384
|
-
function text(value) {
|
|
1385
|
-
if (typeof value === "string") return value;
|
|
1386
|
-
if (typeof value === "object" && value !== null) {
|
|
1387
|
-
const first = Object.values(value).find((item) => typeof item === "string");
|
|
1388
|
-
if (typeof first === "string") return first;
|
|
1389
|
-
}
|
|
1390
|
-
return "";
|
|
1391
|
-
}
|
|
1392
|
-
function proofString(value) {
|
|
953
|
+
function proof(value) {
|
|
1393
954
|
if (typeof value === "string") return value;
|
|
1394
955
|
if (typeof value === "object" && value !== null) {
|
|
1395
|
-
const
|
|
1396
|
-
for (const key of ["token", "code", "passcode", "value"])
|
|
1397
|
-
if (typeof
|
|
1398
|
-
}
|
|
956
|
+
const item = value;
|
|
957
|
+
for (const key of ["token", "code", "passcode", "value", "tagId"])
|
|
958
|
+
if (typeof item[key] === "string") return item[key];
|
|
1399
959
|
}
|
|
1400
960
|
return "";
|
|
1401
961
|
}
|
|
1402
|
-
function
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
const latitude = typeof value.latitude === "number" ? value.latitude : Number.NaN;
|
|
1419
|
-
const longitude = typeof value.longitude === "number" ? value.longitude : Number.NaN;
|
|
1420
|
-
return Number.isFinite(latitude) && Number.isFinite(longitude) && distanceMeters(condition2.latitude, condition2.longitude, latitude, longitude) <= condition2.radiusMeters;
|
|
1421
|
-
}
|
|
1422
|
-
case "custom":
|
|
1423
|
-
return true;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
function asReward(reward) {
|
|
1427
|
-
return { ...reward, description: text(reward.description) };
|
|
1428
|
-
}
|
|
1429
|
-
function initialState(config, timestamp) {
|
|
1430
|
-
const rewards = config.rewards.map(asReward);
|
|
962
|
+
function matches(condition2, value) {
|
|
963
|
+
if (condition2.type === "gps") {
|
|
964
|
+
if (typeof value !== "object" || value === null) return false;
|
|
965
|
+
const item = value;
|
|
966
|
+
const latitude = item.latitude;
|
|
967
|
+
const longitude = item.longitude;
|
|
968
|
+
if (typeof latitude !== "number" || typeof longitude !== "number") return false;
|
|
969
|
+
const radians = (v) => v * Math.PI / 180;
|
|
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
|
+
}
|
|
975
|
+
return proof(value).trim() !== "";
|
|
976
|
+
}
|
|
977
|
+
function emptyState(config, userId, now) {
|
|
1431
978
|
return {
|
|
1432
979
|
rallyId: config.id,
|
|
980
|
+
userId,
|
|
1433
981
|
records: [],
|
|
1434
|
-
rewards: reconcileRewardStates(rewards, [], 0,
|
|
1435
|
-
updatedAt:
|
|
982
|
+
rewards: reconcileRewardStates(config.rewards, [], 0, now),
|
|
983
|
+
updatedAt: now
|
|
1436
984
|
};
|
|
1437
985
|
}
|
|
1438
|
-
var
|
|
986
|
+
var StampRallyClient = class {
|
|
1439
987
|
#listeners = /* @__PURE__ */ new Set();
|
|
1440
988
|
#eventListeners = /* @__PURE__ */ new Set();
|
|
1441
|
-
#config;
|
|
1442
989
|
#storage;
|
|
1443
990
|
#options;
|
|
991
|
+
#config;
|
|
992
|
+
#userId;
|
|
1444
993
|
#state = null;
|
|
1445
994
|
#initialization = null;
|
|
1446
995
|
#queue = Promise.resolve();
|
|
1447
|
-
constructor(config, storageOrOptions = {}
|
|
996
|
+
constructor(config, storageOrOptions = {}) {
|
|
1448
997
|
this.#config = config;
|
|
1449
|
-
this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions
|
|
998
|
+
this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions } : storageOrOptions;
|
|
1450
999
|
this.#storage = this.#options.storage ?? new InMemoryStorage();
|
|
1000
|
+
this.#userId = this.#options.userId ?? null;
|
|
1451
1001
|
}
|
|
1452
1002
|
getConfig() {
|
|
1453
1003
|
return this.#config;
|
|
@@ -1455,6 +1005,9 @@ var UniversalStampRallyClient = class {
|
|
|
1455
1005
|
getState() {
|
|
1456
1006
|
return this.#state;
|
|
1457
1007
|
}
|
|
1008
|
+
getUserId() {
|
|
1009
|
+
return this.#userId;
|
|
1010
|
+
}
|
|
1458
1011
|
subscribe(listener) {
|
|
1459
1012
|
this.#listeners.add(listener);
|
|
1460
1013
|
return () => this.#listeners.delete(listener);
|
|
@@ -1469,11 +1022,11 @@ var UniversalStampRallyClient = class {
|
|
|
1469
1022
|
initialize() {
|
|
1470
1023
|
if (this.#state !== null) return Promise.resolve(this.#state);
|
|
1471
1024
|
if (this.#initialization === null) {
|
|
1472
|
-
this.#initialization = this.#storage.load(this.#config.id).then((
|
|
1473
|
-
const
|
|
1474
|
-
this.#state =
|
|
1475
|
-
this.#emit(
|
|
1476
|
-
return
|
|
1025
|
+
this.#initialization = this.#storage.load(this.#config.id, this.#userId).then((state) => {
|
|
1026
|
+
const next = state === null ? emptyState(this.#config, this.#userId, this.#now()) : this.#reconcile(state);
|
|
1027
|
+
this.#state = next;
|
|
1028
|
+
this.#emit(next);
|
|
1029
|
+
return next;
|
|
1477
1030
|
}).catch((error) => {
|
|
1478
1031
|
this.#initialization = null;
|
|
1479
1032
|
throw error;
|
|
@@ -1481,11 +1034,31 @@ var UniversalStampRallyClient = class {
|
|
|
1481
1034
|
}
|
|
1482
1035
|
return this.#initialization;
|
|
1483
1036
|
}
|
|
1037
|
+
switchUser(newUserId) {
|
|
1038
|
+
return this.#enqueue(async () => {
|
|
1039
|
+
if (this.#userId === newUserId && this.#state !== null) return this.#state;
|
|
1040
|
+
this.#userId = newUserId;
|
|
1041
|
+
this.#state = null;
|
|
1042
|
+
this.#initialization = null;
|
|
1043
|
+
return this.initialize();
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
async getUserState(rallyId, userId) {
|
|
1047
|
+
return this.#storage.load(rallyId, userId);
|
|
1048
|
+
}
|
|
1049
|
+
clearUserState(userId = this.#userId) {
|
|
1050
|
+
return this.#enqueue(async () => {
|
|
1051
|
+
await this.#storage.remove(this.#config.id, userId);
|
|
1052
|
+
if (userId === this.#userId) {
|
|
1053
|
+
await this.#initializeFresh();
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1484
1057
|
checkIn(spotId, proofData, options = {}) {
|
|
1485
1058
|
return this.#enqueue(async () => {
|
|
1486
1059
|
const current = await this.initialize();
|
|
1487
|
-
const
|
|
1488
|
-
if (
|
|
1060
|
+
const spot2 = this.#config.spots.find((item) => item.id === spotId);
|
|
1061
|
+
if (spot2 === void 0)
|
|
1489
1062
|
return this.#fail({ code: "SPOT_NOT_FOUND", spotId, message: "Spot was not found." });
|
|
1490
1063
|
if (current.records.some((record2) => record2.stampId === spotId))
|
|
1491
1064
|
return this.#fail({
|
|
@@ -1494,70 +1067,61 @@ var UniversalStampRallyClient = class {
|
|
|
1494
1067
|
message: "Spot was already claimed."
|
|
1495
1068
|
});
|
|
1496
1069
|
const acquired = new Set(current.records.map((record2) => record2.stampId));
|
|
1497
|
-
if (
|
|
1070
|
+
if (spot2.prerequisites?.some((id2) => !acquired.has(id2)))
|
|
1498
1071
|
return this.#fail({
|
|
1499
1072
|
code: "PREREQUISITES_NOT_MET",
|
|
1500
1073
|
spotId,
|
|
1501
1074
|
message: "Prerequisite spots are not complete."
|
|
1502
1075
|
});
|
|
1503
|
-
for (const condition2 of
|
|
1076
|
+
for (const condition2 of spot2.conditions) {
|
|
1504
1077
|
if (condition2.type === "custom") {
|
|
1505
1078
|
const validator = this.#options.customValidators?.[condition2.validatorName] ?? this.#options.customValidator;
|
|
1506
|
-
if (validator
|
|
1507
|
-
try {
|
|
1508
|
-
const validation = await validator({
|
|
1509
|
-
spotId,
|
|
1510
|
-
proofData,
|
|
1511
|
-
config: this.#config,
|
|
1512
|
-
userState: current
|
|
1513
|
-
});
|
|
1514
|
-
if (!validation.success)
|
|
1515
|
-
return this.#fail({
|
|
1516
|
-
code: "CUSTOM_VALIDATION_FAILED",
|
|
1517
|
-
spotId,
|
|
1518
|
-
message: validation.error ?? "Custom validation failed."
|
|
1519
|
-
});
|
|
1520
|
-
} catch (error) {
|
|
1521
|
-
return this.#fail({
|
|
1522
|
-
code: "CUSTOM_VALIDATION_FAILED",
|
|
1523
|
-
spotId,
|
|
1524
|
-
message: error instanceof Error ? error.message : "Custom validation failed."
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
} else {
|
|
1079
|
+
if (validator === void 0)
|
|
1528
1080
|
return this.#fail({
|
|
1529
1081
|
code: "CUSTOM_VALIDATION_FAILED",
|
|
1530
1082
|
spotId,
|
|
1531
1083
|
message: "No custom validator is registered."
|
|
1532
1084
|
});
|
|
1533
|
-
|
|
1534
|
-
|
|
1085
|
+
const context = {
|
|
1086
|
+
rallyId: this.#config.id,
|
|
1087
|
+
spotId,
|
|
1088
|
+
proofData,
|
|
1089
|
+
condition: { type: "custom", validatorName: condition2.validatorName },
|
|
1090
|
+
userState: current
|
|
1091
|
+
};
|
|
1092
|
+
const result = typeof validator === "function" ? await validator(context) : await validator.validate(context);
|
|
1093
|
+
if (result === false || typeof result === "object" && !result.valid)
|
|
1094
|
+
return this.#fail({
|
|
1095
|
+
code: "CUSTOM_VALIDATION_FAILED",
|
|
1096
|
+
spotId,
|
|
1097
|
+
message: typeof result === "object" && result.message !== void 0 ? result.message : "Custom validation failed."
|
|
1098
|
+
});
|
|
1099
|
+
} else if (!matches(condition2, proofData))
|
|
1535
1100
|
return this.#fail({ code: "INVALID_PROOF", spotId, message: "Verification failed." });
|
|
1536
|
-
}
|
|
1537
1101
|
}
|
|
1538
|
-
const
|
|
1102
|
+
const now = options.now ?? this.#now();
|
|
1539
1103
|
const request = {
|
|
1540
1104
|
rallyId: this.#config.id,
|
|
1105
|
+
userId: this.#userId,
|
|
1541
1106
|
spotId,
|
|
1542
1107
|
proofData,
|
|
1543
|
-
idempotencyKey: options.idempotencyKey ??
|
|
1544
|
-
now
|
|
1108
|
+
idempotencyKey: options.idempotencyKey ?? id("check-in"),
|
|
1109
|
+
now,
|
|
1545
1110
|
state: current
|
|
1546
1111
|
};
|
|
1547
1112
|
const remote = this.#options.syncAdapter?.checkIn;
|
|
1548
1113
|
if (options.sync !== false && remote !== void 0) {
|
|
1549
1114
|
const result = await remote(request);
|
|
1550
|
-
|
|
1551
|
-
return this.#commitCheckIn(result.value.state, result);
|
|
1115
|
+
return result.ok ? this.#commitCheckIn(result) : this.#fail(result.error);
|
|
1552
1116
|
}
|
|
1553
|
-
const record = { stampId: spotId, acquiredAt:
|
|
1117
|
+
const record = { stampId: spotId, acquiredAt: now };
|
|
1554
1118
|
const next = this.#reconcile({
|
|
1555
1119
|
...current,
|
|
1556
1120
|
records: [...current.records, record],
|
|
1557
|
-
updatedAt:
|
|
1121
|
+
updatedAt: now
|
|
1558
1122
|
});
|
|
1559
1123
|
await this.#storage.save(next);
|
|
1560
|
-
return this.#commitCheckIn(
|
|
1124
|
+
return this.#commitCheckIn({ ok: true, value: { state: next, record } });
|
|
1561
1125
|
});
|
|
1562
1126
|
}
|
|
1563
1127
|
claimReward(rewardId, options = {}) {
|
|
@@ -1566,107 +1130,128 @@ var UniversalStampRallyClient = class {
|
|
|
1566
1130
|
const configured = this.#config.rewards.find((item) => item.id === rewardId);
|
|
1567
1131
|
if (configured === void 0)
|
|
1568
1132
|
return this.#fail({ code: "REWARD_NOT_FOUND", rewardId, message: "Reward was not found." });
|
|
1569
|
-
const
|
|
1570
|
-
const state = current.rewards
|
|
1133
|
+
const now = options.now ?? this.#now();
|
|
1134
|
+
const state = current.rewards.find((item) => item.rewardId === rewardId) ?? {
|
|
1571
1135
|
rewardId,
|
|
1572
1136
|
status: "LOCKED"
|
|
1573
1137
|
};
|
|
1574
1138
|
const local = consumeReward({
|
|
1575
|
-
reward:
|
|
1139
|
+
reward: configured,
|
|
1576
1140
|
currentState: state,
|
|
1577
|
-
now
|
|
1141
|
+
now,
|
|
1578
1142
|
...options.staffPasscode === void 0 ? {} : { inputPasscode: options.staffPasscode },
|
|
1579
1143
|
...options.staffId === void 0 ? {} : { staffId: options.staffId }
|
|
1580
1144
|
});
|
|
1581
1145
|
if (!local.ok) return this.#fail(local.error);
|
|
1582
1146
|
const request = {
|
|
1583
1147
|
rallyId: this.#config.id,
|
|
1148
|
+
userId: this.#userId,
|
|
1584
1149
|
rewardId,
|
|
1585
|
-
idempotencyKey: options.idempotencyKey ??
|
|
1586
|
-
now
|
|
1150
|
+
idempotencyKey: options.idempotencyKey ?? id("claim"),
|
|
1151
|
+
now,
|
|
1587
1152
|
options,
|
|
1588
1153
|
state: current
|
|
1589
1154
|
};
|
|
1590
1155
|
const remote = this.#options.syncAdapter?.claimReward;
|
|
1591
1156
|
if (options.sync !== false && remote !== void 0) {
|
|
1592
1157
|
const result = await remote(request);
|
|
1593
|
-
|
|
1594
|
-
return this.#commitClaim(result.value.state, result);
|
|
1158
|
+
return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
|
|
1595
1159
|
}
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1160
|
+
const next = {
|
|
1161
|
+
...current,
|
|
1162
|
+
rewards: current.rewards.map((item) => item.rewardId === rewardId ? local.value : item),
|
|
1163
|
+
updatedAt: now
|
|
1164
|
+
};
|
|
1600
1165
|
await this.#storage.save(next);
|
|
1601
|
-
return this.#commitClaim(
|
|
1166
|
+
return this.#commitClaim({ ok: true, value: { state: next, reward: local.value } });
|
|
1602
1167
|
});
|
|
1603
1168
|
}
|
|
1604
|
-
sync(adapter = this.#options.syncAdapter
|
|
1169
|
+
sync(adapter = this.#options.syncAdapter) {
|
|
1605
1170
|
return this.#enqueue(async () => {
|
|
1606
1171
|
const current = await this.initialize();
|
|
1607
|
-
if (adapter
|
|
1172
|
+
if (adapter?.sync === void 0) {
|
|
1608
1173
|
this.#emitEvent({ type: "sync", state: current });
|
|
1609
1174
|
return;
|
|
1610
1175
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
this.#emitEvent({ type: "sync", state: next });
|
|
1619
|
-
} catch (error) {
|
|
1620
|
-
const failure = {
|
|
1621
|
-
code: "SYNC_FAILED",
|
|
1622
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1623
|
-
};
|
|
1624
|
-
this.#emitEvent({ type: "error", error: failure });
|
|
1625
|
-
throw error;
|
|
1626
|
-
}
|
|
1176
|
+
const next = this.#reconcile(
|
|
1177
|
+
await adapter.sync({ rallyId: this.#config.id, userId: this.#userId, state: current })
|
|
1178
|
+
);
|
|
1179
|
+
await this.#storage.save(next);
|
|
1180
|
+
this.#state = next;
|
|
1181
|
+
this.#emit(next);
|
|
1182
|
+
this.#emitEvent({ type: "sync", state: next });
|
|
1627
1183
|
});
|
|
1628
1184
|
}
|
|
1629
|
-
|
|
1630
|
-
return this.#
|
|
1185
|
+
reset() {
|
|
1186
|
+
return this.#enqueue(async () => {
|
|
1187
|
+
await this.#storage.remove(this.#config.id, this.#userId);
|
|
1188
|
+
return this.#initializeFresh();
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
restore(state) {
|
|
1192
|
+
return this.#enqueue(async () => {
|
|
1193
|
+
if (state.rallyId !== this.#config.id || state.userId !== this.#userId)
|
|
1194
|
+
throw new Error("State belongs to another rally or user.");
|
|
1195
|
+
const next = this.#reconcile(state);
|
|
1196
|
+
await this.#storage.save(next);
|
|
1197
|
+
this.#state = next;
|
|
1198
|
+
this.#initialization = Promise.resolve(next);
|
|
1199
|
+
this.#emit(next);
|
|
1200
|
+
return next;
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
#enqueue(operation) {
|
|
1204
|
+
const next = this.#queue.then(operation, operation);
|
|
1205
|
+
this.#queue = next.then(
|
|
1206
|
+
() => void 0,
|
|
1207
|
+
() => void 0
|
|
1208
|
+
);
|
|
1209
|
+
return next;
|
|
1210
|
+
}
|
|
1211
|
+
#initializeFresh() {
|
|
1212
|
+
const next = emptyState(this.#config, this.#userId, this.#now());
|
|
1213
|
+
this.#state = next;
|
|
1214
|
+
this.#initialization = Promise.resolve(next);
|
|
1215
|
+
this.#emit(next);
|
|
1216
|
+
return Promise.resolve(next);
|
|
1631
1217
|
}
|
|
1632
1218
|
#reconcile(state) {
|
|
1633
|
-
const ids = new Set(this.#config.spots.map((
|
|
1634
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1219
|
+
const ids = new Set(this.#config.spots.map((spot2) => spot2.id));
|
|
1635
1220
|
const records = state.records.filter(
|
|
1636
|
-
(record) => ids.has(record.stampId) &&
|
|
1221
|
+
(record, index, all) => ids.has(record.stampId) && all.findIndex((candidate) => candidate.stampId === record.stampId) === index
|
|
1637
1222
|
);
|
|
1638
1223
|
return {
|
|
1639
1224
|
...cloneState(state),
|
|
1225
|
+
userId: this.#userId,
|
|
1640
1226
|
records,
|
|
1641
1227
|
rewards: reconcileRewardStates(
|
|
1642
|
-
this.#config.rewards
|
|
1643
|
-
state.rewards
|
|
1228
|
+
this.#config.rewards,
|
|
1229
|
+
state.rewards,
|
|
1644
1230
|
records.length,
|
|
1645
1231
|
state.updatedAt
|
|
1646
1232
|
)
|
|
1647
1233
|
};
|
|
1648
1234
|
}
|
|
1649
|
-
#
|
|
1650
|
-
|
|
1651
|
-
this.#queue = next.then(
|
|
1652
|
-
() => void 0,
|
|
1653
|
-
() => void 0
|
|
1654
|
-
);
|
|
1655
|
-
return next;
|
|
1235
|
+
#now() {
|
|
1236
|
+
return this.#options.clock?.() ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1656
1237
|
}
|
|
1657
1238
|
#fail(error) {
|
|
1658
1239
|
this.#emitEvent({ type: "error", error });
|
|
1659
1240
|
return { ok: false, error };
|
|
1660
1241
|
}
|
|
1661
|
-
#commitCheckIn(
|
|
1662
|
-
|
|
1663
|
-
|
|
1242
|
+
#commitCheckIn(result) {
|
|
1243
|
+
if (result.ok) {
|
|
1244
|
+
this.#state = result.value.state;
|
|
1245
|
+
this.#emit(this.#state);
|
|
1246
|
+
}
|
|
1664
1247
|
this.#emitEvent({ type: "checkIn", result });
|
|
1665
1248
|
return result;
|
|
1666
1249
|
}
|
|
1667
|
-
#commitClaim(
|
|
1668
|
-
|
|
1669
|
-
|
|
1250
|
+
#commitClaim(result) {
|
|
1251
|
+
if (result.ok) {
|
|
1252
|
+
this.#state = result.value.state;
|
|
1253
|
+
this.#emit(this.#state);
|
|
1254
|
+
}
|
|
1670
1255
|
this.#emitEvent({ type: "rewardClaimed", result });
|
|
1671
1256
|
return result;
|
|
1672
1257
|
}
|
|
@@ -1739,7 +1324,7 @@ async function encryptPayload(plaintext, secret, api) {
|
|
|
1739
1324
|
);
|
|
1740
1325
|
return encode(new Uint8Array([...iv, ...encrypted]));
|
|
1741
1326
|
}
|
|
1742
|
-
async function verifySecureToken(token, secretKey,
|
|
1327
|
+
async function verifySecureToken(token, secretKey, now = Date.now()) {
|
|
1743
1328
|
try {
|
|
1744
1329
|
const parts = token.split(".");
|
|
1745
1330
|
if (parts.length !== 4 || parts[0] !== "sr3" || parts[1] !== "e" && parts[1] !== "p") {
|
|
@@ -1782,7 +1367,7 @@ async function verifySecureToken(token, secretKey, now2 = Date.now()) {
|
|
|
1782
1367
|
};
|
|
1783
1368
|
}
|
|
1784
1369
|
const payload = parsed;
|
|
1785
|
-
if (typeof payload.exp === "number" &&
|
|
1370
|
+
if (typeof payload.exp === "number" && now >= payload.exp * 1e3) {
|
|
1786
1371
|
return {
|
|
1787
1372
|
ok: false,
|
|
1788
1373
|
valid: false,
|
|
@@ -1811,367 +1396,23 @@ async function decryptPayload(body, secret) {
|
|
|
1811
1396
|
}
|
|
1812
1397
|
|
|
1813
1398
|
// src/domain/i18n.ts
|
|
1814
|
-
function
|
|
1815
|
-
if (
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
if (
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
};
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
function isObject(value) {
|
|
1832
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1833
|
-
}
|
|
1834
|
-
function add(errors, path, code, message) {
|
|
1835
|
-
errors.push({ path, code, message });
|
|
1836
|
-
}
|
|
1837
|
-
function hasText(value) {
|
|
1838
|
-
if (typeof value === "string") return value.trim() !== "";
|
|
1839
|
-
if (!isObject(value)) return false;
|
|
1840
|
-
return Object.values(value).some((item) => typeof item === "string" && item.trim() !== "");
|
|
1841
|
-
}
|
|
1842
|
-
function validateCondition(value, path, errors) {
|
|
1843
|
-
if (!isObject(value) || typeof value.type !== "string") {
|
|
1844
|
-
add(errors, path, "INVALID_TYPE", "Condition must be an object with a type.");
|
|
1845
|
-
return;
|
|
1846
|
-
}
|
|
1847
|
-
switch (value.type) {
|
|
1848
|
-
case "instant":
|
|
1849
|
-
return;
|
|
1850
|
-
case "token":
|
|
1851
|
-
if (typeof value.token !== "string" || value.token.trim() === "") {
|
|
1852
|
-
add(errors, `${path}.token`, "REQUIRED", "Token must not be empty.");
|
|
1853
|
-
}
|
|
1854
|
-
return;
|
|
1855
|
-
case "geo": {
|
|
1856
|
-
const latitude = value.latitude;
|
|
1857
|
-
const longitude = value.longitude;
|
|
1858
|
-
if (typeof latitude !== "number" || !Number.isFinite(latitude) || latitude < -90 || latitude > 90 || typeof longitude !== "number" || !Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
|
|
1859
|
-
add(
|
|
1860
|
-
errors,
|
|
1861
|
-
path,
|
|
1862
|
-
"INVALID_COORDINATES",
|
|
1863
|
-
"Latitude must be between -90 and 90 and longitude between -180 and 180."
|
|
1864
|
-
);
|
|
1865
|
-
}
|
|
1866
|
-
if (typeof value.radiusMeters !== "number" || !Number.isFinite(value.radiusMeters) || value.radiusMeters <= 0) {
|
|
1867
|
-
add(errors, `${path}.radiusMeters`, "INVALID_RADIUS", "Radius must be greater than zero.");
|
|
1868
|
-
}
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
case "composite":
|
|
1872
|
-
if (value.operator !== "AND" && value.operator !== "OR") {
|
|
1873
|
-
add(errors, `${path}.operator`, "INVALID_TYPE", "Operator must be AND or OR.");
|
|
1874
|
-
}
|
|
1875
|
-
if (!Array.isArray(value.conditions)) {
|
|
1876
|
-
add(errors, `${path}.conditions`, "INVALID_TYPE", "Composite conditions must be an array.");
|
|
1877
|
-
return;
|
|
1878
|
-
}
|
|
1879
|
-
value.conditions.forEach((child, index) => {
|
|
1880
|
-
validateCondition(child, `${path}.conditions[${index}]`, errors);
|
|
1881
|
-
});
|
|
1882
|
-
return;
|
|
1883
|
-
case "time_window":
|
|
1884
|
-
if (typeof value.startsAt !== "string" || Number.isNaN(Date.parse(value.startsAt))) {
|
|
1885
|
-
add(errors, `${path}.startsAt`, "INVALID_DATE", "Start must be a valid ISO date.");
|
|
1886
|
-
}
|
|
1887
|
-
if (typeof value.endsAt !== "string" || Number.isNaN(Date.parse(value.endsAt))) {
|
|
1888
|
-
add(errors, `${path}.endsAt`, "INVALID_DATE", "End must be a valid ISO date.");
|
|
1889
|
-
}
|
|
1890
|
-
if (typeof value.startsAt === "string" && typeof value.endsAt === "string" && !Number.isNaN(Date.parse(value.startsAt)) && !Number.isNaN(Date.parse(value.endsAt)) && Date.parse(value.startsAt) >= Date.parse(value.endsAt)) {
|
|
1891
|
-
add(errors, path, "INVALID_DATE", "Time window start must be before its end.");
|
|
1892
|
-
}
|
|
1893
|
-
validateCondition(value.condition, `${path}.condition`, errors);
|
|
1894
|
-
return;
|
|
1895
|
-
default:
|
|
1896
|
-
add(errors, `${path}.type`, "INVALID_TYPE", "Unsupported condition type.");
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
function validateSpot(value, index, errors) {
|
|
1900
|
-
const path = `stamps[${index}]`;
|
|
1901
|
-
if (!isObject(value)) {
|
|
1902
|
-
add(errors, path, "INVALID_TYPE", "Spot must be an object.");
|
|
1903
|
-
return;
|
|
1904
|
-
}
|
|
1905
|
-
if (typeof value.id !== "string") add(errors, `${path}.id`, "REQUIRED", "Spot ID is required.");
|
|
1906
|
-
else if (value.id.trim() === "")
|
|
1907
|
-
add(errors, `${path}.id`, "EMPTY_STRING", "Spot ID must not be empty.");
|
|
1908
|
-
if (value.name === void 0) add(errors, `${path}.name`, "REQUIRED", "Spot name is required.");
|
|
1909
|
-
else if (!hasText(value.name))
|
|
1910
|
-
add(errors, `${path}.name`, "EMPTY_STRING", "Spot name must not be empty.");
|
|
1911
|
-
if (value.order !== void 0 && (typeof value.order !== "number" || !Number.isFinite(value.order))) {
|
|
1912
|
-
add(errors, `${path}.order`, "INVALID_TYPE", "Spot order must be a finite number.");
|
|
1913
|
-
}
|
|
1914
|
-
validateCondition(value.condition, `${path}.condition`, errors);
|
|
1915
|
-
for (const field of ["dependsOn", "requiresStampIds"]) {
|
|
1916
|
-
if (value[field] !== void 0 && (!Array.isArray(value[field]) || value[field].some((item) => typeof item !== "string"))) {
|
|
1917
|
-
add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be an array of IDs.`);
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
for (const field of ["order", "orderIndex"]) {
|
|
1921
|
-
if (value[field] !== void 0 && (typeof value[field] !== "number" || !Number.isFinite(value[field]))) {
|
|
1922
|
-
add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be a finite number.`);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
function validateReward(value, index, stampCount, errors) {
|
|
1927
|
-
const path = `rewards[${index}]`;
|
|
1928
|
-
if (!isObject(value)) {
|
|
1929
|
-
add(errors, path, "INVALID_TYPE", "Reward must be an object.");
|
|
1930
|
-
return;
|
|
1931
|
-
}
|
|
1932
|
-
if (typeof value.id !== "string" || value.id.trim() === "") {
|
|
1933
|
-
add(errors, `${path}.id`, "REQUIRED", "Reward ID must not be empty.");
|
|
1934
|
-
}
|
|
1935
|
-
if (!hasText(value.title)) {
|
|
1936
|
-
add(errors, `${path}.title`, "EMPTY_STRING", "Reward title must not be empty.");
|
|
1937
|
-
}
|
|
1938
|
-
if (!hasText(value.description)) {
|
|
1939
|
-
add(errors, `${path}.description`, "EMPTY_STRING", "Reward description must not be empty.");
|
|
1940
|
-
}
|
|
1941
|
-
const required = value.requiredStampCount;
|
|
1942
|
-
if (typeof required !== "number" || !Number.isInteger(required) || required < 0 || required > stampCount) {
|
|
1943
|
-
add(
|
|
1944
|
-
errors,
|
|
1945
|
-
`${path}.requiredStampCount`,
|
|
1946
|
-
"INVALID_REWARD",
|
|
1947
|
-
"Required stamps must be an integer within the rally."
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
if (value.validUntil !== void 0 && (typeof value.validUntil !== "string" || Number.isNaN(Date.parse(value.validUntil)))) {
|
|
1951
|
-
add(errors, `${path}.validUntil`, "INVALID_DATE", "Reward expiry must be a valid ISO date.");
|
|
1952
|
-
}
|
|
1953
|
-
for (const field of ["maxStock", "limitPerUser", "stockLimit", "userClaimLimit"]) {
|
|
1954
|
-
const count = value[field];
|
|
1955
|
-
if (count !== void 0 && (typeof count !== "number" || !Number.isInteger(count) || count <= 0)) {
|
|
1956
|
-
add(errors, `${path}.${field}`, "INVALID_REWARD", `${field} must be a positive integer.`);
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
function collectDependencies(spot) {
|
|
1961
|
-
const dependencies = /* @__PURE__ */ new Set();
|
|
1962
|
-
for (const field of ["dependsOn", "requiresStampIds"]) {
|
|
1963
|
-
const values = spot[field];
|
|
1964
|
-
if (Array.isArray(values)) {
|
|
1965
|
-
for (const value of values) if (typeof value === "string") dependencies.add(value);
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
return [...dependencies];
|
|
1969
|
-
}
|
|
1970
|
-
function validateDag(stamps, errors) {
|
|
1971
|
-
const graph = /* @__PURE__ */ new Map();
|
|
1972
|
-
for (const value of stamps) {
|
|
1973
|
-
if (!isObject(value) || typeof value.id !== "string") continue;
|
|
1974
|
-
graph.set(value.id, [...graph.get(value.id) ?? [], ...collectDependencies(value)]);
|
|
1975
|
-
}
|
|
1976
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
1977
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1978
|
-
const visit = (id, path) => {
|
|
1979
|
-
if (visiting.has(id)) {
|
|
1980
|
-
add(errors, path, "CYCLE_DETECTED", `Dependency cycle detected at '${id}'.`);
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
if (visited.has(id)) return;
|
|
1984
|
-
visiting.add(id);
|
|
1985
|
-
for (const dependency of graph.get(id) ?? [])
|
|
1986
|
-
if (graph.has(dependency)) visit(dependency, path);
|
|
1987
|
-
visiting.delete(id);
|
|
1988
|
-
visited.add(id);
|
|
1989
|
-
};
|
|
1990
|
-
for (const id of graph.keys()) visit(id, `stamps[${id}]`);
|
|
1991
|
-
}
|
|
1992
|
-
function validateRallyConfig(config) {
|
|
1993
|
-
const errors = [];
|
|
1994
|
-
if (!isObject(config))
|
|
1995
|
-
return {
|
|
1996
|
-
valid: false,
|
|
1997
|
-
errors: [{ path: "", code: "INVALID_TYPE", message: "Rally config must be an object." }]
|
|
1998
|
-
};
|
|
1999
|
-
if (typeof config.id !== "string") add(errors, "id", "REQUIRED", "Rally ID is required.");
|
|
2000
|
-
else if (config.id.trim() === "")
|
|
2001
|
-
add(errors, "id", "EMPTY_STRING", "Rally ID must not be empty.");
|
|
2002
|
-
if (config.title !== void 0 && !hasText(config.title))
|
|
2003
|
-
add(errors, "title", "EMPTY_STRING", "Rally title must not be empty.");
|
|
2004
|
-
if (config.version !== void 0 && config.version !== CURRENT_RALLY_CONFIG_VERSION) {
|
|
2005
|
-
add(
|
|
2006
|
-
errors,
|
|
2007
|
-
"version",
|
|
2008
|
-
"INVALID_VERSION",
|
|
2009
|
-
`Config version must be ${CURRENT_RALLY_CONFIG_VERSION}.`
|
|
2010
|
-
);
|
|
2011
|
-
}
|
|
2012
|
-
for (const field of ["startDate", "endDate"]) {
|
|
2013
|
-
if (config[field] !== void 0 && (typeof config[field] !== "string" || Number.isNaN(Date.parse(config[field])))) {
|
|
2014
|
-
add(errors, field, "INVALID_DATE", `${field} must be a valid ISO date.`);
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
if (typeof config.startDate === "string" && typeof config.endDate === "string" && !Number.isNaN(Date.parse(config.startDate)) && !Number.isNaN(Date.parse(config.endDate)) && Date.parse(config.startDate) >= Date.parse(config.endDate)) {
|
|
2018
|
-
add(errors, "startDate", "INVALID_DATE", "startDate must be before endDate.");
|
|
2019
|
-
}
|
|
2020
|
-
if (!Array.isArray(config.stamps)) {
|
|
2021
|
-
add(errors, "stamps", "INVALID_TYPE", "stamps must be an array.");
|
|
2022
|
-
} else {
|
|
2023
|
-
config.stamps.forEach((spot, index) => {
|
|
2024
|
-
validateSpot(spot, index, errors);
|
|
2025
|
-
});
|
|
2026
|
-
const ids = config.stamps.map(
|
|
2027
|
-
(spot) => isObject(spot) && typeof spot.id === "string" ? spot.id : ""
|
|
2028
|
-
);
|
|
2029
|
-
ids.forEach((id, index) => {
|
|
2030
|
-
if (id !== "" && ids.indexOf(id) !== index)
|
|
2031
|
-
add(errors, `stamps[${index}].id`, "DUPLICATE_ID", `Duplicate spot ID '${id}'.`);
|
|
2032
|
-
});
|
|
2033
|
-
validateDag(config.stamps, errors);
|
|
2034
|
-
}
|
|
2035
|
-
if (config.rewards !== void 0 && !Array.isArray(config.rewards)) {
|
|
2036
|
-
add(errors, "rewards", "INVALID_TYPE", "rewards must be an array.");
|
|
2037
|
-
} else if (Array.isArray(config.rewards)) {
|
|
2038
|
-
const stampCount = Array.isArray(config.stamps) ? config.stamps.length : 0;
|
|
2039
|
-
config.rewards.forEach((reward, index) => {
|
|
2040
|
-
validateReward(reward, index, stampCount, errors);
|
|
2041
|
-
});
|
|
2042
|
-
const ids = config.rewards.map(
|
|
2043
|
-
(reward) => isObject(reward) && typeof reward.id === "string" ? reward.id : ""
|
|
2044
|
-
);
|
|
2045
|
-
ids.forEach((id, index) => {
|
|
2046
|
-
if (id !== "" && ids.indexOf(id) !== index)
|
|
2047
|
-
add(errors, `rewards[${index}].id`, "DUPLICATE_ID", `Duplicate reward ID '${id}'.`);
|
|
2048
|
-
});
|
|
2049
|
-
if (Array.isArray(config.stamps)) {
|
|
2050
|
-
const stampIds = new Set(
|
|
2051
|
-
config.stamps.map((spot) => isObject(spot) && typeof spot.id === "string" ? spot.id : "")
|
|
2052
|
-
);
|
|
2053
|
-
ids.forEach((id, index) => {
|
|
2054
|
-
if (id !== "" && stampIds.has(id))
|
|
2055
|
-
add(
|
|
2056
|
-
errors,
|
|
2057
|
-
`rewards[${index}].id`,
|
|
2058
|
-
"DUPLICATE_ID",
|
|
2059
|
-
`Reward ID '${id}' is already used by a spot.`
|
|
2060
|
-
);
|
|
2061
|
-
});
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
return { valid: errors.length === 0, errors };
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
// src/domain/migration.ts
|
|
2068
|
-
function isObject2(value) {
|
|
2069
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2070
|
-
}
|
|
2071
|
-
function text2(value) {
|
|
2072
|
-
if (typeof value === "string") return value;
|
|
2073
|
-
if (!isObject2(value)) return void 0;
|
|
2074
|
-
const entries = Object.entries(value).filter(([, item]) => typeof item === "string");
|
|
2075
|
-
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
2076
|
-
}
|
|
2077
|
-
function condition(value) {
|
|
2078
|
-
if (!isObject2(value) || typeof value.type !== "string") return { type: "instant" };
|
|
2079
|
-
switch (value.type) {
|
|
2080
|
-
case "instant":
|
|
2081
|
-
return { type: "instant" };
|
|
2082
|
-
case "token":
|
|
2083
|
-
case "passcode":
|
|
2084
|
-
return {
|
|
2085
|
-
type: "token",
|
|
2086
|
-
token: typeof value.token === "string" ? value.token : typeof value.passcode === "string" ? value.passcode : ""
|
|
2087
|
-
};
|
|
2088
|
-
case "geo":
|
|
2089
|
-
return {
|
|
2090
|
-
type: "geo",
|
|
2091
|
-
latitude: typeof value.latitude === "number" ? value.latitude : 0,
|
|
2092
|
-
longitude: typeof value.longitude === "number" ? value.longitude : 0,
|
|
2093
|
-
radiusMeters: typeof value.radiusMeters === "number" ? value.radiusMeters : typeof value.radius === "number" ? value.radius : 1
|
|
2094
|
-
};
|
|
2095
|
-
case "composite":
|
|
2096
|
-
return {
|
|
2097
|
-
type: "composite",
|
|
2098
|
-
operator: value.operator === "OR" ? "OR" : "AND",
|
|
2099
|
-
conditions: Array.isArray(value.conditions) ? value.conditions.map(condition) : []
|
|
2100
|
-
};
|
|
2101
|
-
case "time_window":
|
|
2102
|
-
return {
|
|
2103
|
-
type: "time_window",
|
|
2104
|
-
startsAt: typeof value.startsAt === "string" ? value.startsAt : "1970-01-01T00:00:00.000Z",
|
|
2105
|
-
endsAt: typeof value.endsAt === "string" ? value.endsAt : "9999-12-31T23:59:59.999Z",
|
|
2106
|
-
condition: condition(value.condition)
|
|
2107
|
-
};
|
|
2108
|
-
default:
|
|
2109
|
-
return { type: "instant" };
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
function migrateSpot(value, index) {
|
|
2113
|
-
const source2 = isObject2(value) ? value : {};
|
|
2114
|
-
const id = typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : `spot-${index + 1}`;
|
|
2115
|
-
const description = text2(source2.description);
|
|
2116
|
-
const hint = text2(source2.hint);
|
|
2117
|
-
return {
|
|
2118
|
-
id,
|
|
2119
|
-
name: text2(source2.name) ?? `Spot ${index + 1}`,
|
|
2120
|
-
...description === void 0 ? {} : { description },
|
|
2121
|
-
...hint === void 0 ? {} : { hint },
|
|
2122
|
-
condition: condition(
|
|
2123
|
-
source2.condition ?? (typeof source2.token === "string" ? { type: "token", token: source2.token } : void 0)
|
|
2124
|
-
),
|
|
2125
|
-
...typeof source2.orderIndex === "number" ? { orderIndex: source2.orderIndex } : typeof source2.order === "number" ? { order: source2.order } : {},
|
|
2126
|
-
...typeof source2.deckId === "string" ? { deckId: source2.deckId } : {},
|
|
2127
|
-
...typeof source2.groupId === "string" ? { groupId: source2.groupId } : {},
|
|
2128
|
-
...typeof source2.guideId === "string" ? { guideId: source2.guideId } : {},
|
|
2129
|
-
...typeof source2.iconUrl === "string" ? { iconUrl: source2.iconUrl } : {},
|
|
2130
|
-
...typeof source2.imageUrl === "string" ? { imageUrl: source2.imageUrl } : {},
|
|
2131
|
-
...typeof source2.externalUrl === "string" ? { externalUrl: source2.externalUrl } : {},
|
|
2132
|
-
...typeof source2.redirectUrlAfterClaim === "string" ? { redirectUrlAfterClaim: source2.redirectUrlAfterClaim } : {},
|
|
2133
|
-
...isObject2(source2.metadata) ? { metadata: source2.metadata } : {},
|
|
2134
|
-
...Array.isArray(source2.dependsOn) ? { dependsOn: source2.dependsOn.filter((item) => typeof item === "string") } : {}
|
|
2135
|
-
};
|
|
2136
|
-
}
|
|
2137
|
-
function migrateReward(value, index) {
|
|
2138
|
-
const source2 = isObject2(value) ? value : {};
|
|
2139
|
-
return {
|
|
2140
|
-
id: typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : `reward-${index + 1}`,
|
|
2141
|
-
title: text2(source2.title) ?? `Reward ${index + 1}`,
|
|
2142
|
-
description: text2(source2.description) ?? "",
|
|
2143
|
-
type: source2.type === "digital" ? "digital" : "in_person",
|
|
2144
|
-
redemptionMethod: source2.redemptionMethod === "staff_passcode" || source2.redemptionMethod === "view_only" || source2.redemptionMethod === "server_claim" ? source2.redemptionMethod : "manual_slide",
|
|
2145
|
-
requiredStampCount: typeof source2.requiredStampCount === "number" && Number.isFinite(source2.requiredStampCount) ? Math.max(0, Math.trunc(source2.requiredStampCount)) : 0,
|
|
2146
|
-
...typeof source2.digitalContentUrl === "string" ? { digitalContentUrl: source2.digitalContentUrl } : {},
|
|
2147
|
-
...typeof source2.staffPasscode === "string" ? { staffPasscode: source2.staffPasscode } : {},
|
|
2148
|
-
...typeof source2.validUntil === "string" ? { validUntil: source2.validUntil } : {},
|
|
2149
|
-
...typeof source2.maxStock === "number" ? { maxStock: source2.maxStock } : {},
|
|
2150
|
-
...typeof source2.limitPerUser === "number" ? { limitPerUser: source2.limitPerUser } : {},
|
|
2151
|
-
...typeof source2.stockLimit === "number" ? { stockLimit: source2.stockLimit } : {},
|
|
2152
|
-
...typeof source2.userClaimLimit === "number" ? { userClaimLimit: source2.userClaimLimit } : {},
|
|
2153
|
-
...typeof source2.claimTicketNumber === "string" ? { claimTicketNumber: source2.claimTicketNumber } : {}
|
|
2154
|
-
};
|
|
2155
|
-
}
|
|
2156
|
-
function migrateRallyConfig(raw) {
|
|
2157
|
-
const source2 = isObject2(raw) ? raw : {};
|
|
2158
|
-
const rawStamps = Array.isArray(source2.stamps) ? source2.stamps : Array.isArray(source2.spots) ? source2.spots : [];
|
|
2159
|
-
const rawRewards = Array.isArray(source2.rewards) ? source2.rewards : [];
|
|
2160
|
-
const id = typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : "migrated-rally";
|
|
2161
|
-
const title = text2(source2.title);
|
|
2162
|
-
const description = text2(source2.description);
|
|
2163
|
-
const theme = isObject2(source2.theme) ? source2.theme : void 0;
|
|
2164
|
-
return {
|
|
2165
|
-
id,
|
|
2166
|
-
...title === void 0 ? {} : { title },
|
|
2167
|
-
...description === void 0 ? {} : { description },
|
|
2168
|
-
stamps: rawStamps.map(migrateSpot),
|
|
2169
|
-
...rawRewards.length === 0 ? {} : { rewards: rawRewards.map(migrateReward) },
|
|
2170
|
-
...typeof source2.isSequential === "boolean" ? { isSequential: source2.isSequential } : {},
|
|
2171
|
-
...theme === void 0 ? {} : { theme },
|
|
2172
|
-
version: CURRENT_RALLY_CONFIG_VERSION,
|
|
2173
|
-
...typeof source2.startDate === "string" ? { startDate: source2.startDate } : typeof source2.startsAt === "string" ? { startDate: source2.startsAt } : {},
|
|
2174
|
-
...typeof source2.endDate === "string" ? { endDate: source2.endDate } : typeof source2.endsAt === "string" ? { endDate: source2.endsAt } : {}
|
|
1399
|
+
function updateLocalizedField(current, locale, newValue) {
|
|
1400
|
+
if (typeof current === "string" || current === void 0)
|
|
1401
|
+
return { [locale]: newValue };
|
|
1402
|
+
return { ...current, [locale]: newValue };
|
|
1403
|
+
}
|
|
1404
|
+
function resolveLocalizedText(text, locale, fallbackLocale) {
|
|
1405
|
+
if (text === void 0 || text === "") return "";
|
|
1406
|
+
if (typeof text === "string") return text;
|
|
1407
|
+
const fallback = fallbackLocale === void 0 ? Object.values(text).find((value) => typeof value === "string") : text[fallbackLocale];
|
|
1408
|
+
return text[locale] || fallback || "";
|
|
1409
|
+
}
|
|
1410
|
+
function toLocalizedString(text) {
|
|
1411
|
+
if (text === void 0) return { ja: "", en: "" };
|
|
1412
|
+
return typeof text === "string" ? { ja: text, en: "" } : {
|
|
1413
|
+
ja: text["ja"] ?? "",
|
|
1414
|
+
en: text["en"] ?? "",
|
|
1415
|
+
...text
|
|
2175
1416
|
};
|
|
2176
1417
|
}
|
|
2177
1418
|
|
|
@@ -2186,12 +1427,10 @@ var DEFAULT_SHEET_THEME = {
|
|
|
2186
1427
|
unclaimedOpacity: 1,
|
|
2187
1428
|
fontFamily: "serif"
|
|
2188
1429
|
};
|
|
2189
|
-
|
|
2190
|
-
// src/domain/universalModel.ts
|
|
2191
|
-
function toPublicCondition(condition2) {
|
|
1430
|
+
function publicCondition(condition2) {
|
|
2192
1431
|
switch (condition2.type) {
|
|
2193
1432
|
case "qr":
|
|
2194
|
-
return { type: "qr", qrEntryUrl: condition2.qrEntryUrl
|
|
1433
|
+
return condition2.qrEntryUrl === void 0 ? { type: "qr" } : { type: "qr", qrEntryUrl: condition2.qrEntryUrl };
|
|
2195
1434
|
case "passcode":
|
|
2196
1435
|
return { type: "passcode" };
|
|
2197
1436
|
case "gps":
|
|
@@ -2201,54 +1440,85 @@ function toPublicCondition(condition2) {
|
|
|
2201
1440
|
longitude: condition2.longitude,
|
|
2202
1441
|
radiusMeters: condition2.radiusMeters
|
|
2203
1442
|
};
|
|
1443
|
+
case "nfc":
|
|
1444
|
+
return { type: "nfc" };
|
|
2204
1445
|
case "custom":
|
|
2205
1446
|
return { type: "custom", validatorName: condition2.validatorName };
|
|
2206
1447
|
}
|
|
2207
1448
|
}
|
|
2208
|
-
function
|
|
1449
|
+
function toPublicConfig(config) {
|
|
2209
1450
|
return {
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
1451
|
+
id: config.id,
|
|
1452
|
+
version: config.version,
|
|
1453
|
+
title: config.title,
|
|
1454
|
+
...config.description === void 0 ? {} : { description: config.description },
|
|
1455
|
+
...config.theme === void 0 ? {} : { theme: config.theme },
|
|
1456
|
+
spots: config.spots.map((spot2) => ({
|
|
1457
|
+
...spot2,
|
|
1458
|
+
conditions: spot2.conditions.map(publicCondition)
|
|
2214
1459
|
})),
|
|
2215
1460
|
rewards: config.rewards.map(
|
|
2216
|
-
({
|
|
2217
|
-
)
|
|
1461
|
+
({ digitalContentUrl: _content, staffPasscode: _passcode, ...reward2 }) => reward2
|
|
1462
|
+
),
|
|
1463
|
+
...config.metadata === void 0 ? {} : { metadata: config.metadata },
|
|
1464
|
+
...config.serverEndpoint === void 0 ? {} : { serverEndpoint: config.serverEndpoint }
|
|
2218
1465
|
};
|
|
2219
1466
|
}
|
|
2220
|
-
function
|
|
1467
|
+
function assertPublicConfig(config) {
|
|
1468
|
+
if (!isPublicConfig(config)) throw new Error("Configuration contains private rally fields.");
|
|
1469
|
+
}
|
|
1470
|
+
function isPublicConfig(value) {
|
|
2221
1471
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
2222
1472
|
const candidate = value;
|
|
2223
|
-
|
|
1473
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1474
|
+
const containsPrivateField = (item) => {
|
|
1475
|
+
if (typeof item !== "object" || item === null) return false;
|
|
1476
|
+
if (seen.has(item)) return false;
|
|
1477
|
+
seen.add(item);
|
|
1478
|
+
if (Array.isArray(item)) return item.some(containsPrivateField);
|
|
1479
|
+
const record = item;
|
|
1480
|
+
if (["staffPasscode", "secretToken", "serverMetadata", "secretParams", "digitalContentUrl"].some(
|
|
1481
|
+
(key) => key in record
|
|
1482
|
+
))
|
|
1483
|
+
return true;
|
|
1484
|
+
return Object.values(record).some(containsPrivateField);
|
|
1485
|
+
};
|
|
1486
|
+
if (containsPrivateField(candidate)) return false;
|
|
1487
|
+
for (const key of [
|
|
1488
|
+
"staffPasscode",
|
|
1489
|
+
"secretToken",
|
|
1490
|
+
"serverMetadata",
|
|
1491
|
+
"secretParams",
|
|
1492
|
+
"code",
|
|
1493
|
+
"tagId"
|
|
1494
|
+
]) {
|
|
1495
|
+
if (key in candidate) return false;
|
|
1496
|
+
}
|
|
1497
|
+
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))
|
|
2224
1498
|
return false;
|
|
2225
|
-
return
|
|
2226
|
-
if (typeof
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
1499
|
+
return candidate.spots.every((spot2) => {
|
|
1500
|
+
if (typeof spot2 !== "object" || spot2 === null || Array.isArray(spot2)) return false;
|
|
1501
|
+
const item = spot2;
|
|
1502
|
+
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)
|
|
1503
|
+
return false;
|
|
1504
|
+
return Array.isArray(item.conditions) && item.conditions.every((condition2) => {
|
|
1505
|
+
if (typeof condition2 !== "object" || condition2 === null || Array.isArray(condition2))
|
|
1506
|
+
return false;
|
|
1507
|
+
const value2 = condition2;
|
|
1508
|
+
if ("secretToken" in value2 || "code" in value2 || "tagId" in value2 || "secretParams" in value2)
|
|
1509
|
+
return false;
|
|
1510
|
+
const type = value2.type;
|
|
1511
|
+
if (type === "qr" || type === "passcode" || type === "nfc") return true;
|
|
1512
|
+
if (type === "custom") return typeof value2.validatorName === "string";
|
|
1513
|
+
return type === "gps" && typeof value2.latitude === "number" && typeof value2.longitude === "number" && typeof value2.radiusMeters === "number";
|
|
2232
1514
|
});
|
|
2233
|
-
}) && candidate.rewards.every((
|
|
2234
|
-
if (typeof
|
|
2235
|
-
const item =
|
|
2236
|
-
return item.
|
|
1515
|
+
}) && candidate.rewards.every((reward2) => {
|
|
1516
|
+
if (typeof reward2 !== "object" || reward2 === null || Array.isArray(reward2)) return false;
|
|
1517
|
+
const item = reward2;
|
|
1518
|
+
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);
|
|
2237
1519
|
});
|
|
2238
1520
|
}
|
|
2239
1521
|
|
|
2240
|
-
// src/domain/publicConfig.ts
|
|
2241
|
-
function stripSensitiveConfig(config) {
|
|
2242
|
-
if ("spots" in config && !("stamps" in config)) {
|
|
2243
|
-
return toPublicRallyConfig(config);
|
|
2244
|
-
}
|
|
2245
|
-
const rewards = config.rewards?.map(({ staffPasscode: _staffPasscode, ...reward }) => reward);
|
|
2246
|
-
return {
|
|
2247
|
-
...config,
|
|
2248
|
-
...rewards === void 0 ? {} : { rewards }
|
|
2249
|
-
};
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
1522
|
// src/domain/themePresets.ts
|
|
2253
1523
|
var THEME_PRESETS = [
|
|
2254
1524
|
{
|
|
@@ -2348,178 +1618,322 @@ var THEME_PRESETS = [
|
|
|
2348
1618
|
}
|
|
2349
1619
|
];
|
|
2350
1620
|
|
|
2351
|
-
// src/domain/
|
|
2352
|
-
|
|
1621
|
+
// src/domain/validation.ts
|
|
1622
|
+
var ConfigValidationError = class extends Error {
|
|
1623
|
+
constructor(errors) {
|
|
1624
|
+
super(errors.map((error) => `${error.path}: ${error.message}`).join("; "));
|
|
1625
|
+
this.errors = errors;
|
|
1626
|
+
}
|
|
1627
|
+
errors;
|
|
1628
|
+
name = "ConfigValidationError";
|
|
1629
|
+
};
|
|
1630
|
+
function isRecord2(value) {
|
|
2353
1631
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2354
1632
|
}
|
|
2355
|
-
function
|
|
2356
|
-
|
|
1633
|
+
function hasOwn(value, key) {
|
|
1634
|
+
return Object.hasOwn(value, key);
|
|
1635
|
+
}
|
|
1636
|
+
function add(errors, path, message, code) {
|
|
1637
|
+
errors.push({ path, message, code });
|
|
1638
|
+
}
|
|
1639
|
+
function requiredString(value, key, path, errors) {
|
|
1640
|
+
if (typeof value[key] !== "string" || value[key].length === 0) {
|
|
1641
|
+
add(errors, `${path}.${key}`, "A non-empty string is required.", "required_string");
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
return true;
|
|
1645
|
+
}
|
|
1646
|
+
function optionalString(value, key, path, errors) {
|
|
1647
|
+
if (hasOwn(value, key) && value[key] !== void 0 && typeof value[key] !== "string")
|
|
1648
|
+
add(errors, `${path}.${key}`, "Expected a string.", "invalid_type");
|
|
2357
1649
|
}
|
|
2358
|
-
function
|
|
2359
|
-
if (
|
|
2360
|
-
|
|
1650
|
+
function optionalBoolean(value, key, path, errors) {
|
|
1651
|
+
if (hasOwn(value, key) && value[key] !== void 0 && typeof value[key] !== "boolean")
|
|
1652
|
+
add(errors, `${path}.${key}`, "Expected a boolean.", "invalid_type");
|
|
2361
1653
|
}
|
|
2362
|
-
function
|
|
2363
|
-
|
|
2364
|
-
|
|
1654
|
+
function finiteNumber(value, key, path, errors, minimum) {
|
|
1655
|
+
const item = value[key];
|
|
1656
|
+
if (typeof item !== "number" || !Number.isFinite(item)) {
|
|
1657
|
+
add(errors, `${path}.${key}`, "Expected a finite number.", "invalid_number");
|
|
1658
|
+
} else if (minimum !== void 0 && item < minimum) {
|
|
1659
|
+
add(
|
|
1660
|
+
errors,
|
|
1661
|
+
`${path}.${key}`,
|
|
1662
|
+
`Expected a number greater than or equal to ${minimum}.`,
|
|
1663
|
+
"out_of_range"
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
function localizedText(value, path, errors) {
|
|
1668
|
+
if (typeof value === "string") return;
|
|
1669
|
+
if (!isRecord2(value)) {
|
|
1670
|
+
add(errors, path, "Expected a string or a locale map.", "invalid_localized_text");
|
|
2365
1671
|
return;
|
|
2366
1672
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
1673
|
+
for (const [locale, text] of Object.entries(value)) {
|
|
1674
|
+
if (typeof text !== "string")
|
|
1675
|
+
add(errors, `${path}.${locale}`, "Expected a string.", "invalid_type");
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function theme(value, path, errors) {
|
|
1679
|
+
if (!isRecord2(value)) {
|
|
1680
|
+
add(errors, path, "Expected a theme object.", "invalid_type");
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
for (const key of ["primaryColor", "cardBackgroundColor", "textColor"])
|
|
1684
|
+
requiredString(value, key, path, errors);
|
|
1685
|
+
optionalString(value, "backgroundColor", path, errors);
|
|
1686
|
+
optionalString(value, "backgroundImageUrl", path, errors);
|
|
1687
|
+
optionalString(value, "completedStampColor", path, errors);
|
|
1688
|
+
optionalString(value, "fontFamily", path, errors);
|
|
1689
|
+
const slotShape = value.slotShape;
|
|
1690
|
+
if (slotShape !== "circle" && slotShape !== "square" && slotShape !== "rounded")
|
|
1691
|
+
add(errors, `${path}.slotShape`, "Expected circle, square, or rounded.", "invalid_enum");
|
|
1692
|
+
finiteNumber(value, "gridColumns", path, errors, 1);
|
|
1693
|
+
if (typeof value.gridColumns === "number" && !Number.isInteger(value.gridColumns))
|
|
1694
|
+
add(errors, `${path}.gridColumns`, "Expected an integer.", "invalid_integer");
|
|
1695
|
+
if (hasOwn(value, "unclaimedOpacity")) finiteNumber(value, "unclaimedOpacity", path, errors, 0);
|
|
1696
|
+
}
|
|
1697
|
+
function externalReferences(value, path, errors) {
|
|
1698
|
+
if (!Array.isArray(value)) {
|
|
1699
|
+
add(errors, path, "Expected an array.", "invalid_type");
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
value.forEach((item, index) => {
|
|
1703
|
+
const itemPath = `${path}[${index}]`;
|
|
1704
|
+
if (!isRecord2(item)) {
|
|
1705
|
+
add(errors, itemPath, "Expected an object.", "invalid_type");
|
|
2387
1706
|
return;
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
}
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
1707
|
+
}
|
|
1708
|
+
requiredString(item, "type", itemPath, errors);
|
|
1709
|
+
requiredString(item, "id", itemPath, errors);
|
|
1710
|
+
optionalString(item, "url", itemPath, errors);
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
function condition(value, path, errors, isPublic) {
|
|
1714
|
+
if (!isRecord2(value)) {
|
|
1715
|
+
add(errors, path, "Expected a condition object.", "invalid_type");
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const type = value.type;
|
|
1719
|
+
if (type === "qr") {
|
|
1720
|
+
if (isPublic) {
|
|
1721
|
+
if (hasOwn(value, "secretToken"))
|
|
1722
|
+
add(errors, `${path}.secretToken`, "Private field is not allowed.", "private_field");
|
|
1723
|
+
} else requiredString(value, "secretToken", path, errors);
|
|
1724
|
+
optionalString(value, "qrEntryUrl", path, errors);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (type === "passcode") {
|
|
1728
|
+
if (isPublic) {
|
|
1729
|
+
for (const key of ["code", "caseSensitive"])
|
|
1730
|
+
if (hasOwn(value, key))
|
|
1731
|
+
add(errors, `${path}.${key}`, "Private field is not allowed.", "private_field");
|
|
1732
|
+
} else {
|
|
1733
|
+
requiredString(value, "code", path, errors);
|
|
1734
|
+
optionalBoolean(value, "caseSensitive", path, errors);
|
|
1735
|
+
}
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
if (type === "gps") {
|
|
1739
|
+
finiteNumber(value, "latitude", path, errors);
|
|
1740
|
+
finiteNumber(value, "longitude", path, errors);
|
|
1741
|
+
finiteNumber(value, "radiusMeters", path, errors, 0);
|
|
1742
|
+
if (typeof value.latitude === "number" && (value.latitude < -90 || value.latitude > 90))
|
|
1743
|
+
add(errors, `${path}.latitude`, "Expected a latitude between -90 and 90.", "out_of_range");
|
|
1744
|
+
if (typeof value.longitude === "number" && (value.longitude < -180 || value.longitude > 180))
|
|
1745
|
+
add(
|
|
2404
1746
|
errors,
|
|
2405
|
-
|
|
2406
|
-
"
|
|
2407
|
-
|
|
1747
|
+
`${path}.longitude`,
|
|
1748
|
+
"Expected a longitude between -180 and 180.",
|
|
1749
|
+
"out_of_range"
|
|
2408
1750
|
);
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
}
|
|
2418
|
-
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
if (type === "nfc") {
|
|
1754
|
+
if (isPublic) {
|
|
1755
|
+
if (hasOwn(value, "tagId"))
|
|
1756
|
+
add(errors, `${path}.tagId`, "Private field is not allowed.", "private_field");
|
|
1757
|
+
} else requiredString(value, "tagId", path, errors);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (type === "custom") {
|
|
1761
|
+
requiredString(value, "validatorName", path, errors);
|
|
1762
|
+
if (isPublic && hasOwn(value, "secretParams"))
|
|
1763
|
+
add(errors, `${path}.secretParams`, "Private field is not allowed.", "private_field");
|
|
1764
|
+
if (!isPublic && hasOwn(value, "secretParams") && !isRecord2(value.secretParams))
|
|
1765
|
+
add(errors, `${path}.secretParams`, "Expected an object.", "invalid_type");
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
add(errors, `${path}.type`, "Unknown condition type.", "invalid_enum");
|
|
2419
1769
|
}
|
|
2420
|
-
function
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
return
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
add2(errors, "id", "REQUIRED", "Rally ID is required.");
|
|
2429
|
-
if (typeof value.version !== "string" || value.version.trim() === "")
|
|
2430
|
-
add2(errors, "version", "INVALID_VERSION", "Version is required.");
|
|
2431
|
-
if (!Array.isArray(value.spots)) {
|
|
2432
|
-
add2(errors, "spots", "INVALID_TYPE", "spots must be an array.");
|
|
2433
|
-
} else {
|
|
2434
|
-
const ids = [];
|
|
2435
|
-
value.spots.forEach((spot, index) => {
|
|
2436
|
-
const path = `spots[${index}]`;
|
|
2437
|
-
if (!isObject3(spot)) {
|
|
2438
|
-
add2(errors, path, "INVALID_TYPE", "Spot must be an object.");
|
|
2439
|
-
return;
|
|
2440
|
-
}
|
|
2441
|
-
if (typeof spot.id !== "string" || spot.id.trim() === "")
|
|
2442
|
-
add2(errors, `${path}.id`, "REQUIRED", "Spot ID is required.");
|
|
2443
|
-
else if (ids.includes(spot.id))
|
|
2444
|
-
add2(errors, `${path}.id`, "DUPLICATE_ID", `Duplicate spot ID '${spot.id}'.`);
|
|
2445
|
-
else ids.push(spot.id);
|
|
2446
|
-
if (!hasText2(spot.name)) add2(errors, `${path}.name`, "REQUIRED", "Spot name is required.");
|
|
2447
|
-
if (typeof spot.orderIndex !== "number" || !Number.isInteger(spot.orderIndex))
|
|
2448
|
-
add2(errors, `${path}.orderIndex`, "INVALID_TYPE", "orderIndex must be an integer.");
|
|
2449
|
-
if (!Array.isArray(spot.conditions) || spot.conditions.length === 0)
|
|
2450
|
-
add2(errors, `${path}.conditions`, "REQUIRED", "At least one condition is required.");
|
|
2451
|
-
else {
|
|
2452
|
-
spot.conditions.forEach((condition2, conditionIndex) => {
|
|
2453
|
-
validateCondition2(condition2, `${path}.conditions[${conditionIndex}]`, errors);
|
|
2454
|
-
});
|
|
2455
|
-
}
|
|
2456
|
-
});
|
|
2457
|
-
validateDag2(value.spots, errors);
|
|
2458
|
-
}
|
|
2459
|
-
if (!Array.isArray(value.rewards))
|
|
2460
|
-
add2(errors, "rewards", "INVALID_TYPE", "rewards must be an array.");
|
|
2461
|
-
else {
|
|
2462
|
-
const ids = [];
|
|
2463
|
-
value.rewards.forEach((reward, index) => {
|
|
2464
|
-
const path = `rewards[${index}]`;
|
|
2465
|
-
if (!isObject3(reward)) {
|
|
2466
|
-
add2(errors, path, "INVALID_TYPE", "Reward must be an object.");
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
|
-
if (typeof reward.id !== "string" || reward.id.trim() === "")
|
|
2470
|
-
add2(errors, `${path}.id`, "REQUIRED", "Reward ID is required.");
|
|
2471
|
-
else if (ids.includes(reward.id))
|
|
2472
|
-
add2(errors, `${path}.id`, "DUPLICATE_ID", `Duplicate reward ID '${reward.id}'.`);
|
|
2473
|
-
else ids.push(reward.id);
|
|
2474
|
-
if (!hasText2(reward.title))
|
|
2475
|
-
add2(errors, `${path}.title`, "REQUIRED", "Reward title is required.");
|
|
2476
|
-
if (typeof reward.requiredStampCount !== "number" || !Number.isInteger(reward.requiredStampCount) || reward.requiredStampCount < 0)
|
|
2477
|
-
add2(
|
|
2478
|
-
errors,
|
|
2479
|
-
`${path}.requiredStampCount`,
|
|
2480
|
-
"INVALID_REWARD",
|
|
2481
|
-
"requiredStampCount must be a non-negative integer."
|
|
2482
|
-
);
|
|
2483
|
-
});
|
|
1770
|
+
function unlockCondition(value, path, errors) {
|
|
1771
|
+
if (!isRecord2(value)) {
|
|
1772
|
+
add(errors, path, "Expected an unlock condition object.", "invalid_type");
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (value.type === "stamp_count") {
|
|
1776
|
+
finiteNumber(value, "count", path, errors, 0);
|
|
1777
|
+
return;
|
|
2484
1778
|
}
|
|
2485
|
-
|
|
1779
|
+
if (value.type === "stamps") {
|
|
1780
|
+
if (!Array.isArray(value.stampIds))
|
|
1781
|
+
add(errors, `${path}.stampIds`, "Expected an array.", "invalid_type");
|
|
1782
|
+
else
|
|
1783
|
+
value.stampIds.forEach((item, index) => {
|
|
1784
|
+
if (typeof item !== "string" || item.length === 0)
|
|
1785
|
+
add(
|
|
1786
|
+
errors,
|
|
1787
|
+
`${path}.stampIds[${index}]`,
|
|
1788
|
+
"Expected a non-empty string.",
|
|
1789
|
+
"required_string"
|
|
1790
|
+
);
|
|
1791
|
+
});
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (value.type === "all" || value.type === "any") {
|
|
1795
|
+
if (!Array.isArray(value.conditions))
|
|
1796
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
1797
|
+
else
|
|
1798
|
+
value.conditions.forEach((item, index) => {
|
|
1799
|
+
unlockCondition(item, `${path}.conditions[${index}]`, errors);
|
|
1800
|
+
});
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
add(errors, `${path}.type`, "Unknown unlock condition type.", "invalid_enum");
|
|
2486
1804
|
}
|
|
2487
|
-
function
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
return
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
if (
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
);
|
|
2501
|
-
if (
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
1805
|
+
function spot(value, path, errors, isPublic) {
|
|
1806
|
+
if (!isRecord2(value)) {
|
|
1807
|
+
add(errors, path, "Expected a spot object.", "invalid_type");
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
requiredString(value, "id", path, errors);
|
|
1811
|
+
finiteNumber(value, "orderIndex", path, errors, 0);
|
|
1812
|
+
if (typeof value.orderIndex === "number" && !Number.isInteger(value.orderIndex))
|
|
1813
|
+
add(errors, `${path}.orderIndex`, "Expected an integer.", "invalid_integer");
|
|
1814
|
+
localizedText(value.name, `${path}.name`, errors);
|
|
1815
|
+
for (const key of ["description", "hint"])
|
|
1816
|
+
if (hasOwn(value, key)) localizedText(value[key], `${path}.${key}`, errors);
|
|
1817
|
+
for (const key of ["imageUrl", "iconUrl", "redirectUrlAfterClaim"])
|
|
1818
|
+
optionalString(value, key, path, errors);
|
|
1819
|
+
if (hasOwn(value, "externalReferences") && value.externalReferences !== void 0)
|
|
1820
|
+
externalReferences(value.externalReferences, `${path}.externalReferences`, errors);
|
|
1821
|
+
if (hasOwn(value, "prerequisites") && value.prerequisites !== void 0) {
|
|
1822
|
+
if (!Array.isArray(value.prerequisites))
|
|
1823
|
+
add(errors, `${path}.prerequisites`, "Expected an array.", "invalid_type");
|
|
1824
|
+
else
|
|
1825
|
+
value.prerequisites.forEach((item, index) => {
|
|
1826
|
+
if (typeof item !== "string" || item.length === 0)
|
|
1827
|
+
add(
|
|
2509
1828
|
errors,
|
|
2510
|
-
|
|
2511
|
-
"
|
|
2512
|
-
"
|
|
1829
|
+
`${path}.prerequisites[${index}]`,
|
|
1830
|
+
"Expected a non-empty string.",
|
|
1831
|
+
"required_string"
|
|
2513
1832
|
);
|
|
2514
1833
|
});
|
|
1834
|
+
}
|
|
1835
|
+
if (!Array.isArray(value.conditions))
|
|
1836
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
1837
|
+
else
|
|
1838
|
+
value.conditions.forEach((item, index) => {
|
|
1839
|
+
condition(item, `${path}.conditions[${index}]`, errors, isPublic);
|
|
2515
1840
|
});
|
|
2516
|
-
return { valid: errors.length === 0, errors };
|
|
2517
1841
|
}
|
|
2518
|
-
function
|
|
2519
|
-
|
|
1842
|
+
function reward(value, path, errors, isPublic) {
|
|
1843
|
+
if (!isRecord2(value)) {
|
|
1844
|
+
add(errors, path, "Expected a reward object.", "invalid_type");
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
requiredString(value, "id", path, errors);
|
|
1848
|
+
localizedText(value.title, `${path}.title`, errors);
|
|
1849
|
+
if (hasOwn(value, "description")) localizedText(value.description, `${path}.description`, errors);
|
|
1850
|
+
if (value.type !== "digital" && value.type !== "in_person")
|
|
1851
|
+
add(errors, `${path}.type`, "Unknown reward type.", "invalid_enum");
|
|
1852
|
+
if (!["manual_slide", "staff_passcode", "view_only", "server_claim"].includes(
|
|
1853
|
+
String(value.redemptionMethod)
|
|
1854
|
+
))
|
|
1855
|
+
add(errors, `${path}.redemptionMethod`, "Unknown redemption method.", "invalid_enum");
|
|
1856
|
+
finiteNumber(value, "requiredStampCount", path, errors, 0);
|
|
1857
|
+
for (const key of ["stockLimit", "userClaimLimit"]) {
|
|
1858
|
+
if (hasOwn(value, key) && value[key] !== void 0) {
|
|
1859
|
+
finiteNumber(value, key, path, errors, 0);
|
|
1860
|
+
if (typeof value[key] === "number" && !Number.isInteger(value[key]))
|
|
1861
|
+
add(errors, `${path}.${key}`, "Expected an integer.", "invalid_integer");
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
optionalString(value, "validUntil", path, errors);
|
|
1865
|
+
if (typeof value.validUntil === "string" && Number.isNaN(Date.parse(value.validUntil)))
|
|
1866
|
+
add(errors, `${path}.validUntil`, "Expected a valid date string.", "invalid_date");
|
|
1867
|
+
if (isPublic) {
|
|
1868
|
+
for (const key of ["staffPasscode", "digitalContentUrl"])
|
|
1869
|
+
if (hasOwn(value, key))
|
|
1870
|
+
add(errors, `${path}.${key}`, "Private field is not allowed.", "private_field");
|
|
1871
|
+
} else {
|
|
1872
|
+
optionalString(value, "staffPasscode", path, errors);
|
|
1873
|
+
optionalString(value, "digitalContentUrl", path, errors);
|
|
1874
|
+
}
|
|
1875
|
+
if (hasOwn(value, "conditions") && value.conditions !== void 0) {
|
|
1876
|
+
if (!Array.isArray(value.conditions))
|
|
1877
|
+
add(errors, `${path}.conditions`, "Expected an array.", "invalid_type");
|
|
1878
|
+
else
|
|
1879
|
+
value.conditions.forEach((item, index) => {
|
|
1880
|
+
unlockCondition(item, `${path}.conditions[${index}]`, errors);
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
function validate(value, isPublic) {
|
|
1885
|
+
const errors = [];
|
|
1886
|
+
if (!isRecord2(value)) {
|
|
1887
|
+
add(errors, "$", "Expected a configuration object.", "invalid_type");
|
|
1888
|
+
return errors;
|
|
1889
|
+
}
|
|
1890
|
+
requiredString(value, "id", "$", errors);
|
|
1891
|
+
requiredString(value, "version", "$", errors);
|
|
1892
|
+
localizedText(value.title, "$.title", errors);
|
|
1893
|
+
if (hasOwn(value, "description")) localizedText(value.description, "$.description", errors);
|
|
1894
|
+
if (hasOwn(value, "theme") && value.theme !== void 0) theme(value.theme, "$.theme", errors);
|
|
1895
|
+
if (!Array.isArray(value.spots)) add(errors, "spots", "Expected an array.", "invalid_type");
|
|
1896
|
+
else
|
|
1897
|
+
value.spots.forEach((item, index) => {
|
|
1898
|
+
spot(item, `spots[${index}]`, errors, isPublic);
|
|
1899
|
+
});
|
|
1900
|
+
if (!Array.isArray(value.rewards)) add(errors, "rewards", "Expected an array.", "invalid_type");
|
|
1901
|
+
else
|
|
1902
|
+
value.rewards.forEach((item, index) => {
|
|
1903
|
+
reward(item, `rewards[${index}]`, errors, isPublic);
|
|
1904
|
+
});
|
|
1905
|
+
if (!isPublic) {
|
|
1906
|
+
optionalString(value, "staffPasscode", "$", errors);
|
|
1907
|
+
if (hasOwn(value, "inventory") && value.inventory !== void 0 && !isRecord2(value.inventory))
|
|
1908
|
+
add(errors, "$.inventory", "Expected an object.", "invalid_type");
|
|
1909
|
+
if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord2(value.serverMetadata))
|
|
1910
|
+
add(errors, "$.serverMetadata", "Expected an object.", "invalid_type");
|
|
1911
|
+
optionalString(value, "serverEndpoint", "$", errors);
|
|
1912
|
+
} else {
|
|
1913
|
+
for (const key of ["staffPasscode", "serverMetadata", "inventory"])
|
|
1914
|
+
if (hasOwn(value, key))
|
|
1915
|
+
add(errors, `$.${key}`, "Private field is not allowed.", "private_field");
|
|
1916
|
+
optionalString(value, "serverEndpoint", "$", errors);
|
|
1917
|
+
}
|
|
1918
|
+
return errors;
|
|
1919
|
+
}
|
|
1920
|
+
function safeParseAdminConfig(input) {
|
|
1921
|
+
const errors = validate(input, false);
|
|
1922
|
+
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
1923
|
+
}
|
|
1924
|
+
function parseAdminConfig(input) {
|
|
1925
|
+
const result = safeParseAdminConfig(input);
|
|
1926
|
+
if (!result.success) throw new ConfigValidationError(result.errors);
|
|
1927
|
+
return result.data;
|
|
1928
|
+
}
|
|
1929
|
+
function safeParsePublicConfig(input) {
|
|
1930
|
+
const errors = validate(input, true);
|
|
1931
|
+
return errors.length === 0 ? { success: true, data: input } : { success: false, errors };
|
|
2520
1932
|
}
|
|
2521
|
-
function
|
|
2522
|
-
|
|
1933
|
+
function parsePublicConfig(input) {
|
|
1934
|
+
const result = safeParsePublicConfig(input);
|
|
1935
|
+
if (!result.success) throw new ConfigValidationError(result.errors);
|
|
1936
|
+
return result.data;
|
|
2523
1937
|
}
|
|
2524
1938
|
|
|
2525
1939
|
// src/security/snapshotToken.ts
|
|
@@ -2560,9 +1974,9 @@ async function importAesKey(secret) {
|
|
|
2560
1974
|
const digest = await cryptoApi2().subtle.digest("SHA-256", webCryptoBytes(secret));
|
|
2561
1975
|
return cryptoApi2().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
2562
1976
|
}
|
|
2563
|
-
function isExpired(payload,
|
|
2564
|
-
if (typeof payload.exp === "number" &&
|
|
2565
|
-
return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <=
|
|
1977
|
+
function isExpired(payload, now) {
|
|
1978
|
+
if (typeof payload.exp === "number" && now >= payload.exp * 1e3) return true;
|
|
1979
|
+
return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now;
|
|
2566
1980
|
}
|
|
2567
1981
|
async function createSignedSnapshotToken(payload, secretKey) {
|
|
2568
1982
|
const api = cryptoApi2();
|
|
@@ -2582,7 +1996,7 @@ async function createSignedSnapshotToken(payload, secretKey) {
|
|
|
2582
1996
|
);
|
|
2583
1997
|
return `sr2.${body}.${base64Url(signature)}`;
|
|
2584
1998
|
}
|
|
2585
|
-
async function verifySnapshotToken(token, secretKey,
|
|
1999
|
+
async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
2586
2000
|
try {
|
|
2587
2001
|
const parts = token.split(".");
|
|
2588
2002
|
if (parts.length !== 3 || parts[0] !== "sr2") {
|
|
@@ -2624,7 +2038,7 @@ async function verifySnapshotToken(token, secretKey, now2 = Date.now()) {
|
|
|
2624
2038
|
const payload = JSON.parse(new TextDecoder().decode(payloadText));
|
|
2625
2039
|
if (typeof payload !== "object" || payload === null || Array.isArray(payload))
|
|
2626
2040
|
throw new Error("Invalid payload.");
|
|
2627
|
-
if (isExpired(payload,
|
|
2041
|
+
if (isExpired(payload, now)) {
|
|
2628
2042
|
return {
|
|
2629
2043
|
ok: false,
|
|
2630
2044
|
valid: false,
|
|
@@ -2641,7 +2055,7 @@ async function verifySnapshotToken(token, secretKey, now2 = Date.now()) {
|
|
|
2641
2055
|
}
|
|
2642
2056
|
}
|
|
2643
2057
|
|
|
2644
|
-
exports.
|
|
2058
|
+
exports.ConfigValidationError = ConfigValidationError;
|
|
2645
2059
|
exports.DEFAULT_SHEET_THEME = DEFAULT_SHEET_THEME;
|
|
2646
2060
|
exports.InMemoryStorage = InMemoryStorage;
|
|
2647
2061
|
exports.IndexedDBAdapter = IndexedDBAdapter;
|
|
@@ -2649,7 +2063,7 @@ exports.LocalStorageAdapter = LocalStorageAdapter;
|
|
|
2649
2063
|
exports.StampRallyClient = StampRallyClient;
|
|
2650
2064
|
exports.StorageAdapterError = StorageAdapterError;
|
|
2651
2065
|
exports.THEME_PRESETS = THEME_PRESETS;
|
|
2652
|
-
exports.
|
|
2066
|
+
exports.assertPublicConfig = assertPublicConfig;
|
|
2653
2067
|
exports.calculateDistanceMeters = calculateDistanceMeters;
|
|
2654
2068
|
exports.calculateProgress = calculateProgress;
|
|
2655
2069
|
exports.consumeReward = consumeReward;
|
|
@@ -2657,34 +2071,33 @@ exports.createClaimTicketNumber = createClaimTicketNumber;
|
|
|
2657
2071
|
exports.createSecureToken = createSecureToken;
|
|
2658
2072
|
exports.createSignedSnapshotToken = createSignedSnapshotToken;
|
|
2659
2073
|
exports.createUniqueClaimTicketNumber = createUniqueClaimTicketNumber;
|
|
2660
|
-
exports.evaluateCheckIn = evaluateCheckIn;
|
|
2661
2074
|
exports.evaluateCondition = evaluateCondition;
|
|
2662
2075
|
exports.evaluateConditionDetailed = evaluateConditionDetailed;
|
|
2663
2076
|
exports.exportProgressToken = exportProgressToken;
|
|
2664
2077
|
exports.getCurrentGeoContext = getCurrentGeoContext;
|
|
2078
|
+
exports.getOrderedSpots = getOrderedSpots;
|
|
2665
2079
|
exports.importProgressToken = importProgressToken;
|
|
2666
|
-
exports.isAdminRallyConfig = isAdminRallyConfig;
|
|
2667
2080
|
exports.isGeolocationSupported = isGeolocationSupported;
|
|
2668
2081
|
exports.isNfcSupported = isNfcSupported;
|
|
2669
|
-
exports.
|
|
2670
|
-
exports.isPublicRallyConfigShape = isPublicRallyConfigShape;
|
|
2082
|
+
exports.isPublicConfig = isPublicConfig;
|
|
2671
2083
|
exports.isQrSupported = isQrSupported;
|
|
2672
2084
|
exports.isRewardState = isRewardState;
|
|
2673
2085
|
exports.isStampRallyState = isStampRallyState;
|
|
2674
2086
|
exports.issueClaimTicketNumber = issueClaimTicketNumber;
|
|
2675
|
-
exports.migrateRallyConfig = migrateRallyConfig;
|
|
2676
2087
|
exports.normalizePasscode = normalizePasscode;
|
|
2088
|
+
exports.parseAdminConfig = parseAdminConfig;
|
|
2089
|
+
exports.parsePublicConfig = parsePublicConfig;
|
|
2677
2090
|
exports.processStamp = processStamp;
|
|
2678
2091
|
exports.readNfcContext = readNfcContext;
|
|
2679
2092
|
exports.readQrContext = readQrContext;
|
|
2680
2093
|
exports.reconcileRewardStates = reconcileRewardStates;
|
|
2681
2094
|
exports.resolveLocalizedText = resolveLocalizedText;
|
|
2682
|
-
exports.
|
|
2095
|
+
exports.safeParseAdminConfig = safeParseAdminConfig;
|
|
2096
|
+
exports.safeParsePublicConfig = safeParsePublicConfig;
|
|
2097
|
+
exports.storageKey = storageKey;
|
|
2683
2098
|
exports.toLocalizedString = toLocalizedString;
|
|
2684
|
-
exports.
|
|
2685
|
-
exports.
|
|
2686
|
-
exports.validatePublicRallyConfig = validatePublicRallyConfig;
|
|
2687
|
-
exports.validateRallyConfig = validateRallyConfig;
|
|
2099
|
+
exports.toPublicConfig = toPublicConfig;
|
|
2100
|
+
exports.updateLocalizedField = updateLocalizedField;
|
|
2688
2101
|
exports.verifyPasscode = verifyPasscode;
|
|
2689
2102
|
exports.verifySecureToken = verifySecureToken;
|
|
2690
2103
|
exports.verifySnapshotToken = verifySnapshotToken;
|