@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/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 (
|
|
119
|
-
return { success: false, 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
|
-
(
|
|
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 (
|
|
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
|
|
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
|
|
234
|
-
return
|
|
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(
|
|
246
|
-
return
|
|
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,
|
|
249
|
-
if (condition.type !== "custom") return core.evaluateConditionDetailed(condition,
|
|
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(
|
|
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
|
-
|
|
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
|
|
305
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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((
|
|
525
|
+
const acquired = new Set(current.records.map((record2) => record2.stampId));
|
|
370
526
|
if (spot.prerequisites?.some((id) => !acquired.has(id)))
|
|
371
|
-
return this.#
|
|
527
|
+
return await this.#rememberCheckInTransaction(
|
|
528
|
+
request,
|
|
529
|
+
timestamp,
|
|
372
530
|
key,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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.#
|
|
542
|
+
return await this.#rememberCheckInTransaction(
|
|
543
|
+
request,
|
|
544
|
+
timestamp,
|
|
393
545
|
key,
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
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
|
|
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 (
|
|
714
|
+
} catch (error2) {
|
|
553
715
|
return {
|
|
554
716
|
ok: false,
|
|
555
717
|
code: "PERSISTENCE_FAILED",
|
|
556
|
-
message:
|
|
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
|
|
574
|
-
if (this.#options.authenticate !== void 0)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|