@stamprally/server 0.14.0 → 0.16.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 -3
- package/dist/index.cjs +719 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -11
- package/dist/index.d.ts +80 -11
- package/dist/index.js +713 -156
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,32 +1,72 @@
|
|
|
1
1
|
import { reconcileRewardStates, consumeReward, evaluateConditionDetailed } from '@stamprally/core';
|
|
2
2
|
|
|
3
3
|
// src/examples/transaction.ts
|
|
4
|
-
async function executeClaimRewardTransaction(database, store,
|
|
4
|
+
async function executeClaimRewardTransaction(database, store, params2, mutation2) {
|
|
5
5
|
try {
|
|
6
6
|
return await database.transaction(async (transaction) => {
|
|
7
|
-
const current = await store.readContext(transaction,
|
|
8
|
-
const next =
|
|
7
|
+
const current = await store.readContext(transaction, params2);
|
|
8
|
+
const next = mutation2(current);
|
|
9
9
|
if (next.error !== void 0) {
|
|
10
10
|
await store.writeAudit(transaction, next.auditLog);
|
|
11
|
-
if (
|
|
12
|
-
await store.writeIdempotency(transaction,
|
|
11
|
+
if (params2.idempotencyKey !== void 0 && next.result !== void 0)
|
|
12
|
+
await store.writeIdempotency(transaction, params2, next.result);
|
|
13
13
|
return { success: false, error: next.error };
|
|
14
14
|
}
|
|
15
|
-
if (next.
|
|
16
|
-
|
|
17
|
-
await store.
|
|
15
|
+
if (next.nextSecondaryStock !== void 0 && store.writeSecondaryStock === void 0)
|
|
16
|
+
return { success: false, error: "INVENTORY_NOT_SUPPORTED" };
|
|
17
|
+
if (next.nextStock !== null) await store.writeStock(transaction, params2, next.nextStock);
|
|
18
|
+
if (next.nextSecondaryStock !== void 0 && store.writeSecondaryStock !== void 0) {
|
|
19
|
+
if (next.nextSecondaryStock !== null)
|
|
20
|
+
await store.writeSecondaryStock(transaction, params2, next.nextSecondaryStock);
|
|
21
|
+
}
|
|
22
|
+
await store.writeUserState(transaction, params2, next.nextUserState);
|
|
23
|
+
await store.writeClaimRecord(transaction, params2, next.nextUserState);
|
|
18
24
|
await store.writeAudit(transaction, next.auditLog);
|
|
19
|
-
if (
|
|
20
|
-
await store.writeIdempotency(transaction,
|
|
25
|
+
if (params2.idempotencyKey !== void 0 && next.result !== void 0)
|
|
26
|
+
await store.writeIdempotency(transaction, params2, next.result);
|
|
21
27
|
return { success: true };
|
|
22
28
|
});
|
|
23
|
-
} catch (
|
|
24
|
-
return { success: false, error:
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function executeCheckInTransaction(database, store, params2, mutation2, current) {
|
|
34
|
+
try {
|
|
35
|
+
return await database.transaction(async (transaction) => {
|
|
36
|
+
const userState = current ?? (store.readUserState === void 0 ? (() => {
|
|
37
|
+
throw new Error("A current user state or readUserState implementation is required.");
|
|
38
|
+
})() : await store.readUserState(transaction, params2));
|
|
39
|
+
const next = mutation2({ userState });
|
|
40
|
+
if (next.error !== void 0) {
|
|
41
|
+
await store.writeAudit(transaction, next.auditLog);
|
|
42
|
+
if (params2.idempotencyKey !== void 0 && next.result !== void 0)
|
|
43
|
+
await store.writeIdempotency(transaction, params2, next.result);
|
|
44
|
+
return { success: false, error: next.error };
|
|
45
|
+
}
|
|
46
|
+
await store.writeUserState(transaction, params2, next.nextUserState);
|
|
47
|
+
await store.writeAudit(transaction, next.auditLog);
|
|
48
|
+
if (params2.idempotencyKey !== void 0 && next.result !== void 0)
|
|
49
|
+
await store.writeIdempotency(transaction, params2, next.result);
|
|
50
|
+
return { success: true };
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function executeRedisTransaction(redis, queue) {
|
|
57
|
+
try {
|
|
58
|
+
const multi = redis.multi();
|
|
59
|
+
queue(multi);
|
|
60
|
+
const result = await redis.exec(multi);
|
|
61
|
+
return result === null ? { success: false, error: "Redis transaction was aborted." } : { success: true };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
25
64
|
}
|
|
26
65
|
}
|
|
27
66
|
|
|
28
67
|
// src/persistence.ts
|
|
29
68
|
var InMemoryServerPersistenceAdapter = class {
|
|
69
|
+
supportsRewardStock = true;
|
|
30
70
|
#locks = /* @__PURE__ */ new Map();
|
|
31
71
|
#idempotent = /* @__PURE__ */ new Map();
|
|
32
72
|
#states = /* @__PURE__ */ new Map();
|
|
@@ -35,6 +75,7 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
35
75
|
#claims = /* @__PURE__ */ new Map();
|
|
36
76
|
#claimRecords = [];
|
|
37
77
|
#auditLogs = [];
|
|
78
|
+
#transactionTails = /* @__PURE__ */ new Map();
|
|
38
79
|
constructor(options = {}) {
|
|
39
80
|
this.#stocks = /* @__PURE__ */ new Map();
|
|
40
81
|
this.#stockDefaults = /* @__PURE__ */ new Map();
|
|
@@ -83,96 +124,140 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
83
124
|
const current = this.#stocks.get(key);
|
|
84
125
|
if (current !== void 0) this.#stocks.set(key, current + Math.max(0, count));
|
|
85
126
|
}
|
|
86
|
-
async executeClaimRewardTransaction(
|
|
127
|
+
async executeClaimRewardTransaction(params2, mutation2) {
|
|
87
128
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
const initialStock = params2.initialStock !== void 0 ? params2.initialStock : params2.stockKey === "__shared__" ? params2.sharedStockLimit : params2.rewardStockLimit;
|
|
130
|
+
const initialSecondaryStock = params2.initialSecondaryStock !== void 0 ? params2.initialSecondaryStock : params2.rewardStockLimit;
|
|
131
|
+
return await this.runTransaction(params2.rallyId, async () => {
|
|
132
|
+
if (params2.idempotencyKey !== void 0) {
|
|
133
|
+
const previous = await this.getIdempotentResult(
|
|
134
|
+
params2.rallyId,
|
|
135
|
+
params2.idempotencyKey
|
|
136
|
+
);
|
|
137
|
+
if (previous !== null) return { success: true };
|
|
138
|
+
}
|
|
139
|
+
const userState = await this.getUserState(params2.rallyId, params2.userId) ?? params2.initialUserState;
|
|
90
140
|
if (userState === void 0)
|
|
91
141
|
return { success: false, error: "A user state is required for this transaction." };
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
142
|
+
const storedStock = await this.getRewardStock(
|
|
143
|
+
params2.rallyId,
|
|
144
|
+
params2.stockKey ?? params2.rewardId
|
|
145
|
+
);
|
|
146
|
+
const storedSecondaryStock = params2.secondaryStockKey === void 0 ? null : await this.getRewardStock(params2.rallyId, params2.secondaryStockKey);
|
|
147
|
+
const mutationResult = mutation2({
|
|
148
|
+
stock: storedStock ?? initialStock,
|
|
149
|
+
secondaryStock: storedSecondaryStock ?? initialSecondaryStock,
|
|
150
|
+
claimCount: await this.getUserClaimCount(params2.rallyId, params2.userId, params2.rewardId),
|
|
95
151
|
userState
|
|
96
152
|
});
|
|
97
|
-
const idempotencyKey =
|
|
153
|
+
const idempotencyKey = params2.idempotencyKey;
|
|
98
154
|
if (mutationResult.error !== void 0) {
|
|
99
155
|
await this.recordAuditLog(mutationResult.auditLog);
|
|
100
156
|
if (idempotencyKey !== void 0 && mutationResult.result !== void 0)
|
|
101
157
|
await this.saveIdempotentResult(
|
|
102
|
-
|
|
158
|
+
params2.rallyId,
|
|
103
159
|
idempotencyKey,
|
|
104
160
|
mutationResult.result,
|
|
105
|
-
|
|
161
|
+
params2.idempotencyTtlMs ?? 864e5
|
|
106
162
|
);
|
|
107
163
|
return { success: false, error: mutationResult.error };
|
|
108
164
|
}
|
|
109
|
-
const currentStock = await this.getRewardStock(
|
|
110
|
-
|
|
165
|
+
const currentStock = await this.getRewardStock(
|
|
166
|
+
params2.rallyId,
|
|
167
|
+
params2.stockKey ?? params2.rewardId
|
|
168
|
+
);
|
|
169
|
+
const effectiveStock = currentStock ?? initialStock;
|
|
170
|
+
if (currentStock === null && initialStock !== void 0 && initialStock !== null)
|
|
171
|
+
this.#stocks.set(
|
|
172
|
+
this.#stockKey(params2.rallyId, params2.stockKey ?? params2.rewardId),
|
|
173
|
+
initialStock
|
|
174
|
+
);
|
|
175
|
+
const currentSecondaryStock = params2.secondaryStockKey === void 0 ? null : await this.getRewardStock(params2.rallyId, params2.secondaryStockKey);
|
|
176
|
+
const effectiveSecondaryStock = currentSecondaryStock ?? initialSecondaryStock;
|
|
177
|
+
if (currentSecondaryStock === null && params2.secondaryStockKey !== void 0 && initialSecondaryStock !== void 0 && initialSecondaryStock !== null)
|
|
178
|
+
this.#stocks.set(
|
|
179
|
+
this.#stockKey(params2.rallyId, params2.secondaryStockKey),
|
|
180
|
+
initialSecondaryStock
|
|
181
|
+
);
|
|
182
|
+
if (effectiveStock !== null && (mutationResult.nextStock === null || mutationResult.nextStock < 0))
|
|
111
183
|
throw new Error("The transaction produced an invalid stock value.");
|
|
112
|
-
if (
|
|
184
|
+
if (effectiveStock === null && mutationResult.nextStock !== null)
|
|
113
185
|
throw new Error("The transaction changed an unlimited stock to a limited stock.");
|
|
114
186
|
if (mutationResult.nextStock !== null)
|
|
115
187
|
this.#stocks.set(
|
|
116
|
-
this.#stockKey(
|
|
188
|
+
this.#stockKey(params2.rallyId, params2.stockKey ?? params2.rewardId),
|
|
117
189
|
mutationResult.nextStock
|
|
118
190
|
);
|
|
119
|
-
|
|
191
|
+
if (params2.secondaryStockKey !== void 0 && mutationResult.nextSecondaryStock !== void 0) {
|
|
192
|
+
if (effectiveSecondaryStock !== null && (mutationResult.nextSecondaryStock === null || mutationResult.nextSecondaryStock < 0))
|
|
193
|
+
throw new Error("The transaction produced an invalid secondary stock value.");
|
|
194
|
+
if (effectiveSecondaryStock === null && mutationResult.nextSecondaryStock !== null)
|
|
195
|
+
throw new Error(
|
|
196
|
+
"The transaction changed an unlimited secondary stock to a limited stock."
|
|
197
|
+
);
|
|
198
|
+
if (mutationResult.nextSecondaryStock !== null)
|
|
199
|
+
this.#stocks.set(
|
|
200
|
+
this.#stockKey(params2.rallyId, params2.secondaryStockKey),
|
|
201
|
+
mutationResult.nextSecondaryStock
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
await this.saveUserState(params2.rallyId, params2.userId, mutationResult.nextUserState);
|
|
120
205
|
const reward = mutationResult.nextUserState.rewards.find(
|
|
121
|
-
(item) => item.rewardId ===
|
|
206
|
+
(item) => item.rewardId === params2.rewardId
|
|
122
207
|
);
|
|
123
208
|
if (reward?.claimTicketNumber !== void 0)
|
|
124
209
|
await this.recordUserClaim({
|
|
125
|
-
rallyId:
|
|
126
|
-
userId:
|
|
127
|
-
rewardId:
|
|
210
|
+
rallyId: params2.rallyId,
|
|
211
|
+
userId: params2.userId,
|
|
212
|
+
rewardId: params2.rewardId,
|
|
128
213
|
ticketNumber: reward.claimTicketNumber,
|
|
129
|
-
timestamp:
|
|
214
|
+
timestamp: params2.timestamp
|
|
130
215
|
});
|
|
131
216
|
await this.recordAuditLog(mutationResult.auditLog);
|
|
132
217
|
if (idempotencyKey !== void 0 && mutationResult.result !== void 0)
|
|
133
218
|
await this.saveIdempotentResult(
|
|
134
|
-
|
|
219
|
+
params2.rallyId,
|
|
135
220
|
idempotencyKey,
|
|
136
221
|
mutationResult.result,
|
|
137
|
-
|
|
222
|
+
params2.idempotencyTtlMs ?? 864e5
|
|
138
223
|
);
|
|
139
224
|
return { success: true };
|
|
140
225
|
});
|
|
141
|
-
} catch (
|
|
142
|
-
return { success: false, error:
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
143
228
|
}
|
|
144
229
|
}
|
|
145
|
-
async executeCheckInTransaction(
|
|
230
|
+
async executeCheckInTransaction(params2, mutation2) {
|
|
146
231
|
try {
|
|
147
|
-
return await this.runTransaction(
|
|
148
|
-
const userState = await this.getUserState(
|
|
232
|
+
return await this.runTransaction(params2.rallyId, async () => {
|
|
233
|
+
const userState = await this.getUserState(params2.rallyId, params2.userId) ?? params2.initialUserState;
|
|
149
234
|
if (userState === void 0)
|
|
150
235
|
return { success: false, error: "A user state is required for this transaction." };
|
|
151
|
-
const mutationResult =
|
|
236
|
+
const mutationResult = mutation2({ userState });
|
|
152
237
|
if (mutationResult.error !== void 0) {
|
|
153
238
|
await this.recordAuditLog(mutationResult.auditLog);
|
|
154
|
-
if (
|
|
239
|
+
if (params2.idempotencyKey !== void 0 && mutationResult.result !== void 0)
|
|
155
240
|
await this.saveIdempotentResult(
|
|
156
|
-
|
|
157
|
-
|
|
241
|
+
params2.rallyId,
|
|
242
|
+
params2.idempotencyKey,
|
|
158
243
|
mutationResult.result,
|
|
159
|
-
|
|
244
|
+
params2.idempotencyTtlMs ?? 864e5
|
|
160
245
|
);
|
|
161
246
|
return { success: false, error: mutationResult.error };
|
|
162
247
|
}
|
|
163
|
-
await this.saveUserState(
|
|
248
|
+
await this.saveUserState(params2.rallyId, params2.userId, mutationResult.nextUserState);
|
|
164
249
|
await this.recordAuditLog(mutationResult.auditLog);
|
|
165
|
-
if (
|
|
250
|
+
if (params2.idempotencyKey !== void 0 && mutationResult.result !== void 0)
|
|
166
251
|
await this.saveIdempotentResult(
|
|
167
|
-
|
|
168
|
-
|
|
252
|
+
params2.rallyId,
|
|
253
|
+
params2.idempotencyKey,
|
|
169
254
|
mutationResult.result,
|
|
170
|
-
|
|
255
|
+
params2.idempotencyTtlMs ?? 864e5
|
|
171
256
|
);
|
|
172
257
|
return { success: true };
|
|
173
258
|
});
|
|
174
|
-
} catch (
|
|
175
|
-
return { success: false, error:
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
176
261
|
}
|
|
177
262
|
}
|
|
178
263
|
async rollbackUserState(rallyId, userId, previousState) {
|
|
@@ -201,8 +286,8 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
201
286
|
const value = this.#states.get(`${rallyId}:${userId}`);
|
|
202
287
|
return value === void 0 ? null : structuredClone(value);
|
|
203
288
|
}
|
|
204
|
-
async saveUserState(rallyId, userId,
|
|
205
|
-
this.#states.set(`${rallyId}:${userId}`, structuredClone(
|
|
289
|
+
async saveUserState(rallyId, userId, state2) {
|
|
290
|
+
this.#states.set(`${rallyId}:${userId}`, structuredClone(state2));
|
|
206
291
|
}
|
|
207
292
|
async recordAuditLog(log) {
|
|
208
293
|
this.#auditLogs.push(structuredClone(log));
|
|
@@ -214,11 +299,11 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
214
299
|
getAuditLogs() {
|
|
215
300
|
return structuredClone(this.#auditLogs);
|
|
216
301
|
}
|
|
217
|
-
async recordUserClaim(
|
|
218
|
-
const { rallyId, userId, rewardId } =
|
|
302
|
+
async recordUserClaim(params2) {
|
|
303
|
+
const { rallyId, userId, rewardId } = params2;
|
|
219
304
|
const key = `${rallyId}:${userId}:${rewardId}`;
|
|
220
305
|
this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
|
|
221
|
-
this.#claimRecords.push(structuredClone(
|
|
306
|
+
this.#claimRecords.push(structuredClone(params2));
|
|
222
307
|
}
|
|
223
308
|
async rollbackUserClaim(rallyId, userId, rewardId, ticketNumber) {
|
|
224
309
|
const key = `${rallyId}:${userId}:${rewardId}`;
|
|
@@ -238,46 +323,66 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
238
323
|
return structuredClone(this.#claimRecords);
|
|
239
324
|
}
|
|
240
325
|
recordClaim(paramsOrRallyId, userId, rewardId) {
|
|
241
|
-
const
|
|
326
|
+
const params2 = typeof paramsOrRallyId === "string" ? {
|
|
242
327
|
rallyId: paramsOrRallyId,
|
|
243
328
|
userId: userId ?? "",
|
|
244
329
|
rewardId: rewardId ?? "",
|
|
245
330
|
ticketNumber: "",
|
|
246
331
|
timestamp: Date.now()
|
|
247
332
|
} : paramsOrRallyId;
|
|
248
|
-
return this.recordUserClaim(
|
|
249
|
-
}
|
|
250
|
-
async runTransaction(
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.#idempotent.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
333
|
+
return this.recordUserClaim(params2);
|
|
334
|
+
}
|
|
335
|
+
async runTransaction(rallyId, operation) {
|
|
336
|
+
const previous = this.#transactionTails.get(rallyId) ?? Promise.resolve();
|
|
337
|
+
const current = previous.then(async () => {
|
|
338
|
+
const snapshot = {
|
|
339
|
+
stocks: new Map(this.#stocks),
|
|
340
|
+
idempotent: new Map(this.#idempotent),
|
|
341
|
+
states: new Map(this.#states),
|
|
342
|
+
claims: new Map(this.#claims),
|
|
343
|
+
claimRecords: structuredClone(this.#claimRecords),
|
|
344
|
+
auditLogs: structuredClone(this.#auditLogs)
|
|
345
|
+
};
|
|
346
|
+
try {
|
|
347
|
+
return await operation(this);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
this.#stocks.clear();
|
|
350
|
+
for (const [key, value] of snapshot.stocks) this.#stocks.set(key, value);
|
|
351
|
+
this.#idempotent.clear();
|
|
352
|
+
for (const [key, value] of snapshot.idempotent)
|
|
353
|
+
this.#idempotent.set(key, structuredClone(value));
|
|
354
|
+
this.#states.clear();
|
|
355
|
+
for (const [key, value] of snapshot.states) this.#states.set(key, structuredClone(value));
|
|
356
|
+
this.#claims.clear();
|
|
357
|
+
for (const [key, value] of snapshot.claims) this.#claims.set(key, value);
|
|
358
|
+
this.#claimRecords.splice(0, this.#claimRecords.length, ...snapshot.claimRecords);
|
|
359
|
+
this.#auditLogs.splice(0, this.#auditLogs.length, ...snapshot.auditLogs);
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
this.#transactionTails.set(
|
|
364
|
+
rallyId,
|
|
365
|
+
current.then(
|
|
366
|
+
() => void 0,
|
|
367
|
+
() => void 0
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
return current;
|
|
275
371
|
}
|
|
276
372
|
};
|
|
277
373
|
|
|
278
374
|
// src/security.ts
|
|
279
|
-
|
|
280
|
-
|
|
375
|
+
var RequestValidationException = class extends Error {
|
|
376
|
+
code = "VALIDATION_FAILED";
|
|
377
|
+
errors;
|
|
378
|
+
constructor(errors2) {
|
|
379
|
+
super("Request validation failed.");
|
|
380
|
+
this.name = "RequestValidationException";
|
|
381
|
+
this.errors = errors2;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
function errors(...items) {
|
|
385
|
+
return { success: false, errors: items };
|
|
281
386
|
}
|
|
282
387
|
function record(value) {
|
|
283
388
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -285,47 +390,199 @@ function record(value) {
|
|
|
285
390
|
function nonEmpty(value) {
|
|
286
391
|
return typeof value === "string" && value.trim().length > 0;
|
|
287
392
|
}
|
|
288
|
-
function
|
|
289
|
-
if (!record(value) || typeof value.type !== "string")
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
393
|
+
function contextErrors(value) {
|
|
394
|
+
if (!record(value) || typeof value.type !== "string")
|
|
395
|
+
return [
|
|
396
|
+
{
|
|
397
|
+
path: "proof",
|
|
398
|
+
message: "proof is not a valid verification context.",
|
|
399
|
+
code: "INVALID_TYPE"
|
|
400
|
+
}
|
|
401
|
+
];
|
|
402
|
+
if (value.type === "qr" && !nonEmpty(value.token))
|
|
403
|
+
return [
|
|
404
|
+
{ path: "proof.token", message: "token must be a non-empty string.", code: "INVALID_TYPE" }
|
|
405
|
+
];
|
|
406
|
+
if (value.type === "passcode" && !nonEmpty(value.code))
|
|
407
|
+
return [
|
|
408
|
+
{ path: "proof.code", message: "code must be a non-empty string.", code: "INVALID_TYPE" }
|
|
409
|
+
];
|
|
410
|
+
if (value.type === "nfc" && !nonEmpty(value.tagId))
|
|
411
|
+
return [
|
|
412
|
+
{ path: "proof.tagId", message: "tagId must be a non-empty string.", code: "INVALID_TYPE" }
|
|
413
|
+
];
|
|
414
|
+
if (value.type === "custom")
|
|
415
|
+
return "value" in value ? [] : [{ path: "proof.value", message: "value is required.", code: "REQUIRED" }];
|
|
416
|
+
if (value.type === "qr" || value.type === "passcode" || value.type === "nfc") return [];
|
|
417
|
+
if (value.type !== "gps")
|
|
418
|
+
return [{ path: "proof.type", message: "Unknown verification type.", code: "INVALID_ENUM" }];
|
|
419
|
+
const result = [];
|
|
420
|
+
if (typeof value.latitude !== "number" || !Number.isFinite(value.latitude))
|
|
421
|
+
result.push({
|
|
422
|
+
path: "proof.latitude",
|
|
423
|
+
message: "Latitude must be a finite number.",
|
|
424
|
+
code: "INVALID_TYPE"
|
|
425
|
+
});
|
|
426
|
+
else if (value.latitude < -90 || value.latitude > 90)
|
|
427
|
+
result.push({
|
|
428
|
+
path: "proof.latitude",
|
|
429
|
+
message: "Latitude must be between -90 and 90.",
|
|
430
|
+
code: "INVALID_RANGE"
|
|
431
|
+
});
|
|
432
|
+
if (typeof value.longitude !== "number" || !Number.isFinite(value.longitude))
|
|
433
|
+
result.push({
|
|
434
|
+
path: "proof.longitude",
|
|
435
|
+
message: "Longitude must be a finite number.",
|
|
436
|
+
code: "INVALID_TYPE"
|
|
437
|
+
});
|
|
438
|
+
else if (value.longitude < -180 || value.longitude > 180)
|
|
439
|
+
result.push({
|
|
440
|
+
path: "proof.longitude",
|
|
441
|
+
message: "Longitude must be between -180 and 180.",
|
|
442
|
+
code: "INVALID_RANGE"
|
|
443
|
+
});
|
|
444
|
+
if ("radiusMeters" in value && (typeof value.radiusMeters !== "number" || !Number.isFinite(value.radiusMeters) || value.radiusMeters <= 0))
|
|
445
|
+
result.push({
|
|
446
|
+
path: "proof.radiusMeters",
|
|
447
|
+
message: "Radius must be greater than zero.",
|
|
448
|
+
code: "INVALID_RANGE"
|
|
449
|
+
});
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
function dateInput(value) {
|
|
453
|
+
if (typeof value === "number") return Number.isInteger(value) && value > 0;
|
|
454
|
+
if (typeof value !== "string" || value.trim() === "") return false;
|
|
455
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:?\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
|
|
295
456
|
}
|
|
296
|
-
function
|
|
297
|
-
if (
|
|
298
|
-
return fields.
|
|
457
|
+
function requiredErrors(value, fields) {
|
|
458
|
+
if (record(value) && fields.every((field) => nonEmpty(value[field]))) return [];
|
|
459
|
+
return fields.filter((field) => !record(value) || !nonEmpty(value[field])).map((field) => ({
|
|
460
|
+
path: field,
|
|
461
|
+
message: `${field} must be a non-empty string.`,
|
|
462
|
+
code: "REQUIRED"
|
|
463
|
+
}));
|
|
299
464
|
}
|
|
300
465
|
function validateCheckInRequest(value) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!
|
|
304
|
-
return
|
|
466
|
+
const required = requiredErrors(value, ["rallyId", "spotId", "idempotencyKey"]);
|
|
467
|
+
if (required.length > 0) return errors(...required);
|
|
468
|
+
if (!record(value))
|
|
469
|
+
return errors({ path: "$", message: "Expected an object.", code: "INVALID_TYPE" });
|
|
470
|
+
const proof = contextErrors(value.context);
|
|
471
|
+
if (proof.length > 0) return errors(...proof);
|
|
305
472
|
if (value.userId !== void 0 && !nonEmpty(value.userId))
|
|
306
|
-
return
|
|
307
|
-
if (value.now !== void 0 && !
|
|
308
|
-
return
|
|
473
|
+
return errors({ path: "userId", message: "userId must be non-empty.", code: "INVALID_TYPE" });
|
|
474
|
+
if (value.now !== void 0 && !dateInput(value.now))
|
|
475
|
+
return errors({
|
|
476
|
+
path: "now",
|
|
477
|
+
message: "now must be an ISO 8601 date or positive timestamp.",
|
|
478
|
+
code: "INVALID_DATE"
|
|
479
|
+
});
|
|
309
480
|
return { success: true, data: value };
|
|
310
481
|
}
|
|
311
482
|
function validateClaimRewardRequest(value) {
|
|
312
|
-
|
|
313
|
-
|
|
483
|
+
const required = requiredErrors(value, ["rallyId", "rewardId", "idempotencyKey"]);
|
|
484
|
+
if (required.length > 0) return errors(...required);
|
|
485
|
+
if (!record(value))
|
|
486
|
+
return errors({ path: "$", message: "Expected an object.", code: "INVALID_TYPE" });
|
|
314
487
|
if (value.userId !== void 0 && !nonEmpty(value.userId))
|
|
315
|
-
return
|
|
488
|
+
return errors({ path: "userId", message: "userId must be non-empty.", code: "INVALID_TYPE" });
|
|
316
489
|
if (value.staffPasscode !== void 0 && !nonEmpty(value.staffPasscode))
|
|
317
|
-
return
|
|
490
|
+
return errors({
|
|
491
|
+
path: "staffPasscode",
|
|
492
|
+
message: "staffPasscode must be non-empty.",
|
|
493
|
+
code: "INVALID_TYPE"
|
|
494
|
+
});
|
|
318
495
|
if (value.staffId !== void 0 && !nonEmpty(value.staffId))
|
|
319
|
-
return
|
|
320
|
-
if (value.now !== void 0 && !
|
|
321
|
-
return
|
|
496
|
+
return errors({ path: "staffId", message: "staffId must be non-empty.", code: "INVALID_TYPE" });
|
|
497
|
+
if (value.now !== void 0 && !dateInput(value.now))
|
|
498
|
+
return errors({
|
|
499
|
+
path: "now",
|
|
500
|
+
message: "now must be an ISO 8601 date or positive timestamp.",
|
|
501
|
+
code: "INVALID_DATE"
|
|
502
|
+
});
|
|
322
503
|
return { success: true, data: value };
|
|
323
504
|
}
|
|
505
|
+
function uuid(value) {
|
|
506
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
507
|
+
}
|
|
508
|
+
function identityErrors(value) {
|
|
509
|
+
const result = [];
|
|
510
|
+
if (value.userId === void 0 && value.anonymousSessionId === void 0)
|
|
511
|
+
result.push({
|
|
512
|
+
path: "userId",
|
|
513
|
+
message: "An authenticated userId or anonymousSessionId is required.",
|
|
514
|
+
code: "REQUIRED"
|
|
515
|
+
});
|
|
516
|
+
if (value.userId !== void 0 && !nonEmpty(value.userId))
|
|
517
|
+
result.push({ path: "userId", message: "userId must be non-empty.", code: "INVALID_TYPE" });
|
|
518
|
+
if (value.anonymousSessionId !== void 0 && !uuid(value.anonymousSessionId))
|
|
519
|
+
result.push({
|
|
520
|
+
path: "anonymousSessionId",
|
|
521
|
+
message: "anonymousSessionId must be a UUID v4.",
|
|
522
|
+
code: "INVALID_FORMAT"
|
|
523
|
+
});
|
|
524
|
+
if (value.userId !== void 0 && value.anonymousSessionId !== void 0 && value.userId !== value.anonymousSessionId)
|
|
525
|
+
result.push({
|
|
526
|
+
path: "anonymousSessionId",
|
|
527
|
+
message: "userId and anonymousSessionId must identify the same session.",
|
|
528
|
+
code: "IDENTITY_MISMATCH"
|
|
529
|
+
});
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
function directErrors(value, config, kind) {
|
|
533
|
+
const validated = kind === "check-in" ? validateCheckInRequest(value) : validateClaimRewardRequest(value);
|
|
534
|
+
if (!validated.success) return validated.errors;
|
|
535
|
+
const errors2 = [];
|
|
536
|
+
if (validated.data.rallyId !== config.id)
|
|
537
|
+
errors2.push({
|
|
538
|
+
path: "rallyId",
|
|
539
|
+
message: "The rally does not match this server.",
|
|
540
|
+
code: "INVALID_VALUE"
|
|
541
|
+
});
|
|
542
|
+
const resourceId = kind === "check-in" ? validated.data.spotId : validated.data.rewardId;
|
|
543
|
+
const exists = kind === "check-in" ? config.spots.some((spot) => spot.id === resourceId) : config.rewards.some((reward) => reward.id === resourceId);
|
|
544
|
+
if (!exists)
|
|
545
|
+
errors2.push({
|
|
546
|
+
path: kind === "check-in" ? "spotId" : "rewardId",
|
|
547
|
+
message: `${kind === "check-in" ? "Spot" : "Reward"} was not found.`,
|
|
548
|
+
code: kind === "check-in" ? "SPOT_NOT_FOUND" : "REWARD_NOT_FOUND"
|
|
549
|
+
});
|
|
550
|
+
errors2.push(...identityErrors(validated.data));
|
|
551
|
+
return errors2;
|
|
552
|
+
}
|
|
553
|
+
function assertValidCheckInParams(value, config) {
|
|
554
|
+
const errors2 = directErrors(value, config, "check-in");
|
|
555
|
+
if (errors2.length > 0) throw new RequestValidationException(errors2);
|
|
556
|
+
}
|
|
557
|
+
function assertValidClaimParams(value, config) {
|
|
558
|
+
const errors2 = directErrors(value, config, "claim");
|
|
559
|
+
if (errors2.length > 0) throw new RequestValidationException(errors2);
|
|
560
|
+
}
|
|
561
|
+
function assertValidSyncParams(value, config) {
|
|
562
|
+
const validated = validateSyncRequest(value);
|
|
563
|
+
const errors2 = validated.success ? [
|
|
564
|
+
...validated.data.rallyId !== config.id ? [
|
|
565
|
+
{
|
|
566
|
+
path: "rallyId",
|
|
567
|
+
message: "The rally does not match this server.",
|
|
568
|
+
code: "INVALID_VALUE"
|
|
569
|
+
}
|
|
570
|
+
] : []
|
|
571
|
+
] : [...validated.errors];
|
|
572
|
+
if (validated.success) errors2.push(...identityErrors(validated.data));
|
|
573
|
+
if (errors2.length > 0) throw new RequestValidationException(errors2);
|
|
574
|
+
}
|
|
324
575
|
function validateSyncRequest(value) {
|
|
325
|
-
|
|
576
|
+
const required = requiredErrors(value, ["rallyId"]);
|
|
577
|
+
if (required.length > 0) return errors(...required);
|
|
578
|
+
if (!record(value))
|
|
579
|
+
return errors({ path: "$", message: "Expected an object.", code: "INVALID_TYPE" });
|
|
326
580
|
if (value.userId !== void 0 && !nonEmpty(value.userId))
|
|
327
|
-
return
|
|
328
|
-
return {
|
|
581
|
+
return errors({ path: "userId", message: "userId must be non-empty.", code: "INVALID_TYPE" });
|
|
582
|
+
return {
|
|
583
|
+
success: true,
|
|
584
|
+
data: value
|
|
585
|
+
};
|
|
329
586
|
}
|
|
330
587
|
function json(body, status = 200) {
|
|
331
588
|
return new Response(JSON.stringify(body), {
|
|
@@ -336,6 +593,9 @@ function json(body, status = 200) {
|
|
|
336
593
|
function isObject(value) {
|
|
337
594
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
338
595
|
}
|
|
596
|
+
function isUuidV4(value) {
|
|
597
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
598
|
+
}
|
|
339
599
|
function requestId(prefix) {
|
|
340
600
|
return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
|
|
341
601
|
}
|
|
@@ -346,6 +606,15 @@ function timestampMillis(timestamp) {
|
|
|
346
606
|
const value = Date.parse(timestamp);
|
|
347
607
|
return Number.isFinite(value) ? value : Date.now();
|
|
348
608
|
}
|
|
609
|
+
function validationResponse(errors2) {
|
|
610
|
+
return json(
|
|
611
|
+
{
|
|
612
|
+
error: "VALIDATION_FAILED",
|
|
613
|
+
details: errors2.map(({ path, message, code }) => ({ path, message, code }))
|
|
614
|
+
},
|
|
615
|
+
400
|
|
616
|
+
);
|
|
617
|
+
}
|
|
349
618
|
function initialState(config, userId, timestamp) {
|
|
350
619
|
return {
|
|
351
620
|
rallyId: config.id,
|
|
@@ -355,16 +624,37 @@ function initialState(config, userId, timestamp) {
|
|
|
355
624
|
updatedAt: timestamp
|
|
356
625
|
};
|
|
357
626
|
}
|
|
358
|
-
function
|
|
359
|
-
|
|
627
|
+
function rewardStock(config, rewardId, stockLimit) {
|
|
628
|
+
const configured = config.inventory?.[rewardId];
|
|
629
|
+
if (stockLimit === void 0) return configured ?? null;
|
|
630
|
+
if (configured === void 0) return stockLimit;
|
|
631
|
+
return Math.min(stockLimit, configured);
|
|
632
|
+
}
|
|
633
|
+
function sharedStock(config) {
|
|
634
|
+
return config.inventory?.sharedStock ?? config.inventory?.global ?? null;
|
|
360
635
|
}
|
|
361
|
-
|
|
362
|
-
|
|
636
|
+
function inventoryPlan(config, rewardId, stockLimit) {
|
|
637
|
+
const individual = rewardStock(config, rewardId, stockLimit);
|
|
638
|
+
const shared = sharedStock(config);
|
|
639
|
+
if (config.inventoryMode === "shared" && shared !== null) {
|
|
640
|
+
return {
|
|
641
|
+
primaryKey: "__shared__",
|
|
642
|
+
primaryInitial: shared,
|
|
643
|
+
...individual === null ? {} : { secondaryKey: rewardId, secondaryInitial: individual }
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return { primaryKey: rewardId, primaryInitial: individual };
|
|
647
|
+
}
|
|
648
|
+
function getProof(context) {
|
|
649
|
+
return context.type === "qr" ? context.token : context.type === "passcode" ? context.code : context.type === "gps" ? { latitude: context.latitude, longitude: context.longitude } : context.type === "nfc" ? context.tagId : context.value;
|
|
650
|
+
}
|
|
651
|
+
async function evaluate(condition, context, validator, base) {
|
|
652
|
+
if (condition.type !== "custom") return evaluateConditionDetailed(condition, context).ok;
|
|
363
653
|
if (validator === void 0) return false;
|
|
364
654
|
const validationContext = {
|
|
365
655
|
rallyId: base.rallyId,
|
|
366
656
|
spotId: base.spotId,
|
|
367
|
-
proofData: getProof(
|
|
657
|
+
proofData: getProof(context),
|
|
368
658
|
condition,
|
|
369
659
|
userState: base.state
|
|
370
660
|
};
|
|
@@ -388,6 +678,18 @@ function operationStatus(result) {
|
|
|
388
678
|
if (result.ok) return "ACCEPTED";
|
|
389
679
|
return result.code === "CONFLICT" || result.code === "PERSISTENCE_FAILED" ? "RETRYABLE_ERROR" : "REJECTED_PERMANENT";
|
|
390
680
|
}
|
|
681
|
+
function withDirectIdentity(request) {
|
|
682
|
+
if (request.userId !== void 0) return { ...request, userId: request.userId };
|
|
683
|
+
if (request.anonymousSessionId !== void 0 && isUuidV4(request.anonymousSessionId))
|
|
684
|
+
return { ...request, userId: request.anonymousSessionId };
|
|
685
|
+
throw new RequestValidationException([
|
|
686
|
+
{
|
|
687
|
+
path: "userId",
|
|
688
|
+
message: "An authenticated userId or anonymousSessionId is required.",
|
|
689
|
+
code: "REQUIRED"
|
|
690
|
+
}
|
|
691
|
+
]);
|
|
692
|
+
}
|
|
391
693
|
var StampRallyServer = class {
|
|
392
694
|
#config;
|
|
393
695
|
#persistence;
|
|
@@ -408,18 +710,33 @@ var StampRallyServer = class {
|
|
|
408
710
|
}
|
|
409
711
|
async handleCheckIn(request) {
|
|
410
712
|
const body = validateCheckInRequest(await this.#body(request));
|
|
411
|
-
if (!body.success
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
713
|
+
if (!body.success) return validationResponse(body.errors);
|
|
714
|
+
if (body.data.rallyId !== this.#config.id)
|
|
715
|
+
return validationResponse([
|
|
716
|
+
{
|
|
717
|
+
path: "rallyId",
|
|
718
|
+
message: "The rally does not match this server.",
|
|
719
|
+
code: "INVALID_VALUE"
|
|
720
|
+
}
|
|
721
|
+
]);
|
|
416
722
|
const userId = await this.#user(request);
|
|
417
723
|
if (userId === null)
|
|
418
724
|
return json(
|
|
419
725
|
{ ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
|
|
420
726
|
401
|
|
421
727
|
);
|
|
422
|
-
const
|
|
728
|
+
const sessionId = request.headers.get("x-anonymous-session-id");
|
|
729
|
+
let result;
|
|
730
|
+
try {
|
|
731
|
+
result = await this.checkIn({
|
|
732
|
+
...body.data,
|
|
733
|
+
userId,
|
|
734
|
+
...sessionId === null ? {} : { anonymousSessionId: sessionId }
|
|
735
|
+
});
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (error instanceof RequestValidationException) return validationResponse(error.errors);
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
423
740
|
return json(
|
|
424
741
|
{ ...result, status: operationStatus(result) },
|
|
425
742
|
result.ok ? 200 : result.code === "SPOT_NOT_FOUND" ? 404 : 422
|
|
@@ -427,15 +744,33 @@ var StampRallyServer = class {
|
|
|
427
744
|
}
|
|
428
745
|
async handleClaimReward(request) {
|
|
429
746
|
const body = validateClaimRewardRequest(await this.#body(request));
|
|
430
|
-
if (!body.success
|
|
431
|
-
|
|
747
|
+
if (!body.success) return validationResponse(body.errors);
|
|
748
|
+
if (body.data.rallyId !== this.#config.id)
|
|
749
|
+
return validationResponse([
|
|
750
|
+
{
|
|
751
|
+
path: "rallyId",
|
|
752
|
+
message: "The rally does not match this server.",
|
|
753
|
+
code: "INVALID_VALUE"
|
|
754
|
+
}
|
|
755
|
+
]);
|
|
432
756
|
const userId = await this.#user(request);
|
|
433
757
|
if (userId === null)
|
|
434
758
|
return json(
|
|
435
759
|
{ ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
|
|
436
760
|
401
|
|
437
761
|
);
|
|
438
|
-
const
|
|
762
|
+
const sessionId = request.headers.get("x-anonymous-session-id");
|
|
763
|
+
let result;
|
|
764
|
+
try {
|
|
765
|
+
result = await this.claimReward({
|
|
766
|
+
...body.data,
|
|
767
|
+
userId,
|
|
768
|
+
...sessionId === null ? {} : { anonymousSessionId: sessionId }
|
|
769
|
+
});
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (error instanceof RequestValidationException) return validationResponse(error.errors);
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
439
774
|
return json(
|
|
440
775
|
{ ...result, status: operationStatus(result) },
|
|
441
776
|
result.ok ? 200 : result.code === "REWARD_NOT_FOUND" ? 404 : 422
|
|
@@ -443,24 +778,47 @@ var StampRallyServer = class {
|
|
|
443
778
|
}
|
|
444
779
|
async handleSync(request) {
|
|
445
780
|
const body = validateSyncRequest(await this.#body(request));
|
|
446
|
-
if (!body.success
|
|
447
|
-
|
|
781
|
+
if (!body.success) return validationResponse(body.errors);
|
|
782
|
+
if (body.data.rallyId !== this.#config.id)
|
|
783
|
+
return validationResponse([
|
|
784
|
+
{
|
|
785
|
+
path: "rallyId",
|
|
786
|
+
message: "The rally does not match this server.",
|
|
787
|
+
code: "INVALID_VALUE"
|
|
788
|
+
}
|
|
789
|
+
]);
|
|
448
790
|
const userId = await this.#user(request);
|
|
449
791
|
if (userId === null)
|
|
450
792
|
return json(
|
|
451
793
|
{ ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
|
|
452
794
|
401
|
|
453
795
|
);
|
|
454
|
-
|
|
796
|
+
const sessionId = request.headers.get("x-anonymous-session-id");
|
|
797
|
+
try {
|
|
798
|
+
return json({
|
|
799
|
+
ok: true,
|
|
800
|
+
state: await this.syncProgress({
|
|
801
|
+
rallyId: body.data.rallyId,
|
|
802
|
+
userId,
|
|
803
|
+
...sessionId === null ? {} : { anonymousSessionId: sessionId }
|
|
804
|
+
})
|
|
805
|
+
});
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (error instanceof RequestValidationException) return validationResponse(error.errors);
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
455
810
|
}
|
|
456
811
|
async checkIn(request) {
|
|
457
|
-
const
|
|
812
|
+
const directRequest = withDirectIdentity(request);
|
|
813
|
+
assertValidCheckInParams(directRequest, this.#config);
|
|
814
|
+
const { userId } = directRequest;
|
|
815
|
+
const key = `check-in:${request.rallyId}:${userId}:${request.idempotencyKey}`;
|
|
458
816
|
const previous = await this.#persistence.getIdempotentResult(
|
|
459
817
|
request.rallyId,
|
|
460
818
|
key
|
|
461
819
|
);
|
|
462
820
|
if (previous !== null) return previous;
|
|
463
|
-
const lockKey = `state:${request.rallyId}:${
|
|
821
|
+
const lockKey = `state:${request.rallyId}:${userId}`;
|
|
464
822
|
if (!await this.#persistence.acquireLock(
|
|
465
823
|
request.rallyId,
|
|
466
824
|
lockKey,
|
|
@@ -469,11 +827,11 @@ var StampRallyServer = class {
|
|
|
469
827
|
return { ok: false, code: "CONFLICT", message: "The user state is being updated." };
|
|
470
828
|
const timestamp = now(this.#options);
|
|
471
829
|
try {
|
|
472
|
-
const current = await this.#persistence.getUserState(request.rallyId,
|
|
830
|
+
const current = await this.#persistence.getUserState(request.rallyId, userId) ?? initialState(this.#config, userId, timestamp);
|
|
473
831
|
const responseHolder = { value: null };
|
|
474
832
|
const makeAudit = (status, code) => audit(
|
|
475
833
|
request.rallyId,
|
|
476
|
-
|
|
834
|
+
userId,
|
|
477
835
|
"CHECK_IN",
|
|
478
836
|
request.spotId,
|
|
479
837
|
request.idempotencyKey,
|
|
@@ -489,7 +847,7 @@ var StampRallyServer = class {
|
|
|
489
847
|
message: "Spot was not found."
|
|
490
848
|
};
|
|
491
849
|
return await this.#rememberCheckInTransaction(
|
|
492
|
-
|
|
850
|
+
directRequest,
|
|
493
851
|
timestamp,
|
|
494
852
|
key,
|
|
495
853
|
current,
|
|
@@ -513,7 +871,7 @@ var StampRallyServer = class {
|
|
|
513
871
|
};
|
|
514
872
|
if (current.records.some((record2) => record2.stampId === request.spotId))
|
|
515
873
|
return await this.#rememberCheckInTransaction(
|
|
516
|
-
|
|
874
|
+
directRequest,
|
|
517
875
|
timestamp,
|
|
518
876
|
key,
|
|
519
877
|
current,
|
|
@@ -523,7 +881,7 @@ var StampRallyServer = class {
|
|
|
523
881
|
const acquired = new Set(current.records.map((record2) => record2.stampId));
|
|
524
882
|
if (spot.prerequisites?.some((id) => !acquired.has(id)))
|
|
525
883
|
return await this.#rememberCheckInTransaction(
|
|
526
|
-
|
|
884
|
+
directRequest,
|
|
527
885
|
timestamp,
|
|
528
886
|
key,
|
|
529
887
|
current,
|
|
@@ -538,7 +896,7 @@ var StampRallyServer = class {
|
|
|
538
896
|
{ rallyId: request.rallyId, spotId: request.spotId, state: current }
|
|
539
897
|
))
|
|
540
898
|
return await this.#rememberCheckInTransaction(
|
|
541
|
-
|
|
899
|
+
directRequest,
|
|
542
900
|
timestamp,
|
|
543
901
|
key,
|
|
544
902
|
current,
|
|
@@ -560,7 +918,7 @@ var StampRallyServer = class {
|
|
|
560
918
|
const transaction = await this.#persistence.executeCheckInTransaction(
|
|
561
919
|
{
|
|
562
920
|
rallyId: request.rallyId,
|
|
563
|
-
userId
|
|
921
|
+
userId,
|
|
564
922
|
spotId: request.spotId,
|
|
565
923
|
timestamp: timestampMillis(timestamp),
|
|
566
924
|
idempotencyKey: key,
|
|
@@ -585,7 +943,10 @@ var StampRallyServer = class {
|
|
|
585
943
|
}
|
|
586
944
|
}
|
|
587
945
|
async claimReward(request) {
|
|
588
|
-
const
|
|
946
|
+
const directRequest = withDirectIdentity(request);
|
|
947
|
+
assertValidClaimParams(directRequest, this.#config);
|
|
948
|
+
const { userId } = directRequest;
|
|
949
|
+
const key = `claim:${request.rallyId}:${userId}:${request.rewardId}:${request.idempotencyKey}`;
|
|
589
950
|
const previous = await this.#persistence.getIdempotentResult(
|
|
590
951
|
request.rallyId,
|
|
591
952
|
key
|
|
@@ -596,10 +957,22 @@ var StampRallyServer = class {
|
|
|
596
957
|
return this.#rememberClaim(
|
|
597
958
|
key,
|
|
598
959
|
{ ok: false, code: "REWARD_NOT_FOUND", message: "Reward was not found." },
|
|
599
|
-
|
|
960
|
+
directRequest,
|
|
600
961
|
now(this.#options)
|
|
601
962
|
);
|
|
602
|
-
const
|
|
963
|
+
const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
|
|
964
|
+
if (rewardStock(this.#config, reward.id, reward.stockLimit) !== null && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function"))
|
|
965
|
+
return this.#rememberClaim(
|
|
966
|
+
key,
|
|
967
|
+
{
|
|
968
|
+
ok: false,
|
|
969
|
+
code: "INVENTORY_NOT_SUPPORTED",
|
|
970
|
+
message: "This persistence adapter cannot store per-reward inventory."
|
|
971
|
+
},
|
|
972
|
+
directRequest,
|
|
973
|
+
now(this.#options)
|
|
974
|
+
);
|
|
975
|
+
const lockKey = this.#config.inventoryMode === "shared" ? "inventory:shared" : `reward:${request.rallyId}:${reward.id}`;
|
|
603
976
|
if (!await this.#persistence.acquireLock(
|
|
604
977
|
request.rallyId,
|
|
605
978
|
lockKey,
|
|
@@ -617,16 +990,22 @@ var StampRallyServer = class {
|
|
|
617
990
|
const result = await this.#persistence.executeClaimRewardTransaction(
|
|
618
991
|
{
|
|
619
992
|
rallyId: request.rallyId,
|
|
620
|
-
userId
|
|
993
|
+
userId,
|
|
621
994
|
rewardId: reward.id,
|
|
995
|
+
stockKey: plan.primaryKey,
|
|
996
|
+
...plan.secondaryKey === void 0 ? {} : { secondaryStockKey: plan.secondaryKey },
|
|
997
|
+
rewardStockLimit: rewardStock(this.#config, reward.id, reward.stockLimit),
|
|
998
|
+
sharedStockLimit: this.#config.inventoryMode === "shared" ? sharedStock(this.#config) : null,
|
|
999
|
+
initialStock: plan.primaryInitial,
|
|
1000
|
+
...plan.secondaryInitial === void 0 ? {} : { initialSecondaryStock: plan.secondaryInitial },
|
|
622
1001
|
ticketNumber: request.idempotencyKey,
|
|
623
1002
|
timestamp: Number.isNaN(Date.parse(timestamp)) ? Date.now() : Date.parse(timestamp),
|
|
624
1003
|
idempotencyKey: key,
|
|
625
1004
|
...request.staffPasscode === void 0 ? {} : { proofData: request.staffPasscode },
|
|
626
1005
|
...this.#options.idempotencyTtlMs === void 0 ? {} : { idempotencyTtlMs: this.#options.idempotencyTtlMs },
|
|
627
|
-
initialUserState: initialState(this.#config,
|
|
1006
|
+
initialUserState: initialState(this.#config, userId, timestamp)
|
|
628
1007
|
},
|
|
629
|
-
({ stock, claimCount, userState }) => {
|
|
1008
|
+
({ stock, secondaryStock, claimCount, userState }) => {
|
|
630
1009
|
const storedReward = userState.rewards.find((item) => item.rewardId === reward.id) ?? {
|
|
631
1010
|
rewardId: reward.id,
|
|
632
1011
|
status: "LOCKED"
|
|
@@ -634,7 +1013,7 @@ var StampRallyServer = class {
|
|
|
634
1013
|
const currentReward = reward.redemptionMethod === "server_claim" && storedReward.status === "CONSUMED" && (reward.userClaimLimit === void 0 || claimCount < reward.userClaimLimit) ? { ...storedReward, status: "AVAILABLE" } : storedReward;
|
|
635
1014
|
const makeAudit = (status, code) => audit(
|
|
636
1015
|
request.rallyId,
|
|
637
|
-
|
|
1016
|
+
userId,
|
|
638
1017
|
"CLAIM_REWARD",
|
|
639
1018
|
reward.id,
|
|
640
1019
|
request.idempotencyKey,
|
|
@@ -642,7 +1021,7 @@ var StampRallyServer = class {
|
|
|
642
1021
|
timestamp,
|
|
643
1022
|
code
|
|
644
1023
|
);
|
|
645
|
-
if (stock !== null && stock <= 0) {
|
|
1024
|
+
if (stock !== null && stock <= 0 || secondaryStock !== null && secondaryStock <= 0) {
|
|
646
1025
|
responseHolder.value = {
|
|
647
1026
|
ok: false,
|
|
648
1027
|
code: "OUT_OF_STOCK",
|
|
@@ -650,6 +1029,7 @@ var StampRallyServer = class {
|
|
|
650
1029
|
};
|
|
651
1030
|
return {
|
|
652
1031
|
nextStock: stock,
|
|
1032
|
+
...plan.secondaryKey === void 0 ? {} : { nextSecondaryStock: secondaryStock },
|
|
653
1033
|
nextUserState: userState,
|
|
654
1034
|
auditLog: makeAudit("REJECTED", "OUT_OF_STOCK"),
|
|
655
1035
|
result: responseHolder.value,
|
|
@@ -680,14 +1060,35 @@ var StampRallyServer = class {
|
|
|
680
1060
|
};
|
|
681
1061
|
}
|
|
682
1062
|
const nextRewards = userState.rewards.some((item) => item.rewardId === reward.id) ? userState.rewards.map((item) => item.rewardId === reward.id ? local.value : item) : [...userState.rewards, local.value];
|
|
1063
|
+
const consumed = local.value.claimTicketNumber !== void 0;
|
|
1064
|
+
const nextStock = consumed && stock !== null ? Math.max(0, stock - 1) : stock;
|
|
1065
|
+
const nextSecondaryStock = consumed && secondaryStock !== null ? Math.max(0, secondaryStock - 1) : secondaryStock;
|
|
683
1066
|
const next = {
|
|
684
1067
|
...userState,
|
|
685
1068
|
rewards: nextRewards,
|
|
686
|
-
updatedAt: timestamp
|
|
1069
|
+
updatedAt: timestamp,
|
|
1070
|
+
inventory: {
|
|
1071
|
+
...plan.primaryKey === "__shared__" && nextStock !== null ? { sharedRemaining: nextStock } : {},
|
|
1072
|
+
...plan.secondaryKey !== void 0 && nextSecondaryStock !== null ? { rewardRemaining: { [reward.id]: nextSecondaryStock } } : plan.primaryKey === reward.id && nextStock !== null ? { rewardRemaining: { [reward.id]: nextStock } } : {}
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
const inventory = {
|
|
1076
|
+
...plan.primaryKey === "__shared__" && nextStock !== null ? { sharedRemaining: nextStock } : {},
|
|
1077
|
+
...plan.secondaryKey !== void 0 && nextSecondaryStock !== null ? { rewardRemaining: nextSecondaryStock } : plan.primaryKey === reward.id && nextStock !== null ? { rewardRemaining: nextStock } : {}
|
|
1078
|
+
};
|
|
1079
|
+
responseHolder.value = local.value.claimTicketNumber === void 0 ? {
|
|
1080
|
+
ok: true,
|
|
1081
|
+
state: next,
|
|
1082
|
+
...Object.keys(inventory).length === 0 ? {} : { inventory }
|
|
1083
|
+
} : {
|
|
1084
|
+
ok: true,
|
|
1085
|
+
state: next,
|
|
1086
|
+
claimTicketNumber: local.value.claimTicketNumber,
|
|
1087
|
+
...Object.keys(inventory).length === 0 ? {} : { inventory }
|
|
687
1088
|
};
|
|
688
|
-
responseHolder.value = local.value.claimTicketNumber === void 0 ? { ok: true, state: next } : { ok: true, state: next, claimTicketNumber: local.value.claimTicketNumber };
|
|
689
1089
|
return {
|
|
690
|
-
nextStock
|
|
1090
|
+
nextStock,
|
|
1091
|
+
...plan.secondaryKey === void 0 ? {} : { nextSecondaryStock },
|
|
691
1092
|
nextUserState: next,
|
|
692
1093
|
auditLog: makeAudit("SUCCESS"),
|
|
693
1094
|
result: responseHolder.value
|
|
@@ -697,6 +1098,12 @@ var StampRallyServer = class {
|
|
|
697
1098
|
const response = responseHolder.value;
|
|
698
1099
|
if (!result.success) {
|
|
699
1100
|
if (response !== null && !response.ok && response.code === result.error) return response;
|
|
1101
|
+
if (result.error === "INVENTORY_NOT_SUPPORTED")
|
|
1102
|
+
return {
|
|
1103
|
+
ok: false,
|
|
1104
|
+
code: "INVENTORY_NOT_SUPPORTED",
|
|
1105
|
+
message: "This persistence adapter cannot store per-reward inventory."
|
|
1106
|
+
};
|
|
700
1107
|
return {
|
|
701
1108
|
ok: false,
|
|
702
1109
|
code: "PERSISTENCE_FAILED",
|
|
@@ -709,18 +1116,52 @@ var StampRallyServer = class {
|
|
|
709
1116
|
code: "PERSISTENCE_FAILED",
|
|
710
1117
|
message: result.error ?? "Reward claim failed."
|
|
711
1118
|
};
|
|
712
|
-
} catch (
|
|
1119
|
+
} catch (error) {
|
|
713
1120
|
return {
|
|
714
1121
|
ok: false,
|
|
715
1122
|
code: "PERSISTENCE_FAILED",
|
|
716
|
-
message:
|
|
1123
|
+
message: error instanceof Error ? error.message : "Reward claim failed."
|
|
717
1124
|
};
|
|
718
1125
|
} finally {
|
|
719
1126
|
await this.#persistence.releaseLock(request.rallyId, lockKey);
|
|
720
1127
|
}
|
|
721
1128
|
}
|
|
722
1129
|
async sync(rallyId, userId) {
|
|
723
|
-
|
|
1130
|
+
assertValidSyncParams({ rallyId, userId }, this.#config);
|
|
1131
|
+
const state2 = await this.#persistence.getUserState(rallyId, userId) ?? initialState(this.#config, userId, now(this.#options));
|
|
1132
|
+
return this.#attachInventory(state2);
|
|
1133
|
+
}
|
|
1134
|
+
async syncProgress(request) {
|
|
1135
|
+
const directRequest = withDirectIdentity(request);
|
|
1136
|
+
assertValidSyncParams(directRequest, this.#config);
|
|
1137
|
+
return this.sync(directRequest.rallyId, directRequest.userId);
|
|
1138
|
+
}
|
|
1139
|
+
async #attachInventory(state2) {
|
|
1140
|
+
const rewardRemaining = {};
|
|
1141
|
+
for (const reward of this.#config.rewards) {
|
|
1142
|
+
const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
|
|
1143
|
+
if (plan.secondaryKey !== void 0) {
|
|
1144
|
+
const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.secondaryKey);
|
|
1145
|
+
const remaining = stock ?? plan.secondaryInitial ?? null;
|
|
1146
|
+
if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);
|
|
1147
|
+
} else if (plan.primaryKey !== "__shared__") {
|
|
1148
|
+
const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.primaryKey);
|
|
1149
|
+
const remaining = stock ?? plan.primaryInitial;
|
|
1150
|
+
if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const shared = sharedStock(this.#config);
|
|
1154
|
+
const storedShared = await this.#persistence.getRewardStock(state2.rallyId, "__shared__");
|
|
1155
|
+
const sharedRemaining = this.#config.inventoryMode === "shared" && shared !== null ? Math.max(0, storedShared ?? shared) : void 0;
|
|
1156
|
+
return {
|
|
1157
|
+
...state2,
|
|
1158
|
+
...Object.keys(rewardRemaining).length === 0 && sharedRemaining === void 0 ? {} : {
|
|
1159
|
+
inventory: {
|
|
1160
|
+
...sharedRemaining === void 0 ? {} : { sharedRemaining },
|
|
1161
|
+
...Object.keys(rewardRemaining).length === 0 ? {} : { rewardRemaining }
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
724
1165
|
}
|
|
725
1166
|
async #body(request) {
|
|
726
1167
|
try {
|
|
@@ -738,9 +1179,14 @@ var StampRallyServer = class {
|
|
|
738
1179
|
const authenticatedUserId = identity.authenticatedUserId;
|
|
739
1180
|
return authenticatedUserId.length > 0 ? authenticatedUserId : null;
|
|
740
1181
|
}
|
|
1182
|
+
const policy = this.#options.anonymousPolicy ?? "session_scoped";
|
|
1183
|
+
if (policy === "reject") return null;
|
|
1184
|
+
const sessionId = request.headers.get("X-Anonymous-Session-Id");
|
|
1185
|
+
if (sessionId !== null) return isUuidV4(sessionId) ? sessionId : null;
|
|
1186
|
+
if (policy === "session_scoped") return null;
|
|
741
1187
|
return "anonymous";
|
|
742
1188
|
}
|
|
743
|
-
async #rememberCheckInTransaction(request, timestamp, key, current,
|
|
1189
|
+
async #rememberCheckInTransaction(request, timestamp, key, current, mutation2, responseHolder) {
|
|
744
1190
|
const transaction = await this.#persistence.executeCheckInTransaction(
|
|
745
1191
|
{
|
|
746
1192
|
rallyId: request.rallyId,
|
|
@@ -751,9 +1197,9 @@ var StampRallyServer = class {
|
|
|
751
1197
|
...this.#options.idempotencyTtlMs === void 0 ? {} : { idempotencyTtlMs: this.#options.idempotencyTtlMs },
|
|
752
1198
|
initialUserState: current
|
|
753
1199
|
},
|
|
754
|
-
() =>
|
|
1200
|
+
() => mutation2
|
|
755
1201
|
);
|
|
756
|
-
if (responseHolder.value !== null && (transaction.success || transaction.error ===
|
|
1202
|
+
if (responseHolder.value !== null && (transaction.success || transaction.error === mutation2.error))
|
|
757
1203
|
return responseHolder.value;
|
|
758
1204
|
return {
|
|
759
1205
|
ok: false,
|
|
@@ -784,6 +1230,117 @@ var StampRallyServer = class {
|
|
|
784
1230
|
}
|
|
785
1231
|
};
|
|
786
1232
|
|
|
787
|
-
|
|
1233
|
+
// src/testing/compliance.ts
|
|
1234
|
+
var state = (userId) => ({
|
|
1235
|
+
rallyId: "compliance-rally",
|
|
1236
|
+
userId,
|
|
1237
|
+
records: [],
|
|
1238
|
+
rewards: [{ rewardId: "reward", status: "AVAILABLE" }],
|
|
1239
|
+
updatedAt: "2026-01-01T00:00:00.000Z"
|
|
1240
|
+
});
|
|
1241
|
+
function audit2(idempotencyKey, userId) {
|
|
1242
|
+
return {
|
|
1243
|
+
id: `audit-${idempotencyKey}`,
|
|
1244
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
1245
|
+
rallyId: "compliance-rally",
|
|
1246
|
+
userId,
|
|
1247
|
+
action: "CLAIM_REWARD",
|
|
1248
|
+
resourceId: "reward",
|
|
1249
|
+
status: "SUCCESS",
|
|
1250
|
+
idempotencyKey
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function params(userId, idempotencyKey) {
|
|
1254
|
+
return {
|
|
1255
|
+
rallyId: "compliance-rally",
|
|
1256
|
+
userId,
|
|
1257
|
+
rewardId: "reward",
|
|
1258
|
+
ticketNumber: `ticket-${idempotencyKey}`,
|
|
1259
|
+
timestamp: Date.parse("2026-01-01T00:00:00.000Z"),
|
|
1260
|
+
idempotencyKey,
|
|
1261
|
+
rewardStockLimit: 1,
|
|
1262
|
+
sharedStockLimit: 1,
|
|
1263
|
+
stockKey: "__shared__",
|
|
1264
|
+
secondaryStockKey: "reward",
|
|
1265
|
+
initialStock: 1,
|
|
1266
|
+
initialSecondaryStock: 1,
|
|
1267
|
+
initialUserState: state(userId)
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function mutation(current) {
|
|
1271
|
+
if (current.stock === 0 || current.secondaryStock === 0)
|
|
1272
|
+
return {
|
|
1273
|
+
nextStock: current.stock,
|
|
1274
|
+
nextSecondaryStock: current.secondaryStock,
|
|
1275
|
+
nextUserState: current.userState,
|
|
1276
|
+
auditLog: audit2("rejected", current.userState.userId ?? "unknown"),
|
|
1277
|
+
error: "OUT_OF_STOCK"
|
|
1278
|
+
};
|
|
1279
|
+
return {
|
|
1280
|
+
nextStock: current.stock === null ? null : current.stock - 1,
|
|
1281
|
+
nextSecondaryStock: current.secondaryStock === null ? null : current.secondaryStock - 1,
|
|
1282
|
+
nextUserState: {
|
|
1283
|
+
...current.userState,
|
|
1284
|
+
rewards: [{ rewardId: "reward", status: "CONSUMED" }],
|
|
1285
|
+
updatedAt: "2026-01-01T00:00:00.000Z"
|
|
1286
|
+
},
|
|
1287
|
+
auditLog: audit2("success", current.userState.userId ?? "unknown"),
|
|
1288
|
+
result: { ok: true }
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function assert(condition, message) {
|
|
1292
|
+
if (!condition) throw new Error(`Persistence adapter compliance failed: ${message}`);
|
|
1293
|
+
}
|
|
1294
|
+
async function runPersistenceAdapterComplianceTests(createAdapter) {
|
|
1295
|
+
const adapter = await createAdapter();
|
|
1296
|
+
assert(
|
|
1297
|
+
adapter.supportsRewardStock !== false,
|
|
1298
|
+
"the adapter must explicitly support reward stock for this suite"
|
|
1299
|
+
);
|
|
1300
|
+
const [first, second] = await Promise.all([
|
|
1301
|
+
adapter.executeClaimRewardTransaction(params("alice", "race-a"), mutation),
|
|
1302
|
+
adapter.executeClaimRewardTransaction(params("bob", "race-b"), mutation)
|
|
1303
|
+
]);
|
|
1304
|
+
assert([first.success, second.success].filter(Boolean).length === 1, "race was not serialized");
|
|
1305
|
+
assert(
|
|
1306
|
+
await adapter.getRewardStock("compliance-rally", "__shared__") === 0,
|
|
1307
|
+
"shared stock was not decremented atomically"
|
|
1308
|
+
);
|
|
1309
|
+
assert(
|
|
1310
|
+
await adapter.getRewardStock("compliance-rally", "reward") === 0,
|
|
1311
|
+
"per-reward stock was not decremented atomically"
|
|
1312
|
+
);
|
|
1313
|
+
const idempotentAdapter = await createAdapter();
|
|
1314
|
+
const idempotentParams = params("alice", "same-key");
|
|
1315
|
+
const firstClaim = await idempotentAdapter.executeClaimRewardTransaction(
|
|
1316
|
+
idempotentParams,
|
|
1317
|
+
mutation
|
|
1318
|
+
);
|
|
1319
|
+
const secondClaim = await idempotentAdapter.executeClaimRewardTransaction(
|
|
1320
|
+
idempotentParams,
|
|
1321
|
+
mutation
|
|
1322
|
+
);
|
|
1323
|
+
assert(firstClaim.success && secondClaim.success, "idempotent claim did not remain successful");
|
|
1324
|
+
assert(
|
|
1325
|
+
await idempotentAdapter.getRewardStock("compliance-rally", "__shared__") === 0,
|
|
1326
|
+
"idempotent retry decremented shared stock twice"
|
|
1327
|
+
);
|
|
1328
|
+
const rollbackAdapter = await createAdapter();
|
|
1329
|
+
const rollbackParams = params("alice", "rollback");
|
|
1330
|
+
const rollback = await rollbackAdapter.executeClaimRewardTransaction(rollbackParams, () => {
|
|
1331
|
+
throw new Error("forced rollback");
|
|
1332
|
+
});
|
|
1333
|
+
assert(!rollback.success, "a failed mutation was committed");
|
|
1334
|
+
assert(
|
|
1335
|
+
await rollbackAdapter.getRewardStock("compliance-rally", "__shared__") === null,
|
|
1336
|
+
"rollback changed shared stock"
|
|
1337
|
+
);
|
|
1338
|
+
assert(
|
|
1339
|
+
await rollbackAdapter.getRewardStock("compliance-rally", "reward") === null,
|
|
1340
|
+
"rollback changed per-reward stock"
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export { InMemoryServerPersistenceAdapter, RequestValidationException, StampRallyServer, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
|
|
788
1345
|
//# sourceMappingURL=index.js.map
|
|
789
1346
|
//# sourceMappingURL=index.js.map
|