@stamprally/server 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -18
- package/dist/index.cjs +187 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -14
- package/dist/index.d.ts +40 -14
- package/dist/index.js +187 -50
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
# @stamprally/server
|
|
2
2
|
|
|
3
|
-
Web Standard `Request` / `Response` handlers for server-authoritative
|
|
4
|
-
check-ins, reward claims, and offline synchronization.
|
|
3
|
+
Web Standard `Request` / `Response` handlers for server-authoritative check-ins and reward claims.
|
|
5
4
|
|
|
6
5
|
```ts
|
|
7
6
|
import { StampRallyServer } from "@stamprally/server";
|
|
8
7
|
|
|
9
|
-
const server = new StampRallyServer(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
secretKey: process.env.RALLY_SECRET ?? "replace-me",
|
|
13
|
-
stamps: [{ id: "gate", name: { en: "Gate" }, condition: { type: "instant" } }],
|
|
14
|
-
},
|
|
15
|
-
storage,
|
|
16
|
-
);
|
|
17
|
-
|
|
8
|
+
const server = new StampRallyServer(adminConfig, persistence, {
|
|
9
|
+
authenticate: (request) => request.headers.get("x-user-id"),
|
|
10
|
+
});
|
|
18
11
|
const response = await server.handle(request);
|
|
19
12
|
```
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
|
|
16
|
+
Hono can mount the handler directly because it accepts the same Web Standard request and response types.
|
|
17
|
+
|
|
18
|
+
## License
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
`authenticate` to return the authenticated user ID. The handler rejects bodies
|
|
28
|
-
that attempt to act for a different user.
|
|
20
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -8,42 +8,73 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
8
8
|
#idempotent = /* @__PURE__ */ new Map();
|
|
9
9
|
#states = /* @__PURE__ */ new Map();
|
|
10
10
|
#stocks;
|
|
11
|
+
#stockDefaults;
|
|
11
12
|
#claims = /* @__PURE__ */ new Map();
|
|
13
|
+
#claimRecords = [];
|
|
12
14
|
#auditLogs = [];
|
|
13
15
|
constructor(options = {}) {
|
|
14
|
-
this.#stocks = new Map(
|
|
16
|
+
this.#stocks = /* @__PURE__ */ new Map();
|
|
17
|
+
this.#stockDefaults = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const [key, stock] of Object.entries(options.stocks ?? {})) {
|
|
19
|
+
if (key.includes(":")) this.#stocks.set(key, stock);
|
|
20
|
+
else this.#stockDefaults.set(key, stock);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
#key(rallyId, key) {
|
|
24
|
+
return `${rallyId}:${key}`;
|
|
25
|
+
}
|
|
26
|
+
#stockKey(rallyId, rewardId) {
|
|
27
|
+
const scopedKey = `${rallyId}:${rewardId}`;
|
|
28
|
+
if (this.#stocks.has(scopedKey)) return scopedKey;
|
|
29
|
+
const defaultStock = this.#stockDefaults.get(rewardId);
|
|
30
|
+
if (defaultStock !== void 0) {
|
|
31
|
+
this.#stocks.set(scopedKey, defaultStock);
|
|
32
|
+
return scopedKey;
|
|
33
|
+
}
|
|
34
|
+
return scopedKey;
|
|
15
35
|
}
|
|
16
|
-
async acquireLock(key, ttlMs) {
|
|
36
|
+
async acquireLock(rallyId, key, ttlMs) {
|
|
37
|
+
const scopedKey = this.#key(rallyId, key);
|
|
17
38
|
const now2 = Date.now();
|
|
18
|
-
const until = this.#locks.get(
|
|
39
|
+
const until = this.#locks.get(scopedKey);
|
|
19
40
|
if (until !== void 0 && until > now2) return false;
|
|
20
|
-
this.#locks.set(
|
|
41
|
+
this.#locks.set(scopedKey, now2 + Math.max(1, ttlMs));
|
|
21
42
|
return true;
|
|
22
43
|
}
|
|
23
|
-
async releaseLock(key) {
|
|
24
|
-
this.#locks.delete(key);
|
|
44
|
+
async releaseLock(rallyId, key) {
|
|
45
|
+
this.#locks.delete(this.#key(rallyId, key));
|
|
46
|
+
}
|
|
47
|
+
async getRewardStock(rallyId, rewardId) {
|
|
48
|
+
return this.#stocks.get(this.#stockKey(rallyId, rewardId)) ?? null;
|
|
25
49
|
}
|
|
26
|
-
async decrementRewardStock(rewardId) {
|
|
27
|
-
const
|
|
50
|
+
async decrementRewardStock(rallyId, rewardId) {
|
|
51
|
+
const key = this.#stockKey(rallyId, rewardId);
|
|
52
|
+
const current = this.#stocks.get(key);
|
|
28
53
|
if (current === void 0) return { success: true, remainingStock: Number.POSITIVE_INFINITY };
|
|
29
54
|
if (current <= 0) return { success: false, remainingStock: 0 };
|
|
30
|
-
this.#stocks.set(
|
|
55
|
+
this.#stocks.set(key, current - 1);
|
|
31
56
|
return { success: true, remainingStock: current - 1 };
|
|
32
57
|
}
|
|
33
|
-
async
|
|
34
|
-
const
|
|
35
|
-
|
|
58
|
+
async restoreRewardStock(rallyId, rewardId, count = 1) {
|
|
59
|
+
const key = this.#stockKey(rallyId, rewardId);
|
|
60
|
+
const current = this.#stocks.get(key);
|
|
61
|
+
if (current !== void 0) this.#stocks.set(key, current + Math.max(0, count));
|
|
62
|
+
}
|
|
63
|
+
async rollbackUserState(rallyId, userId, previousState) {
|
|
64
|
+
const key = `${rallyId}:${userId}`;
|
|
65
|
+
if (previousState === null) this.#states.delete(key);
|
|
66
|
+
else this.#states.set(key, structuredClone(previousState));
|
|
36
67
|
}
|
|
37
|
-
async getIdempotentResult(key) {
|
|
38
|
-
const value = this.#idempotent.get(key);
|
|
68
|
+
async getIdempotentResult(rallyId, key) {
|
|
69
|
+
const value = this.#idempotent.get(this.#key(rallyId, key));
|
|
39
70
|
if (value === void 0 || value.expiresAt <= Date.now()) {
|
|
40
|
-
this.#idempotent.delete(key);
|
|
71
|
+
this.#idempotent.delete(this.#key(rallyId, key));
|
|
41
72
|
return null;
|
|
42
73
|
}
|
|
43
74
|
return structuredClone(value.value);
|
|
44
75
|
}
|
|
45
|
-
async saveIdempotentResult(key, result, ttlMs) {
|
|
46
|
-
this.#idempotent.set(key, {
|
|
76
|
+
async saveIdempotentResult(rallyId, key, result, ttlMs) {
|
|
77
|
+
this.#idempotent.set(this.#key(rallyId, key), {
|
|
47
78
|
value: structuredClone(result),
|
|
48
79
|
expiresAt: Date.now() + Math.max(1, ttlMs)
|
|
49
80
|
});
|
|
@@ -61,13 +92,72 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
61
92
|
async recordAuditLog(log) {
|
|
62
93
|
this.#auditLogs.push(structuredClone(log));
|
|
63
94
|
}
|
|
95
|
+
async removeAuditLog(auditId) {
|
|
96
|
+
const index = this.#auditLogs.findIndex((log) => log.id === auditId);
|
|
97
|
+
if (index >= 0) this.#auditLogs.splice(index, 1);
|
|
98
|
+
}
|
|
64
99
|
getAuditLogs() {
|
|
65
100
|
return structuredClone(this.#auditLogs);
|
|
66
101
|
}
|
|
67
|
-
|
|
102
|
+
async recordUserClaim(params) {
|
|
103
|
+
const { rallyId, userId, rewardId } = params;
|
|
104
|
+
const key = `${rallyId}:${userId}:${rewardId}`;
|
|
105
|
+
this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
|
|
106
|
+
this.#claimRecords.push(structuredClone(params));
|
|
107
|
+
}
|
|
108
|
+
async rollbackUserClaim(rallyId, userId, rewardId, ticketNumber) {
|
|
109
|
+
const key = `${rallyId}:${userId}:${rewardId}`;
|
|
110
|
+
const count = this.#claims.get(key) ?? 0;
|
|
111
|
+
if (count <= 1) this.#claims.delete(key);
|
|
112
|
+
else this.#claims.set(key, count - 1);
|
|
113
|
+
const index = this.#claimRecords.findIndex(
|
|
114
|
+
(record) => record.rallyId === rallyId && record.userId === userId && record.rewardId === rewardId && record.ticketNumber === ticketNumber
|
|
115
|
+
);
|
|
116
|
+
if (index >= 0) this.#claimRecords.splice(index, 1);
|
|
117
|
+
}
|
|
118
|
+
async incrementUserClaimCount(rallyId, userId, rewardId) {
|
|
68
119
|
const key = `${rallyId}:${userId}:${rewardId}`;
|
|
69
120
|
this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
|
|
70
121
|
}
|
|
122
|
+
getClaimRecords() {
|
|
123
|
+
return structuredClone(this.#claimRecords);
|
|
124
|
+
}
|
|
125
|
+
recordClaim(paramsOrRallyId, userId, rewardId) {
|
|
126
|
+
const params = typeof paramsOrRallyId === "string" ? {
|
|
127
|
+
rallyId: paramsOrRallyId,
|
|
128
|
+
userId: userId ?? "",
|
|
129
|
+
rewardId: rewardId ?? "",
|
|
130
|
+
ticketNumber: "",
|
|
131
|
+
timestamp: Date.now()
|
|
132
|
+
} : paramsOrRallyId;
|
|
133
|
+
return this.recordUserClaim(params);
|
|
134
|
+
}
|
|
135
|
+
async runTransaction(_rallyId, operation) {
|
|
136
|
+
const snapshot = {
|
|
137
|
+
stocks: new Map(this.#stocks),
|
|
138
|
+
idempotent: new Map(this.#idempotent),
|
|
139
|
+
states: new Map(this.#states),
|
|
140
|
+
claims: new Map(this.#claims),
|
|
141
|
+
claimRecords: structuredClone(this.#claimRecords),
|
|
142
|
+
auditLogs: structuredClone(this.#auditLogs)
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
return await operation(this);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
this.#stocks.clear();
|
|
148
|
+
for (const [key, value] of snapshot.stocks) this.#stocks.set(key, value);
|
|
149
|
+
this.#idempotent.clear();
|
|
150
|
+
for (const [key, value] of snapshot.idempotent)
|
|
151
|
+
this.#idempotent.set(key, structuredClone(value));
|
|
152
|
+
this.#states.clear();
|
|
153
|
+
for (const [key, value] of snapshot.states) this.#states.set(key, structuredClone(value));
|
|
154
|
+
this.#claims.clear();
|
|
155
|
+
for (const [key, value] of snapshot.claims) this.#claims.set(key, value);
|
|
156
|
+
this.#claimRecords.splice(0, this.#claimRecords.length, ...snapshot.claimRecords);
|
|
157
|
+
this.#auditLogs.splice(0, this.#auditLogs.length, ...snapshot.auditLogs);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
71
161
|
};
|
|
72
162
|
function json(body, status = 200) {
|
|
73
163
|
return new Response(JSON.stringify(body), {
|
|
@@ -179,10 +269,17 @@ var StampRallyServer = class {
|
|
|
179
269
|
}
|
|
180
270
|
async checkIn(request) {
|
|
181
271
|
const key = `check-in:${request.rallyId}:${request.userId}:${request.idempotencyKey}`;
|
|
182
|
-
const previous = await this.#persistence.getIdempotentResult(
|
|
272
|
+
const previous = await this.#persistence.getIdempotentResult(
|
|
273
|
+
request.rallyId,
|
|
274
|
+
key
|
|
275
|
+
);
|
|
183
276
|
if (previous !== null) return previous;
|
|
184
277
|
const lockKey = `state:${request.rallyId}:${request.userId}`;
|
|
185
|
-
if (!await this.#persistence.acquireLock(
|
|
278
|
+
if (!await this.#persistence.acquireLock(
|
|
279
|
+
request.rallyId,
|
|
280
|
+
lockKey,
|
|
281
|
+
this.#options.lockTtlMs ?? 5e3
|
|
282
|
+
))
|
|
186
283
|
return { ok: false, code: "CONFLICT", message: "The user state is being updated." };
|
|
187
284
|
const timestamp = now(this.#options, request.now);
|
|
188
285
|
try {
|
|
@@ -266,12 +363,20 @@ var StampRallyServer = class {
|
|
|
266
363
|
timestamp
|
|
267
364
|
);
|
|
268
365
|
} finally {
|
|
269
|
-
await this.#persistence.releaseLock(lockKey);
|
|
366
|
+
await this.#persistence.releaseLock(request.rallyId, lockKey);
|
|
270
367
|
}
|
|
271
368
|
}
|
|
272
369
|
async claimReward(request) {
|
|
370
|
+
if (this.#persistence.runTransaction !== void 0)
|
|
371
|
+
return this.#persistence.runTransaction(
|
|
372
|
+
request.rallyId,
|
|
373
|
+
(transaction) => this.#claimRewardMutation(request, transaction)
|
|
374
|
+
);
|
|
375
|
+
return this.#claimRewardMutation(request, this.#persistence);
|
|
376
|
+
}
|
|
377
|
+
async #claimRewardMutation(request, persistence) {
|
|
273
378
|
const key = `claim:${request.rallyId}:${request.userId}:${request.rewardId}:${request.idempotencyKey}`;
|
|
274
|
-
const previous = await
|
|
379
|
+
const previous = await persistence.getIdempotentResult(request.rallyId, key);
|
|
275
380
|
if (previous !== null) return previous;
|
|
276
381
|
const reward = this.#config.rewards.find((item) => item.id === request.rewardId);
|
|
277
382
|
if (reward === void 0)
|
|
@@ -279,18 +384,24 @@ var StampRallyServer = class {
|
|
|
279
384
|
key,
|
|
280
385
|
{ ok: false, code: "REWARD_NOT_FOUND", message: "Reward was not found." },
|
|
281
386
|
request,
|
|
282
|
-
now(this.#options, request.now)
|
|
387
|
+
now(this.#options, request.now),
|
|
388
|
+
persistence
|
|
283
389
|
);
|
|
284
390
|
const lockKey = `reward:${request.rallyId}:${reward.id}`;
|
|
285
|
-
if (!await
|
|
391
|
+
if (!await persistence.acquireLock(request.rallyId, lockKey, this.#options.lockTtlMs ?? 5e3))
|
|
286
392
|
return { ok: false, code: "CONFLICT", message: "The reward is being claimed." };
|
|
287
393
|
const timestamp = now(this.#options, request.now);
|
|
288
394
|
let decremented = false;
|
|
395
|
+
let previousState = null;
|
|
396
|
+
let recordedTicket = null;
|
|
397
|
+
let successAuditId = null;
|
|
289
398
|
try {
|
|
290
|
-
const checked = await
|
|
399
|
+
const checked = await persistence.getIdempotentResult(request.rallyId, key);
|
|
291
400
|
if (checked !== null) return checked;
|
|
292
|
-
const
|
|
293
|
-
|
|
401
|
+
const storedState = await persistence.getUserState(request.rallyId, request.userId);
|
|
402
|
+
previousState = storedState;
|
|
403
|
+
const current = storedState ?? initialState(this.#config, request.userId, timestamp);
|
|
404
|
+
const claimCount = await persistence.getUserClaimCount(
|
|
294
405
|
request.rallyId,
|
|
295
406
|
request.userId,
|
|
296
407
|
reward.id
|
|
@@ -313,15 +424,17 @@ var StampRallyServer = class {
|
|
|
313
424
|
key,
|
|
314
425
|
{ ok: false, code: local.error.code, message: "Reward cannot be claimed." },
|
|
315
426
|
request,
|
|
316
|
-
timestamp
|
|
427
|
+
timestamp,
|
|
428
|
+
persistence
|
|
317
429
|
);
|
|
318
|
-
const stock = await
|
|
430
|
+
const stock = await persistence.decrementRewardStock(request.rallyId, reward.id);
|
|
319
431
|
if (!stock.success)
|
|
320
432
|
return this.#rememberClaim(
|
|
321
433
|
key,
|
|
322
434
|
{ ok: false, code: "OUT_OF_STOCK", message: "Reward is out of stock." },
|
|
323
435
|
request,
|
|
324
|
-
timestamp
|
|
436
|
+
timestamp,
|
|
437
|
+
persistence
|
|
325
438
|
);
|
|
326
439
|
decremented = true;
|
|
327
440
|
const next = {
|
|
@@ -330,41 +443,59 @@ var StampRallyServer = class {
|
|
|
330
443
|
updatedAt: timestamp
|
|
331
444
|
};
|
|
332
445
|
try {
|
|
333
|
-
await
|
|
446
|
+
await persistence.saveUserState(request.rallyId, request.userId, next);
|
|
334
447
|
const response = local.value.claimTicketNumber === void 0 ? { ok: true, state: next } : { ok: true, state: next, claimTicketNumber: local.value.claimTicketNumber };
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
448
|
+
recordedTicket = local.value.claimTicketNumber ?? "";
|
|
449
|
+
await persistence.recordUserClaim({
|
|
450
|
+
rallyId: request.rallyId,
|
|
451
|
+
userId: request.userId,
|
|
452
|
+
rewardId: reward.id,
|
|
453
|
+
ticketNumber: recordedTicket,
|
|
454
|
+
timestamp: Number.isNaN(Date.parse(timestamp)) ? Date.now() : Date.parse(timestamp)
|
|
455
|
+
});
|
|
456
|
+
const successAudit = audit(
|
|
457
|
+
request.rallyId,
|
|
458
|
+
request.userId,
|
|
459
|
+
"CLAIM_REWARD",
|
|
460
|
+
reward.id,
|
|
461
|
+
request.idempotencyKey,
|
|
462
|
+
"SUCCESS",
|
|
463
|
+
timestamp
|
|
345
464
|
);
|
|
346
|
-
|
|
465
|
+
successAuditId = successAudit.id;
|
|
466
|
+
await persistence.recordAuditLog(successAudit);
|
|
467
|
+
await persistence.saveIdempotentResult(
|
|
468
|
+
request.rallyId,
|
|
347
469
|
key,
|
|
348
470
|
response,
|
|
349
471
|
this.#options.idempotencyTtlMs ?? 864e5
|
|
350
472
|
);
|
|
351
|
-
if (this.#persistence instanceof Object && "recordClaim" in this.#persistence && typeof this.#persistence.recordClaim === "function")
|
|
352
|
-
this.#persistence.recordClaim(request.rallyId, request.userId, reward.id);
|
|
353
473
|
return response;
|
|
354
474
|
} catch (error) {
|
|
355
|
-
|
|
475
|
+
if (recordedTicket !== null && persistence.rollbackUserClaim !== void 0)
|
|
476
|
+
await persistence.rollbackUserClaim(
|
|
477
|
+
request.rallyId,
|
|
478
|
+
request.userId,
|
|
479
|
+
reward.id,
|
|
480
|
+
recordedTicket
|
|
481
|
+
);
|
|
482
|
+
if (persistence.rollbackUserState !== void 0)
|
|
483
|
+
await persistence.rollbackUserState(request.rallyId, request.userId, previousState);
|
|
484
|
+
if (successAuditId !== null && persistence.removeAuditLog !== void 0)
|
|
485
|
+
await persistence.removeAuditLog(successAuditId);
|
|
486
|
+
await this.#restoreRewardStock(persistence, request.rallyId, reward.id);
|
|
356
487
|
decremented = false;
|
|
357
488
|
throw error;
|
|
358
489
|
}
|
|
359
490
|
} catch (error) {
|
|
360
|
-
if (decremented) await this.#persistence.
|
|
491
|
+
if (decremented) await this.#restoreRewardStock(persistence, request.rallyId, reward.id);
|
|
361
492
|
return {
|
|
362
493
|
ok: false,
|
|
363
494
|
code: "PERSISTENCE_FAILED",
|
|
364
495
|
message: error instanceof Error ? error.message : "Reward claim failed."
|
|
365
496
|
};
|
|
366
497
|
} finally {
|
|
367
|
-
await
|
|
498
|
+
await persistence.releaseLock(request.rallyId, lockKey);
|
|
368
499
|
}
|
|
369
500
|
}
|
|
370
501
|
async sync(rallyId, userId) {
|
|
@@ -397,14 +528,15 @@ var StampRallyServer = class {
|
|
|
397
528
|
)
|
|
398
529
|
);
|
|
399
530
|
await this.#persistence.saveIdempotentResult(
|
|
531
|
+
rallyId,
|
|
400
532
|
key,
|
|
401
533
|
result,
|
|
402
534
|
this.#options.idempotencyTtlMs ?? 864e5
|
|
403
535
|
);
|
|
404
536
|
return result;
|
|
405
537
|
}
|
|
406
|
-
async #rememberClaim(key, result, request, timestamp) {
|
|
407
|
-
await
|
|
538
|
+
async #rememberClaim(key, result, request, timestamp, persistence = this.#persistence) {
|
|
539
|
+
await persistence.recordAuditLog(
|
|
408
540
|
audit(
|
|
409
541
|
request.rallyId,
|
|
410
542
|
request.userId,
|
|
@@ -416,13 +548,18 @@ var StampRallyServer = class {
|
|
|
416
548
|
result.ok ? void 0 : result.code
|
|
417
549
|
)
|
|
418
550
|
);
|
|
419
|
-
await
|
|
551
|
+
await persistence.saveIdempotentResult(
|
|
552
|
+
request.rallyId,
|
|
420
553
|
key,
|
|
421
554
|
result,
|
|
422
555
|
this.#options.idempotencyTtlMs ?? 864e5
|
|
423
556
|
);
|
|
424
557
|
return result;
|
|
425
558
|
}
|
|
559
|
+
async #restoreRewardStock(persistence, rallyId, rewardId) {
|
|
560
|
+
if (persistence.restoreRewardStock !== void 0)
|
|
561
|
+
await persistence.restoreRewardStock(rallyId, rewardId);
|
|
562
|
+
}
|
|
426
563
|
};
|
|
427
564
|
|
|
428
565
|
exports.InMemoryServerPersistenceAdapter = InMemoryServerPersistenceAdapter;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/persistence.ts","../src/server.ts"],"names":["now","reconcileRewardStates","evaluateConditionDetailed","consumeReward"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { UserRallyState } from \"@stamprally/core\";\nimport type { AuditLog } from \"./index.js\";\n\nexport interface ServerPersistenceAdapter {\n acquireLock(lockKey: string, ttlMs: number): Promise<boolean>;\n releaseLock(lockKey: string): Promise<void>;\n decrementRewardStock(\n rewardId: string,\n ): Promise<{ readonly success: boolean; readonly remainingStock: number }>;\n incrementRewardStock(rewardId: string): Promise<void>;\n getIdempotentResult<T>(key: string): Promise<T | null>;\n saveIdempotentResult<T>(key: string, result: T, ttlMs: number): Promise<void>;\n getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;\n getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;\n saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;\n recordAuditLog(log: AuditLog): Promise<void>;\n}\ninterface TimedValue<T> {\n readonly value: T;\n readonly expiresAt: number;\n}\nexport interface InMemoryServerPersistenceOptions {\n readonly stocks?: Readonly<Record<string, number>>;\n}\nexport class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapter {\n readonly #locks = new Map<string, number>();\n readonly #idempotent = new Map<string, TimedValue<unknown>>();\n readonly #states = new Map<string, UserRallyState>();\n readonly #stocks: Map<string, number>;\n readonly #claims = new Map<string, number>();\n readonly #auditLogs: AuditLog[] = [];\n constructor(options: InMemoryServerPersistenceOptions = {}) {\n this.#stocks = new Map(Object.entries(options.stocks ?? {}));\n }\n async acquireLock(key: string, ttlMs: number): Promise<boolean> {\n const now = Date.now();\n const until = this.#locks.get(key);\n if (until !== undefined && until > now) return false;\n this.#locks.set(key, now + Math.max(1, ttlMs));\n return true;\n }\n async releaseLock(key: string): Promise<void> {\n this.#locks.delete(key);\n }\n async decrementRewardStock(\n rewardId: string,\n ): Promise<{ success: boolean; remainingStock: number }> {\n const current = this.#stocks.get(rewardId);\n if (current === undefined) return { success: true, remainingStock: Number.POSITIVE_INFINITY };\n if (current <= 0) return { success: false, remainingStock: 0 };\n this.#stocks.set(rewardId, current - 1);\n return { success: true, remainingStock: current - 1 };\n }\n async incrementRewardStock(rewardId: string): Promise<void> {\n const current = this.#stocks.get(rewardId);\n if (current !== undefined) this.#stocks.set(rewardId, current + 1);\n }\n async getIdempotentResult<T>(key: string): Promise<T | null> {\n const value = this.#idempotent.get(key);\n if (value === undefined || value.expiresAt <= Date.now()) {\n this.#idempotent.delete(key);\n return null;\n }\n return structuredClone(value.value) as T;\n }\n async saveIdempotentResult<T>(key: string, result: T, ttlMs: number): Promise<void> {\n this.#idempotent.set(key, {\n value: structuredClone(result),\n expiresAt: Date.now() + Math.max(1, ttlMs),\n });\n }\n async getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number> {\n return this.#claims.get(`${rallyId}:${userId}:${rewardId}`) ?? 0;\n }\n async getUserState(rallyId: string, userId: string): Promise<UserRallyState | null> {\n const value = this.#states.get(`${rallyId}:${userId}`);\n return value === undefined ? null : structuredClone(value);\n }\n async saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void> {\n this.#states.set(`${rallyId}:${userId}`, structuredClone(state));\n }\n async recordAuditLog(log: AuditLog): Promise<void> {\n this.#auditLogs.push(structuredClone(log));\n }\n getAuditLogs(): ReadonlyArray<AuditLog> {\n return structuredClone(this.#auditLogs);\n }\n recordClaim(rallyId: string, userId: string, rewardId: string): void {\n const key = `${rallyId}:${userId}:${rewardId}`;\n this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);\n }\n}\n","import {\n type AdminRallyConfig,\n type CheckInCondition,\n consumeReward,\n evaluateConditionDetailed,\n reconcileRewardStates,\n type UserRallyState,\n type Validator,\n type VerificationContext,\n} from \"@stamprally/core\";\nimport type {\n AuditLog,\n CheckInRequest,\n CheckInResponse,\n ClaimRewardRequest,\n ServerOptions,\n} from \"./index.js\";\nimport type { ServerPersistenceAdapter } from \"./persistence.js\";\n\ntype ErrorResponse = { readonly ok: false; readonly code: string; readonly message: string };\ntype ClaimResponse =\n | { readonly ok: true; readonly state: UserRallyState; readonly claimTicketNumber?: string }\n | ErrorResponse;\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json; charset=utf-8\" },\n });\n}\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\nfunction requestId(prefix: string): string {\n return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;\n}\nfunction now(options: ServerOptions, requested?: string): string {\n return requested ?? options.now?.() ?? new Date().toISOString();\n}\nfunction initialState(config: AdminRallyConfig, userId: string, timestamp: string): UserRallyState {\n return {\n rallyId: config.id,\n userId,\n records: [],\n rewards: reconcileRewardStates(config.rewards, [], 0, timestamp),\n updatedAt: timestamp,\n };\n}\nfunction getProof(context: VerificationContext): unknown {\n return context.type === \"qr\"\n ? context.token\n : context.type === \"passcode\"\n ? context.code\n : context.type === \"gps\"\n ? { latitude: context.latitude, longitude: context.longitude }\n : context.type === \"nfc\"\n ? context.tagId\n : context.value;\n}\nasync function evaluate(\n condition: CheckInCondition,\n context: VerificationContext,\n validator: Validator | undefined,\n base: { rallyId: string; spotId: string; state: UserRallyState },\n): Promise<boolean> {\n if (condition.type !== \"custom\") return evaluateConditionDetailed(condition, context).ok;\n if (validator === undefined) return false;\n const validationContext = {\n rallyId: base.rallyId,\n spotId: base.spotId,\n proofData: getProof(context),\n condition,\n userState: base.state,\n };\n const result =\n typeof validator === \"function\"\n ? await validator(validationContext)\n : await validator.validate(validationContext);\n return result === true || (typeof result === \"object\" && result.valid);\n}\nfunction audit(\n rallyId: string,\n userId: string,\n action: AuditLog[\"action\"],\n resourceId: string,\n key: string,\n status: AuditLog[\"status\"],\n timestamp: string,\n code?: string,\n): AuditLog {\n return {\n id: requestId(\"audit\"),\n timestamp,\n rallyId,\n userId,\n action,\n resourceId,\n status,\n idempotencyKey: key,\n ...(code === undefined ? {} : { metadata: { errorCode: code } }),\n };\n}\n\nexport class StampRallyServer {\n readonly #config: AdminRallyConfig;\n readonly #persistence: ServerPersistenceAdapter;\n readonly #options: ServerOptions;\n constructor(\n config: AdminRallyConfig,\n persistence: ServerPersistenceAdapter,\n options: ServerOptions = {},\n ) {\n this.#config = config;\n this.#persistence = persistence;\n this.#options = options;\n }\n async handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\")\n return json({ ok: false, code: \"METHOD_NOT_ALLOWED\", message: \"POST is required.\" }, 405);\n const path = new URL(request.url).pathname;\n if (path.endsWith(\"/check-in\")) return this.handleCheckIn(request);\n if (path.endsWith(\"/claim-reward\")) return this.handleClaimReward(request);\n if (path.endsWith(\"/sync\")) return this.handleSync(request);\n return json({ ok: false, code: \"NOT_FOUND\", message: \"Route not found.\" }, 404);\n }\n async handleCheckIn(request: Request): Promise<Response> {\n const body = await this.#body<CheckInRequest>(request);\n const userId = await this.#user(request, body?.userId);\n if (\n body === null ||\n userId === null ||\n body.rallyId !== this.#config.id ||\n body.spotId === \"\" ||\n body.idempotencyKey === \"\" ||\n body.context === undefined\n )\n return json(\n {\n ok: false,\n code: \"INVALID_REQUEST\",\n message: \"rallyId, spotId, context, and idempotencyKey are required.\",\n },\n 400,\n );\n const result = await this.checkIn({ ...body, userId });\n return json(result, result.ok ? 200 : result.code === \"SPOT_NOT_FOUND\" ? 404 : 422);\n }\n async handleClaimReward(request: Request): Promise<Response> {\n const body = await this.#body<ClaimRewardRequest>(request);\n const userId = await this.#user(request, body?.userId);\n if (\n body === null ||\n userId === null ||\n body.rallyId !== this.#config.id ||\n body.rewardId === \"\" ||\n body.idempotencyKey === \"\"\n )\n return json(\n {\n ok: false,\n code: \"INVALID_REQUEST\",\n message: \"rallyId, rewardId, and idempotencyKey are required.\",\n },\n 400,\n );\n const result = await this.claimReward({ ...body, userId });\n return json(result, result.ok ? 200 : result.code === \"REWARD_NOT_FOUND\" ? 404 : 422);\n }\n async handleSync(request: Request): Promise<Response> {\n const body = await this.#body<{ readonly rallyId: string; readonly userId?: string }>(request);\n const userId = await this.#user(request, body?.userId);\n if (body === null || userId === null || body.rallyId !== this.#config.id)\n return json({ ok: false, code: \"INVALID_REQUEST\", message: \"rallyId is required.\" }, 400);\n return json({ ok: true, state: await this.sync(body.rallyId, userId) });\n }\n async checkIn(request: CheckInRequest & { readonly userId: string }): Promise<CheckInResponse> {\n const key = `check-in:${request.rallyId}:${request.userId}:${request.idempotencyKey}`;\n const previous = await this.#persistence.getIdempotentResult<CheckInResponse>(key);\n if (previous !== null) return previous;\n const lockKey = `state:${request.rallyId}:${request.userId}`;\n if (!(await this.#persistence.acquireLock(lockKey, this.#options.lockTtlMs ?? 5_000)))\n return { ok: false, code: \"CONFLICT\", message: \"The user state is being updated.\" };\n const timestamp = now(this.#options, request.now);\n try {\n const spot = this.#config.spots.find((item) => item.id === request.spotId);\n if (spot === undefined)\n return this.#remember(\n key,\n { ok: false, code: \"SPOT_NOT_FOUND\", message: \"Spot was not found.\" },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n const current =\n (await this.#persistence.getUserState(request.rallyId, request.userId)) ??\n initialState(this.#config, request.userId, timestamp);\n if (current.records.some((record) => record.stampId === request.spotId))\n return this.#remember(\n key,\n { ok: false, code: \"STAMP_ALREADY_ACQUIRED\", message: \"Spot was already claimed.\" },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n const acquired = new Set(current.records.map((record) => record.stampId));\n if (spot.prerequisites?.some((id) => !acquired.has(id)))\n return this.#remember(\n key,\n {\n ok: false,\n code: \"PREREQUISITES_NOT_MET\",\n message: \"Prerequisite spots are not complete.\",\n },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n for (const condition of spot.conditions)\n if (\n !(await evaluate(\n condition,\n request.context,\n condition.type === \"custom\"\n ? this.#options.customValidators?.[condition.validatorName]\n : undefined,\n { rallyId: request.rallyId, spotId: request.spotId, state: current },\n ))\n )\n return this.#remember(\n key,\n { ok: false, code: \"INVALID_PROOF\", message: \"Verification failed.\" },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n const next: UserRallyState = {\n ...current,\n records: [...current.records, { stampId: request.spotId, acquiredAt: timestamp }],\n rewards: reconcileRewardStates(\n this.#config.rewards,\n current.rewards,\n current.records.length + 1,\n timestamp,\n ),\n updatedAt: timestamp,\n };\n await this.#persistence.saveUserState(request.rallyId, request.userId, next);\n return this.#remember(\n key,\n { ok: true, state: next },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n } finally {\n await this.#persistence.releaseLock(lockKey);\n }\n }\n async claimReward(\n request: ClaimRewardRequest & { readonly userId: string },\n ): Promise<ClaimResponse> {\n const key = `claim:${request.rallyId}:${request.userId}:${request.rewardId}:${request.idempotencyKey}`;\n const previous = await this.#persistence.getIdempotentResult<ClaimResponse>(key);\n if (previous !== null) return previous;\n const reward = this.#config.rewards.find((item) => item.id === request.rewardId);\n if (reward === undefined)\n return this.#rememberClaim(\n key,\n { ok: false, code: \"REWARD_NOT_FOUND\", message: \"Reward was not found.\" },\n request,\n now(this.#options, request.now),\n );\n const lockKey = `reward:${request.rallyId}:${reward.id}`;\n if (!(await this.#persistence.acquireLock(lockKey, this.#options.lockTtlMs ?? 5_000)))\n return { ok: false, code: \"CONFLICT\", message: \"The reward is being claimed.\" };\n const timestamp = now(this.#options, request.now);\n let decremented = false;\n try {\n const checked = await this.#persistence.getIdempotentResult<ClaimResponse>(key);\n if (checked !== null) return checked;\n const current =\n (await this.#persistence.getUserState(request.rallyId, request.userId)) ??\n initialState(this.#config, request.userId, timestamp);\n const claimCount = await this.#persistence.getUserClaimCount(\n request.rallyId,\n request.userId,\n reward.id,\n );\n const storedReward = current.rewards.find((item) => item.rewardId === reward.id) ?? {\n rewardId: reward.id,\n status: \"LOCKED\" as const,\n };\n const currentReward =\n reward.redemptionMethod === \"server_claim\" &&\n storedReward.status === \"CONSUMED\" &&\n (reward.userClaimLimit === undefined || claimCount < reward.userClaimLimit)\n ? { ...storedReward, status: \"AVAILABLE\" as const }\n : storedReward;\n const local = consumeReward({\n reward,\n currentState: currentReward,\n now: timestamp,\n userRedemptionCount: claimCount,\n ...(request.staffPasscode === undefined ? {} : { inputPasscode: request.staffPasscode }),\n ...(request.staffId === undefined ? {} : { staffId: request.staffId }),\n });\n if (!local.ok)\n return this.#rememberClaim(\n key,\n { ok: false, code: local.error.code, message: \"Reward cannot be claimed.\" },\n request,\n timestamp,\n );\n const stock = await this.#persistence.decrementRewardStock(reward.id);\n if (!stock.success)\n return this.#rememberClaim(\n key,\n { ok: false, code: \"OUT_OF_STOCK\", message: \"Reward is out of stock.\" },\n request,\n timestamp,\n );\n decremented = true;\n const next: UserRallyState = {\n ...current,\n rewards: current.rewards.map((item) => (item.rewardId === reward.id ? local.value : item)),\n updatedAt: timestamp,\n };\n try {\n await this.#persistence.saveUserState(request.rallyId, request.userId, next);\n const response: ClaimResponse =\n local.value.claimTicketNumber === undefined\n ? { ok: true, state: next }\n : { ok: true, state: next, claimTicketNumber: local.value.claimTicketNumber };\n await this.#persistence.recordAuditLog(\n audit(\n request.rallyId,\n request.userId,\n \"CLAIM_REWARD\",\n reward.id,\n request.idempotencyKey,\n \"SUCCESS\",\n timestamp,\n ),\n );\n await this.#persistence.saveIdempotentResult(\n key,\n response,\n this.#options.idempotencyTtlMs ?? 86_400_000,\n );\n if (\n this.#persistence instanceof Object &&\n \"recordClaim\" in this.#persistence &&\n typeof this.#persistence.recordClaim === \"function\"\n )\n this.#persistence.recordClaim(request.rallyId, request.userId, reward.id);\n return response;\n } catch (error) {\n await this.#persistence.incrementRewardStock(reward.id);\n decremented = false;\n throw error;\n }\n } catch (error) {\n if (decremented) await this.#persistence.incrementRewardStock(reward.id);\n return {\n ok: false,\n code: \"PERSISTENCE_FAILED\",\n message: error instanceof Error ? error.message : \"Reward claim failed.\",\n };\n } finally {\n await this.#persistence.releaseLock(lockKey);\n }\n }\n async sync(rallyId: string, userId: string): Promise<UserRallyState> {\n return (\n (await this.#persistence.getUserState(rallyId, userId)) ??\n initialState(this.#config, userId, now(this.#options))\n );\n }\n async #body<T>(request: Request): Promise<T | null> {\n try {\n const value: unknown = await request.json();\n return isObject(value) ? (value as T) : null;\n } catch {\n return null;\n }\n }\n async #user(request: Request, requested: string | undefined): Promise<string | null> {\n if (this.#options.authenticate !== undefined)\n return (await this.#options.authenticate(request)) ?? null;\n return requested ?? null;\n }\n async #remember(\n key: string,\n result: CheckInResponse,\n rallyId: string,\n userId: string,\n action: AuditLog[\"action\"],\n resourceId: string,\n idempotencyKey: string,\n timestamp: string,\n ): Promise<CheckInResponse> {\n await this.#persistence.recordAuditLog(\n audit(\n rallyId,\n userId,\n action,\n resourceId,\n idempotencyKey,\n result.ok ? \"SUCCESS\" : \"REJECTED\",\n timestamp,\n result.ok ? undefined : result.code,\n ),\n );\n await this.#persistence.saveIdempotentResult(\n key,\n result,\n this.#options.idempotencyTtlMs ?? 86_400_000,\n );\n return result;\n }\n async #rememberClaim(\n key: string,\n result: ClaimResponse,\n request: ClaimRewardRequest & { readonly userId: string },\n timestamp: string,\n ): Promise<ClaimResponse> {\n await this.#persistence.recordAuditLog(\n audit(\n request.rallyId,\n request.userId,\n \"CLAIM_REWARD\",\n request.rewardId,\n request.idempotencyKey,\n \"REJECTED\",\n timestamp,\n result.ok ? undefined : result.code,\n ),\n );\n await this.#persistence.saveIdempotentResult(\n key,\n result,\n this.#options.idempotencyTtlMs ?? 86_400_000,\n );\n return result;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/persistence.ts","../src/server.ts"],"names":["now","reconcileRewardStates","evaluateConditionDetailed","consumeReward"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { UserRallyState } from \"@stamprally/core\";\nimport type { AuditLog } from \"./index.js\";\n\nexport interface UserClaimRecord {\n readonly rallyId: string;\n readonly userId: string;\n readonly rewardId: string;\n readonly ticketNumber: string;\n readonly timestamp: number;\n}\n\nexport interface ServerPersistenceAdapter {\n /** Runs the callback in a storage-native transaction when supported. */\n runTransaction?<T>(\n rallyId: string,\n operation: (transaction: ServerPersistenceAdapter) => Promise<T>,\n ): Promise<T>;\n acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;\n releaseLock(rallyId: string, lockKey: string): Promise<void>;\n getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;\n decrementRewardStock(\n rallyId: string,\n rewardId: string,\n ): Promise<{ readonly success: boolean; readonly remainingStock: number }>;\n restoreRewardStock?(rallyId: string, rewardId: string, count?: number): Promise<void>;\n rollbackUserState?(\n rallyId: string,\n userId: string,\n previousState: UserRallyState | null,\n ): Promise<void>;\n rollbackUserClaim?(\n rallyId: string,\n userId: string,\n rewardId: string,\n ticketNumber: string,\n ): Promise<void>;\n getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;\n saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;\n getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;\n recordUserClaim(params: UserClaimRecord): Promise<void>;\n recordClaim?(params: UserClaimRecord): Promise<void>;\n incrementUserClaimCount?(rallyId: string, userId: string, rewardId: string): Promise<void>;\n getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;\n saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;\n recordAuditLog(log: AuditLog): Promise<void>;\n removeAuditLog?(auditId: string): Promise<void>;\n}\ninterface TimedValue<T> {\n readonly value: T;\n readonly expiresAt: number;\n}\nexport interface InMemoryServerPersistenceOptions {\n readonly stocks?: Readonly<Record<string, number>>;\n}\nexport class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapter {\n readonly #locks = new Map<string, number>();\n readonly #idempotent = new Map<string, TimedValue<unknown>>();\n readonly #states = new Map<string, UserRallyState>();\n readonly #stocks: Map<string, number>;\n readonly #stockDefaults: Map<string, number>;\n readonly #claims = new Map<string, number>();\n readonly #claimRecords: UserClaimRecord[] = [];\n readonly #auditLogs: AuditLog[] = [];\n constructor(options: InMemoryServerPersistenceOptions = {}) {\n this.#stocks = new Map();\n this.#stockDefaults = new Map();\n for (const [key, stock] of Object.entries(options.stocks ?? {})) {\n if (key.includes(\":\")) this.#stocks.set(key, stock);\n else this.#stockDefaults.set(key, stock);\n }\n }\n #key(rallyId: string, key: string): string {\n return `${rallyId}:${key}`;\n }\n #stockKey(rallyId: string, rewardId: string): string {\n const scopedKey = `${rallyId}:${rewardId}`;\n if (this.#stocks.has(scopedKey)) return scopedKey;\n const defaultStock = this.#stockDefaults.get(rewardId);\n if (defaultStock !== undefined) {\n this.#stocks.set(scopedKey, defaultStock);\n return scopedKey;\n }\n return scopedKey;\n }\n async acquireLock(rallyId: string, key: string, ttlMs: number): Promise<boolean> {\n const scopedKey = this.#key(rallyId, key);\n const now = Date.now();\n const until = this.#locks.get(scopedKey);\n if (until !== undefined && until > now) return false;\n this.#locks.set(scopedKey, now + Math.max(1, ttlMs));\n return true;\n }\n async releaseLock(rallyId: string, key: string): Promise<void> {\n this.#locks.delete(this.#key(rallyId, key));\n }\n async getRewardStock(rallyId: string, rewardId: string): Promise<number | null> {\n return this.#stocks.get(this.#stockKey(rallyId, rewardId)) ?? null;\n }\n async decrementRewardStock(\n rallyId: string,\n rewardId: string,\n ): Promise<{ success: boolean; remainingStock: number }> {\n const key = this.#stockKey(rallyId, rewardId);\n const current = this.#stocks.get(key);\n if (current === undefined) return { success: true, remainingStock: Number.POSITIVE_INFINITY };\n if (current <= 0) return { success: false, remainingStock: 0 };\n this.#stocks.set(key, current - 1);\n return { success: true, remainingStock: current - 1 };\n }\n async restoreRewardStock(rallyId: string, rewardId: string, count = 1): Promise<void> {\n const key = this.#stockKey(rallyId, rewardId);\n const current = this.#stocks.get(key);\n if (current !== undefined) this.#stocks.set(key, current + Math.max(0, count));\n }\n\n async rollbackUserState(\n rallyId: string,\n userId: string,\n previousState: UserRallyState | null,\n ): Promise<void> {\n const key = `${rallyId}:${userId}`;\n if (previousState === null) this.#states.delete(key);\n else this.#states.set(key, structuredClone(previousState));\n }\n async getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null> {\n const value = this.#idempotent.get(this.#key(rallyId, key));\n if (value === undefined || value.expiresAt <= Date.now()) {\n this.#idempotent.delete(this.#key(rallyId, key));\n return null;\n }\n return structuredClone(value.value) as T;\n }\n async saveIdempotentResult<T>(\n rallyId: string,\n key: string,\n result: T,\n ttlMs: number,\n ): Promise<void> {\n this.#idempotent.set(this.#key(rallyId, key), {\n value: structuredClone(result),\n expiresAt: Date.now() + Math.max(1, ttlMs),\n });\n }\n async getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number> {\n return this.#claims.get(`${rallyId}:${userId}:${rewardId}`) ?? 0;\n }\n async getUserState(rallyId: string, userId: string): Promise<UserRallyState | null> {\n const value = this.#states.get(`${rallyId}:${userId}`);\n return value === undefined ? null : structuredClone(value);\n }\n async saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void> {\n this.#states.set(`${rallyId}:${userId}`, structuredClone(state));\n }\n async recordAuditLog(log: AuditLog): Promise<void> {\n this.#auditLogs.push(structuredClone(log));\n }\n async removeAuditLog(auditId: string): Promise<void> {\n const index = this.#auditLogs.findIndex((log) => log.id === auditId);\n if (index >= 0) this.#auditLogs.splice(index, 1);\n }\n getAuditLogs(): ReadonlyArray<AuditLog> {\n return structuredClone(this.#auditLogs);\n }\n async recordUserClaim(params: UserClaimRecord): Promise<void> {\n const { rallyId, userId, rewardId } = params;\n const key = `${rallyId}:${userId}:${rewardId}`;\n this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);\n this.#claimRecords.push(structuredClone(params));\n }\n async rollbackUserClaim(\n rallyId: string,\n userId: string,\n rewardId: string,\n ticketNumber: string,\n ): Promise<void> {\n const key = `${rallyId}:${userId}:${rewardId}`;\n const count = this.#claims.get(key) ?? 0;\n if (count <= 1) this.#claims.delete(key);\n else this.#claims.set(key, count - 1);\n const index = this.#claimRecords.findIndex(\n (record) =>\n record.rallyId === rallyId &&\n record.userId === userId &&\n record.rewardId === rewardId &&\n record.ticketNumber === ticketNumber,\n );\n if (index >= 0) this.#claimRecords.splice(index, 1);\n }\n async incrementUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<void> {\n const key = `${rallyId}:${userId}:${rewardId}`;\n this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);\n }\n getClaimRecords(): ReadonlyArray<UserClaimRecord> {\n return structuredClone(this.#claimRecords);\n }\n /** @deprecated Use recordUserClaim with a ticket number and timestamp. */\n recordClaim(params: UserClaimRecord): Promise<void>;\n recordClaim(rallyId: string, userId: string, rewardId: string): Promise<void>;\n recordClaim(\n paramsOrRallyId: UserClaimRecord | string,\n userId?: string,\n rewardId?: string,\n ): Promise<void> {\n const params: UserClaimRecord =\n typeof paramsOrRallyId === \"string\"\n ? {\n rallyId: paramsOrRallyId,\n userId: userId ?? \"\",\n rewardId: rewardId ?? \"\",\n ticketNumber: \"\",\n timestamp: Date.now(),\n }\n : paramsOrRallyId;\n return this.recordUserClaim(params);\n }\n\n async runTransaction<T>(\n _rallyId: string,\n operation: (transaction: ServerPersistenceAdapter) => Promise<T>,\n ): Promise<T> {\n const snapshot = {\n stocks: new Map(this.#stocks),\n idempotent: new Map(this.#idempotent),\n states: new Map(this.#states),\n claims: new Map(this.#claims),\n claimRecords: structuredClone(this.#claimRecords),\n auditLogs: structuredClone(this.#auditLogs),\n };\n try {\n return await operation(this);\n } catch (error) {\n this.#stocks.clear();\n for (const [key, value] of snapshot.stocks) this.#stocks.set(key, value);\n this.#idempotent.clear();\n for (const [key, value] of snapshot.idempotent)\n this.#idempotent.set(key, structuredClone(value));\n this.#states.clear();\n for (const [key, value] of snapshot.states) this.#states.set(key, structuredClone(value));\n this.#claims.clear();\n for (const [key, value] of snapshot.claims) this.#claims.set(key, value);\n this.#claimRecords.splice(0, this.#claimRecords.length, ...snapshot.claimRecords);\n this.#auditLogs.splice(0, this.#auditLogs.length, ...snapshot.auditLogs);\n throw error;\n }\n }\n}\n","import {\n type AdminRallyConfig,\n type CheckInCondition,\n consumeReward,\n evaluateConditionDetailed,\n reconcileRewardStates,\n type UserRallyState,\n type Validator,\n type VerificationContext,\n} from \"@stamprally/core\";\nimport type {\n AuditLog,\n CheckInRequest,\n CheckInResponse,\n ClaimRewardRequest,\n ServerOptions,\n} from \"./index.js\";\nimport type { ServerPersistenceAdapter } from \"./persistence.js\";\n\ntype ErrorResponse = { readonly ok: false; readonly code: string; readonly message: string };\ntype ClaimResponse =\n | { readonly ok: true; readonly state: UserRallyState; readonly claimTicketNumber?: string }\n | ErrorResponse;\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json; charset=utf-8\" },\n });\n}\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\nfunction requestId(prefix: string): string {\n return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;\n}\nfunction now(options: ServerOptions, requested?: string): string {\n return requested ?? options.now?.() ?? new Date().toISOString();\n}\nfunction initialState(config: AdminRallyConfig, userId: string, timestamp: string): UserRallyState {\n return {\n rallyId: config.id,\n userId,\n records: [],\n rewards: reconcileRewardStates(config.rewards, [], 0, timestamp),\n updatedAt: timestamp,\n };\n}\nfunction getProof(context: VerificationContext): unknown {\n return context.type === \"qr\"\n ? context.token\n : context.type === \"passcode\"\n ? context.code\n : context.type === \"gps\"\n ? { latitude: context.latitude, longitude: context.longitude }\n : context.type === \"nfc\"\n ? context.tagId\n : context.value;\n}\nasync function evaluate(\n condition: CheckInCondition,\n context: VerificationContext,\n validator: Validator | undefined,\n base: { rallyId: string; spotId: string; state: UserRallyState },\n): Promise<boolean> {\n if (condition.type !== \"custom\") return evaluateConditionDetailed(condition, context).ok;\n if (validator === undefined) return false;\n const validationContext = {\n rallyId: base.rallyId,\n spotId: base.spotId,\n proofData: getProof(context),\n condition,\n userState: base.state,\n };\n const result =\n typeof validator === \"function\"\n ? await validator(validationContext)\n : await validator.validate(validationContext);\n return result === true || (typeof result === \"object\" && result.valid);\n}\nfunction audit(\n rallyId: string,\n userId: string,\n action: AuditLog[\"action\"],\n resourceId: string,\n key: string,\n status: AuditLog[\"status\"],\n timestamp: string,\n code?: string,\n): AuditLog {\n return {\n id: requestId(\"audit\"),\n timestamp,\n rallyId,\n userId,\n action,\n resourceId,\n status,\n idempotencyKey: key,\n ...(code === undefined ? {} : { metadata: { errorCode: code } }),\n };\n}\n\nexport class StampRallyServer {\n readonly #config: AdminRallyConfig;\n readonly #persistence: ServerPersistenceAdapter;\n readonly #options: ServerOptions;\n constructor(\n config: AdminRallyConfig,\n persistence: ServerPersistenceAdapter,\n options: ServerOptions = {},\n ) {\n this.#config = config;\n this.#persistence = persistence;\n this.#options = options;\n }\n async handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\")\n return json({ ok: false, code: \"METHOD_NOT_ALLOWED\", message: \"POST is required.\" }, 405);\n const path = new URL(request.url).pathname;\n if (path.endsWith(\"/check-in\")) return this.handleCheckIn(request);\n if (path.endsWith(\"/claim-reward\")) return this.handleClaimReward(request);\n if (path.endsWith(\"/sync\")) return this.handleSync(request);\n return json({ ok: false, code: \"NOT_FOUND\", message: \"Route not found.\" }, 404);\n }\n async handleCheckIn(request: Request): Promise<Response> {\n const body = await this.#body<CheckInRequest>(request);\n const userId = await this.#user(request, body?.userId);\n if (\n body === null ||\n userId === null ||\n body.rallyId !== this.#config.id ||\n body.spotId === \"\" ||\n body.idempotencyKey === \"\" ||\n body.context === undefined\n )\n return json(\n {\n ok: false,\n code: \"INVALID_REQUEST\",\n message: \"rallyId, spotId, context, and idempotencyKey are required.\",\n },\n 400,\n );\n const result = await this.checkIn({ ...body, userId });\n return json(result, result.ok ? 200 : result.code === \"SPOT_NOT_FOUND\" ? 404 : 422);\n }\n async handleClaimReward(request: Request): Promise<Response> {\n const body = await this.#body<ClaimRewardRequest>(request);\n const userId = await this.#user(request, body?.userId);\n if (\n body === null ||\n userId === null ||\n body.rallyId !== this.#config.id ||\n body.rewardId === \"\" ||\n body.idempotencyKey === \"\"\n )\n return json(\n {\n ok: false,\n code: \"INVALID_REQUEST\",\n message: \"rallyId, rewardId, and idempotencyKey are required.\",\n },\n 400,\n );\n const result = await this.claimReward({ ...body, userId });\n return json(result, result.ok ? 200 : result.code === \"REWARD_NOT_FOUND\" ? 404 : 422);\n }\n async handleSync(request: Request): Promise<Response> {\n const body = await this.#body<{ readonly rallyId: string; readonly userId?: string }>(request);\n const userId = await this.#user(request, body?.userId);\n if (body === null || userId === null || body.rallyId !== this.#config.id)\n return json({ ok: false, code: \"INVALID_REQUEST\", message: \"rallyId is required.\" }, 400);\n return json({ ok: true, state: await this.sync(body.rallyId, userId) });\n }\n async checkIn(request: CheckInRequest & { readonly userId: string }): Promise<CheckInResponse> {\n const key = `check-in:${request.rallyId}:${request.userId}:${request.idempotencyKey}`;\n const previous = await this.#persistence.getIdempotentResult<CheckInResponse>(\n request.rallyId,\n key,\n );\n if (previous !== null) return previous;\n const lockKey = `state:${request.rallyId}:${request.userId}`;\n if (\n !(await this.#persistence.acquireLock(\n request.rallyId,\n lockKey,\n this.#options.lockTtlMs ?? 5_000,\n ))\n )\n return { ok: false, code: \"CONFLICT\", message: \"The user state is being updated.\" };\n const timestamp = now(this.#options, request.now);\n try {\n const spot = this.#config.spots.find((item) => item.id === request.spotId);\n if (spot === undefined)\n return this.#remember(\n key,\n { ok: false, code: \"SPOT_NOT_FOUND\", message: \"Spot was not found.\" },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n const current =\n (await this.#persistence.getUserState(request.rallyId, request.userId)) ??\n initialState(this.#config, request.userId, timestamp);\n if (current.records.some((record) => record.stampId === request.spotId))\n return this.#remember(\n key,\n { ok: false, code: \"STAMP_ALREADY_ACQUIRED\", message: \"Spot was already claimed.\" },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n const acquired = new Set(current.records.map((record) => record.stampId));\n if (spot.prerequisites?.some((id) => !acquired.has(id)))\n return this.#remember(\n key,\n {\n ok: false,\n code: \"PREREQUISITES_NOT_MET\",\n message: \"Prerequisite spots are not complete.\",\n },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n for (const condition of spot.conditions)\n if (\n !(await evaluate(\n condition,\n request.context,\n condition.type === \"custom\"\n ? this.#options.customValidators?.[condition.validatorName]\n : undefined,\n { rallyId: request.rallyId, spotId: request.spotId, state: current },\n ))\n )\n return this.#remember(\n key,\n { ok: false, code: \"INVALID_PROOF\", message: \"Verification failed.\" },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n const next: UserRallyState = {\n ...current,\n records: [...current.records, { stampId: request.spotId, acquiredAt: timestamp }],\n rewards: reconcileRewardStates(\n this.#config.rewards,\n current.rewards,\n current.records.length + 1,\n timestamp,\n ),\n updatedAt: timestamp,\n };\n await this.#persistence.saveUserState(request.rallyId, request.userId, next);\n return this.#remember(\n key,\n { ok: true, state: next },\n request.rallyId,\n request.userId,\n \"CHECK_IN\",\n request.spotId,\n request.idempotencyKey,\n timestamp,\n );\n } finally {\n await this.#persistence.releaseLock(request.rallyId, lockKey);\n }\n }\n async claimReward(\n request: ClaimRewardRequest & { readonly userId: string },\n ): Promise<ClaimResponse> {\n if (this.#persistence.runTransaction !== undefined)\n return this.#persistence.runTransaction(request.rallyId, (transaction) =>\n this.#claimRewardMutation(request, transaction),\n );\n return this.#claimRewardMutation(request, this.#persistence);\n }\n async #claimRewardMutation(\n request: ClaimRewardRequest & { readonly userId: string },\n persistence: ServerPersistenceAdapter,\n ): Promise<ClaimResponse> {\n const key = `claim:${request.rallyId}:${request.userId}:${request.rewardId}:${request.idempotencyKey}`;\n const previous = await persistence.getIdempotentResult<ClaimResponse>(request.rallyId, key);\n if (previous !== null) return previous;\n const reward = this.#config.rewards.find((item) => item.id === request.rewardId);\n if (reward === undefined)\n return this.#rememberClaim(\n key,\n { ok: false, code: \"REWARD_NOT_FOUND\", message: \"Reward was not found.\" },\n request,\n now(this.#options, request.now),\n persistence,\n );\n const lockKey = `reward:${request.rallyId}:${reward.id}`;\n if (\n !(await persistence.acquireLock(request.rallyId, lockKey, this.#options.lockTtlMs ?? 5_000))\n )\n return { ok: false, code: \"CONFLICT\", message: \"The reward is being claimed.\" };\n const timestamp = now(this.#options, request.now);\n let decremented = false;\n let previousState: UserRallyState | null = null;\n let recordedTicket: string | null = null;\n let successAuditId: string | null = null;\n try {\n const checked = await persistence.getIdempotentResult<ClaimResponse>(request.rallyId, key);\n if (checked !== null) return checked;\n const storedState = await persistence.getUserState(request.rallyId, request.userId);\n previousState = storedState;\n const current = storedState ?? initialState(this.#config, request.userId, timestamp);\n const claimCount = await persistence.getUserClaimCount(\n request.rallyId,\n request.userId,\n reward.id,\n );\n const storedReward = current.rewards.find((item) => item.rewardId === reward.id) ?? {\n rewardId: reward.id,\n status: \"LOCKED\" as const,\n };\n const currentReward =\n reward.redemptionMethod === \"server_claim\" &&\n storedReward.status === \"CONSUMED\" &&\n (reward.userClaimLimit === undefined || claimCount < reward.userClaimLimit)\n ? { ...storedReward, status: \"AVAILABLE\" as const }\n : storedReward;\n const local = consumeReward({\n reward,\n currentState: currentReward,\n now: timestamp,\n userRedemptionCount: claimCount,\n ...(request.staffPasscode === undefined ? {} : { inputPasscode: request.staffPasscode }),\n ...(request.staffId === undefined ? {} : { staffId: request.staffId }),\n });\n if (!local.ok)\n return this.#rememberClaim(\n key,\n { ok: false, code: local.error.code, message: \"Reward cannot be claimed.\" },\n request,\n timestamp,\n persistence,\n );\n const stock = await persistence.decrementRewardStock(request.rallyId, reward.id);\n if (!stock.success)\n return this.#rememberClaim(\n key,\n { ok: false, code: \"OUT_OF_STOCK\", message: \"Reward is out of stock.\" },\n request,\n timestamp,\n persistence,\n );\n decremented = true;\n const next: UserRallyState = {\n ...current,\n rewards: current.rewards.map((item) => (item.rewardId === reward.id ? local.value : item)),\n updatedAt: timestamp,\n };\n try {\n await persistence.saveUserState(request.rallyId, request.userId, next);\n const response: ClaimResponse =\n local.value.claimTicketNumber === undefined\n ? { ok: true, state: next }\n : { ok: true, state: next, claimTicketNumber: local.value.claimTicketNumber };\n recordedTicket = local.value.claimTicketNumber ?? \"\";\n await persistence.recordUserClaim({\n rallyId: request.rallyId,\n userId: request.userId,\n rewardId: reward.id,\n ticketNumber: recordedTicket,\n timestamp: Number.isNaN(Date.parse(timestamp)) ? Date.now() : Date.parse(timestamp),\n });\n const successAudit = audit(\n request.rallyId,\n request.userId,\n \"CLAIM_REWARD\",\n reward.id,\n request.idempotencyKey,\n \"SUCCESS\",\n timestamp,\n );\n successAuditId = successAudit.id;\n await persistence.recordAuditLog(successAudit);\n await persistence.saveIdempotentResult(\n request.rallyId,\n key,\n response,\n this.#options.idempotencyTtlMs ?? 86_400_000,\n );\n return response;\n } catch (error) {\n if (recordedTicket !== null && persistence.rollbackUserClaim !== undefined)\n await persistence.rollbackUserClaim(\n request.rallyId,\n request.userId,\n reward.id,\n recordedTicket,\n );\n if (persistence.rollbackUserState !== undefined)\n await persistence.rollbackUserState(request.rallyId, request.userId, previousState);\n if (successAuditId !== null && persistence.removeAuditLog !== undefined)\n await persistence.removeAuditLog(successAuditId);\n await this.#restoreRewardStock(persistence, request.rallyId, reward.id);\n decremented = false;\n throw error;\n }\n } catch (error) {\n if (decremented) await this.#restoreRewardStock(persistence, request.rallyId, reward.id);\n return {\n ok: false,\n code: \"PERSISTENCE_FAILED\",\n message: error instanceof Error ? error.message : \"Reward claim failed.\",\n };\n } finally {\n await persistence.releaseLock(request.rallyId, lockKey);\n }\n }\n async sync(rallyId: string, userId: string): Promise<UserRallyState> {\n return (\n (await this.#persistence.getUserState(rallyId, userId)) ??\n initialState(this.#config, userId, now(this.#options))\n );\n }\n async #body<T>(request: Request): Promise<T | null> {\n try {\n const value: unknown = await request.json();\n return isObject(value) ? (value as T) : null;\n } catch {\n return null;\n }\n }\n async #user(request: Request, requested: string | undefined): Promise<string | null> {\n if (this.#options.authenticate !== undefined)\n return (await this.#options.authenticate(request)) ?? null;\n return requested ?? null;\n }\n async #remember(\n key: string,\n result: CheckInResponse,\n rallyId: string,\n userId: string,\n action: AuditLog[\"action\"],\n resourceId: string,\n idempotencyKey: string,\n timestamp: string,\n ): Promise<CheckInResponse> {\n await this.#persistence.recordAuditLog(\n audit(\n rallyId,\n userId,\n action,\n resourceId,\n idempotencyKey,\n result.ok ? \"SUCCESS\" : \"REJECTED\",\n timestamp,\n result.ok ? undefined : result.code,\n ),\n );\n await this.#persistence.saveIdempotentResult(\n rallyId,\n key,\n result,\n this.#options.idempotencyTtlMs ?? 86_400_000,\n );\n return result;\n }\n async #rememberClaim(\n key: string,\n result: ClaimResponse,\n request: ClaimRewardRequest & { readonly userId: string },\n timestamp: string,\n persistence: ServerPersistenceAdapter = this.#persistence,\n ): Promise<ClaimResponse> {\n await persistence.recordAuditLog(\n audit(\n request.rallyId,\n request.userId,\n \"CLAIM_REWARD\",\n request.rewardId,\n request.idempotencyKey,\n \"REJECTED\",\n timestamp,\n result.ok ? undefined : result.code,\n ),\n );\n await persistence.saveIdempotentResult(\n request.rallyId,\n key,\n result,\n this.#options.idempotencyTtlMs ?? 86_400_000,\n );\n return result;\n }\n async #restoreRewardStock(\n persistence: ServerPersistenceAdapter,\n rallyId: string,\n rewardId: string,\n ): Promise<void> {\n if (persistence.restoreRewardStock !== undefined)\n await persistence.restoreRewardStock(rallyId, rewardId);\n }\n}\n"]}
|