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