@stamprally/server 0.5.1
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/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/index.cjs +375 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +91 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +372 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nitta-a
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @stamprally/server
|
|
2
|
+
|
|
3
|
+
Web Standard `Request` / `Response` handlers for server-authoritative stamp rally
|
|
4
|
+
check-ins, reward claims, and offline synchronization.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { StampRallyServer } from "@stamprally/server";
|
|
8
|
+
|
|
9
|
+
const server = new StampRallyServer(
|
|
10
|
+
{
|
|
11
|
+
id: "spring-rally",
|
|
12
|
+
secretKey: process.env.RALLY_SECRET ?? "replace-me",
|
|
13
|
+
stamps: [{ id: "gate", name: { en: "Gate" }, condition: { type: "instant" } }],
|
|
14
|
+
},
|
|
15
|
+
storage,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const response = await server.handle(request);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Provide a Redis-backed `ServerStorageAdapter` in production. Its
|
|
22
|
+
`decrementRewardStock` implementation should use a single atomic Redis
|
|
23
|
+
operation (for example, a Lua script or `DECR` guarded by a non-negative check).
|
|
24
|
+
The built-in `InMemoryServerStorage` is intended for tests and local demos.
|
|
25
|
+
|
|
26
|
+
If authentication is already handled by the host application, use
|
|
27
|
+
`authenticate` to return the authenticated user ID. The handler rejects bodies
|
|
28
|
+
that attempt to act for a different user.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@stamprally/core');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var InMemoryServerStorage = class {
|
|
7
|
+
#states = /* @__PURE__ */ new Map();
|
|
8
|
+
#stocks;
|
|
9
|
+
#claims = /* @__PURE__ */ new Map();
|
|
10
|
+
#auditLogs = [];
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.#stocks = new Map(Object.entries(options.stocks ?? {}));
|
|
13
|
+
}
|
|
14
|
+
async getRewardStock(rewardId) {
|
|
15
|
+
return this.#stocks.get(rewardId) ?? null;
|
|
16
|
+
}
|
|
17
|
+
async decrementRewardStock(rewardId) {
|
|
18
|
+
const stock = this.#stocks.get(rewardId);
|
|
19
|
+
if (stock === void 0) return true;
|
|
20
|
+
if (stock <= 0) return false;
|
|
21
|
+
this.#stocks.set(rewardId, stock - 1);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
async getUserClaims(userId, rewardId) {
|
|
25
|
+
return this.#claims.get(`${userId}:${rewardId}`) ?? 0;
|
|
26
|
+
}
|
|
27
|
+
async recordAuditLog(log) {
|
|
28
|
+
this.#auditLogs.push({ ...log });
|
|
29
|
+
}
|
|
30
|
+
async saveUserState(userId, state) {
|
|
31
|
+
this.#states.set(userId, cloneUserState(state));
|
|
32
|
+
}
|
|
33
|
+
async getUserState(userId) {
|
|
34
|
+
const state = this.#states.get(userId);
|
|
35
|
+
return state === void 0 ? null : cloneUserState(state);
|
|
36
|
+
}
|
|
37
|
+
getAuditLogs() {
|
|
38
|
+
return this.#auditLogs.map((log) => ({ ...log }));
|
|
39
|
+
}
|
|
40
|
+
recordClaim(userId, rewardId) {
|
|
41
|
+
const key = `${userId}:${rewardId}`;
|
|
42
|
+
this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function cloneUserState(state) {
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
records: state.records.map((record) => ({
|
|
49
|
+
...record,
|
|
50
|
+
...record.metadata === void 0 ? {} : { metadata: { ...record.metadata } }
|
|
51
|
+
})),
|
|
52
|
+
...state.rewards === void 0 ? {} : { rewards: state.rewards.map((reward) => ({ ...reward })) }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function jsonResponse(body, status = 200) {
|
|
56
|
+
return new Response(JSON.stringify(body), {
|
|
57
|
+
status,
|
|
58
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function errorResponse(code, message, status) {
|
|
62
|
+
return jsonResponse({ ok: false, error: { code, message } }, status);
|
|
63
|
+
}
|
|
64
|
+
function isObject(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
function contextForClaim(method, proofData) {
|
|
68
|
+
if (method === "token" || method === "qr" || method === "passcode") {
|
|
69
|
+
return {
|
|
70
|
+
type: "token",
|
|
71
|
+
token: isObject(proofData) && typeof proofData.token === "string" ? proofData.token : String(proofData ?? "")
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (method === "geo" || method === "geolocation") {
|
|
75
|
+
const value = isObject(proofData) ? proofData : {};
|
|
76
|
+
return {
|
|
77
|
+
type: "geo",
|
|
78
|
+
currentLatitude: typeof value.latitude === "number" ? value.latitude : typeof value.currentLatitude === "number" ? value.currentLatitude : Number.NaN,
|
|
79
|
+
currentLongitude: typeof value.longitude === "number" ? value.longitude : typeof value.currentLongitude === "number" ? value.currentLongitude : Number.NaN
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { type: "instant" };
|
|
83
|
+
}
|
|
84
|
+
function now() {
|
|
85
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
86
|
+
}
|
|
87
|
+
function hasText(value) {
|
|
88
|
+
return typeof value === "string" && value.trim() !== "";
|
|
89
|
+
}
|
|
90
|
+
function safeProofData(value) {
|
|
91
|
+
return isObject(value) && typeof value.token === "string" ? { type: "token" } : value;
|
|
92
|
+
}
|
|
93
|
+
function proofFromContext(context) {
|
|
94
|
+
if (context?.type === "token") return { token: context.token };
|
|
95
|
+
if (context?.type === "geo") {
|
|
96
|
+
return { latitude: context.currentLatitude, longitude: context.currentLongitude };
|
|
97
|
+
}
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
function id(prefix) {
|
|
101
|
+
return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
|
|
102
|
+
}
|
|
103
|
+
var StampRallyServer = class {
|
|
104
|
+
#config;
|
|
105
|
+
#storage;
|
|
106
|
+
#idempotent = /* @__PURE__ */ new Map();
|
|
107
|
+
#claims = /* @__PURE__ */ new Map();
|
|
108
|
+
#queue = Promise.resolve();
|
|
109
|
+
constructor(config, storage) {
|
|
110
|
+
this.#config = config;
|
|
111
|
+
this.#storage = storage;
|
|
112
|
+
}
|
|
113
|
+
handle(request) {
|
|
114
|
+
const path = new URL(request.url).pathname;
|
|
115
|
+
if (request.method !== "POST")
|
|
116
|
+
return Promise.resolve(errorResponse("METHOD_NOT_ALLOWED", "POST is required.", 405));
|
|
117
|
+
if (path.endsWith("/check-in")) return this.verifyCheckIn(request);
|
|
118
|
+
if (path.endsWith("/claim-reward")) return this.claimReward(request);
|
|
119
|
+
if (path.endsWith("/sync")) return this.syncProgress(request);
|
|
120
|
+
return Promise.resolve(errorResponse("NOT_FOUND", "Route not found.", 404));
|
|
121
|
+
}
|
|
122
|
+
verifyCheckIn(request) {
|
|
123
|
+
return this.#enqueue(() => this.#verifyCheckIn(request));
|
|
124
|
+
}
|
|
125
|
+
claimReward(request) {
|
|
126
|
+
return this.#enqueue(() => this.#claimReward(request));
|
|
127
|
+
}
|
|
128
|
+
syncProgress(request) {
|
|
129
|
+
return this.#enqueue(() => this.#syncProgress(request));
|
|
130
|
+
}
|
|
131
|
+
#enqueue(operation) {
|
|
132
|
+
const next = this.#queue.then(operation, operation);
|
|
133
|
+
this.#queue = next.then(
|
|
134
|
+
() => void 0,
|
|
135
|
+
() => void 0
|
|
136
|
+
);
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
139
|
+
async #authenticate(request) {
|
|
140
|
+
return this.#config.authenticate === void 0 ? null : this.#config.authenticate(request);
|
|
141
|
+
}
|
|
142
|
+
async #parse(request) {
|
|
143
|
+
try {
|
|
144
|
+
const value = await request.json();
|
|
145
|
+
return isObject(value) ? value : null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async #verifyCheckIn(request) {
|
|
151
|
+
const authenticatedUser = await this.#authenticate(request);
|
|
152
|
+
const body = await this.#parse(request);
|
|
153
|
+
const userId = body?.userId ?? authenticatedUser;
|
|
154
|
+
if (userId === null || userId === void 0 || body === null || !hasText(userId) || !hasText(body.spotId) || !hasText(body.claimMethod) || !hasText(body.idempotencyKey)) {
|
|
155
|
+
return errorResponse(
|
|
156
|
+
"INVALID_REQUEST",
|
|
157
|
+
"userId, spotId, and idempotencyKey are required.",
|
|
158
|
+
400
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (authenticatedUser !== null && authenticatedUser !== userId)
|
|
162
|
+
return errorResponse("UNAUTHORIZED", "User identity does not match authentication.", 401);
|
|
163
|
+
const key = `check-in:${userId}:${body.idempotencyKey}`;
|
|
164
|
+
const previous = this.#idempotent.get(key);
|
|
165
|
+
if (previous !== void 0) return previous.clone();
|
|
166
|
+
const timestamp = now();
|
|
167
|
+
const current = await this.#storage.getUserState(userId) ?? emptyState(this.#config, timestamp);
|
|
168
|
+
const result = core.processStamp(
|
|
169
|
+
current,
|
|
170
|
+
this.#config,
|
|
171
|
+
body.spotId,
|
|
172
|
+
contextForClaim(body.claimMethod, body.proofData),
|
|
173
|
+
timestamp
|
|
174
|
+
);
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
await this.#audit(
|
|
177
|
+
userId,
|
|
178
|
+
"CHECK_IN",
|
|
179
|
+
body.spotId,
|
|
180
|
+
body.idempotencyKey,
|
|
181
|
+
"REJECTED",
|
|
182
|
+
safeProofData(body.proofData),
|
|
183
|
+
result.error
|
|
184
|
+
);
|
|
185
|
+
return this.#remember(key, jsonResponse({ ok: false, error: result.error }, 422));
|
|
186
|
+
}
|
|
187
|
+
const ttl = this.#config.proofTtlSeconds ?? 3600;
|
|
188
|
+
const token = await core.createSecureToken(
|
|
189
|
+
{
|
|
190
|
+
type: "stamp_claim",
|
|
191
|
+
rallyId: this.#config.id,
|
|
192
|
+
userId,
|
|
193
|
+
spotId: body.spotId,
|
|
194
|
+
acquiredAt: timestamp,
|
|
195
|
+
exp: Math.floor(Date.now() / 1e3) + ttl
|
|
196
|
+
},
|
|
197
|
+
this.#config.secretKey,
|
|
198
|
+
{ encrypt: true }
|
|
199
|
+
);
|
|
200
|
+
await this.#storage.saveUserState(userId, result.value.nextState);
|
|
201
|
+
await this.#audit(
|
|
202
|
+
userId,
|
|
203
|
+
"CHECK_IN",
|
|
204
|
+
body.spotId,
|
|
205
|
+
body.idempotencyKey,
|
|
206
|
+
"SUCCESS",
|
|
207
|
+
safeProofData(body.proofData)
|
|
208
|
+
);
|
|
209
|
+
return this.#remember(
|
|
210
|
+
key,
|
|
211
|
+
jsonResponse({
|
|
212
|
+
ok: true,
|
|
213
|
+
state: result.value.nextState,
|
|
214
|
+
proof: {
|
|
215
|
+
token,
|
|
216
|
+
rallyId: this.#config.id,
|
|
217
|
+
userId,
|
|
218
|
+
spotId: body.spotId,
|
|
219
|
+
acquiredAt: timestamp
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
async #claimReward(request) {
|
|
225
|
+
const authenticatedUser = await this.#authenticate(request);
|
|
226
|
+
const body = await this.#parse(request);
|
|
227
|
+
const userId = body?.userId ?? authenticatedUser;
|
|
228
|
+
if (userId === null || userId === void 0 || body === null || !hasText(userId) || !hasText(body.rewardId) || !hasText(body.idempotencyKey))
|
|
229
|
+
return errorResponse(
|
|
230
|
+
"INVALID_REQUEST",
|
|
231
|
+
"userId, rewardId, and idempotencyKey are required.",
|
|
232
|
+
400
|
|
233
|
+
);
|
|
234
|
+
if (authenticatedUser !== null && authenticatedUser !== userId)
|
|
235
|
+
return errorResponse("UNAUTHORIZED", "User identity does not match authentication.", 401);
|
|
236
|
+
const key = `claim-reward:${userId}:${body.rewardId}:${body.idempotencyKey}`;
|
|
237
|
+
const previous = this.#idempotent.get(key);
|
|
238
|
+
if (previous !== void 0) return previous.clone();
|
|
239
|
+
const reward = this.#config.rewards?.find((item) => item.id === body.rewardId);
|
|
240
|
+
if (reward === void 0) {
|
|
241
|
+
await this.#audit(userId, "CLAIM_REWARD", body.rewardId, body.idempotencyKey, "REJECTED");
|
|
242
|
+
return this.#remember(key, errorResponse("REWARD_NOT_FOUND", "Reward was not found.", 404));
|
|
243
|
+
}
|
|
244
|
+
const timestamp = now();
|
|
245
|
+
const current = await this.#storage.getUserState(userId) ?? emptyState(this.#config, timestamp);
|
|
246
|
+
const rewardState = current.rewards?.find((item) => item.rewardId === reward.id);
|
|
247
|
+
if (rewardState === void 0) {
|
|
248
|
+
await this.#audit(userId, "CLAIM_REWARD", reward.id, body.idempotencyKey, "REJECTED");
|
|
249
|
+
return this.#remember(key, errorResponse("NOT_AVAILABLE", "Reward is not available.", 422));
|
|
250
|
+
}
|
|
251
|
+
const userClaims = Math.max(
|
|
252
|
+
await this.#storage.getUserClaims(userId, reward.id),
|
|
253
|
+
this.#claims.get(`${userId}:${reward.id}`) ?? 0
|
|
254
|
+
);
|
|
255
|
+
const stock = await this.#storage.getRewardStock(reward.id);
|
|
256
|
+
const userLimit = reward.userClaimLimit ?? reward.limitPerUser;
|
|
257
|
+
const canReclaimServerReward = reward.redemptionMethod === "server_claim" && (userLimit === void 0 || userClaims < userLimit) && (stock === null || stock > 0);
|
|
258
|
+
const claimableState = canReclaimServerReward && rewardState.status === "CONSUMED" ? { ...rewardState, status: "AVAILABLE" } : rewardState;
|
|
259
|
+
const local = core.consumeReward({
|
|
260
|
+
reward,
|
|
261
|
+
currentState: claimableState,
|
|
262
|
+
now: timestamp,
|
|
263
|
+
...body.staffPasscode === void 0 ? {} : { inputPasscode: body.staffPasscode },
|
|
264
|
+
...body.staffId === void 0 ? {} : { staffId: body.staffId },
|
|
265
|
+
userId,
|
|
266
|
+
userRedemptionCount: userClaims
|
|
267
|
+
});
|
|
268
|
+
if (!local.ok)
|
|
269
|
+
return this.#remember(
|
|
270
|
+
key,
|
|
271
|
+
await this.#rewardError(userId, reward.id, body.idempotencyKey, local.error)
|
|
272
|
+
);
|
|
273
|
+
if (stock !== null && !await this.#storage.decrementRewardStock(reward.id))
|
|
274
|
+
return this.#remember(
|
|
275
|
+
key,
|
|
276
|
+
await this.#rewardError(userId, reward.id, body.idempotencyKey, {
|
|
277
|
+
code: "OUT_OF_STOCK",
|
|
278
|
+
rewardId: reward.id
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
const nextState = {
|
|
282
|
+
...current,
|
|
283
|
+
rewards: (current.rewards ?? []).map(
|
|
284
|
+
(item) => item.rewardId === reward.id ? local.value : item
|
|
285
|
+
),
|
|
286
|
+
updatedAt: timestamp
|
|
287
|
+
};
|
|
288
|
+
await this.#storage.saveUserState(userId, nextState);
|
|
289
|
+
const claimKey = `${userId}:${reward.id}`;
|
|
290
|
+
this.#claims.set(claimKey, userClaims + 1);
|
|
291
|
+
if (this.#storage instanceof InMemoryServerStorage)
|
|
292
|
+
this.#storage.recordClaim(userId, reward.id);
|
|
293
|
+
await this.#audit(userId, "CLAIM_REWARD", reward.id, body.idempotencyKey, "SUCCESS", {
|
|
294
|
+
staffId: body.staffId
|
|
295
|
+
});
|
|
296
|
+
return this.#remember(
|
|
297
|
+
key,
|
|
298
|
+
jsonResponse({
|
|
299
|
+
ok: true,
|
|
300
|
+
state: nextState,
|
|
301
|
+
claimTicketNumber: local.value.claimTicketNumber
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
async #rewardError(userId, rewardId, key, error) {
|
|
306
|
+
await this.#audit(userId, "CLAIM_REWARD", rewardId, key, "REJECTED", void 0, error);
|
|
307
|
+
return jsonResponse({ ok: false, error }, 422);
|
|
308
|
+
}
|
|
309
|
+
async #syncProgress(request) {
|
|
310
|
+
const authenticatedUser = await this.#authenticate(request);
|
|
311
|
+
const body = await this.#parse(request);
|
|
312
|
+
if (body === null || body.userId === void 0)
|
|
313
|
+
return errorResponse("INVALID_REQUEST", "userId is required.", 400);
|
|
314
|
+
if (authenticatedUser !== null && authenticatedUser !== body.userId)
|
|
315
|
+
return errorResponse("UNAUTHORIZED", "User identity does not match authentication.", 401);
|
|
316
|
+
const queue = body.queue ?? body.operations ?? [];
|
|
317
|
+
for (const operation of queue) {
|
|
318
|
+
const userId = operation.userId ?? body.userId;
|
|
319
|
+
const spotId = operation.spotId ?? operation.stampId;
|
|
320
|
+
const claimMethod = operation.claimMethod ?? operation.context?.type;
|
|
321
|
+
if (!hasText(userId) || !hasText(spotId) || !hasText(claimMethod)) continue;
|
|
322
|
+
const synthetic = new Request(new URL("/api/check-in", request.url), {
|
|
323
|
+
method: "POST",
|
|
324
|
+
body: JSON.stringify({
|
|
325
|
+
userId,
|
|
326
|
+
spotId,
|
|
327
|
+
claimMethod,
|
|
328
|
+
proofData: operation.proofData ?? proofFromContext(operation.context),
|
|
329
|
+
idempotencyKey: operation.idempotencyKey
|
|
330
|
+
}),
|
|
331
|
+
headers: { "content-type": "application/json" }
|
|
332
|
+
});
|
|
333
|
+
await this.#verifyCheckIn(synthetic);
|
|
334
|
+
}
|
|
335
|
+
const timestamp = now();
|
|
336
|
+
const state = await this.#storage.getUserState(body.userId) ?? emptyState(this.#config, timestamp);
|
|
337
|
+
await this.#storage.saveUserState(body.userId, state);
|
|
338
|
+
return jsonResponse({ ok: true, state, accepted: queue.length });
|
|
339
|
+
}
|
|
340
|
+
async #audit(userId, action, resourceId, idempotencyKey, status, proofData, error) {
|
|
341
|
+
await this.#storage.recordAuditLog({
|
|
342
|
+
id: id("audit"),
|
|
343
|
+
timestamp: now(),
|
|
344
|
+
rallyId: this.#config.id,
|
|
345
|
+
userId,
|
|
346
|
+
action,
|
|
347
|
+
resourceId,
|
|
348
|
+
status,
|
|
349
|
+
idempotencyKey,
|
|
350
|
+
...proofData === void 0 ? {} : { proofData },
|
|
351
|
+
...error === void 0 ? {} : {
|
|
352
|
+
metadata: {
|
|
353
|
+
errorCode: isObject(error) && typeof error.code === "string" ? error.code : "UNKNOWN"
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
#remember(key, response) {
|
|
359
|
+
this.#idempotent.set(key, response.clone());
|
|
360
|
+
return response;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
function emptyState(config, timestamp) {
|
|
364
|
+
return {
|
|
365
|
+
rallyId: config.id,
|
|
366
|
+
records: [],
|
|
367
|
+
...config.rewards === void 0 ? {} : { rewards: core.reconcileRewardStates(config.rewards, [], 0, timestamp) },
|
|
368
|
+
updatedAt: timestamp
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
exports.InMemoryServerStorage = InMemoryServerStorage;
|
|
373
|
+
exports.StampRallyServer = StampRallyServer;
|
|
374
|
+
//# sourceMappingURL=index.cjs.map
|
|
375
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["processStamp","createSecureToken","consumeReward","reconcileRewardStates"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n consumeReward,\n createSecureToken,\n processStamp,\n type RallyConfig,\n type RewardConsumeError,\n reconcileRewardStates,\n type SecureTokenSecretKey,\n type StampError,\n type StampRallyState,\n type StampRecord,\n type VerificationContext,\n} from \"@stamprally/core\";\n\nexport interface UserRallyState extends StampRallyState {\n readonly userId?: string;\n}\n\nexport interface RallyAuditLog {\n readonly id: string;\n readonly timestamp: string;\n readonly rallyId: string;\n readonly userId: string;\n readonly action: \"CHECK_IN\" | \"CLAIM_REWARD\";\n readonly resourceId: string;\n readonly status: \"SUCCESS\" | \"REJECTED\";\n readonly idempotencyKey: string;\n readonly proofData?: unknown;\n readonly metadata?: Readonly<Record<string, unknown>>;\n}\n\nexport interface ServerStorageAdapter {\n getRewardStock(rewardId: string): Promise<number | null>;\n decrementRewardStock(rewardId: string): Promise<boolean>;\n getUserClaims(userId: string, rewardId: string): Promise<number>;\n recordAuditLog(log: RallyAuditLog): Promise<void>;\n saveUserState(userId: string, state: UserRallyState): Promise<void>;\n getUserState(userId: string): Promise<UserRallyState | null>;\n}\n\nexport interface AdminRallyConfig extends RallyConfig {\n readonly secretKey: SecureTokenSecretKey;\n readonly proofTtlSeconds?: number;\n readonly authenticate?: (request: Request) => Promise<string | null> | string | null;\n}\n\nexport interface CheckInRequest {\n readonly userId: string;\n readonly spotId: string;\n readonly claimMethod: string;\n readonly proofData?: unknown;\n readonly idempotencyKey: string;\n}\n\nexport interface ClaimRewardRequest {\n readonly userId: string;\n readonly rewardId: string;\n readonly staffPasscode?: string;\n readonly idempotencyKey: string;\n readonly staffId?: string;\n}\n\nexport interface SyncRequest {\n readonly userId: string;\n readonly queue?: ReadonlyArray<SyncCheckInOperation>;\n readonly operations?: ReadonlyArray<SyncCheckInOperation>;\n}\n\nexport interface SyncCheckInOperation {\n readonly userId?: string;\n readonly spotId?: string;\n readonly stampId?: string;\n readonly claimMethod?: string;\n readonly proofData?: unknown;\n readonly context?: VerificationContext;\n readonly idempotencyKey: string;\n}\n\nexport interface StampClaimProof {\n readonly token: string;\n readonly rallyId: string;\n readonly userId: string;\n readonly spotId: string;\n readonly acquiredAt: string;\n}\n\nexport interface InMemoryServerStorageOptions {\n readonly stocks?: Readonly<Record<string, number>>;\n}\n\nexport class InMemoryServerStorage implements ServerStorageAdapter {\n readonly #states = new Map<string, UserRallyState>();\n readonly #stocks: Map<string, number>;\n readonly #claims = new Map<string, number>();\n readonly #auditLogs: RallyAuditLog[] = [];\n\n constructor(options: InMemoryServerStorageOptions = {}) {\n this.#stocks = new Map(Object.entries(options.stocks ?? {}));\n }\n\n async getRewardStock(rewardId: string): Promise<number | null> {\n return this.#stocks.get(rewardId) ?? null;\n }\n\n async decrementRewardStock(rewardId: string): Promise<boolean> {\n const stock = this.#stocks.get(rewardId);\n if (stock === undefined) return true;\n if (stock <= 0) return false;\n this.#stocks.set(rewardId, stock - 1);\n return true;\n }\n\n async getUserClaims(userId: string, rewardId: string): Promise<number> {\n return this.#claims.get(`${userId}:${rewardId}`) ?? 0;\n }\n\n async recordAuditLog(log: RallyAuditLog): Promise<void> {\n this.#auditLogs.push({ ...log });\n }\n\n async saveUserState(userId: string, state: UserRallyState): Promise<void> {\n this.#states.set(userId, cloneUserState(state));\n }\n\n async getUserState(userId: string): Promise<UserRallyState | null> {\n const state = this.#states.get(userId);\n return state === undefined ? null : cloneUserState(state);\n }\n\n getAuditLogs(): ReadonlyArray<RallyAuditLog> {\n return this.#auditLogs.map((log) => ({ ...log }));\n }\n\n recordClaim(userId: string, rewardId: string): void {\n const key = `${userId}:${rewardId}`;\n this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);\n }\n}\n\nfunction cloneUserState(state: UserRallyState): UserRallyState {\n return {\n ...state,\n records: state.records.map((record) => ({\n ...record,\n ...(record.metadata === undefined ? {} : { metadata: { ...record.metadata } }),\n })),\n ...(state.rewards === undefined\n ? {}\n : { rewards: state.rewards.map((reward) => ({ ...reward })) }),\n };\n}\n\nfunction jsonResponse(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}\n\nfunction errorResponse(code: string, message: string, status: number): Response {\n return jsonResponse({ ok: false, error: { code, message } }, status);\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction contextForClaim(method: string, proofData: unknown): VerificationContext {\n if (method === \"token\" || method === \"qr\" || method === \"passcode\") {\n return {\n type: \"token\",\n token:\n isObject(proofData) && typeof proofData.token === \"string\"\n ? proofData.token\n : String(proofData ?? \"\"),\n };\n }\n if (method === \"geo\" || method === \"geolocation\") {\n const value = isObject(proofData) ? proofData : {};\n return {\n type: \"geo\",\n currentLatitude:\n typeof value.latitude === \"number\"\n ? value.latitude\n : typeof value.currentLatitude === \"number\"\n ? value.currentLatitude\n : Number.NaN,\n currentLongitude:\n typeof value.longitude === \"number\"\n ? value.longitude\n : typeof value.currentLongitude === \"number\"\n ? value.currentLongitude\n : Number.NaN,\n };\n }\n return { type: \"instant\" };\n}\n\nfunction now(): string {\n return new Date().toISOString();\n}\n\nfunction hasText(value: unknown): value is string {\n return typeof value === \"string\" && value.trim() !== \"\";\n}\n\nfunction safeProofData(value: unknown): unknown {\n return isObject(value) && typeof value.token === \"string\" ? { type: \"token\" } : value;\n}\n\nfunction proofFromContext(context: VerificationContext | undefined): unknown {\n if (context?.type === \"token\") return { token: context.token };\n if (context?.type === \"geo\") {\n return { latitude: context.currentLatitude, longitude: context.currentLongitude };\n }\n return undefined;\n}\n\nfunction id(prefix: string): string {\n return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;\n}\n\nexport class StampRallyServer {\n readonly #config: AdminRallyConfig;\n readonly #storage: ServerStorageAdapter;\n readonly #idempotent = new Map<string, Response>();\n readonly #claims = new Map<string, number>();\n #queue: Promise<unknown> = Promise.resolve();\n\n constructor(config: AdminRallyConfig, storage: ServerStorageAdapter) {\n this.#config = config;\n this.#storage = storage;\n }\n\n handle(request: Request): Promise<Response> {\n const path = new URL(request.url).pathname;\n if (request.method !== \"POST\")\n return Promise.resolve(errorResponse(\"METHOD_NOT_ALLOWED\", \"POST is required.\", 405));\n if (path.endsWith(\"/check-in\")) return this.verifyCheckIn(request);\n if (path.endsWith(\"/claim-reward\")) return this.claimReward(request);\n if (path.endsWith(\"/sync\")) return this.syncProgress(request);\n return Promise.resolve(errorResponse(\"NOT_FOUND\", \"Route not found.\", 404));\n }\n\n verifyCheckIn(request: Request): Promise<Response> {\n return this.#enqueue(() => this.#verifyCheckIn(request));\n }\n\n claimReward(request: Request): Promise<Response> {\n return this.#enqueue(() => this.#claimReward(request));\n }\n\n syncProgress(request: Request): Promise<Response> {\n return this.#enqueue(() => this.#syncProgress(request));\n }\n\n #enqueue<T>(operation: () => Promise<T>): Promise<T> {\n const next = this.#queue.then(operation, operation);\n this.#queue = next.then(\n () => undefined,\n () => undefined,\n );\n return next;\n }\n\n async #authenticate(request: Request): Promise<string | null> {\n return this.#config.authenticate === undefined ? null : this.#config.authenticate(request);\n }\n\n async #parse<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\n async #verifyCheckIn(request: Request): Promise<Response> {\n const authenticatedUser = await this.#authenticate(request);\n const body = await this.#parse<CheckInRequest>(request);\n const userId = body?.userId ?? authenticatedUser;\n if (\n userId === null ||\n userId === undefined ||\n body === null ||\n !hasText(userId) ||\n !hasText(body.spotId) ||\n !hasText(body.claimMethod) ||\n !hasText(body.idempotencyKey)\n ) {\n return errorResponse(\n \"INVALID_REQUEST\",\n \"userId, spotId, and idempotencyKey are required.\",\n 400,\n );\n }\n if (authenticatedUser !== null && authenticatedUser !== userId)\n return errorResponse(\"UNAUTHORIZED\", \"User identity does not match authentication.\", 401);\n const key = `check-in:${userId}:${body.idempotencyKey}`;\n const previous = this.#idempotent.get(key);\n if (previous !== undefined) return previous.clone();\n const timestamp = now();\n const current =\n (await this.#storage.getUserState(userId)) ?? emptyState(this.#config, timestamp);\n const result = processStamp(\n current,\n this.#config,\n body.spotId,\n contextForClaim(body.claimMethod, body.proofData),\n timestamp,\n );\n if (!result.ok) {\n await this.#audit(\n userId,\n \"CHECK_IN\",\n body.spotId,\n body.idempotencyKey,\n \"REJECTED\",\n safeProofData(body.proofData),\n result.error,\n );\n return this.#remember(key, jsonResponse({ ok: false, error: result.error }, 422));\n }\n const ttl = this.#config.proofTtlSeconds ?? 3600;\n const token = await createSecureToken(\n {\n type: \"stamp_claim\",\n rallyId: this.#config.id,\n userId,\n spotId: body.spotId,\n acquiredAt: timestamp,\n exp: Math.floor(Date.now() / 1000) + ttl,\n },\n this.#config.secretKey,\n { encrypt: true },\n );\n await this.#storage.saveUserState(userId, result.value.nextState);\n await this.#audit(\n userId,\n \"CHECK_IN\",\n body.spotId,\n body.idempotencyKey,\n \"SUCCESS\",\n safeProofData(body.proofData),\n );\n return this.#remember(\n key,\n jsonResponse({\n ok: true,\n state: result.value.nextState,\n proof: {\n token,\n rallyId: this.#config.id,\n userId,\n spotId: body.spotId,\n acquiredAt: timestamp,\n },\n }),\n );\n }\n\n async #claimReward(request: Request): Promise<Response> {\n const authenticatedUser = await this.#authenticate(request);\n const body = await this.#parse<ClaimRewardRequest>(request);\n const userId = body?.userId ?? authenticatedUser;\n if (\n userId === null ||\n userId === undefined ||\n body === null ||\n !hasText(userId) ||\n !hasText(body.rewardId) ||\n !hasText(body.idempotencyKey)\n )\n return errorResponse(\n \"INVALID_REQUEST\",\n \"userId, rewardId, and idempotencyKey are required.\",\n 400,\n );\n if (authenticatedUser !== null && authenticatedUser !== userId)\n return errorResponse(\"UNAUTHORIZED\", \"User identity does not match authentication.\", 401);\n const key = `claim-reward:${userId}:${body.rewardId}:${body.idempotencyKey}`;\n const previous = this.#idempotent.get(key);\n if (previous !== undefined) return previous.clone();\n const reward = this.#config.rewards?.find((item) => item.id === body.rewardId);\n if (reward === undefined) {\n await this.#audit(userId, \"CLAIM_REWARD\", body.rewardId, body.idempotencyKey, \"REJECTED\");\n return this.#remember(key, errorResponse(\"REWARD_NOT_FOUND\", \"Reward was not found.\", 404));\n }\n const timestamp = now();\n const current =\n (await this.#storage.getUserState(userId)) ?? emptyState(this.#config, timestamp);\n const rewardState = current.rewards?.find((item) => item.rewardId === reward.id);\n if (rewardState === undefined) {\n await this.#audit(userId, \"CLAIM_REWARD\", reward.id, body.idempotencyKey, \"REJECTED\");\n return this.#remember(key, errorResponse(\"NOT_AVAILABLE\", \"Reward is not available.\", 422));\n }\n const userClaims = Math.max(\n await this.#storage.getUserClaims(userId, reward.id),\n this.#claims.get(`${userId}:${reward.id}`) ?? 0,\n );\n const stock = await this.#storage.getRewardStock(reward.id);\n const userLimit = reward.userClaimLimit ?? reward.limitPerUser;\n const canReclaimServerReward =\n reward.redemptionMethod === \"server_claim\" &&\n (userLimit === undefined || userClaims < userLimit) &&\n (stock === null || stock > 0);\n const claimableState =\n canReclaimServerReward && rewardState.status === \"CONSUMED\"\n ? { ...rewardState, status: \"AVAILABLE\" as const }\n : rewardState;\n const local = consumeReward({\n reward,\n currentState: claimableState,\n now: timestamp,\n ...(body.staffPasscode === undefined ? {} : { inputPasscode: body.staffPasscode }),\n ...(body.staffId === undefined ? {} : { staffId: body.staffId }),\n userId,\n userRedemptionCount: userClaims,\n });\n if (!local.ok)\n return this.#remember(\n key,\n await this.#rewardError(userId, reward.id, body.idempotencyKey, local.error),\n );\n if (stock !== null && !(await this.#storage.decrementRewardStock(reward.id)))\n return this.#remember(\n key,\n await this.#rewardError(userId, reward.id, body.idempotencyKey, {\n code: \"OUT_OF_STOCK\",\n rewardId: reward.id,\n }),\n );\n const nextState: StampRallyState = {\n ...current,\n rewards: (current.rewards ?? []).map((item) =>\n item.rewardId === reward.id ? local.value : item,\n ),\n updatedAt: timestamp,\n };\n await this.#storage.saveUserState(userId, nextState);\n const claimKey = `${userId}:${reward.id}`;\n this.#claims.set(claimKey, userClaims + 1);\n if (this.#storage instanceof InMemoryServerStorage)\n this.#storage.recordClaim(userId, reward.id);\n await this.#audit(userId, \"CLAIM_REWARD\", reward.id, body.idempotencyKey, \"SUCCESS\", {\n staffId: body.staffId,\n });\n return this.#remember(\n key,\n jsonResponse({\n ok: true,\n state: nextState,\n claimTicketNumber: local.value.claimTicketNumber,\n }),\n );\n }\n\n async #rewardError(\n userId: string,\n rewardId: string,\n key: string,\n error: RewardConsumeError,\n ): Promise<Response> {\n await this.#audit(userId, \"CLAIM_REWARD\", rewardId, key, \"REJECTED\", undefined, error);\n return jsonResponse({ ok: false, error }, 422);\n }\n\n async #syncProgress(request: Request): Promise<Response> {\n const authenticatedUser = await this.#authenticate(request);\n const body = await this.#parse<SyncRequest>(request);\n if (body === null || body.userId === undefined)\n return errorResponse(\"INVALID_REQUEST\", \"userId is required.\", 400);\n if (authenticatedUser !== null && authenticatedUser !== body.userId)\n return errorResponse(\"UNAUTHORIZED\", \"User identity does not match authentication.\", 401);\n const queue = body.queue ?? body.operations ?? [];\n for (const operation of queue) {\n const userId = operation.userId ?? body.userId;\n const spotId = operation.spotId ?? operation.stampId;\n const claimMethod = operation.claimMethod ?? operation.context?.type;\n if (!hasText(userId) || !hasText(spotId) || !hasText(claimMethod)) continue;\n const synthetic = new Request(new URL(\"/api/check-in\", request.url), {\n method: \"POST\",\n body: JSON.stringify({\n userId,\n spotId,\n claimMethod,\n proofData: operation.proofData ?? proofFromContext(operation.context),\n idempotencyKey: operation.idempotencyKey,\n }),\n headers: { \"content-type\": \"application/json\" },\n });\n await this.#verifyCheckIn(synthetic);\n }\n const timestamp = now();\n const state =\n (await this.#storage.getUserState(body.userId)) ?? emptyState(this.#config, timestamp);\n await this.#storage.saveUserState(body.userId, state);\n return jsonResponse({ ok: true, state, accepted: queue.length });\n }\n\n async #audit(\n userId: string,\n action: RallyAuditLog[\"action\"],\n resourceId: string,\n idempotencyKey: string,\n status: RallyAuditLog[\"status\"],\n proofData?: unknown,\n error?: unknown,\n ): Promise<void> {\n await this.#storage.recordAuditLog({\n id: id(\"audit\"),\n timestamp: now(),\n rallyId: this.#config.id,\n userId,\n action,\n resourceId,\n status,\n idempotencyKey,\n ...(proofData === undefined ? {} : { proofData }),\n ...(error === undefined\n ? {}\n : {\n metadata: {\n errorCode: isObject(error) && typeof error.code === \"string\" ? error.code : \"UNKNOWN\",\n },\n }),\n });\n }\n\n #remember(key: string, response: Response): Response {\n this.#idempotent.set(key, response.clone());\n return response;\n }\n}\n\nfunction emptyState(config: RallyConfig, timestamp: string): UserRallyState {\n return {\n rallyId: config.id,\n records: [],\n ...(config.rewards === undefined\n ? {}\n : { rewards: reconcileRewardStates(config.rewards, [], 0, timestamp) }),\n updatedAt: timestamp,\n };\n}\n\nexport type { StampError, StampRecord };\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RallyConfig, SecureTokenSecretKey, StampRallyState, VerificationContext } from '@stamprally/core';
|
|
2
|
+
export { StampError, StampRecord } from '@stamprally/core';
|
|
3
|
+
|
|
4
|
+
interface UserRallyState extends StampRallyState {
|
|
5
|
+
readonly userId?: string;
|
|
6
|
+
}
|
|
7
|
+
interface RallyAuditLog {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly timestamp: string;
|
|
10
|
+
readonly rallyId: string;
|
|
11
|
+
readonly userId: string;
|
|
12
|
+
readonly action: "CHECK_IN" | "CLAIM_REWARD";
|
|
13
|
+
readonly resourceId: string;
|
|
14
|
+
readonly status: "SUCCESS" | "REJECTED";
|
|
15
|
+
readonly idempotencyKey: string;
|
|
16
|
+
readonly proofData?: unknown;
|
|
17
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
interface ServerStorageAdapter {
|
|
20
|
+
getRewardStock(rewardId: string): Promise<number | null>;
|
|
21
|
+
decrementRewardStock(rewardId: string): Promise<boolean>;
|
|
22
|
+
getUserClaims(userId: string, rewardId: string): Promise<number>;
|
|
23
|
+
recordAuditLog(log: RallyAuditLog): Promise<void>;
|
|
24
|
+
saveUserState(userId: string, state: UserRallyState): Promise<void>;
|
|
25
|
+
getUserState(userId: string): Promise<UserRallyState | null>;
|
|
26
|
+
}
|
|
27
|
+
interface AdminRallyConfig extends RallyConfig {
|
|
28
|
+
readonly secretKey: SecureTokenSecretKey;
|
|
29
|
+
readonly proofTtlSeconds?: number;
|
|
30
|
+
readonly authenticate?: (request: Request) => Promise<string | null> | string | null;
|
|
31
|
+
}
|
|
32
|
+
interface CheckInRequest {
|
|
33
|
+
readonly userId: string;
|
|
34
|
+
readonly spotId: string;
|
|
35
|
+
readonly claimMethod: string;
|
|
36
|
+
readonly proofData?: unknown;
|
|
37
|
+
readonly idempotencyKey: string;
|
|
38
|
+
}
|
|
39
|
+
interface ClaimRewardRequest {
|
|
40
|
+
readonly userId: string;
|
|
41
|
+
readonly rewardId: string;
|
|
42
|
+
readonly staffPasscode?: string;
|
|
43
|
+
readonly idempotencyKey: string;
|
|
44
|
+
readonly staffId?: string;
|
|
45
|
+
}
|
|
46
|
+
interface SyncRequest {
|
|
47
|
+
readonly userId: string;
|
|
48
|
+
readonly queue?: ReadonlyArray<SyncCheckInOperation>;
|
|
49
|
+
readonly operations?: ReadonlyArray<SyncCheckInOperation>;
|
|
50
|
+
}
|
|
51
|
+
interface SyncCheckInOperation {
|
|
52
|
+
readonly userId?: string;
|
|
53
|
+
readonly spotId?: string;
|
|
54
|
+
readonly stampId?: string;
|
|
55
|
+
readonly claimMethod?: string;
|
|
56
|
+
readonly proofData?: unknown;
|
|
57
|
+
readonly context?: VerificationContext;
|
|
58
|
+
readonly idempotencyKey: string;
|
|
59
|
+
}
|
|
60
|
+
interface StampClaimProof {
|
|
61
|
+
readonly token: string;
|
|
62
|
+
readonly rallyId: string;
|
|
63
|
+
readonly userId: string;
|
|
64
|
+
readonly spotId: string;
|
|
65
|
+
readonly acquiredAt: string;
|
|
66
|
+
}
|
|
67
|
+
interface InMemoryServerStorageOptions {
|
|
68
|
+
readonly stocks?: Readonly<Record<string, number>>;
|
|
69
|
+
}
|
|
70
|
+
declare class InMemoryServerStorage implements ServerStorageAdapter {
|
|
71
|
+
#private;
|
|
72
|
+
constructor(options?: InMemoryServerStorageOptions);
|
|
73
|
+
getRewardStock(rewardId: string): Promise<number | null>;
|
|
74
|
+
decrementRewardStock(rewardId: string): Promise<boolean>;
|
|
75
|
+
getUserClaims(userId: string, rewardId: string): Promise<number>;
|
|
76
|
+
recordAuditLog(log: RallyAuditLog): Promise<void>;
|
|
77
|
+
saveUserState(userId: string, state: UserRallyState): Promise<void>;
|
|
78
|
+
getUserState(userId: string): Promise<UserRallyState | null>;
|
|
79
|
+
getAuditLogs(): ReadonlyArray<RallyAuditLog>;
|
|
80
|
+
recordClaim(userId: string, rewardId: string): void;
|
|
81
|
+
}
|
|
82
|
+
declare class StampRallyServer {
|
|
83
|
+
#private;
|
|
84
|
+
constructor(config: AdminRallyConfig, storage: ServerStorageAdapter);
|
|
85
|
+
handle(request: Request): Promise<Response>;
|
|
86
|
+
verifyCheckIn(request: Request): Promise<Response>;
|
|
87
|
+
claimReward(request: Request): Promise<Response>;
|
|
88
|
+
syncProgress(request: Request): Promise<Response>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { type AdminRallyConfig, type CheckInRequest, type ClaimRewardRequest, InMemoryServerStorage, type InMemoryServerStorageOptions, type RallyAuditLog, type ServerStorageAdapter, type StampClaimProof, StampRallyServer, type SyncCheckInOperation, type SyncRequest, type UserRallyState };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RallyConfig, SecureTokenSecretKey, StampRallyState, VerificationContext } from '@stamprally/core';
|
|
2
|
+
export { StampError, StampRecord } from '@stamprally/core';
|
|
3
|
+
|
|
4
|
+
interface UserRallyState extends StampRallyState {
|
|
5
|
+
readonly userId?: string;
|
|
6
|
+
}
|
|
7
|
+
interface RallyAuditLog {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly timestamp: string;
|
|
10
|
+
readonly rallyId: string;
|
|
11
|
+
readonly userId: string;
|
|
12
|
+
readonly action: "CHECK_IN" | "CLAIM_REWARD";
|
|
13
|
+
readonly resourceId: string;
|
|
14
|
+
readonly status: "SUCCESS" | "REJECTED";
|
|
15
|
+
readonly idempotencyKey: string;
|
|
16
|
+
readonly proofData?: unknown;
|
|
17
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
interface ServerStorageAdapter {
|
|
20
|
+
getRewardStock(rewardId: string): Promise<number | null>;
|
|
21
|
+
decrementRewardStock(rewardId: string): Promise<boolean>;
|
|
22
|
+
getUserClaims(userId: string, rewardId: string): Promise<number>;
|
|
23
|
+
recordAuditLog(log: RallyAuditLog): Promise<void>;
|
|
24
|
+
saveUserState(userId: string, state: UserRallyState): Promise<void>;
|
|
25
|
+
getUserState(userId: string): Promise<UserRallyState | null>;
|
|
26
|
+
}
|
|
27
|
+
interface AdminRallyConfig extends RallyConfig {
|
|
28
|
+
readonly secretKey: SecureTokenSecretKey;
|
|
29
|
+
readonly proofTtlSeconds?: number;
|
|
30
|
+
readonly authenticate?: (request: Request) => Promise<string | null> | string | null;
|
|
31
|
+
}
|
|
32
|
+
interface CheckInRequest {
|
|
33
|
+
readonly userId: string;
|
|
34
|
+
readonly spotId: string;
|
|
35
|
+
readonly claimMethod: string;
|
|
36
|
+
readonly proofData?: unknown;
|
|
37
|
+
readonly idempotencyKey: string;
|
|
38
|
+
}
|
|
39
|
+
interface ClaimRewardRequest {
|
|
40
|
+
readonly userId: string;
|
|
41
|
+
readonly rewardId: string;
|
|
42
|
+
readonly staffPasscode?: string;
|
|
43
|
+
readonly idempotencyKey: string;
|
|
44
|
+
readonly staffId?: string;
|
|
45
|
+
}
|
|
46
|
+
interface SyncRequest {
|
|
47
|
+
readonly userId: string;
|
|
48
|
+
readonly queue?: ReadonlyArray<SyncCheckInOperation>;
|
|
49
|
+
readonly operations?: ReadonlyArray<SyncCheckInOperation>;
|
|
50
|
+
}
|
|
51
|
+
interface SyncCheckInOperation {
|
|
52
|
+
readonly userId?: string;
|
|
53
|
+
readonly spotId?: string;
|
|
54
|
+
readonly stampId?: string;
|
|
55
|
+
readonly claimMethod?: string;
|
|
56
|
+
readonly proofData?: unknown;
|
|
57
|
+
readonly context?: VerificationContext;
|
|
58
|
+
readonly idempotencyKey: string;
|
|
59
|
+
}
|
|
60
|
+
interface StampClaimProof {
|
|
61
|
+
readonly token: string;
|
|
62
|
+
readonly rallyId: string;
|
|
63
|
+
readonly userId: string;
|
|
64
|
+
readonly spotId: string;
|
|
65
|
+
readonly acquiredAt: string;
|
|
66
|
+
}
|
|
67
|
+
interface InMemoryServerStorageOptions {
|
|
68
|
+
readonly stocks?: Readonly<Record<string, number>>;
|
|
69
|
+
}
|
|
70
|
+
declare class InMemoryServerStorage implements ServerStorageAdapter {
|
|
71
|
+
#private;
|
|
72
|
+
constructor(options?: InMemoryServerStorageOptions);
|
|
73
|
+
getRewardStock(rewardId: string): Promise<number | null>;
|
|
74
|
+
decrementRewardStock(rewardId: string): Promise<boolean>;
|
|
75
|
+
getUserClaims(userId: string, rewardId: string): Promise<number>;
|
|
76
|
+
recordAuditLog(log: RallyAuditLog): Promise<void>;
|
|
77
|
+
saveUserState(userId: string, state: UserRallyState): Promise<void>;
|
|
78
|
+
getUserState(userId: string): Promise<UserRallyState | null>;
|
|
79
|
+
getAuditLogs(): ReadonlyArray<RallyAuditLog>;
|
|
80
|
+
recordClaim(userId: string, rewardId: string): void;
|
|
81
|
+
}
|
|
82
|
+
declare class StampRallyServer {
|
|
83
|
+
#private;
|
|
84
|
+
constructor(config: AdminRallyConfig, storage: ServerStorageAdapter);
|
|
85
|
+
handle(request: Request): Promise<Response>;
|
|
86
|
+
verifyCheckIn(request: Request): Promise<Response>;
|
|
87
|
+
claimReward(request: Request): Promise<Response>;
|
|
88
|
+
syncProgress(request: Request): Promise<Response>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { type AdminRallyConfig, type CheckInRequest, type ClaimRewardRequest, InMemoryServerStorage, type InMemoryServerStorageOptions, type RallyAuditLog, type ServerStorageAdapter, type StampClaimProof, StampRallyServer, type SyncCheckInOperation, type SyncRequest, type UserRallyState };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { processStamp, createSecureToken, consumeReward, reconcileRewardStates } from '@stamprally/core';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var InMemoryServerStorage = class {
|
|
5
|
+
#states = /* @__PURE__ */ new Map();
|
|
6
|
+
#stocks;
|
|
7
|
+
#claims = /* @__PURE__ */ new Map();
|
|
8
|
+
#auditLogs = [];
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.#stocks = new Map(Object.entries(options.stocks ?? {}));
|
|
11
|
+
}
|
|
12
|
+
async getRewardStock(rewardId) {
|
|
13
|
+
return this.#stocks.get(rewardId) ?? null;
|
|
14
|
+
}
|
|
15
|
+
async decrementRewardStock(rewardId) {
|
|
16
|
+
const stock = this.#stocks.get(rewardId);
|
|
17
|
+
if (stock === void 0) return true;
|
|
18
|
+
if (stock <= 0) return false;
|
|
19
|
+
this.#stocks.set(rewardId, stock - 1);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
async getUserClaims(userId, rewardId) {
|
|
23
|
+
return this.#claims.get(`${userId}:${rewardId}`) ?? 0;
|
|
24
|
+
}
|
|
25
|
+
async recordAuditLog(log) {
|
|
26
|
+
this.#auditLogs.push({ ...log });
|
|
27
|
+
}
|
|
28
|
+
async saveUserState(userId, state) {
|
|
29
|
+
this.#states.set(userId, cloneUserState(state));
|
|
30
|
+
}
|
|
31
|
+
async getUserState(userId) {
|
|
32
|
+
const state = this.#states.get(userId);
|
|
33
|
+
return state === void 0 ? null : cloneUserState(state);
|
|
34
|
+
}
|
|
35
|
+
getAuditLogs() {
|
|
36
|
+
return this.#auditLogs.map((log) => ({ ...log }));
|
|
37
|
+
}
|
|
38
|
+
recordClaim(userId, rewardId) {
|
|
39
|
+
const key = `${userId}:${rewardId}`;
|
|
40
|
+
this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function cloneUserState(state) {
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
records: state.records.map((record) => ({
|
|
47
|
+
...record,
|
|
48
|
+
...record.metadata === void 0 ? {} : { metadata: { ...record.metadata } }
|
|
49
|
+
})),
|
|
50
|
+
...state.rewards === void 0 ? {} : { rewards: state.rewards.map((reward) => ({ ...reward })) }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function jsonResponse(body, status = 200) {
|
|
54
|
+
return new Response(JSON.stringify(body), {
|
|
55
|
+
status,
|
|
56
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function errorResponse(code, message, status) {
|
|
60
|
+
return jsonResponse({ ok: false, error: { code, message } }, status);
|
|
61
|
+
}
|
|
62
|
+
function isObject(value) {
|
|
63
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
function contextForClaim(method, proofData) {
|
|
66
|
+
if (method === "token" || method === "qr" || method === "passcode") {
|
|
67
|
+
return {
|
|
68
|
+
type: "token",
|
|
69
|
+
token: isObject(proofData) && typeof proofData.token === "string" ? proofData.token : String(proofData ?? "")
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (method === "geo" || method === "geolocation") {
|
|
73
|
+
const value = isObject(proofData) ? proofData : {};
|
|
74
|
+
return {
|
|
75
|
+
type: "geo",
|
|
76
|
+
currentLatitude: typeof value.latitude === "number" ? value.latitude : typeof value.currentLatitude === "number" ? value.currentLatitude : Number.NaN,
|
|
77
|
+
currentLongitude: typeof value.longitude === "number" ? value.longitude : typeof value.currentLongitude === "number" ? value.currentLongitude : Number.NaN
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { type: "instant" };
|
|
81
|
+
}
|
|
82
|
+
function now() {
|
|
83
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
84
|
+
}
|
|
85
|
+
function hasText(value) {
|
|
86
|
+
return typeof value === "string" && value.trim() !== "";
|
|
87
|
+
}
|
|
88
|
+
function safeProofData(value) {
|
|
89
|
+
return isObject(value) && typeof value.token === "string" ? { type: "token" } : value;
|
|
90
|
+
}
|
|
91
|
+
function proofFromContext(context) {
|
|
92
|
+
if (context?.type === "token") return { token: context.token };
|
|
93
|
+
if (context?.type === "geo") {
|
|
94
|
+
return { latitude: context.currentLatitude, longitude: context.currentLongitude };
|
|
95
|
+
}
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
function id(prefix) {
|
|
99
|
+
return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
|
|
100
|
+
}
|
|
101
|
+
var StampRallyServer = class {
|
|
102
|
+
#config;
|
|
103
|
+
#storage;
|
|
104
|
+
#idempotent = /* @__PURE__ */ new Map();
|
|
105
|
+
#claims = /* @__PURE__ */ new Map();
|
|
106
|
+
#queue = Promise.resolve();
|
|
107
|
+
constructor(config, storage) {
|
|
108
|
+
this.#config = config;
|
|
109
|
+
this.#storage = storage;
|
|
110
|
+
}
|
|
111
|
+
handle(request) {
|
|
112
|
+
const path = new URL(request.url).pathname;
|
|
113
|
+
if (request.method !== "POST")
|
|
114
|
+
return Promise.resolve(errorResponse("METHOD_NOT_ALLOWED", "POST is required.", 405));
|
|
115
|
+
if (path.endsWith("/check-in")) return this.verifyCheckIn(request);
|
|
116
|
+
if (path.endsWith("/claim-reward")) return this.claimReward(request);
|
|
117
|
+
if (path.endsWith("/sync")) return this.syncProgress(request);
|
|
118
|
+
return Promise.resolve(errorResponse("NOT_FOUND", "Route not found.", 404));
|
|
119
|
+
}
|
|
120
|
+
verifyCheckIn(request) {
|
|
121
|
+
return this.#enqueue(() => this.#verifyCheckIn(request));
|
|
122
|
+
}
|
|
123
|
+
claimReward(request) {
|
|
124
|
+
return this.#enqueue(() => this.#claimReward(request));
|
|
125
|
+
}
|
|
126
|
+
syncProgress(request) {
|
|
127
|
+
return this.#enqueue(() => this.#syncProgress(request));
|
|
128
|
+
}
|
|
129
|
+
#enqueue(operation) {
|
|
130
|
+
const next = this.#queue.then(operation, operation);
|
|
131
|
+
this.#queue = next.then(
|
|
132
|
+
() => void 0,
|
|
133
|
+
() => void 0
|
|
134
|
+
);
|
|
135
|
+
return next;
|
|
136
|
+
}
|
|
137
|
+
async #authenticate(request) {
|
|
138
|
+
return this.#config.authenticate === void 0 ? null : this.#config.authenticate(request);
|
|
139
|
+
}
|
|
140
|
+
async #parse(request) {
|
|
141
|
+
try {
|
|
142
|
+
const value = await request.json();
|
|
143
|
+
return isObject(value) ? value : null;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async #verifyCheckIn(request) {
|
|
149
|
+
const authenticatedUser = await this.#authenticate(request);
|
|
150
|
+
const body = await this.#parse(request);
|
|
151
|
+
const userId = body?.userId ?? authenticatedUser;
|
|
152
|
+
if (userId === null || userId === void 0 || body === null || !hasText(userId) || !hasText(body.spotId) || !hasText(body.claimMethod) || !hasText(body.idempotencyKey)) {
|
|
153
|
+
return errorResponse(
|
|
154
|
+
"INVALID_REQUEST",
|
|
155
|
+
"userId, spotId, and idempotencyKey are required.",
|
|
156
|
+
400
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (authenticatedUser !== null && authenticatedUser !== userId)
|
|
160
|
+
return errorResponse("UNAUTHORIZED", "User identity does not match authentication.", 401);
|
|
161
|
+
const key = `check-in:${userId}:${body.idempotencyKey}`;
|
|
162
|
+
const previous = this.#idempotent.get(key);
|
|
163
|
+
if (previous !== void 0) return previous.clone();
|
|
164
|
+
const timestamp = now();
|
|
165
|
+
const current = await this.#storage.getUserState(userId) ?? emptyState(this.#config, timestamp);
|
|
166
|
+
const result = processStamp(
|
|
167
|
+
current,
|
|
168
|
+
this.#config,
|
|
169
|
+
body.spotId,
|
|
170
|
+
contextForClaim(body.claimMethod, body.proofData),
|
|
171
|
+
timestamp
|
|
172
|
+
);
|
|
173
|
+
if (!result.ok) {
|
|
174
|
+
await this.#audit(
|
|
175
|
+
userId,
|
|
176
|
+
"CHECK_IN",
|
|
177
|
+
body.spotId,
|
|
178
|
+
body.idempotencyKey,
|
|
179
|
+
"REJECTED",
|
|
180
|
+
safeProofData(body.proofData),
|
|
181
|
+
result.error
|
|
182
|
+
);
|
|
183
|
+
return this.#remember(key, jsonResponse({ ok: false, error: result.error }, 422));
|
|
184
|
+
}
|
|
185
|
+
const ttl = this.#config.proofTtlSeconds ?? 3600;
|
|
186
|
+
const token = await createSecureToken(
|
|
187
|
+
{
|
|
188
|
+
type: "stamp_claim",
|
|
189
|
+
rallyId: this.#config.id,
|
|
190
|
+
userId,
|
|
191
|
+
spotId: body.spotId,
|
|
192
|
+
acquiredAt: timestamp,
|
|
193
|
+
exp: Math.floor(Date.now() / 1e3) + ttl
|
|
194
|
+
},
|
|
195
|
+
this.#config.secretKey,
|
|
196
|
+
{ encrypt: true }
|
|
197
|
+
);
|
|
198
|
+
await this.#storage.saveUserState(userId, result.value.nextState);
|
|
199
|
+
await this.#audit(
|
|
200
|
+
userId,
|
|
201
|
+
"CHECK_IN",
|
|
202
|
+
body.spotId,
|
|
203
|
+
body.idempotencyKey,
|
|
204
|
+
"SUCCESS",
|
|
205
|
+
safeProofData(body.proofData)
|
|
206
|
+
);
|
|
207
|
+
return this.#remember(
|
|
208
|
+
key,
|
|
209
|
+
jsonResponse({
|
|
210
|
+
ok: true,
|
|
211
|
+
state: result.value.nextState,
|
|
212
|
+
proof: {
|
|
213
|
+
token,
|
|
214
|
+
rallyId: this.#config.id,
|
|
215
|
+
userId,
|
|
216
|
+
spotId: body.spotId,
|
|
217
|
+
acquiredAt: timestamp
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
async #claimReward(request) {
|
|
223
|
+
const authenticatedUser = await this.#authenticate(request);
|
|
224
|
+
const body = await this.#parse(request);
|
|
225
|
+
const userId = body?.userId ?? authenticatedUser;
|
|
226
|
+
if (userId === null || userId === void 0 || body === null || !hasText(userId) || !hasText(body.rewardId) || !hasText(body.idempotencyKey))
|
|
227
|
+
return errorResponse(
|
|
228
|
+
"INVALID_REQUEST",
|
|
229
|
+
"userId, rewardId, and idempotencyKey are required.",
|
|
230
|
+
400
|
|
231
|
+
);
|
|
232
|
+
if (authenticatedUser !== null && authenticatedUser !== userId)
|
|
233
|
+
return errorResponse("UNAUTHORIZED", "User identity does not match authentication.", 401);
|
|
234
|
+
const key = `claim-reward:${userId}:${body.rewardId}:${body.idempotencyKey}`;
|
|
235
|
+
const previous = this.#idempotent.get(key);
|
|
236
|
+
if (previous !== void 0) return previous.clone();
|
|
237
|
+
const reward = this.#config.rewards?.find((item) => item.id === body.rewardId);
|
|
238
|
+
if (reward === void 0) {
|
|
239
|
+
await this.#audit(userId, "CLAIM_REWARD", body.rewardId, body.idempotencyKey, "REJECTED");
|
|
240
|
+
return this.#remember(key, errorResponse("REWARD_NOT_FOUND", "Reward was not found.", 404));
|
|
241
|
+
}
|
|
242
|
+
const timestamp = now();
|
|
243
|
+
const current = await this.#storage.getUserState(userId) ?? emptyState(this.#config, timestamp);
|
|
244
|
+
const rewardState = current.rewards?.find((item) => item.rewardId === reward.id);
|
|
245
|
+
if (rewardState === void 0) {
|
|
246
|
+
await this.#audit(userId, "CLAIM_REWARD", reward.id, body.idempotencyKey, "REJECTED");
|
|
247
|
+
return this.#remember(key, errorResponse("NOT_AVAILABLE", "Reward is not available.", 422));
|
|
248
|
+
}
|
|
249
|
+
const userClaims = Math.max(
|
|
250
|
+
await this.#storage.getUserClaims(userId, reward.id),
|
|
251
|
+
this.#claims.get(`${userId}:${reward.id}`) ?? 0
|
|
252
|
+
);
|
|
253
|
+
const stock = await this.#storage.getRewardStock(reward.id);
|
|
254
|
+
const userLimit = reward.userClaimLimit ?? reward.limitPerUser;
|
|
255
|
+
const canReclaimServerReward = reward.redemptionMethod === "server_claim" && (userLimit === void 0 || userClaims < userLimit) && (stock === null || stock > 0);
|
|
256
|
+
const claimableState = canReclaimServerReward && rewardState.status === "CONSUMED" ? { ...rewardState, status: "AVAILABLE" } : rewardState;
|
|
257
|
+
const local = consumeReward({
|
|
258
|
+
reward,
|
|
259
|
+
currentState: claimableState,
|
|
260
|
+
now: timestamp,
|
|
261
|
+
...body.staffPasscode === void 0 ? {} : { inputPasscode: body.staffPasscode },
|
|
262
|
+
...body.staffId === void 0 ? {} : { staffId: body.staffId },
|
|
263
|
+
userId,
|
|
264
|
+
userRedemptionCount: userClaims
|
|
265
|
+
});
|
|
266
|
+
if (!local.ok)
|
|
267
|
+
return this.#remember(
|
|
268
|
+
key,
|
|
269
|
+
await this.#rewardError(userId, reward.id, body.idempotencyKey, local.error)
|
|
270
|
+
);
|
|
271
|
+
if (stock !== null && !await this.#storage.decrementRewardStock(reward.id))
|
|
272
|
+
return this.#remember(
|
|
273
|
+
key,
|
|
274
|
+
await this.#rewardError(userId, reward.id, body.idempotencyKey, {
|
|
275
|
+
code: "OUT_OF_STOCK",
|
|
276
|
+
rewardId: reward.id
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
const nextState = {
|
|
280
|
+
...current,
|
|
281
|
+
rewards: (current.rewards ?? []).map(
|
|
282
|
+
(item) => item.rewardId === reward.id ? local.value : item
|
|
283
|
+
),
|
|
284
|
+
updatedAt: timestamp
|
|
285
|
+
};
|
|
286
|
+
await this.#storage.saveUserState(userId, nextState);
|
|
287
|
+
const claimKey = `${userId}:${reward.id}`;
|
|
288
|
+
this.#claims.set(claimKey, userClaims + 1);
|
|
289
|
+
if (this.#storage instanceof InMemoryServerStorage)
|
|
290
|
+
this.#storage.recordClaim(userId, reward.id);
|
|
291
|
+
await this.#audit(userId, "CLAIM_REWARD", reward.id, body.idempotencyKey, "SUCCESS", {
|
|
292
|
+
staffId: body.staffId
|
|
293
|
+
});
|
|
294
|
+
return this.#remember(
|
|
295
|
+
key,
|
|
296
|
+
jsonResponse({
|
|
297
|
+
ok: true,
|
|
298
|
+
state: nextState,
|
|
299
|
+
claimTicketNumber: local.value.claimTicketNumber
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
async #rewardError(userId, rewardId, key, error) {
|
|
304
|
+
await this.#audit(userId, "CLAIM_REWARD", rewardId, key, "REJECTED", void 0, error);
|
|
305
|
+
return jsonResponse({ ok: false, error }, 422);
|
|
306
|
+
}
|
|
307
|
+
async #syncProgress(request) {
|
|
308
|
+
const authenticatedUser = await this.#authenticate(request);
|
|
309
|
+
const body = await this.#parse(request);
|
|
310
|
+
if (body === null || body.userId === void 0)
|
|
311
|
+
return errorResponse("INVALID_REQUEST", "userId is required.", 400);
|
|
312
|
+
if (authenticatedUser !== null && authenticatedUser !== body.userId)
|
|
313
|
+
return errorResponse("UNAUTHORIZED", "User identity does not match authentication.", 401);
|
|
314
|
+
const queue = body.queue ?? body.operations ?? [];
|
|
315
|
+
for (const operation of queue) {
|
|
316
|
+
const userId = operation.userId ?? body.userId;
|
|
317
|
+
const spotId = operation.spotId ?? operation.stampId;
|
|
318
|
+
const claimMethod = operation.claimMethod ?? operation.context?.type;
|
|
319
|
+
if (!hasText(userId) || !hasText(spotId) || !hasText(claimMethod)) continue;
|
|
320
|
+
const synthetic = new Request(new URL("/api/check-in", request.url), {
|
|
321
|
+
method: "POST",
|
|
322
|
+
body: JSON.stringify({
|
|
323
|
+
userId,
|
|
324
|
+
spotId,
|
|
325
|
+
claimMethod,
|
|
326
|
+
proofData: operation.proofData ?? proofFromContext(operation.context),
|
|
327
|
+
idempotencyKey: operation.idempotencyKey
|
|
328
|
+
}),
|
|
329
|
+
headers: { "content-type": "application/json" }
|
|
330
|
+
});
|
|
331
|
+
await this.#verifyCheckIn(synthetic);
|
|
332
|
+
}
|
|
333
|
+
const timestamp = now();
|
|
334
|
+
const state = await this.#storage.getUserState(body.userId) ?? emptyState(this.#config, timestamp);
|
|
335
|
+
await this.#storage.saveUserState(body.userId, state);
|
|
336
|
+
return jsonResponse({ ok: true, state, accepted: queue.length });
|
|
337
|
+
}
|
|
338
|
+
async #audit(userId, action, resourceId, idempotencyKey, status, proofData, error) {
|
|
339
|
+
await this.#storage.recordAuditLog({
|
|
340
|
+
id: id("audit"),
|
|
341
|
+
timestamp: now(),
|
|
342
|
+
rallyId: this.#config.id,
|
|
343
|
+
userId,
|
|
344
|
+
action,
|
|
345
|
+
resourceId,
|
|
346
|
+
status,
|
|
347
|
+
idempotencyKey,
|
|
348
|
+
...proofData === void 0 ? {} : { proofData },
|
|
349
|
+
...error === void 0 ? {} : {
|
|
350
|
+
metadata: {
|
|
351
|
+
errorCode: isObject(error) && typeof error.code === "string" ? error.code : "UNKNOWN"
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
#remember(key, response) {
|
|
357
|
+
this.#idempotent.set(key, response.clone());
|
|
358
|
+
return response;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
function emptyState(config, timestamp) {
|
|
362
|
+
return {
|
|
363
|
+
rallyId: config.id,
|
|
364
|
+
records: [],
|
|
365
|
+
...config.rewards === void 0 ? {} : { rewards: reconcileRewardStates(config.rewards, [], 0, timestamp) },
|
|
366
|
+
updatedAt: timestamp
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export { InMemoryServerStorage, StampRallyServer };
|
|
371
|
+
//# sourceMappingURL=index.js.map
|
|
372
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n consumeReward,\n createSecureToken,\n processStamp,\n type RallyConfig,\n type RewardConsumeError,\n reconcileRewardStates,\n type SecureTokenSecretKey,\n type StampError,\n type StampRallyState,\n type StampRecord,\n type VerificationContext,\n} from \"@stamprally/core\";\n\nexport interface UserRallyState extends StampRallyState {\n readonly userId?: string;\n}\n\nexport interface RallyAuditLog {\n readonly id: string;\n readonly timestamp: string;\n readonly rallyId: string;\n readonly userId: string;\n readonly action: \"CHECK_IN\" | \"CLAIM_REWARD\";\n readonly resourceId: string;\n readonly status: \"SUCCESS\" | \"REJECTED\";\n readonly idempotencyKey: string;\n readonly proofData?: unknown;\n readonly metadata?: Readonly<Record<string, unknown>>;\n}\n\nexport interface ServerStorageAdapter {\n getRewardStock(rewardId: string): Promise<number | null>;\n decrementRewardStock(rewardId: string): Promise<boolean>;\n getUserClaims(userId: string, rewardId: string): Promise<number>;\n recordAuditLog(log: RallyAuditLog): Promise<void>;\n saveUserState(userId: string, state: UserRallyState): Promise<void>;\n getUserState(userId: string): Promise<UserRallyState | null>;\n}\n\nexport interface AdminRallyConfig extends RallyConfig {\n readonly secretKey: SecureTokenSecretKey;\n readonly proofTtlSeconds?: number;\n readonly authenticate?: (request: Request) => Promise<string | null> | string | null;\n}\n\nexport interface CheckInRequest {\n readonly userId: string;\n readonly spotId: string;\n readonly claimMethod: string;\n readonly proofData?: unknown;\n readonly idempotencyKey: string;\n}\n\nexport interface ClaimRewardRequest {\n readonly userId: string;\n readonly rewardId: string;\n readonly staffPasscode?: string;\n readonly idempotencyKey: string;\n readonly staffId?: string;\n}\n\nexport interface SyncRequest {\n readonly userId: string;\n readonly queue?: ReadonlyArray<SyncCheckInOperation>;\n readonly operations?: ReadonlyArray<SyncCheckInOperation>;\n}\n\nexport interface SyncCheckInOperation {\n readonly userId?: string;\n readonly spotId?: string;\n readonly stampId?: string;\n readonly claimMethod?: string;\n readonly proofData?: unknown;\n readonly context?: VerificationContext;\n readonly idempotencyKey: string;\n}\n\nexport interface StampClaimProof {\n readonly token: string;\n readonly rallyId: string;\n readonly userId: string;\n readonly spotId: string;\n readonly acquiredAt: string;\n}\n\nexport interface InMemoryServerStorageOptions {\n readonly stocks?: Readonly<Record<string, number>>;\n}\n\nexport class InMemoryServerStorage implements ServerStorageAdapter {\n readonly #states = new Map<string, UserRallyState>();\n readonly #stocks: Map<string, number>;\n readonly #claims = new Map<string, number>();\n readonly #auditLogs: RallyAuditLog[] = [];\n\n constructor(options: InMemoryServerStorageOptions = {}) {\n this.#stocks = new Map(Object.entries(options.stocks ?? {}));\n }\n\n async getRewardStock(rewardId: string): Promise<number | null> {\n return this.#stocks.get(rewardId) ?? null;\n }\n\n async decrementRewardStock(rewardId: string): Promise<boolean> {\n const stock = this.#stocks.get(rewardId);\n if (stock === undefined) return true;\n if (stock <= 0) return false;\n this.#stocks.set(rewardId, stock - 1);\n return true;\n }\n\n async getUserClaims(userId: string, rewardId: string): Promise<number> {\n return this.#claims.get(`${userId}:${rewardId}`) ?? 0;\n }\n\n async recordAuditLog(log: RallyAuditLog): Promise<void> {\n this.#auditLogs.push({ ...log });\n }\n\n async saveUserState(userId: string, state: UserRallyState): Promise<void> {\n this.#states.set(userId, cloneUserState(state));\n }\n\n async getUserState(userId: string): Promise<UserRallyState | null> {\n const state = this.#states.get(userId);\n return state === undefined ? null : cloneUserState(state);\n }\n\n getAuditLogs(): ReadonlyArray<RallyAuditLog> {\n return this.#auditLogs.map((log) => ({ ...log }));\n }\n\n recordClaim(userId: string, rewardId: string): void {\n const key = `${userId}:${rewardId}`;\n this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);\n }\n}\n\nfunction cloneUserState(state: UserRallyState): UserRallyState {\n return {\n ...state,\n records: state.records.map((record) => ({\n ...record,\n ...(record.metadata === undefined ? {} : { metadata: { ...record.metadata } }),\n })),\n ...(state.rewards === undefined\n ? {}\n : { rewards: state.rewards.map((reward) => ({ ...reward })) }),\n };\n}\n\nfunction jsonResponse(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}\n\nfunction errorResponse(code: string, message: string, status: number): Response {\n return jsonResponse({ ok: false, error: { code, message } }, status);\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction contextForClaim(method: string, proofData: unknown): VerificationContext {\n if (method === \"token\" || method === \"qr\" || method === \"passcode\") {\n return {\n type: \"token\",\n token:\n isObject(proofData) && typeof proofData.token === \"string\"\n ? proofData.token\n : String(proofData ?? \"\"),\n };\n }\n if (method === \"geo\" || method === \"geolocation\") {\n const value = isObject(proofData) ? proofData : {};\n return {\n type: \"geo\",\n currentLatitude:\n typeof value.latitude === \"number\"\n ? value.latitude\n : typeof value.currentLatitude === \"number\"\n ? value.currentLatitude\n : Number.NaN,\n currentLongitude:\n typeof value.longitude === \"number\"\n ? value.longitude\n : typeof value.currentLongitude === \"number\"\n ? value.currentLongitude\n : Number.NaN,\n };\n }\n return { type: \"instant\" };\n}\n\nfunction now(): string {\n return new Date().toISOString();\n}\n\nfunction hasText(value: unknown): value is string {\n return typeof value === \"string\" && value.trim() !== \"\";\n}\n\nfunction safeProofData(value: unknown): unknown {\n return isObject(value) && typeof value.token === \"string\" ? { type: \"token\" } : value;\n}\n\nfunction proofFromContext(context: VerificationContext | undefined): unknown {\n if (context?.type === \"token\") return { token: context.token };\n if (context?.type === \"geo\") {\n return { latitude: context.currentLatitude, longitude: context.currentLongitude };\n }\n return undefined;\n}\n\nfunction id(prefix: string): string {\n return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;\n}\n\nexport class StampRallyServer {\n readonly #config: AdminRallyConfig;\n readonly #storage: ServerStorageAdapter;\n readonly #idempotent = new Map<string, Response>();\n readonly #claims = new Map<string, number>();\n #queue: Promise<unknown> = Promise.resolve();\n\n constructor(config: AdminRallyConfig, storage: ServerStorageAdapter) {\n this.#config = config;\n this.#storage = storage;\n }\n\n handle(request: Request): Promise<Response> {\n const path = new URL(request.url).pathname;\n if (request.method !== \"POST\")\n return Promise.resolve(errorResponse(\"METHOD_NOT_ALLOWED\", \"POST is required.\", 405));\n if (path.endsWith(\"/check-in\")) return this.verifyCheckIn(request);\n if (path.endsWith(\"/claim-reward\")) return this.claimReward(request);\n if (path.endsWith(\"/sync\")) return this.syncProgress(request);\n return Promise.resolve(errorResponse(\"NOT_FOUND\", \"Route not found.\", 404));\n }\n\n verifyCheckIn(request: Request): Promise<Response> {\n return this.#enqueue(() => this.#verifyCheckIn(request));\n }\n\n claimReward(request: Request): Promise<Response> {\n return this.#enqueue(() => this.#claimReward(request));\n }\n\n syncProgress(request: Request): Promise<Response> {\n return this.#enqueue(() => this.#syncProgress(request));\n }\n\n #enqueue<T>(operation: () => Promise<T>): Promise<T> {\n const next = this.#queue.then(operation, operation);\n this.#queue = next.then(\n () => undefined,\n () => undefined,\n );\n return next;\n }\n\n async #authenticate(request: Request): Promise<string | null> {\n return this.#config.authenticate === undefined ? null : this.#config.authenticate(request);\n }\n\n async #parse<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\n async #verifyCheckIn(request: Request): Promise<Response> {\n const authenticatedUser = await this.#authenticate(request);\n const body = await this.#parse<CheckInRequest>(request);\n const userId = body?.userId ?? authenticatedUser;\n if (\n userId === null ||\n userId === undefined ||\n body === null ||\n !hasText(userId) ||\n !hasText(body.spotId) ||\n !hasText(body.claimMethod) ||\n !hasText(body.idempotencyKey)\n ) {\n return errorResponse(\n \"INVALID_REQUEST\",\n \"userId, spotId, and idempotencyKey are required.\",\n 400,\n );\n }\n if (authenticatedUser !== null && authenticatedUser !== userId)\n return errorResponse(\"UNAUTHORIZED\", \"User identity does not match authentication.\", 401);\n const key = `check-in:${userId}:${body.idempotencyKey}`;\n const previous = this.#idempotent.get(key);\n if (previous !== undefined) return previous.clone();\n const timestamp = now();\n const current =\n (await this.#storage.getUserState(userId)) ?? emptyState(this.#config, timestamp);\n const result = processStamp(\n current,\n this.#config,\n body.spotId,\n contextForClaim(body.claimMethod, body.proofData),\n timestamp,\n );\n if (!result.ok) {\n await this.#audit(\n userId,\n \"CHECK_IN\",\n body.spotId,\n body.idempotencyKey,\n \"REJECTED\",\n safeProofData(body.proofData),\n result.error,\n );\n return this.#remember(key, jsonResponse({ ok: false, error: result.error }, 422));\n }\n const ttl = this.#config.proofTtlSeconds ?? 3600;\n const token = await createSecureToken(\n {\n type: \"stamp_claim\",\n rallyId: this.#config.id,\n userId,\n spotId: body.spotId,\n acquiredAt: timestamp,\n exp: Math.floor(Date.now() / 1000) + ttl,\n },\n this.#config.secretKey,\n { encrypt: true },\n );\n await this.#storage.saveUserState(userId, result.value.nextState);\n await this.#audit(\n userId,\n \"CHECK_IN\",\n body.spotId,\n body.idempotencyKey,\n \"SUCCESS\",\n safeProofData(body.proofData),\n );\n return this.#remember(\n key,\n jsonResponse({\n ok: true,\n state: result.value.nextState,\n proof: {\n token,\n rallyId: this.#config.id,\n userId,\n spotId: body.spotId,\n acquiredAt: timestamp,\n },\n }),\n );\n }\n\n async #claimReward(request: Request): Promise<Response> {\n const authenticatedUser = await this.#authenticate(request);\n const body = await this.#parse<ClaimRewardRequest>(request);\n const userId = body?.userId ?? authenticatedUser;\n if (\n userId === null ||\n userId === undefined ||\n body === null ||\n !hasText(userId) ||\n !hasText(body.rewardId) ||\n !hasText(body.idempotencyKey)\n )\n return errorResponse(\n \"INVALID_REQUEST\",\n \"userId, rewardId, and idempotencyKey are required.\",\n 400,\n );\n if (authenticatedUser !== null && authenticatedUser !== userId)\n return errorResponse(\"UNAUTHORIZED\", \"User identity does not match authentication.\", 401);\n const key = `claim-reward:${userId}:${body.rewardId}:${body.idempotencyKey}`;\n const previous = this.#idempotent.get(key);\n if (previous !== undefined) return previous.clone();\n const reward = this.#config.rewards?.find((item) => item.id === body.rewardId);\n if (reward === undefined) {\n await this.#audit(userId, \"CLAIM_REWARD\", body.rewardId, body.idempotencyKey, \"REJECTED\");\n return this.#remember(key, errorResponse(\"REWARD_NOT_FOUND\", \"Reward was not found.\", 404));\n }\n const timestamp = now();\n const current =\n (await this.#storage.getUserState(userId)) ?? emptyState(this.#config, timestamp);\n const rewardState = current.rewards?.find((item) => item.rewardId === reward.id);\n if (rewardState === undefined) {\n await this.#audit(userId, \"CLAIM_REWARD\", reward.id, body.idempotencyKey, \"REJECTED\");\n return this.#remember(key, errorResponse(\"NOT_AVAILABLE\", \"Reward is not available.\", 422));\n }\n const userClaims = Math.max(\n await this.#storage.getUserClaims(userId, reward.id),\n this.#claims.get(`${userId}:${reward.id}`) ?? 0,\n );\n const stock = await this.#storage.getRewardStock(reward.id);\n const userLimit = reward.userClaimLimit ?? reward.limitPerUser;\n const canReclaimServerReward =\n reward.redemptionMethod === \"server_claim\" &&\n (userLimit === undefined || userClaims < userLimit) &&\n (stock === null || stock > 0);\n const claimableState =\n canReclaimServerReward && rewardState.status === \"CONSUMED\"\n ? { ...rewardState, status: \"AVAILABLE\" as const }\n : rewardState;\n const local = consumeReward({\n reward,\n currentState: claimableState,\n now: timestamp,\n ...(body.staffPasscode === undefined ? {} : { inputPasscode: body.staffPasscode }),\n ...(body.staffId === undefined ? {} : { staffId: body.staffId }),\n userId,\n userRedemptionCount: userClaims,\n });\n if (!local.ok)\n return this.#remember(\n key,\n await this.#rewardError(userId, reward.id, body.idempotencyKey, local.error),\n );\n if (stock !== null && !(await this.#storage.decrementRewardStock(reward.id)))\n return this.#remember(\n key,\n await this.#rewardError(userId, reward.id, body.idempotencyKey, {\n code: \"OUT_OF_STOCK\",\n rewardId: reward.id,\n }),\n );\n const nextState: StampRallyState = {\n ...current,\n rewards: (current.rewards ?? []).map((item) =>\n item.rewardId === reward.id ? local.value : item,\n ),\n updatedAt: timestamp,\n };\n await this.#storage.saveUserState(userId, nextState);\n const claimKey = `${userId}:${reward.id}`;\n this.#claims.set(claimKey, userClaims + 1);\n if (this.#storage instanceof InMemoryServerStorage)\n this.#storage.recordClaim(userId, reward.id);\n await this.#audit(userId, \"CLAIM_REWARD\", reward.id, body.idempotencyKey, \"SUCCESS\", {\n staffId: body.staffId,\n });\n return this.#remember(\n key,\n jsonResponse({\n ok: true,\n state: nextState,\n claimTicketNumber: local.value.claimTicketNumber,\n }),\n );\n }\n\n async #rewardError(\n userId: string,\n rewardId: string,\n key: string,\n error: RewardConsumeError,\n ): Promise<Response> {\n await this.#audit(userId, \"CLAIM_REWARD\", rewardId, key, \"REJECTED\", undefined, error);\n return jsonResponse({ ok: false, error }, 422);\n }\n\n async #syncProgress(request: Request): Promise<Response> {\n const authenticatedUser = await this.#authenticate(request);\n const body = await this.#parse<SyncRequest>(request);\n if (body === null || body.userId === undefined)\n return errorResponse(\"INVALID_REQUEST\", \"userId is required.\", 400);\n if (authenticatedUser !== null && authenticatedUser !== body.userId)\n return errorResponse(\"UNAUTHORIZED\", \"User identity does not match authentication.\", 401);\n const queue = body.queue ?? body.operations ?? [];\n for (const operation of queue) {\n const userId = operation.userId ?? body.userId;\n const spotId = operation.spotId ?? operation.stampId;\n const claimMethod = operation.claimMethod ?? operation.context?.type;\n if (!hasText(userId) || !hasText(spotId) || !hasText(claimMethod)) continue;\n const synthetic = new Request(new URL(\"/api/check-in\", request.url), {\n method: \"POST\",\n body: JSON.stringify({\n userId,\n spotId,\n claimMethod,\n proofData: operation.proofData ?? proofFromContext(operation.context),\n idempotencyKey: operation.idempotencyKey,\n }),\n headers: { \"content-type\": \"application/json\" },\n });\n await this.#verifyCheckIn(synthetic);\n }\n const timestamp = now();\n const state =\n (await this.#storage.getUserState(body.userId)) ?? emptyState(this.#config, timestamp);\n await this.#storage.saveUserState(body.userId, state);\n return jsonResponse({ ok: true, state, accepted: queue.length });\n }\n\n async #audit(\n userId: string,\n action: RallyAuditLog[\"action\"],\n resourceId: string,\n idempotencyKey: string,\n status: RallyAuditLog[\"status\"],\n proofData?: unknown,\n error?: unknown,\n ): Promise<void> {\n await this.#storage.recordAuditLog({\n id: id(\"audit\"),\n timestamp: now(),\n rallyId: this.#config.id,\n userId,\n action,\n resourceId,\n status,\n idempotencyKey,\n ...(proofData === undefined ? {} : { proofData }),\n ...(error === undefined\n ? {}\n : {\n metadata: {\n errorCode: isObject(error) && typeof error.code === \"string\" ? error.code : \"UNKNOWN\",\n },\n }),\n });\n }\n\n #remember(key: string, response: Response): Response {\n this.#idempotent.set(key, response.clone());\n return response;\n }\n}\n\nfunction emptyState(config: RallyConfig, timestamp: string): UserRallyState {\n return {\n rallyId: config.id,\n records: [],\n ...(config.rewards === undefined\n ? {}\n : { rewards: reconcileRewardStates(config.rewards, [], 0, timestamp) }),\n updatedAt: timestamp,\n };\n}\n\nexport type { StampError, StampRecord };\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stamprally/server",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Web Standard server handlers for stamp rally verification and claims.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/nitta-a/stamprally-core-app",
|
|
9
|
+
"directory": "packages/server"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/nitta-a/stamprally-core-app/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/nitta-a/stamprally-core-app#readme",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@stamprally/core": "0.5.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"vitest": "^4.1.11"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
43
|
+
}
|
|
44
|
+
}
|