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