@stamprally/server 0.15.0 → 0.17.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/dist/index.cjs CHANGED
@@ -3,50 +3,61 @@
3
3
  var core = require('@stamprally/core');
4
4
 
5
5
  // src/examples/transaction.ts
6
- async function executeClaimRewardTransaction(database, store, params, mutation) {
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, params);
10
- const next = mutation(current);
9
+ const current = await store.readContext(transaction, params2);
10
+ const secondaryStock = current.secondaryStock ?? null;
11
+ const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : current.stock;
12
+ const next = mutation2({
13
+ ...current,
14
+ rewardStock: rewardStock2,
15
+ sharedStock: params2.stockKey === "__shared__" ? current.stock : null,
16
+ primaryStock: rewardStock2,
17
+ secondaryStock,
18
+ stock: current.stock
19
+ });
11
20
  if (next.error !== void 0) {
12
21
  await store.writeAudit(transaction, next.auditLog);
13
- if (params.idempotencyKey !== void 0 && next.result !== void 0)
14
- await store.writeIdempotency(transaction, params, next.result);
22
+ if (params2.idempotencyKey !== void 0 && next.result !== void 0)
23
+ await store.writeIdempotency(transaction, params2, next.result);
15
24
  return { success: false, error: next.error };
16
25
  }
17
- if (next.nextStock !== null) await store.writeStock(transaction, params, next.nextStock);
26
+ if (next.nextSecondaryStock !== void 0 && store.writeSecondaryStock === void 0)
27
+ return { success: false, error: "INVENTORY_STORAGE_NOT_IMPLEMENTED" };
28
+ if (next.nextStock !== null) await store.writeStock(transaction, params2, next.nextStock);
18
29
  if (next.nextSecondaryStock !== void 0 && store.writeSecondaryStock !== void 0) {
19
30
  if (next.nextSecondaryStock !== null)
20
- await store.writeSecondaryStock(transaction, params, next.nextSecondaryStock);
31
+ await store.writeSecondaryStock(transaction, params2, next.nextSecondaryStock);
21
32
  }
22
- await store.writeUserState(transaction, params, next.nextUserState);
23
- await store.writeClaimRecord(transaction, params, next.nextUserState);
33
+ await store.writeUserState(transaction, params2, next.nextUserState);
34
+ await store.writeClaimRecord(transaction, params2, next.nextUserState);
24
35
  await store.writeAudit(transaction, next.auditLog);
25
- if (params.idempotencyKey !== void 0 && next.result !== void 0)
26
- await store.writeIdempotency(transaction, params, next.result);
36
+ if (params2.idempotencyKey !== void 0 && next.result !== void 0)
37
+ await store.writeIdempotency(transaction, params2, next.result);
27
38
  return { success: true };
28
39
  });
29
40
  } catch (error) {
30
41
  return { success: false, error: error instanceof Error ? error.message : String(error) };
31
42
  }
32
43
  }
33
- async function executeCheckInTransaction(database, store, params, mutation, current) {
44
+ async function executeCheckInTransaction(database, store, params2, mutation2, current) {
34
45
  try {
35
46
  return await database.transaction(async (transaction) => {
36
47
  const userState = current ?? (store.readUserState === void 0 ? (() => {
37
48
  throw new Error("A current user state or readUserState implementation is required.");
38
- })() : await store.readUserState(transaction, params));
39
- const next = mutation({ userState });
49
+ })() : await store.readUserState(transaction, params2));
50
+ const next = mutation2({ userState });
40
51
  if (next.error !== void 0) {
41
52
  await store.writeAudit(transaction, next.auditLog);
42
- if (params.idempotencyKey !== void 0 && next.result !== void 0)
43
- await store.writeIdempotency(transaction, params, next.result);
53
+ if (params2.idempotencyKey !== void 0 && next.result !== void 0)
54
+ await store.writeIdempotency(transaction, params2, next.result);
44
55
  return { success: false, error: next.error };
45
56
  }
46
- await store.writeUserState(transaction, params, next.nextUserState);
57
+ await store.writeUserState(transaction, params2, next.nextUserState);
47
58
  await store.writeAudit(transaction, next.auditLog);
48
- if (params.idempotencyKey !== void 0 && next.result !== void 0)
49
- await store.writeIdempotency(transaction, params, next.result);
59
+ if (params2.idempotencyKey !== void 0 && next.result !== void 0)
60
+ await store.writeIdempotency(transaction, params2, next.result);
50
61
  return { success: true };
51
62
  });
52
63
  } catch (error) {
@@ -66,6 +77,7 @@ async function executeRedisTransaction(redis, queue) {
66
77
 
67
78
  // src/persistence.ts
68
79
  var InMemoryServerPersistenceAdapter = class {
80
+ supportsRewardStock = true;
69
81
  #locks = /* @__PURE__ */ new Map();
70
82
  #idempotent = /* @__PURE__ */ new Map();
71
83
  #states = /* @__PURE__ */ new Map();
@@ -74,6 +86,7 @@ var InMemoryServerPersistenceAdapter = class {
74
86
  #claims = /* @__PURE__ */ new Map();
75
87
  #claimRecords = [];
76
88
  #auditLogs = [];
89
+ #transactionTails = /* @__PURE__ */ new Map();
77
90
  constructor(options = {}) {
78
91
  this.#stocks = /* @__PURE__ */ new Map();
79
92
  this.#stockDefaults = /* @__PURE__ */ new Map();
@@ -122,51 +135,67 @@ var InMemoryServerPersistenceAdapter = class {
122
135
  const current = this.#stocks.get(key);
123
136
  if (current !== void 0) this.#stocks.set(key, current + Math.max(0, count));
124
137
  }
125
- async executeClaimRewardTransaction(params, mutation) {
138
+ async executeClaimRewardTransaction(params2, mutation2) {
126
139
  try {
127
- return await this.runTransaction(params.rallyId, async () => {
128
- const userState = await this.getUserState(params.rallyId, params.userId) ?? params.initialUserState;
140
+ const initialStock = params2.initialStock !== void 0 ? params2.initialStock : params2.stockKey === "__shared__" ? params2.sharedStockLimit : params2.rewardStockLimit;
141
+ const initialSecondaryStock = params2.initialSecondaryStock !== void 0 ? params2.initialSecondaryStock : params2.rewardStockLimit;
142
+ return await this.runTransaction(params2.rallyId, async () => {
143
+ if (params2.idempotencyKey !== void 0) {
144
+ const previous = await this.getIdempotentResult(
145
+ params2.rallyId,
146
+ params2.idempotencyKey
147
+ );
148
+ if (previous !== null) return { success: true };
149
+ }
150
+ const userState = await this.getUserState(params2.rallyId, params2.userId) ?? params2.initialUserState;
129
151
  if (userState === void 0)
130
152
  return { success: false, error: "A user state is required for this transaction." };
131
153
  const storedStock = await this.getRewardStock(
132
- params.rallyId,
133
- params.stockKey ?? params.rewardId
154
+ params2.rallyId,
155
+ params2.stockKey ?? params2.rewardId
134
156
  );
135
- const storedSecondaryStock = params.secondaryStockKey === void 0 ? null : await this.getRewardStock(params.rallyId, params.secondaryStockKey);
136
- const mutationResult = mutation({
137
- stock: storedStock ?? params.initialStock ?? null,
138
- secondaryStock: storedSecondaryStock ?? params.initialSecondaryStock ?? null,
139
- claimCount: await this.getUserClaimCount(params.rallyId, params.userId, params.rewardId),
157
+ const storedSecondaryStock = params2.secondaryStockKey === void 0 ? null : await this.getRewardStock(params2.rallyId, params2.secondaryStockKey);
158
+ const stock = storedStock ?? initialStock;
159
+ const secondaryStock = storedSecondaryStock ?? initialSecondaryStock;
160
+ const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : stock;
161
+ const sharedStock2 = params2.stockKey === "__shared__" ? stock : null;
162
+ const mutationResult = mutation2({
163
+ rewardStock: rewardStock2,
164
+ sharedStock: sharedStock2,
165
+ primaryStock: rewardStock2,
166
+ secondaryStock,
167
+ stock,
168
+ claimCount: await this.getUserClaimCount(params2.rallyId, params2.userId, params2.rewardId),
140
169
  userState
141
170
  });
142
- const idempotencyKey = params.idempotencyKey;
171
+ const idempotencyKey = params2.idempotencyKey;
143
172
  if (mutationResult.error !== void 0) {
144
173
  await this.recordAuditLog(mutationResult.auditLog);
145
174
  if (idempotencyKey !== void 0 && mutationResult.result !== void 0)
146
175
  await this.saveIdempotentResult(
147
- params.rallyId,
176
+ params2.rallyId,
148
177
  idempotencyKey,
149
178
  mutationResult.result,
150
- params.idempotencyTtlMs ?? 864e5
179
+ params2.idempotencyTtlMs ?? 864e5
151
180
  );
152
181
  return { success: false, error: mutationResult.error };
153
182
  }
154
183
  const currentStock = await this.getRewardStock(
155
- params.rallyId,
156
- params.stockKey ?? params.rewardId
184
+ params2.rallyId,
185
+ params2.stockKey ?? params2.rewardId
157
186
  );
158
- const effectiveStock = currentStock ?? params.initialStock ?? null;
159
- if (currentStock === null && params.initialStock !== void 0 && params.initialStock !== null)
187
+ const effectiveStock = currentStock ?? initialStock;
188
+ if (currentStock === null && initialStock !== void 0 && initialStock !== null)
160
189
  this.#stocks.set(
161
- this.#stockKey(params.rallyId, params.stockKey ?? params.rewardId),
162
- params.initialStock
190
+ this.#stockKey(params2.rallyId, params2.stockKey ?? params2.rewardId),
191
+ initialStock
163
192
  );
164
- const currentSecondaryStock = params.secondaryStockKey === void 0 ? null : await this.getRewardStock(params.rallyId, params.secondaryStockKey);
165
- const effectiveSecondaryStock = currentSecondaryStock ?? params.initialSecondaryStock ?? null;
166
- if (currentSecondaryStock === null && params.secondaryStockKey !== void 0 && params.initialSecondaryStock !== void 0 && params.initialSecondaryStock !== null)
193
+ const currentSecondaryStock = params2.secondaryStockKey === void 0 ? null : await this.getRewardStock(params2.rallyId, params2.secondaryStockKey);
194
+ const effectiveSecondaryStock = currentSecondaryStock ?? initialSecondaryStock;
195
+ if (currentSecondaryStock === null && params2.secondaryStockKey !== void 0 && initialSecondaryStock !== void 0 && initialSecondaryStock !== null)
167
196
  this.#stocks.set(
168
- this.#stockKey(params.rallyId, params.secondaryStockKey),
169
- params.initialSecondaryStock
197
+ this.#stockKey(params2.rallyId, params2.secondaryStockKey),
198
+ initialSecondaryStock
170
199
  );
171
200
  if (effectiveStock !== null && (mutationResult.nextStock === null || mutationResult.nextStock < 0))
172
201
  throw new Error("The transaction produced an invalid stock value.");
@@ -174,10 +203,10 @@ var InMemoryServerPersistenceAdapter = class {
174
203
  throw new Error("The transaction changed an unlimited stock to a limited stock.");
175
204
  if (mutationResult.nextStock !== null)
176
205
  this.#stocks.set(
177
- this.#stockKey(params.rallyId, params.stockKey ?? params.rewardId),
206
+ this.#stockKey(params2.rallyId, params2.stockKey ?? params2.rewardId),
178
207
  mutationResult.nextStock
179
208
  );
180
- if (params.secondaryStockKey !== void 0 && mutationResult.nextSecondaryStock !== void 0) {
209
+ if (params2.secondaryStockKey !== void 0 && mutationResult.nextSecondaryStock !== void 0) {
181
210
  if (effectiveSecondaryStock !== null && (mutationResult.nextSecondaryStock === null || mutationResult.nextSecondaryStock < 0))
182
211
  throw new Error("The transaction produced an invalid secondary stock value.");
183
212
  if (effectiveSecondaryStock === null && mutationResult.nextSecondaryStock !== null)
@@ -186,29 +215,29 @@ var InMemoryServerPersistenceAdapter = class {
186
215
  );
187
216
  if (mutationResult.nextSecondaryStock !== null)
188
217
  this.#stocks.set(
189
- this.#stockKey(params.rallyId, params.secondaryStockKey),
218
+ this.#stockKey(params2.rallyId, params2.secondaryStockKey),
190
219
  mutationResult.nextSecondaryStock
191
220
  );
192
221
  }
193
- await this.saveUserState(params.rallyId, params.userId, mutationResult.nextUserState);
222
+ await this.saveUserState(params2.rallyId, params2.userId, mutationResult.nextUserState);
194
223
  const reward = mutationResult.nextUserState.rewards.find(
195
- (item) => item.rewardId === params.rewardId
224
+ (item) => item.rewardId === params2.rewardId
196
225
  );
197
226
  if (reward?.claimTicketNumber !== void 0)
198
227
  await this.recordUserClaim({
199
- rallyId: params.rallyId,
200
- userId: params.userId,
201
- rewardId: params.rewardId,
228
+ rallyId: params2.rallyId,
229
+ userId: params2.userId,
230
+ rewardId: params2.rewardId,
202
231
  ticketNumber: reward.claimTicketNumber,
203
- timestamp: params.timestamp
232
+ timestamp: params2.timestamp
204
233
  });
205
234
  await this.recordAuditLog(mutationResult.auditLog);
206
235
  if (idempotencyKey !== void 0 && mutationResult.result !== void 0)
207
236
  await this.saveIdempotentResult(
208
- params.rallyId,
237
+ params2.rallyId,
209
238
  idempotencyKey,
210
239
  mutationResult.result,
211
- params.idempotencyTtlMs ?? 864e5
240
+ params2.idempotencyTtlMs ?? 864e5
212
241
  );
213
242
  return { success: true };
214
243
  });
@@ -216,32 +245,32 @@ var InMemoryServerPersistenceAdapter = class {
216
245
  return { success: false, error: error instanceof Error ? error.message : String(error) };
217
246
  }
218
247
  }
219
- async executeCheckInTransaction(params, mutation) {
248
+ async executeCheckInTransaction(params2, mutation2) {
220
249
  try {
221
- return await this.runTransaction(params.rallyId, async () => {
222
- const userState = await this.getUserState(params.rallyId, params.userId) ?? params.initialUserState;
250
+ return await this.runTransaction(params2.rallyId, async () => {
251
+ const userState = await this.getUserState(params2.rallyId, params2.userId) ?? params2.initialUserState;
223
252
  if (userState === void 0)
224
253
  return { success: false, error: "A user state is required for this transaction." };
225
- const mutationResult = mutation({ userState });
254
+ const mutationResult = mutation2({ userState });
226
255
  if (mutationResult.error !== void 0) {
227
256
  await this.recordAuditLog(mutationResult.auditLog);
228
- if (params.idempotencyKey !== void 0 && mutationResult.result !== void 0)
257
+ if (params2.idempotencyKey !== void 0 && mutationResult.result !== void 0)
229
258
  await this.saveIdempotentResult(
230
- params.rallyId,
231
- params.idempotencyKey,
259
+ params2.rallyId,
260
+ params2.idempotencyKey,
232
261
  mutationResult.result,
233
- params.idempotencyTtlMs ?? 864e5
262
+ params2.idempotencyTtlMs ?? 864e5
234
263
  );
235
264
  return { success: false, error: mutationResult.error };
236
265
  }
237
- await this.saveUserState(params.rallyId, params.userId, mutationResult.nextUserState);
266
+ await this.saveUserState(params2.rallyId, params2.userId, mutationResult.nextUserState);
238
267
  await this.recordAuditLog(mutationResult.auditLog);
239
- if (params.idempotencyKey !== void 0 && mutationResult.result !== void 0)
268
+ if (params2.idempotencyKey !== void 0 && mutationResult.result !== void 0)
240
269
  await this.saveIdempotentResult(
241
- params.rallyId,
242
- params.idempotencyKey,
270
+ params2.rallyId,
271
+ params2.idempotencyKey,
243
272
  mutationResult.result,
244
- params.idempotencyTtlMs ?? 864e5
273
+ params2.idempotencyTtlMs ?? 864e5
245
274
  );
246
275
  return { success: true };
247
276
  });
@@ -275,8 +304,8 @@ var InMemoryServerPersistenceAdapter = class {
275
304
  const value = this.#states.get(`${rallyId}:${userId}`);
276
305
  return value === void 0 ? null : structuredClone(value);
277
306
  }
278
- async saveUserState(rallyId, userId, state) {
279
- this.#states.set(`${rallyId}:${userId}`, structuredClone(state));
307
+ async saveUserState(rallyId, userId, state2) {
308
+ this.#states.set(`${rallyId}:${userId}`, structuredClone(state2));
280
309
  }
281
310
  async recordAuditLog(log) {
282
311
  this.#auditLogs.push(structuredClone(log));
@@ -288,11 +317,11 @@ var InMemoryServerPersistenceAdapter = class {
288
317
  getAuditLogs() {
289
318
  return structuredClone(this.#auditLogs);
290
319
  }
291
- async recordUserClaim(params) {
292
- const { rallyId, userId, rewardId } = params;
320
+ async recordUserClaim(params2) {
321
+ const { rallyId, userId, rewardId } = params2;
293
322
  const key = `${rallyId}:${userId}:${rewardId}`;
294
323
  this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
295
- this.#claimRecords.push(structuredClone(params));
324
+ this.#claimRecords.push(structuredClone(params2));
296
325
  }
297
326
  async rollbackUserClaim(rallyId, userId, rewardId, ticketNumber) {
298
327
  const key = `${rallyId}:${userId}:${rewardId}`;
@@ -312,44 +341,64 @@ var InMemoryServerPersistenceAdapter = class {
312
341
  return structuredClone(this.#claimRecords);
313
342
  }
314
343
  recordClaim(paramsOrRallyId, userId, rewardId) {
315
- const params = typeof paramsOrRallyId === "string" ? {
344
+ const params2 = typeof paramsOrRallyId === "string" ? {
316
345
  rallyId: paramsOrRallyId,
317
346
  userId: userId ?? "",
318
347
  rewardId: rewardId ?? "",
319
348
  ticketNumber: "",
320
349
  timestamp: Date.now()
321
350
  } : paramsOrRallyId;
322
- return this.recordUserClaim(params);
351
+ return this.recordUserClaim(params2);
323
352
  }
324
- async runTransaction(_rallyId, operation) {
325
- const snapshot = {
326
- stocks: new Map(this.#stocks),
327
- idempotent: new Map(this.#idempotent),
328
- states: new Map(this.#states),
329
- claims: new Map(this.#claims),
330
- claimRecords: structuredClone(this.#claimRecords),
331
- auditLogs: structuredClone(this.#auditLogs)
332
- };
333
- try {
334
- return await operation(this);
335
- } catch (error) {
336
- this.#stocks.clear();
337
- for (const [key, value] of snapshot.stocks) this.#stocks.set(key, value);
338
- this.#idempotent.clear();
339
- for (const [key, value] of snapshot.idempotent)
340
- this.#idempotent.set(key, structuredClone(value));
341
- this.#states.clear();
342
- for (const [key, value] of snapshot.states) this.#states.set(key, structuredClone(value));
343
- this.#claims.clear();
344
- for (const [key, value] of snapshot.claims) this.#claims.set(key, value);
345
- this.#claimRecords.splice(0, this.#claimRecords.length, ...snapshot.claimRecords);
346
- this.#auditLogs.splice(0, this.#auditLogs.length, ...snapshot.auditLogs);
347
- throw error;
348
- }
353
+ async runTransaction(rallyId, operation) {
354
+ const previous = this.#transactionTails.get(rallyId) ?? Promise.resolve();
355
+ const current = previous.then(async () => {
356
+ const snapshot = {
357
+ stocks: new Map(this.#stocks),
358
+ idempotent: new Map(this.#idempotent),
359
+ states: new Map(this.#states),
360
+ claims: new Map(this.#claims),
361
+ claimRecords: structuredClone(this.#claimRecords),
362
+ auditLogs: structuredClone(this.#auditLogs)
363
+ };
364
+ try {
365
+ return await operation(this);
366
+ } catch (error) {
367
+ this.#stocks.clear();
368
+ for (const [key, value] of snapshot.stocks) this.#stocks.set(key, value);
369
+ this.#idempotent.clear();
370
+ for (const [key, value] of snapshot.idempotent)
371
+ this.#idempotent.set(key, structuredClone(value));
372
+ this.#states.clear();
373
+ for (const [key, value] of snapshot.states) this.#states.set(key, structuredClone(value));
374
+ this.#claims.clear();
375
+ for (const [key, value] of snapshot.claims) this.#claims.set(key, value);
376
+ this.#claimRecords.splice(0, this.#claimRecords.length, ...snapshot.claimRecords);
377
+ this.#auditLogs.splice(0, this.#auditLogs.length, ...snapshot.auditLogs);
378
+ throw error;
379
+ }
380
+ });
381
+ this.#transactionTails.set(
382
+ rallyId,
383
+ current.then(
384
+ () => void 0,
385
+ () => void 0
386
+ )
387
+ );
388
+ return current;
349
389
  }
350
390
  };
351
391
 
352
392
  // src/security.ts
393
+ var RequestValidationException = class extends Error {
394
+ code = "VALIDATION_FAILED";
395
+ errors;
396
+ constructor(errors2) {
397
+ super("Request validation failed.");
398
+ this.name = "RequestValidationException";
399
+ this.errors = errors2;
400
+ }
401
+ };
353
402
  function errors(...items) {
354
403
  return { success: false, errors: items };
355
404
  }
@@ -463,7 +512,7 @@ function validateClaimRewardRequest(value) {
463
512
  });
464
513
  if (value.staffId !== void 0 && !nonEmpty(value.staffId))
465
514
  return errors({ path: "staffId", message: "staffId must be non-empty.", code: "INVALID_TYPE" });
466
- if (value.now !== void 0 && !nonEmpty(value.now))
515
+ if (value.now !== void 0 && !dateInput(value.now))
467
516
  return errors({
468
517
  path: "now",
469
518
  message: "now must be an ISO 8601 date or positive timestamp.",
@@ -471,6 +520,76 @@ function validateClaimRewardRequest(value) {
471
520
  });
472
521
  return { success: true, data: value };
473
522
  }
523
+ function uuid(value) {
524
+ 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);
525
+ }
526
+ function identityErrors(value) {
527
+ const result = [];
528
+ if (value.userId === void 0 && value.anonymousSessionId === void 0)
529
+ result.push({
530
+ path: "userId",
531
+ message: "An authenticated userId or anonymousSessionId is required.",
532
+ code: "REQUIRED"
533
+ });
534
+ if (value.userId !== void 0 && !nonEmpty(value.userId))
535
+ result.push({ path: "userId", message: "userId must be non-empty.", code: "INVALID_TYPE" });
536
+ if (value.anonymousSessionId !== void 0 && !uuid(value.anonymousSessionId))
537
+ result.push({
538
+ path: "anonymousSessionId",
539
+ message: "anonymousSessionId must be a UUID v4.",
540
+ code: "INVALID_FORMAT"
541
+ });
542
+ if (value.userId !== void 0 && value.anonymousSessionId !== void 0 && value.userId !== value.anonymousSessionId)
543
+ result.push({
544
+ path: "anonymousSessionId",
545
+ message: "userId and anonymousSessionId must identify the same session.",
546
+ code: "IDENTITY_MISMATCH"
547
+ });
548
+ return result;
549
+ }
550
+ function directErrors(value, config, kind) {
551
+ const validated = kind === "check-in" ? validateCheckInRequest(value) : validateClaimRewardRequest(value);
552
+ if (!validated.success) return validated.errors;
553
+ const errors2 = [];
554
+ if (validated.data.rallyId !== config.id)
555
+ errors2.push({
556
+ path: "rallyId",
557
+ message: "The rally does not match this server.",
558
+ code: "INVALID_VALUE"
559
+ });
560
+ const resourceId = kind === "check-in" ? validated.data.spotId : validated.data.rewardId;
561
+ const exists = kind === "check-in" ? config.spots.some((spot) => spot.id === resourceId) : config.rewards.some((reward) => reward.id === resourceId);
562
+ if (!exists)
563
+ errors2.push({
564
+ path: kind === "check-in" ? "spotId" : "rewardId",
565
+ message: `${kind === "check-in" ? "Spot" : "Reward"} was not found.`,
566
+ code: kind === "check-in" ? "SPOT_NOT_FOUND" : "REWARD_NOT_FOUND"
567
+ });
568
+ errors2.push(...identityErrors(validated.data));
569
+ return errors2;
570
+ }
571
+ function assertValidCheckInParams(value, config) {
572
+ const errors2 = directErrors(value, config, "check-in");
573
+ if (errors2.length > 0) throw new RequestValidationException(errors2);
574
+ }
575
+ function assertValidClaimParams(value, config) {
576
+ const errors2 = directErrors(value, config, "claim");
577
+ if (errors2.length > 0) throw new RequestValidationException(errors2);
578
+ }
579
+ function assertValidSyncParams(value, config) {
580
+ const validated = validateSyncRequest(value);
581
+ const errors2 = validated.success ? [
582
+ ...validated.data.rallyId !== config.id ? [
583
+ {
584
+ path: "rallyId",
585
+ message: "The rally does not match this server.",
586
+ code: "INVALID_VALUE"
587
+ }
588
+ ] : []
589
+ ] : [...validated.errors];
590
+ if (validated.success) errors2.push(...identityErrors(validated.data));
591
+ if (errors2.length > 0) throw new RequestValidationException(errors2);
592
+ }
474
593
  function validateSyncRequest(value) {
475
594
  const required = requiredErrors(value, ["rallyId"]);
476
595
  if (required.length > 0) return errors(...required);
@@ -478,7 +597,10 @@ function validateSyncRequest(value) {
478
597
  return errors({ path: "$", message: "Expected an object.", code: "INVALID_TYPE" });
479
598
  if (value.userId !== void 0 && !nonEmpty(value.userId))
480
599
  return errors({ path: "userId", message: "userId must be non-empty.", code: "INVALID_TYPE" });
481
- return { success: true, data: value };
600
+ return {
601
+ success: true,
602
+ data: value
603
+ };
482
604
  }
483
605
  function json(body, status = 200) {
484
606
  return new Response(JSON.stringify(body), {
@@ -574,6 +696,29 @@ function operationStatus(result) {
574
696
  if (result.ok) return "ACCEPTED";
575
697
  return result.code === "CONFLICT" || result.code === "PERSISTENCE_FAILED" ? "RETRYABLE_ERROR" : "REJECTED_PERMANENT";
576
698
  }
699
+ function withDirectIdentity(request, authContext) {
700
+ if (authContext !== void 0) {
701
+ if (authContext.authenticatedUserId.trim() === "")
702
+ throw new RequestValidationException([
703
+ {
704
+ path: "authContext.authenticatedUserId",
705
+ message: "An authenticated userId is required.",
706
+ code: "REQUIRED"
707
+ }
708
+ ]);
709
+ return { ...request, userId: authContext.authenticatedUserId };
710
+ }
711
+ if (request.userId !== void 0) return { ...request, userId: request.userId };
712
+ if (request.anonymousSessionId !== void 0 && isUuidV4(request.anonymousSessionId))
713
+ return { ...request, userId: request.anonymousSessionId };
714
+ throw new RequestValidationException([
715
+ {
716
+ path: "userId",
717
+ message: "An authenticated userId or anonymousSessionId is required.",
718
+ code: "REQUIRED"
719
+ }
720
+ ]);
721
+ }
577
722
  var StampRallyServer = class {
578
723
  #config;
579
724
  #persistence;
@@ -609,7 +754,18 @@ var StampRallyServer = class {
609
754
  { ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
610
755
  401
611
756
  );
612
- const result = await this.checkIn({ ...body.data, userId });
757
+ const sessionId = request.headers.get("x-anonymous-session-id");
758
+ let result;
759
+ try {
760
+ result = await this.checkIn({
761
+ ...body.data,
762
+ userId,
763
+ ...sessionId === null ? {} : { anonymousSessionId: sessionId }
764
+ });
765
+ } catch (error) {
766
+ if (error instanceof RequestValidationException) return validationResponse(error.errors);
767
+ throw error;
768
+ }
613
769
  return json(
614
770
  { ...result, status: operationStatus(result) },
615
771
  result.ok ? 200 : result.code === "SPOT_NOT_FOUND" ? 404 : 422
@@ -632,7 +788,18 @@ var StampRallyServer = class {
632
788
  { ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
633
789
  401
634
790
  );
635
- const result = await this.claimReward({ ...body.data, userId });
791
+ const sessionId = request.headers.get("x-anonymous-session-id");
792
+ let result;
793
+ try {
794
+ result = await this.claimReward({
795
+ ...body.data,
796
+ userId,
797
+ ...sessionId === null ? {} : { anonymousSessionId: sessionId }
798
+ });
799
+ } catch (error) {
800
+ if (error instanceof RequestValidationException) return validationResponse(error.errors);
801
+ throw error;
802
+ }
636
803
  return json(
637
804
  { ...result, status: operationStatus(result) },
638
805
  result.ok ? 200 : result.code === "REWARD_NOT_FOUND" ? 404 : 422
@@ -655,16 +822,32 @@ var StampRallyServer = class {
655
822
  { ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
656
823
  401
657
824
  );
658
- return json({ ok: true, state: await this.sync(body.data.rallyId, userId) });
825
+ const sessionId = request.headers.get("x-anonymous-session-id");
826
+ try {
827
+ return json({
828
+ ok: true,
829
+ state: await this.syncProgress({
830
+ rallyId: body.data.rallyId,
831
+ userId,
832
+ ...sessionId === null ? {} : { anonymousSessionId: sessionId }
833
+ })
834
+ });
835
+ } catch (error) {
836
+ if (error instanceof RequestValidationException) return validationResponse(error.errors);
837
+ throw error;
838
+ }
659
839
  }
660
- async checkIn(request) {
661
- const key = `check-in:${request.rallyId}:${request.userId}:${request.idempotencyKey}`;
840
+ async checkIn(request, authContext) {
841
+ const directRequest = withDirectIdentity(request, authContext);
842
+ assertValidCheckInParams(directRequest, this.#config);
843
+ const { userId } = directRequest;
844
+ const key = `check-in:${request.rallyId}:${userId}:${request.idempotencyKey}`;
662
845
  const previous = await this.#persistence.getIdempotentResult(
663
846
  request.rallyId,
664
847
  key
665
848
  );
666
849
  if (previous !== null) return previous;
667
- const lockKey = `state:${request.rallyId}:${request.userId}`;
850
+ const lockKey = `state:${request.rallyId}:${userId}`;
668
851
  if (!await this.#persistence.acquireLock(
669
852
  request.rallyId,
670
853
  lockKey,
@@ -673,11 +856,11 @@ var StampRallyServer = class {
673
856
  return { ok: false, code: "CONFLICT", message: "The user state is being updated." };
674
857
  const timestamp = now(this.#options);
675
858
  try {
676
- const current = await this.#persistence.getUserState(request.rallyId, request.userId) ?? initialState(this.#config, request.userId, timestamp);
859
+ const current = await this.#persistence.getUserState(request.rallyId, userId) ?? initialState(this.#config, userId, timestamp);
677
860
  const responseHolder = { value: null };
678
861
  const makeAudit = (status, code) => audit(
679
862
  request.rallyId,
680
- request.userId,
863
+ userId,
681
864
  "CHECK_IN",
682
865
  request.spotId,
683
866
  request.idempotencyKey,
@@ -693,7 +876,7 @@ var StampRallyServer = class {
693
876
  message: "Spot was not found."
694
877
  };
695
878
  return await this.#rememberCheckInTransaction(
696
- request,
879
+ directRequest,
697
880
  timestamp,
698
881
  key,
699
882
  current,
@@ -717,7 +900,7 @@ var StampRallyServer = class {
717
900
  };
718
901
  if (current.records.some((record2) => record2.stampId === request.spotId))
719
902
  return await this.#rememberCheckInTransaction(
720
- request,
903
+ directRequest,
721
904
  timestamp,
722
905
  key,
723
906
  current,
@@ -727,7 +910,7 @@ var StampRallyServer = class {
727
910
  const acquired = new Set(current.records.map((record2) => record2.stampId));
728
911
  if (spot.prerequisites?.some((id) => !acquired.has(id)))
729
912
  return await this.#rememberCheckInTransaction(
730
- request,
913
+ directRequest,
731
914
  timestamp,
732
915
  key,
733
916
  current,
@@ -742,7 +925,7 @@ var StampRallyServer = class {
742
925
  { rallyId: request.rallyId, spotId: request.spotId, state: current }
743
926
  ))
744
927
  return await this.#rememberCheckInTransaction(
745
- request,
928
+ directRequest,
746
929
  timestamp,
747
930
  key,
748
931
  current,
@@ -764,7 +947,7 @@ var StampRallyServer = class {
764
947
  const transaction = await this.#persistence.executeCheckInTransaction(
765
948
  {
766
949
  rallyId: request.rallyId,
767
- userId: request.userId,
950
+ userId,
768
951
  spotId: request.spotId,
769
952
  timestamp: timestampMillis(timestamp),
770
953
  idempotencyKey: key,
@@ -788,8 +971,11 @@ var StampRallyServer = class {
788
971
  await this.#persistence.releaseLock(request.rallyId, lockKey);
789
972
  }
790
973
  }
791
- async claimReward(request) {
792
- const key = `claim:${request.rallyId}:${request.userId}:${request.rewardId}:${request.idempotencyKey}`;
974
+ async claimReward(request, authContext) {
975
+ const directRequest = withDirectIdentity(request, authContext);
976
+ assertValidClaimParams(directRequest, this.#config);
977
+ const { userId } = directRequest;
978
+ const key = `claim:${request.rallyId}:${userId}:${request.rewardId}:${request.idempotencyKey}`;
793
979
  const previous = await this.#persistence.getIdempotentResult(
794
980
  request.rallyId,
795
981
  key
@@ -800,7 +986,20 @@ var StampRallyServer = class {
800
986
  return this.#rememberClaim(
801
987
  key,
802
988
  { ok: false, code: "REWARD_NOT_FOUND", message: "Reward was not found." },
803
- request,
989
+ directRequest,
990
+ now(this.#options)
991
+ );
992
+ const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
993
+ const inventoryEnabled = plan.primaryInitial !== null || plan.secondaryInitial !== void 0 && plan.secondaryInitial !== null;
994
+ if (inventoryEnabled && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function" || typeof this.#persistence.executeClaimRewardTransaction !== "function"))
995
+ return this.#rememberClaim(
996
+ key,
997
+ {
998
+ ok: false,
999
+ code: "INVENTORY_STORAGE_NOT_IMPLEMENTED",
1000
+ message: "This persistence adapter cannot atomically store inventory."
1001
+ },
1002
+ directRequest,
804
1003
  now(this.#options)
805
1004
  );
806
1005
  const lockKey = this.#config.inventoryMode === "shared" ? "inventory:shared" : `reward:${request.rallyId}:${reward.id}`;
@@ -811,7 +1010,6 @@ var StampRallyServer = class {
811
1010
  ))
812
1011
  return { ok: false, code: "CONFLICT", message: "The reward is being claimed." };
813
1012
  const timestamp = now(this.#options);
814
- const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
815
1013
  try {
816
1014
  const checked = await this.#persistence.getIdempotentResult(
817
1015
  request.rallyId,
@@ -822,10 +1020,12 @@ var StampRallyServer = class {
822
1020
  const result = await this.#persistence.executeClaimRewardTransaction(
823
1021
  {
824
1022
  rallyId: request.rallyId,
825
- userId: request.userId,
1023
+ userId,
826
1024
  rewardId: reward.id,
827
1025
  stockKey: plan.primaryKey,
828
1026
  ...plan.secondaryKey === void 0 ? {} : { secondaryStockKey: plan.secondaryKey },
1027
+ rewardStockLimit: rewardStock(this.#config, reward.id, reward.stockLimit),
1028
+ sharedStockLimit: this.#config.inventoryMode === "shared" ? sharedStock(this.#config) : null,
829
1029
  initialStock: plan.primaryInitial,
830
1030
  ...plan.secondaryInitial === void 0 ? {} : { initialSecondaryStock: plan.secondaryInitial },
831
1031
  ticketNumber: request.idempotencyKey,
@@ -833,7 +1033,7 @@ var StampRallyServer = class {
833
1033
  idempotencyKey: key,
834
1034
  ...request.staffPasscode === void 0 ? {} : { proofData: request.staffPasscode },
835
1035
  ...this.#options.idempotencyTtlMs === void 0 ? {} : { idempotencyTtlMs: this.#options.idempotencyTtlMs },
836
- initialUserState: initialState(this.#config, request.userId, timestamp)
1036
+ initialUserState: initialState(this.#config, userId, timestamp)
837
1037
  },
838
1038
  ({ stock, secondaryStock, claimCount, userState }) => {
839
1039
  const storedReward = userState.rewards.find((item) => item.rewardId === reward.id) ?? {
@@ -843,7 +1043,7 @@ var StampRallyServer = class {
843
1043
  const currentReward = reward.redemptionMethod === "server_claim" && storedReward.status === "CONSUMED" && (reward.userClaimLimit === void 0 || claimCount < reward.userClaimLimit) ? { ...storedReward, status: "AVAILABLE" } : storedReward;
844
1044
  const makeAudit = (status, code) => audit(
845
1045
  request.rallyId,
846
- request.userId,
1046
+ userId,
847
1047
  "CLAIM_REWARD",
848
1048
  reward.id,
849
1049
  request.idempotencyKey,
@@ -928,6 +1128,12 @@ var StampRallyServer = class {
928
1128
  const response = responseHolder.value;
929
1129
  if (!result.success) {
930
1130
  if (response !== null && !response.ok && response.code === result.error) return response;
1131
+ if (result.error === "INVENTORY_STORAGE_NOT_IMPLEMENTED")
1132
+ return {
1133
+ ok: false,
1134
+ code: "INVENTORY_STORAGE_NOT_IMPLEMENTED",
1135
+ message: "This persistence adapter cannot atomically store inventory."
1136
+ };
931
1137
  return {
932
1138
  ok: false,
933
1139
  code: "PERSISTENCE_FAILED",
@@ -951,28 +1157,34 @@ var StampRallyServer = class {
951
1157
  }
952
1158
  }
953
1159
  async sync(rallyId, userId) {
954
- const state = await this.#persistence.getUserState(rallyId, userId) ?? initialState(this.#config, userId, now(this.#options));
955
- return this.#attachInventory(state);
1160
+ assertValidSyncParams({ rallyId, userId }, this.#config);
1161
+ const state2 = await this.#persistence.getUserState(rallyId, userId) ?? initialState(this.#config, userId, now(this.#options));
1162
+ return this.#attachInventory(state2);
1163
+ }
1164
+ async syncProgress(request) {
1165
+ const directRequest = withDirectIdentity(request);
1166
+ assertValidSyncParams(directRequest, this.#config);
1167
+ return this.sync(directRequest.rallyId, directRequest.userId);
956
1168
  }
957
- async #attachInventory(state) {
1169
+ async #attachInventory(state2) {
958
1170
  const rewardRemaining = {};
959
1171
  for (const reward of this.#config.rewards) {
960
1172
  const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
961
1173
  if (plan.secondaryKey !== void 0) {
962
- const stock = await this.#persistence.getRewardStock(state.rallyId, plan.secondaryKey);
1174
+ const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.secondaryKey);
963
1175
  const remaining = stock ?? plan.secondaryInitial ?? null;
964
1176
  if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);
965
1177
  } else if (plan.primaryKey !== "__shared__") {
966
- const stock = await this.#persistence.getRewardStock(state.rallyId, plan.primaryKey);
1178
+ const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.primaryKey);
967
1179
  const remaining = stock ?? plan.primaryInitial;
968
1180
  if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);
969
1181
  }
970
1182
  }
971
1183
  const shared = sharedStock(this.#config);
972
- const storedShared = await this.#persistence.getRewardStock(state.rallyId, "__shared__");
1184
+ const storedShared = await this.#persistence.getRewardStock(state2.rallyId, "__shared__");
973
1185
  const sharedRemaining = this.#config.inventoryMode === "shared" && shared !== null ? Math.max(0, storedShared ?? shared) : void 0;
974
1186
  return {
975
- ...state,
1187
+ ...state2,
976
1188
  ...Object.keys(rewardRemaining).length === 0 && sharedRemaining === void 0 ? {} : {
977
1189
  inventory: {
978
1190
  ...sharedRemaining === void 0 ? {} : { sharedRemaining },
@@ -997,13 +1209,14 @@ var StampRallyServer = class {
997
1209
  const authenticatedUserId = identity.authenticatedUserId;
998
1210
  return authenticatedUserId.length > 0 ? authenticatedUserId : null;
999
1211
  }
1000
- if (this.#options.anonymousPolicy === "reject") return null;
1212
+ const policy = this.#options.anonymousPolicy ?? "session_scoped";
1213
+ if (policy === "reject") return null;
1001
1214
  const sessionId = request.headers.get("X-Anonymous-Session-Id");
1002
- if (sessionId !== null && isUuidV4(sessionId)) return sessionId;
1003
- if (this.#options.anonymousPolicy === "session_scoped") return null;
1215
+ if (sessionId !== null) return isUuidV4(sessionId) ? sessionId : null;
1216
+ if (policy === "session_scoped") return null;
1004
1217
  return "anonymous";
1005
1218
  }
1006
- async #rememberCheckInTransaction(request, timestamp, key, current, mutation, responseHolder) {
1219
+ async #rememberCheckInTransaction(request, timestamp, key, current, mutation2, responseHolder) {
1007
1220
  const transaction = await this.#persistence.executeCheckInTransaction(
1008
1221
  {
1009
1222
  rallyId: request.rallyId,
@@ -1014,9 +1227,9 @@ var StampRallyServer = class {
1014
1227
  ...this.#options.idempotencyTtlMs === void 0 ? {} : { idempotencyTtlMs: this.#options.idempotencyTtlMs },
1015
1228
  initialUserState: current
1016
1229
  },
1017
- () => mutation
1230
+ () => mutation2
1018
1231
  );
1019
- if (responseHolder.value !== null && (transaction.success || transaction.error === mutation.error))
1232
+ if (responseHolder.value !== null && (transaction.success || transaction.error === mutation2.error))
1020
1233
  return responseHolder.value;
1021
1234
  return {
1022
1235
  ok: false,
@@ -1047,11 +1260,140 @@ var StampRallyServer = class {
1047
1260
  }
1048
1261
  };
1049
1262
 
1263
+ // src/testing/compliance.ts
1264
+ var state = (userId) => ({
1265
+ rallyId: "compliance-rally",
1266
+ userId,
1267
+ records: [],
1268
+ rewards: [{ rewardId: "reward", status: "AVAILABLE" }],
1269
+ updatedAt: "2026-01-01T00:00:00.000Z"
1270
+ });
1271
+ function audit2(idempotencyKey, userId) {
1272
+ return {
1273
+ id: `audit-${idempotencyKey}`,
1274
+ timestamp: "2026-01-01T00:00:00.000Z",
1275
+ rallyId: "compliance-rally",
1276
+ userId,
1277
+ action: "CLAIM_REWARD",
1278
+ resourceId: "reward",
1279
+ status: "SUCCESS",
1280
+ idempotencyKey
1281
+ };
1282
+ }
1283
+ function params(userId, idempotencyKey) {
1284
+ return {
1285
+ rallyId: "compliance-rally",
1286
+ userId,
1287
+ rewardId: "reward",
1288
+ ticketNumber: `ticket-${idempotencyKey}`,
1289
+ timestamp: Date.parse("2026-01-01T00:00:00.000Z"),
1290
+ idempotencyKey,
1291
+ rewardStockLimit: 1,
1292
+ sharedStockLimit: 1,
1293
+ stockKey: "__shared__",
1294
+ secondaryStockKey: "reward",
1295
+ initialStock: 1,
1296
+ initialSecondaryStock: 1,
1297
+ initialUserState: state(userId)
1298
+ };
1299
+ }
1300
+ function mutation(current) {
1301
+ if (current.stock === 0 || current.secondaryStock === 0)
1302
+ return {
1303
+ nextStock: current.stock,
1304
+ nextSecondaryStock: current.secondaryStock,
1305
+ nextUserState: current.userState,
1306
+ auditLog: audit2("rejected", current.userState.userId ?? "unknown"),
1307
+ error: "OUT_OF_STOCK"
1308
+ };
1309
+ return {
1310
+ nextStock: current.stock === null ? null : current.stock - 1,
1311
+ nextSecondaryStock: current.secondaryStock === null ? null : current.secondaryStock - 1,
1312
+ nextUserState: {
1313
+ ...current.userState,
1314
+ rewards: [{ rewardId: "reward", status: "CONSUMED" }],
1315
+ updatedAt: "2026-01-01T00:00:00.000Z"
1316
+ },
1317
+ auditLog: audit2("success", current.userState.userId ?? "unknown"),
1318
+ result: { ok: true }
1319
+ };
1320
+ }
1321
+ function assert(condition, message) {
1322
+ if (!condition) throw new Error(`Persistence adapter compliance failed: ${message}`);
1323
+ }
1324
+ async function runPersistenceAdapterComplianceTests(createAdapter) {
1325
+ const adapter = await createAdapter();
1326
+ assert(
1327
+ adapter.supportsRewardStock !== false,
1328
+ "the adapter must explicitly support reward stock for this suite"
1329
+ );
1330
+ const [first, second] = await Promise.all([
1331
+ adapter.executeClaimRewardTransaction(params("alice", "race-a"), mutation),
1332
+ adapter.executeClaimRewardTransaction(params("bob", "race-b"), mutation)
1333
+ ]);
1334
+ assert([first.success, second.success].filter(Boolean).length === 1, "race was not serialized");
1335
+ assert(
1336
+ await adapter.getRewardStock("compliance-rally", "__shared__") === 0,
1337
+ "shared stock was not decremented atomically"
1338
+ );
1339
+ assert(
1340
+ await adapter.getRewardStock("compliance-rally", "reward") === 0,
1341
+ "per-reward stock was not decremented atomically"
1342
+ );
1343
+ const exhausted = await adapter.executeClaimRewardTransaction(
1344
+ params("carol", "boundary"),
1345
+ mutation
1346
+ );
1347
+ assert(!exhausted.success, "a zero-stock claim was accepted");
1348
+ assert(
1349
+ await adapter.getRewardStock("compliance-rally", "__shared__") === 0 && await adapter.getRewardStock("compliance-rally", "reward") === 0,
1350
+ "zero-stock rejection did not preserve both stock boundaries"
1351
+ );
1352
+ const idempotentAdapter = await createAdapter();
1353
+ const idempotentParams = params("alice", "same-key");
1354
+ const firstClaim = await idempotentAdapter.executeClaimRewardTransaction(
1355
+ idempotentParams,
1356
+ mutation
1357
+ );
1358
+ const secondClaim = await idempotentAdapter.executeClaimRewardTransaction(
1359
+ idempotentParams,
1360
+ mutation
1361
+ );
1362
+ assert(firstClaim.success && secondClaim.success, "idempotent claim did not remain successful");
1363
+ assert(
1364
+ await idempotentAdapter.getRewardStock("compliance-rally", "__shared__") === 0,
1365
+ "idempotent retry decremented shared stock twice"
1366
+ );
1367
+ assert(
1368
+ await idempotentAdapter.getRewardStock("compliance-rally", "reward") === 0,
1369
+ "idempotent retry decremented per-reward stock twice"
1370
+ );
1371
+ const rollbackAdapter = await createAdapter();
1372
+ const rollbackParams = params("alice", "rollback");
1373
+ const rollback = await rollbackAdapter.executeClaimRewardTransaction(rollbackParams, () => {
1374
+ throw new Error("forced rollback");
1375
+ });
1376
+ assert(!rollback.success, "a failed mutation was committed");
1377
+ assert(
1378
+ await rollbackAdapter.getRewardStock("compliance-rally", "__shared__") === null,
1379
+ "rollback changed shared stock"
1380
+ );
1381
+ assert(
1382
+ await rollbackAdapter.getRewardStock("compliance-rally", "reward") === null,
1383
+ "rollback changed per-reward stock"
1384
+ );
1385
+ }
1386
+
1050
1387
  exports.InMemoryServerPersistenceAdapter = InMemoryServerPersistenceAdapter;
1388
+ exports.RequestValidationException = RequestValidationException;
1051
1389
  exports.StampRallyServer = StampRallyServer;
1390
+ exports.assertValidCheckInParams = assertValidCheckInParams;
1391
+ exports.assertValidClaimParams = assertValidClaimParams;
1392
+ exports.assertValidSyncParams = assertValidSyncParams;
1052
1393
  exports.executeCheckInTransaction = executeCheckInTransaction;
1053
1394
  exports.executeClaimRewardTransaction = executeClaimRewardTransaction;
1054
1395
  exports.executeRedisTransaction = executeRedisTransaction;
1396
+ exports.runPersistenceAdapterComplianceTests = runPersistenceAdapterComplianceTests;
1055
1397
  exports.validateCheckInRequest = validateCheckInRequest;
1056
1398
  exports.validateClaimRewardRequest = validateClaimRewardRequest;
1057
1399
  exports.validateSyncRequest = validateSyncRequest;