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