@stamprally/server 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,12 @@ const response = await server.handle(request);
13
13
 
14
14
  `ServerPersistenceAdapter` scopes locks, reward stock, idempotency records, user state, claim records, and audit logs by `rallyId`. Its `recordUserClaim` operation receives the rally, user, reward, issued ticket number, and timestamp. Use a transactional database or equivalent Redis primitives for multi-instance production deployments.
15
15
 
16
+ HTTP check-in and reward-claim responses include an operation status: `ACCEPTED`,
17
+ `REJECTED_PERMANENT`, or `RETRYABLE_ERROR`. Clients can pass the response directly
18
+ to an `OfflineQueue` sender; the queue removes accepted/permanent operations and
19
+ retains retryable failures. The SQL transaction contract and all-or-nothing example
20
+ are exported as `executeClaimRewardTransaction` from `src/examples/transaction.ts`.
21
+
16
22
  Hono can mount the handler directly because it accepts the same Web Standard request and response types.
17
23
 
18
24
  ## License
package/dist/index.cjs CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  var core = require('@stamprally/core');
4
4
 
5
+ // src/examples/transaction.ts
6
+ async function executeClaimRewardTransaction(database, store, params, mutation) {
7
+ try {
8
+ return await database.transaction(async (transaction) => {
9
+ const current = await store.readContext(transaction, params);
10
+ const next = mutation(current);
11
+ if (next.error !== void 0) {
12
+ await store.writeAudit(transaction, next.auditLog);
13
+ if (params.idempotencyKey !== void 0 && next.result !== void 0)
14
+ await store.writeIdempotency(transaction, params, next.result);
15
+ return { success: false, error: next.error };
16
+ }
17
+ if (next.nextStock !== null) await store.writeStock(transaction, params, next.nextStock);
18
+ await store.writeUserState(transaction, params, next.nextUserState);
19
+ await store.writeClaimRecord(transaction, params, next.nextUserState);
20
+ await store.writeAudit(transaction, next.auditLog);
21
+ if (params.idempotencyKey !== void 0 && next.result !== void 0)
22
+ await store.writeIdempotency(transaction, params, next.result);
23
+ return { success: true };
24
+ });
25
+ } catch (error2) {
26
+ return { success: false, error: error2 instanceof Error ? error2.message : String(error2) };
27
+ }
28
+ }
29
+
5
30
  // src/persistence.ts
6
31
  var InMemoryServerPersistenceAdapter = class {
7
32
  #locks = /* @__PURE__ */ new Map();
@@ -115,8 +140,41 @@ var InMemoryServerPersistenceAdapter = class {
115
140
  );
116
141
  return { success: true };
117
142
  });
118
- } catch (error) {
119
- return { success: false, error: error instanceof Error ? error.message : String(error) };
143
+ } catch (error2) {
144
+ return { success: false, error: error2 instanceof Error ? error2.message : String(error2) };
145
+ }
146
+ }
147
+ async executeCheckInTransaction(params, mutation) {
148
+ try {
149
+ return await this.runTransaction(params.rallyId, async () => {
150
+ const userState = await this.getUserState(params.rallyId, params.userId) ?? params.initialUserState;
151
+ if (userState === void 0)
152
+ return { success: false, error: "A user state is required for this transaction." };
153
+ const mutationResult = mutation({ userState });
154
+ if (mutationResult.error !== void 0) {
155
+ await this.recordAuditLog(mutationResult.auditLog);
156
+ if (params.idempotencyKey !== void 0 && mutationResult.result !== void 0)
157
+ await this.saveIdempotentResult(
158
+ params.rallyId,
159
+ params.idempotencyKey,
160
+ mutationResult.result,
161
+ params.idempotencyTtlMs ?? 864e5
162
+ );
163
+ return { success: false, error: mutationResult.error };
164
+ }
165
+ await this.saveUserState(params.rallyId, params.userId, mutationResult.nextUserState);
166
+ await this.recordAuditLog(mutationResult.auditLog);
167
+ if (params.idempotencyKey !== void 0 && mutationResult.result !== void 0)
168
+ await this.saveIdempotentResult(
169
+ params.rallyId,
170
+ params.idempotencyKey,
171
+ mutationResult.result,
172
+ params.idempotencyTtlMs ?? 864e5
173
+ );
174
+ return { success: true };
175
+ });
176
+ } catch (error2) {
177
+ return { success: false, error: error2 instanceof Error ? error2.message : String(error2) };
120
178
  }
121
179
  }
122
180
  async rollbackUserState(rallyId, userId, previousState) {
@@ -170,7 +228,7 @@ var InMemoryServerPersistenceAdapter = class {
170
228
  if (count <= 1) this.#claims.delete(key);
171
229
  else this.#claims.set(key, count - 1);
172
230
  const index = this.#claimRecords.findIndex(
173
- (record) => record.rallyId === rallyId && record.userId === userId && record.rewardId === rewardId && record.ticketNumber === ticketNumber
231
+ (record2) => record2.rallyId === rallyId && record2.userId === userId && record2.rewardId === rewardId && record2.ticketNumber === ticketNumber
174
232
  );
175
233
  if (index >= 0) this.#claimRecords.splice(index, 1);
176
234
  }
@@ -202,7 +260,7 @@ var InMemoryServerPersistenceAdapter = class {
202
260
  };
203
261
  try {
204
262
  return await operation(this);
205
- } catch (error) {
263
+ } catch (error2) {
206
264
  this.#stocks.clear();
207
265
  for (const [key, value] of snapshot.stocks) this.#stocks.set(key, value);
208
266
  this.#idempotent.clear();
@@ -214,10 +272,63 @@ var InMemoryServerPersistenceAdapter = class {
214
272
  for (const [key, value] of snapshot.claims) this.#claims.set(key, value);
215
273
  this.#claimRecords.splice(0, this.#claimRecords.length, ...snapshot.claimRecords);
216
274
  this.#auditLogs.splice(0, this.#auditLogs.length, ...snapshot.auditLogs);
217
- throw error;
275
+ throw error2;
218
276
  }
219
277
  }
220
278
  };
279
+
280
+ // src/security.ts
281
+ function error(path, message) {
282
+ return { success: false, errors: [{ path, message, code: "invalid_request" }] };
283
+ }
284
+ function record(value) {
285
+ return typeof value === "object" && value !== null && !Array.isArray(value);
286
+ }
287
+ function nonEmpty(value) {
288
+ return typeof value === "string" && value.trim().length > 0;
289
+ }
290
+ function context(value) {
291
+ if (!record(value) || typeof value.type !== "string") return false;
292
+ if (value.type === "qr") return nonEmpty(value.token);
293
+ if (value.type === "passcode") return nonEmpty(value.code);
294
+ if (value.type === "nfc") return nonEmpty(value.tagId);
295
+ if (value.type === "custom") return "value" in value;
296
+ return value.type === "gps" && typeof value.latitude === "number" && Number.isFinite(value.latitude) && typeof value.longitude === "number" && Number.isFinite(value.longitude);
297
+ }
298
+ function common(value, fields) {
299
+ if (!record(value)) return false;
300
+ return fields.every((field) => nonEmpty(value[field]));
301
+ }
302
+ function validateCheckInRequest(value) {
303
+ if (!common(value, ["rallyId", "spotId", "idempotencyKey"]))
304
+ return error("$", "rallyId, spotId, and idempotencyKey must be non-empty strings.");
305
+ if (!context(value.context))
306
+ return error("context", "context is not a valid verification context.");
307
+ if (value.userId !== void 0 && !nonEmpty(value.userId))
308
+ return error("userId", "userId must be non-empty.");
309
+ if (value.now !== void 0 && !nonEmpty(value.now))
310
+ return error("now", "now must be non-empty when provided.");
311
+ return { success: true, data: value };
312
+ }
313
+ function validateClaimRewardRequest(value) {
314
+ if (!common(value, ["rallyId", "rewardId", "idempotencyKey"]))
315
+ return error("$", "rallyId, rewardId, and idempotencyKey must be non-empty strings.");
316
+ if (value.userId !== void 0 && !nonEmpty(value.userId))
317
+ return error("userId", "userId must be non-empty.");
318
+ if (value.staffPasscode !== void 0 && !nonEmpty(value.staffPasscode))
319
+ return error("staffPasscode", "staffPasscode must be non-empty.");
320
+ if (value.staffId !== void 0 && !nonEmpty(value.staffId))
321
+ return error("staffId", "staffId must be non-empty.");
322
+ if (value.now !== void 0 && !nonEmpty(value.now))
323
+ return error("now", "now must be non-empty when provided.");
324
+ return { success: true, data: value };
325
+ }
326
+ function validateSyncRequest(value) {
327
+ if (!common(value, ["rallyId"])) return error("rallyId", "rallyId must be a non-empty string.");
328
+ if (value.userId !== void 0 && !nonEmpty(value.userId))
329
+ return error("userId", "userId must be non-empty.");
330
+ return { success: true, data: value };
331
+ }
221
332
  function json(body, status = 200) {
222
333
  return new Response(JSON.stringify(body), {
223
334
  status,
@@ -230,8 +341,12 @@ function isObject(value) {
230
341
  function requestId(prefix) {
231
342
  return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
232
343
  }
233
- function now(options, requested) {
234
- return requested ?? options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
344
+ function now(options) {
345
+ return options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
346
+ }
347
+ function timestampMillis(timestamp) {
348
+ const value = Date.parse(timestamp);
349
+ return Number.isFinite(value) ? value : Date.now();
235
350
  }
236
351
  function initialState(config, userId, timestamp) {
237
352
  return {
@@ -242,16 +357,16 @@ function initialState(config, userId, timestamp) {
242
357
  updatedAt: timestamp
243
358
  };
244
359
  }
245
- function getProof(context) {
246
- return context.type === "qr" ? context.token : context.type === "passcode" ? context.code : context.type === "gps" ? { latitude: context.latitude, longitude: context.longitude } : context.type === "nfc" ? context.tagId : context.value;
360
+ function getProof(context2) {
361
+ return context2.type === "qr" ? context2.token : context2.type === "passcode" ? context2.code : context2.type === "gps" ? { latitude: context2.latitude, longitude: context2.longitude } : context2.type === "nfc" ? context2.tagId : context2.value;
247
362
  }
248
- async function evaluate(condition, context, validator, base) {
249
- if (condition.type !== "custom") return core.evaluateConditionDetailed(condition, context).ok;
363
+ async function evaluate(condition, context2, validator, base) {
364
+ if (condition.type !== "custom") return core.evaluateConditionDetailed(condition, context2).ok;
250
365
  if (validator === void 0) return false;
251
366
  const validationContext = {
252
367
  rallyId: base.rallyId,
253
368
  spotId: base.spotId,
254
- proofData: getProof(context),
369
+ proofData: getProof(context2),
255
370
  condition,
256
371
  userState: base.state
257
372
  };
@@ -271,6 +386,10 @@ function audit(rallyId, userId, action, resourceId, key, status, timestamp, code
271
386
  ...code === void 0 ? {} : { metadata: { errorCode: code } }
272
387
  };
273
388
  }
389
+ function operationStatus(result) {
390
+ if (result.ok) return "ACCEPTED";
391
+ return result.code === "CONFLICT" || result.code === "PERSISTENCE_FAILED" ? "RETRYABLE_ERROR" : "REJECTED_PERMANENT";
392
+ }
274
393
  var StampRallyServer = class {
275
394
  #config;
276
395
  #persistence;
@@ -290,41 +409,51 @@ var StampRallyServer = class {
290
409
  return json({ ok: false, code: "NOT_FOUND", message: "Route not found." }, 404);
291
410
  }
292
411
  async handleCheckIn(request) {
293
- const body = await this.#body(request);
294
- const userId = await this.#user(request, body?.userId);
295
- if (body === null || userId === null || body.rallyId !== this.#config.id || body.spotId === "" || body.idempotencyKey === "" || body.context === void 0)
412
+ const body = validateCheckInRequest(await this.#body(request));
413
+ if (!body.success || body.data.rallyId !== this.#config.id)
296
414
  return json(
297
- {
298
- ok: false,
299
- code: "INVALID_REQUEST",
300
- message: "rallyId, spotId, context, and idempotencyKey are required."
301
- },
415
+ { ok: false, code: "INVALID_REQUEST", message: "Invalid check-in request." },
302
416
  400
303
417
  );
304
- const result = await this.checkIn({ ...body, userId });
305
- return json(result, result.ok ? 200 : result.code === "SPOT_NOT_FOUND" ? 404 : 422);
418
+ const userId = await this.#user(request);
419
+ if (userId === null)
420
+ return json(
421
+ { ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
422
+ 401
423
+ );
424
+ const result = await this.checkIn({ ...body.data, userId });
425
+ return json(
426
+ { ...result, status: operationStatus(result) },
427
+ result.ok ? 200 : result.code === "SPOT_NOT_FOUND" ? 404 : 422
428
+ );
306
429
  }
307
430
  async handleClaimReward(request) {
308
- const body = await this.#body(request);
309
- const userId = await this.#user(request, body?.userId);
310
- if (body === null || userId === null || body.rallyId !== this.#config.id || body.rewardId === "" || body.idempotencyKey === "")
431
+ const body = validateClaimRewardRequest(await this.#body(request));
432
+ if (!body.success || body.data.rallyId !== this.#config.id)
433
+ return json({ ok: false, code: "INVALID_REQUEST", message: "Invalid reward request." }, 400);
434
+ const userId = await this.#user(request);
435
+ if (userId === null)
311
436
  return json(
312
- {
313
- ok: false,
314
- code: "INVALID_REQUEST",
315
- message: "rallyId, rewardId, and idempotencyKey are required."
316
- },
317
- 400
437
+ { ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
438
+ 401
318
439
  );
319
- const result = await this.claimReward({ ...body, userId });
320
- return json(result, result.ok ? 200 : result.code === "REWARD_NOT_FOUND" ? 404 : 422);
440
+ const result = await this.claimReward({ ...body.data, userId });
441
+ return json(
442
+ { ...result, status: operationStatus(result) },
443
+ result.ok ? 200 : result.code === "REWARD_NOT_FOUND" ? 404 : 422
444
+ );
321
445
  }
322
446
  async handleSync(request) {
323
- const body = await this.#body(request);
324
- const userId = await this.#user(request, body?.userId);
325
- if (body === null || userId === null || body.rallyId !== this.#config.id)
326
- return json({ ok: false, code: "INVALID_REQUEST", message: "rallyId is required." }, 400);
327
- return json({ ok: true, state: await this.sync(body.rallyId, userId) });
447
+ const body = validateSyncRequest(await this.#body(request));
448
+ if (!body.success || body.data.rallyId !== this.#config.id)
449
+ return json({ ok: false, code: "INVALID_REQUEST", message: "Invalid sync request." }, 400);
450
+ const userId = await this.#user(request);
451
+ if (userId === null)
452
+ return json(
453
+ { ok: false, code: "UNAUTHENTICATED", message: "Authentication is required." },
454
+ 401
455
+ );
456
+ return json({ ok: true, state: await this.sync(body.data.rallyId, userId) });
328
457
  }
329
458
  async checkIn(request) {
330
459
  const key = `check-in:${request.rallyId}:${request.userId}:${request.idempotencyKey}`;
@@ -340,47 +469,68 @@ var StampRallyServer = class {
340
469
  this.#options.lockTtlMs ?? 5e3
341
470
  ))
342
471
  return { ok: false, code: "CONFLICT", message: "The user state is being updated." };
343
- const timestamp = now(this.#options, request.now);
472
+ const timestamp = now(this.#options);
344
473
  try {
474
+ const current = await this.#persistence.getUserState(request.rallyId, request.userId) ?? initialState(this.#config, request.userId, timestamp);
475
+ const responseHolder = { value: null };
476
+ const makeAudit = (status, code) => audit(
477
+ request.rallyId,
478
+ request.userId,
479
+ "CHECK_IN",
480
+ request.spotId,
481
+ request.idempotencyKey,
482
+ status,
483
+ timestamp,
484
+ code
485
+ );
345
486
  const spot = this.#config.spots.find((item) => item.id === request.spotId);
346
- if (spot === void 0)
347
- return this.#remember(
487
+ if (spot === void 0) {
488
+ responseHolder.value = {
489
+ ok: false,
490
+ code: "SPOT_NOT_FOUND",
491
+ message: "Spot was not found."
492
+ };
493
+ return await this.#rememberCheckInTransaction(
494
+ request,
495
+ timestamp,
348
496
  key,
349
- { ok: false, code: "SPOT_NOT_FOUND", message: "Spot was not found." },
350
- request.rallyId,
351
- request.userId,
352
- "CHECK_IN",
353
- request.spotId,
354
- request.idempotencyKey,
355
- timestamp
497
+ current,
498
+ {
499
+ nextUserState: current,
500
+ auditLog: makeAudit("REJECTED", "SPOT_NOT_FOUND"),
501
+ result: responseHolder.value,
502
+ error: "SPOT_NOT_FOUND"
503
+ },
504
+ responseHolder
356
505
  );
357
- const current = await this.#persistence.getUserState(request.rallyId, request.userId) ?? initialState(this.#config, request.userId, timestamp);
358
- if (current.records.some((record) => record.stampId === request.spotId))
359
- return this.#remember(
506
+ }
507
+ const rejected = (code, message) => {
508
+ responseHolder.value = { ok: false, code, message };
509
+ return {
510
+ nextUserState: current,
511
+ auditLog: makeAudit("REJECTED", code),
512
+ result: responseHolder.value,
513
+ error: code
514
+ };
515
+ };
516
+ if (current.records.some((record2) => record2.stampId === request.spotId))
517
+ return await this.#rememberCheckInTransaction(
518
+ request,
519
+ timestamp,
360
520
  key,
361
- { ok: false, code: "STAMP_ALREADY_ACQUIRED", message: "Spot was already claimed." },
362
- request.rallyId,
363
- request.userId,
364
- "CHECK_IN",
365
- request.spotId,
366
- request.idempotencyKey,
367
- timestamp
521
+ current,
522
+ rejected("STAMP_ALREADY_ACQUIRED", "Spot was already claimed."),
523
+ responseHolder
368
524
  );
369
- const acquired = new Set(current.records.map((record) => record.stampId));
525
+ const acquired = new Set(current.records.map((record2) => record2.stampId));
370
526
  if (spot.prerequisites?.some((id) => !acquired.has(id)))
371
- return this.#remember(
527
+ return await this.#rememberCheckInTransaction(
528
+ request,
529
+ timestamp,
372
530
  key,
373
- {
374
- ok: false,
375
- code: "PREREQUISITES_NOT_MET",
376
- message: "Prerequisite spots are not complete."
377
- },
378
- request.rallyId,
379
- request.userId,
380
- "CHECK_IN",
381
- request.spotId,
382
- request.idempotencyKey,
383
- timestamp
531
+ current,
532
+ rejected("PREREQUISITES_NOT_MET", "Prerequisite spots are not complete."),
533
+ responseHolder
384
534
  );
385
535
  for (const condition of spot.conditions)
386
536
  if (!await evaluate(
@@ -389,15 +539,13 @@ var StampRallyServer = class {
389
539
  condition.type === "custom" ? this.#options.customValidators?.[condition.validatorName] : void 0,
390
540
  { rallyId: request.rallyId, spotId: request.spotId, state: current }
391
541
  ))
392
- return this.#remember(
542
+ return await this.#rememberCheckInTransaction(
543
+ request,
544
+ timestamp,
393
545
  key,
394
- { ok: false, code: "INVALID_PROOF", message: "Verification failed." },
395
- request.rallyId,
396
- request.userId,
397
- "CHECK_IN",
398
- request.spotId,
399
- request.idempotencyKey,
400
- timestamp
546
+ current,
547
+ rejected("INVALID_PROOF", "Verification failed."),
548
+ responseHolder
401
549
  );
402
550
  const next = {
403
551
  ...current,
@@ -410,17 +558,30 @@ var StampRallyServer = class {
410
558
  ),
411
559
  updatedAt: timestamp
412
560
  };
413
- await this.#persistence.saveUserState(request.rallyId, request.userId, next);
414
- return this.#remember(
415
- key,
416
- { ok: true, state: next },
417
- request.rallyId,
418
- request.userId,
419
- "CHECK_IN",
420
- request.spotId,
421
- request.idempotencyKey,
422
- timestamp
561
+ responseHolder.value = { ok: true, state: next };
562
+ const transaction = await this.#persistence.executeCheckInTransaction(
563
+ {
564
+ rallyId: request.rallyId,
565
+ userId: request.userId,
566
+ spotId: request.spotId,
567
+ timestamp: timestampMillis(timestamp),
568
+ idempotencyKey: key,
569
+ proofData: request.context,
570
+ ...this.#options.idempotencyTtlMs === void 0 ? {} : { idempotencyTtlMs: this.#options.idempotencyTtlMs },
571
+ initialUserState: current
572
+ },
573
+ () => ({
574
+ nextUserState: next,
575
+ auditLog: makeAudit("SUCCESS"),
576
+ result: responseHolder.value
577
+ })
423
578
  );
579
+ if (transaction.success && responseHolder.value !== null) return responseHolder.value;
580
+ return {
581
+ ok: false,
582
+ code: "PERSISTENCE_FAILED",
583
+ message: transaction.error ?? "Check-in failed."
584
+ };
424
585
  } finally {
425
586
  await this.#persistence.releaseLock(request.rallyId, lockKey);
426
587
  }
@@ -438,7 +599,7 @@ var StampRallyServer = class {
438
599
  key,
439
600
  { ok: false, code: "REWARD_NOT_FOUND", message: "Reward was not found." },
440
601
  request,
441
- now(this.#options, request.now)
602
+ now(this.#options)
442
603
  );
443
604
  const lockKey = `reward:${request.rallyId}:${reward.id}`;
444
605
  if (!await this.#persistence.acquireLock(
@@ -447,7 +608,7 @@ var StampRallyServer = class {
447
608
  this.#options.lockTtlMs ?? 5e3
448
609
  ))
449
610
  return { ok: false, code: "CONFLICT", message: "The reward is being claimed." };
450
- const timestamp = now(this.#options, request.now);
611
+ const timestamp = now(this.#options);
451
612
  try {
452
613
  const checked = await this.#persistence.getIdempotentResult(
453
614
  request.rallyId,
@@ -497,8 +658,9 @@ var StampRallyServer = class {
497
658
  error: "OUT_OF_STOCK"
498
659
  };
499
660
  }
661
+ const effectiveReward = reward.staffPasscode === void 0 && this.#config.staffPasscode !== void 0 ? { ...reward, staffPasscode: this.#config.staffPasscode } : reward;
500
662
  const local = core.consumeReward({
501
- reward,
663
+ reward: effectiveReward,
502
664
  currentState: currentReward,
503
665
  now: timestamp,
504
666
  userRedemptionCount: claimCount,
@@ -549,11 +711,11 @@ var StampRallyServer = class {
549
711
  code: "PERSISTENCE_FAILED",
550
712
  message: result.error ?? "Reward claim failed."
551
713
  };
552
- } catch (error) {
714
+ } catch (error2) {
553
715
  return {
554
716
  ok: false,
555
717
  code: "PERSISTENCE_FAILED",
556
- message: error instanceof Error ? error.message : "Reward claim failed."
718
+ message: error2 instanceof Error ? error2.message : "Reward claim failed."
557
719
  };
558
720
  } finally {
559
721
  await this.#persistence.releaseLock(request.rallyId, lockKey);
@@ -570,31 +732,36 @@ var StampRallyServer = class {
570
732
  return null;
571
733
  }
572
734
  }
573
- async #user(request, requested) {
574
- if (this.#options.authenticate !== void 0)
575
- return await this.#options.authenticate(request) ?? null;
576
- return requested ?? null;
577
- }
578
- async #remember(key, result, rallyId, userId, action, resourceId, idempotencyKey, timestamp) {
579
- await this.#persistence.recordAuditLog(
580
- audit(
581
- rallyId,
582
- userId,
583
- action,
584
- resourceId,
585
- idempotencyKey,
586
- result.ok ? "SUCCESS" : "REJECTED",
587
- timestamp,
588
- result.ok ? void 0 : result.code
589
- )
590
- );
591
- await this.#persistence.saveIdempotentResult(
592
- rallyId,
593
- key,
594
- result,
595
- this.#options.idempotencyTtlMs ?? 864e5
735
+ async #user(request) {
736
+ if (this.#options.authenticate !== void 0) {
737
+ const identity = await this.#options.authenticate(request);
738
+ if (typeof identity === "string") return identity.length > 0 ? identity : null;
739
+ if (identity === null) return null;
740
+ const authenticatedUserId = identity.authenticatedUserId;
741
+ return authenticatedUserId.length > 0 ? authenticatedUserId : null;
742
+ }
743
+ return "anonymous";
744
+ }
745
+ async #rememberCheckInTransaction(request, timestamp, key, current, mutation, responseHolder) {
746
+ const transaction = await this.#persistence.executeCheckInTransaction(
747
+ {
748
+ rallyId: request.rallyId,
749
+ userId: request.userId,
750
+ spotId: request.spotId,
751
+ timestamp: timestampMillis(timestamp),
752
+ idempotencyKey: key,
753
+ ...this.#options.idempotencyTtlMs === void 0 ? {} : { idempotencyTtlMs: this.#options.idempotencyTtlMs },
754
+ initialUserState: current
755
+ },
756
+ () => mutation
596
757
  );
597
- return result;
758
+ if (responseHolder.value !== null && (transaction.success || transaction.error === mutation.error))
759
+ return responseHolder.value;
760
+ return {
761
+ ok: false,
762
+ code: "PERSISTENCE_FAILED",
763
+ message: transaction.error ?? "Check-in failed."
764
+ };
598
765
  }
599
766
  async #rememberClaim(key, result, request, timestamp) {
600
767
  await this.#persistence.recordAuditLog(
@@ -621,5 +788,9 @@ var StampRallyServer = class {
621
788
 
622
789
  exports.InMemoryServerPersistenceAdapter = InMemoryServerPersistenceAdapter;
623
790
  exports.StampRallyServer = StampRallyServer;
791
+ exports.executeClaimRewardTransaction = executeClaimRewardTransaction;
792
+ exports.validateCheckInRequest = validateCheckInRequest;
793
+ exports.validateClaimRewardRequest = validateClaimRewardRequest;
794
+ exports.validateSyncRequest = validateSyncRequest;
624
795
  //# sourceMappingURL=index.cjs.map
625
796
  //# sourceMappingURL=index.cjs.map