@stamprally/core 0.3.0 → 0.5.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/dist/index.cjs +263 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -3
- package/dist/index.d.ts +74 -3
- package/dist/index.js +261 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -225,7 +225,7 @@ function evaluateCheckIn(spot, input) {
|
|
|
225
225
|
// src/engine/order.ts
|
|
226
226
|
function getOrderedStamps(config) {
|
|
227
227
|
return config.stamps.map((stamp, index) => ({ stamp, index })).sort((left, right) => {
|
|
228
|
-
const orderDifference = (left.stamp.order ?? Number.POSITIVE_INFINITY) - (right.stamp.order ?? Number.POSITIVE_INFINITY);
|
|
228
|
+
const orderDifference = (left.stamp.orderIndex ?? left.stamp.order ?? Number.POSITIVE_INFINITY) - (right.stamp.orderIndex ?? right.stamp.order ?? Number.POSITIVE_INFINITY);
|
|
229
229
|
return orderDifference === 0 ? left.index - right.index : orderDifference;
|
|
230
230
|
}).map(({ stamp }) => stamp);
|
|
231
231
|
}
|
|
@@ -260,11 +260,27 @@ function hash(value) {
|
|
|
260
260
|
}
|
|
261
261
|
return result.toString(36).toUpperCase().padStart(7, "0");
|
|
262
262
|
}
|
|
263
|
+
function createRandomHash() {
|
|
264
|
+
const cryptoApi3 = globalThis.crypto;
|
|
265
|
+
if (cryptoApi3 !== void 0 && typeof cryptoApi3.getRandomValues === "function") {
|
|
266
|
+
const values = new Uint32Array(2);
|
|
267
|
+
cryptoApi3.getRandomValues(values);
|
|
268
|
+
return Array.from(values, (value) => value.toString(36).toUpperCase().padStart(7, "0")).join(
|
|
269
|
+
""
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return `${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).slice(2).toUpperCase()}`;
|
|
273
|
+
}
|
|
263
274
|
function createClaimTicketNumber(rewardId, options = {}) {
|
|
264
275
|
const issuedAt = options.issuedAt ?? "";
|
|
265
276
|
const sequence = options.sequence ?? 0;
|
|
266
277
|
return `SR-${hash(`${rewardId}|${issuedAt}|${sequence}`)}`;
|
|
267
278
|
}
|
|
279
|
+
function createUniqueClaimTicketNumber(rewardId, issuedAt) {
|
|
280
|
+
const timestamp = Date.parse(issuedAt);
|
|
281
|
+
const timestampPart = Number.isNaN(timestamp) ? Date.now() : timestamp;
|
|
282
|
+
return `CLAIM-${rewardId}-${timestampPart}-${createRandomHash()}`;
|
|
283
|
+
}
|
|
268
284
|
function issueClaimTicketNumber(reward, currentState, options = {}) {
|
|
269
285
|
if (currentState.claimTicketNumber !== void 0) return currentState;
|
|
270
286
|
const claimTicketNumber = reward.claimTicketNumber ?? createClaimTicketNumber(reward.id, options);
|
|
@@ -617,7 +633,8 @@ function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now)
|
|
|
617
633
|
if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(now)) {
|
|
618
634
|
return { rewardId: reward.id, status: "EXPIRED" };
|
|
619
635
|
}
|
|
620
|
-
|
|
636
|
+
const stockLimit = reward.stockLimit ?? reward.maxStock;
|
|
637
|
+
if (stockLimit !== void 0 && (current?.redeemedCount ?? 0) >= stockLimit) {
|
|
621
638
|
return {
|
|
622
639
|
rewardId: reward.id,
|
|
623
640
|
status: "EXPIRED",
|
|
@@ -652,13 +669,18 @@ function consumeReward(params) {
|
|
|
652
669
|
};
|
|
653
670
|
}
|
|
654
671
|
if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(params.now)) {
|
|
655
|
-
return { ok: false, error: { code: "EXPIRED", rewardId: reward.id } };
|
|
672
|
+
return { ok: false, error: { code: "EXPIRED", reason: "EXPIRED", rewardId: reward.id } };
|
|
656
673
|
}
|
|
657
|
-
|
|
674
|
+
const stockLimit = reward.stockLimit ?? reward.maxStock;
|
|
675
|
+
const userClaimLimit = reward.userClaimLimit ?? reward.limitPerUser;
|
|
676
|
+
if (stockLimit !== void 0 && (currentState.redeemedCount ?? 0) >= stockLimit) {
|
|
658
677
|
return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward.id } };
|
|
659
678
|
}
|
|
660
|
-
if (
|
|
661
|
-
return {
|
|
679
|
+
if (userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= userClaimLimit) {
|
|
680
|
+
return {
|
|
681
|
+
ok: false,
|
|
682
|
+
error: { code: "USER_LIMIT_REACHED", reason: "LIMIT_EXCEEDED", rewardId: reward.id }
|
|
683
|
+
};
|
|
662
684
|
}
|
|
663
685
|
if (reward.redemptionMethod === "staff_passcode") {
|
|
664
686
|
const passcodeResult = reward.staffPasscode === void 0 ? null : verifyPasscode(params.inputPasscode ?? "", { passcode: reward.staffPasscode });
|
|
@@ -682,7 +704,8 @@ function consumeReward(params) {
|
|
|
682
704
|
...currentState,
|
|
683
705
|
status: "CONSUMED",
|
|
684
706
|
consumedAt: params.now,
|
|
685
|
-
|
|
707
|
+
claimTicketNumber: createUniqueClaimTicketNumber(reward.id, params.now),
|
|
708
|
+
...stockLimit !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
|
|
686
709
|
...params.userId === void 0 ? {} : { userRedemptionCount: (currentState.userRedemptionCount ?? 0) + 1 },
|
|
687
710
|
...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
|
|
688
711
|
}
|
|
@@ -1179,6 +1202,7 @@ var IndexedDBAdapter = class {
|
|
|
1179
1202
|
var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1180
1203
|
var StampRallyClient = class {
|
|
1181
1204
|
#listeners = /* @__PURE__ */ new Set();
|
|
1205
|
+
#eventListeners = /* @__PURE__ */ new Set();
|
|
1182
1206
|
#config;
|
|
1183
1207
|
#storage;
|
|
1184
1208
|
#clock;
|
|
@@ -1196,12 +1220,38 @@ var StampRallyClient = class {
|
|
|
1196
1220
|
getConfig() {
|
|
1197
1221
|
return this.#config;
|
|
1198
1222
|
}
|
|
1199
|
-
subscribe(listener) {
|
|
1223
|
+
subscribe(listener, options = {}) {
|
|
1224
|
+
if (options.events === true) {
|
|
1225
|
+
this.#eventListeners.add(listener);
|
|
1226
|
+
return () => this.#eventListeners.delete(listener);
|
|
1227
|
+
}
|
|
1200
1228
|
this.#listeners.add(listener);
|
|
1201
1229
|
return () => {
|
|
1202
1230
|
this.#listeners.delete(listener);
|
|
1203
1231
|
};
|
|
1204
1232
|
}
|
|
1233
|
+
subscribeEvents(listener) {
|
|
1234
|
+
this.#eventListeners.add(listener);
|
|
1235
|
+
return () => this.#eventListeners.delete(listener);
|
|
1236
|
+
}
|
|
1237
|
+
async updateConfig(newConfig) {
|
|
1238
|
+
return this.#enqueue(async () => {
|
|
1239
|
+
const current = await this.initialize();
|
|
1240
|
+
this.#config = newConfig;
|
|
1241
|
+
const next = this.#reconcileState(cloneState(current), this.#clock());
|
|
1242
|
+
await this.#storage.save(next);
|
|
1243
|
+
this.#currentState = next;
|
|
1244
|
+
this.#initialization = Promise.resolve(next);
|
|
1245
|
+
this.#emit(next);
|
|
1246
|
+
return next;
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
notifyRewardClaimed(rewardId, state = this.#currentState) {
|
|
1250
|
+
if (state !== null) this.#emitEvent({ type: "rewardClaimed", rewardId, state });
|
|
1251
|
+
}
|
|
1252
|
+
notifySyncCompleted(state = this.#currentState) {
|
|
1253
|
+
if (state !== null) this.#emitEvent({ type: "syncCompleted", state });
|
|
1254
|
+
}
|
|
1205
1255
|
init() {
|
|
1206
1256
|
return this.initialize();
|
|
1207
1257
|
}
|
|
@@ -1227,11 +1277,13 @@ var StampRallyClient = class {
|
|
|
1227
1277
|
const currentState = await this.initialize();
|
|
1228
1278
|
const result = processStamp(currentState, this.#config, stampId, context, now);
|
|
1229
1279
|
if (!result.ok) {
|
|
1280
|
+
this.#emitEvent({ type: "error", error: result.error });
|
|
1230
1281
|
return result;
|
|
1231
1282
|
}
|
|
1232
1283
|
await this.#storage.save(result.value.nextState);
|
|
1233
1284
|
this.#currentState = result.value.nextState;
|
|
1234
1285
|
this.#emit(result.value.nextState);
|
|
1286
|
+
this.#emitEvent({ type: "checkIn", stampId, state: result.value.nextState });
|
|
1235
1287
|
return result;
|
|
1236
1288
|
});
|
|
1237
1289
|
}
|
|
@@ -1281,6 +1333,9 @@ var StampRallyClient = class {
|
|
|
1281
1333
|
listener(state);
|
|
1282
1334
|
}
|
|
1283
1335
|
}
|
|
1336
|
+
#emitEvent(event) {
|
|
1337
|
+
for (const listener of this.#eventListeners) listener(event);
|
|
1338
|
+
}
|
|
1284
1339
|
#createEmptyState(now) {
|
|
1285
1340
|
const state = {
|
|
1286
1341
|
rallyId: this.#config.id,
|
|
@@ -1316,12 +1371,144 @@ var StampRallyClient = class {
|
|
|
1316
1371
|
}
|
|
1317
1372
|
};
|
|
1318
1373
|
|
|
1374
|
+
// src/crypto/token.ts
|
|
1375
|
+
var encoder = new TextEncoder();
|
|
1376
|
+
var decoder = new TextDecoder();
|
|
1377
|
+
function cryptoApi() {
|
|
1378
|
+
if (globalThis.crypto?.subtle === void 0) {
|
|
1379
|
+
throw new Error("Web Crypto API is unavailable in this environment.");
|
|
1380
|
+
}
|
|
1381
|
+
return globalThis.crypto;
|
|
1382
|
+
}
|
|
1383
|
+
function bytes(value) {
|
|
1384
|
+
return typeof value === "string" ? encoder.encode(value) : new Uint8Array(value);
|
|
1385
|
+
}
|
|
1386
|
+
function source(value) {
|
|
1387
|
+
return value.buffer;
|
|
1388
|
+
}
|
|
1389
|
+
function encode(value) {
|
|
1390
|
+
let binary = "";
|
|
1391
|
+
for (const byte of value) binary += String.fromCharCode(byte);
|
|
1392
|
+
return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
1393
|
+
}
|
|
1394
|
+
function decode(value) {
|
|
1395
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
1396
|
+
const padded = `${base64}${"=".repeat((4 - base64.length % 4) % 4)}`;
|
|
1397
|
+
return Uint8Array.from(globalThis.atob(padded), (character) => character.charCodeAt(0));
|
|
1398
|
+
}
|
|
1399
|
+
async function hmacKey(secret) {
|
|
1400
|
+
return cryptoApi().subtle.importKey(
|
|
1401
|
+
"raw",
|
|
1402
|
+
source(secret),
|
|
1403
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1404
|
+
false,
|
|
1405
|
+
["sign", "verify"]
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
async function aesKey(secret) {
|
|
1409
|
+
const digest = await cryptoApi().subtle.digest("SHA-256", source(secret));
|
|
1410
|
+
return cryptoApi().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
1411
|
+
}
|
|
1412
|
+
async function createSecureToken(payload, secretKey, options = {}) {
|
|
1413
|
+
const api = cryptoApi();
|
|
1414
|
+
const expiresInSeconds = options.expiresInSeconds;
|
|
1415
|
+
const effectivePayload = expiresInSeconds === void 0 || payload.exp !== void 0 ? payload : { ...payload, exp: Math.floor(Date.now() / 1e3) + expiresInSeconds };
|
|
1416
|
+
const secret = bytes(secretKey);
|
|
1417
|
+
const plaintext = encoder.encode(JSON.stringify(effectivePayload));
|
|
1418
|
+
const actualBody = options.encrypt === true ? await encryptPayload(plaintext, secret, api) : encode(plaintext);
|
|
1419
|
+
const signature = new Uint8Array(
|
|
1420
|
+
await api.subtle.sign("HMAC", await hmacKey(secret), encoder.encode(actualBody))
|
|
1421
|
+
);
|
|
1422
|
+
return `sr3.${options.encrypt === true ? "e" : "p"}.${actualBody}.${encode(signature)}`;
|
|
1423
|
+
}
|
|
1424
|
+
async function encryptPayload(plaintext, secret, api) {
|
|
1425
|
+
const iv = api.getRandomValues(new Uint8Array(12));
|
|
1426
|
+
const encrypted = new Uint8Array(
|
|
1427
|
+
await api.subtle.encrypt(
|
|
1428
|
+
{ name: "AES-GCM", iv: source(iv) },
|
|
1429
|
+
await aesKey(secret),
|
|
1430
|
+
source(plaintext)
|
|
1431
|
+
)
|
|
1432
|
+
);
|
|
1433
|
+
return encode(new Uint8Array([...iv, ...encrypted]));
|
|
1434
|
+
}
|
|
1435
|
+
async function verifySecureToken(token, secretKey, now = Date.now()) {
|
|
1436
|
+
try {
|
|
1437
|
+
const parts = token.split(".");
|
|
1438
|
+
if (parts.length !== 4 || parts[0] !== "sr3" || parts[1] !== "e" && parts[1] !== "p") {
|
|
1439
|
+
return {
|
|
1440
|
+
ok: false,
|
|
1441
|
+
valid: false,
|
|
1442
|
+
error: { code: "MALFORMED", message: "Secure token is malformed." }
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
const mode = parts[1];
|
|
1446
|
+
const body = parts[2];
|
|
1447
|
+
const encodedSignature = parts[3];
|
|
1448
|
+
if (body === void 0 || encodedSignature === void 0 || mode === void 0) {
|
|
1449
|
+
return {
|
|
1450
|
+
ok: false,
|
|
1451
|
+
valid: false,
|
|
1452
|
+
error: { code: "MALFORMED", message: "Secure token is malformed." }
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const secret = bytes(secretKey);
|
|
1456
|
+
const valid = await cryptoApi().subtle.verify(
|
|
1457
|
+
"HMAC",
|
|
1458
|
+
await hmacKey(secret),
|
|
1459
|
+
source(decode(encodedSignature)),
|
|
1460
|
+
source(encoder.encode(body))
|
|
1461
|
+
);
|
|
1462
|
+
if (!valid)
|
|
1463
|
+
return {
|
|
1464
|
+
ok: false,
|
|
1465
|
+
valid: false,
|
|
1466
|
+
error: { code: "INVALID_SIGNATURE", message: "Secure token signature is invalid." }
|
|
1467
|
+
};
|
|
1468
|
+
const plaintext = mode === "e" ? await decryptPayload(body, secret) : decode(body);
|
|
1469
|
+
const parsed = JSON.parse(decoder.decode(plaintext));
|
|
1470
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1471
|
+
return {
|
|
1472
|
+
ok: false,
|
|
1473
|
+
valid: false,
|
|
1474
|
+
error: { code: "INVALID_PAYLOAD", message: "Secure token payload is invalid." }
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
const payload = parsed;
|
|
1478
|
+
if (typeof payload.exp === "number" && now >= payload.exp * 1e3) {
|
|
1479
|
+
return {
|
|
1480
|
+
ok: false,
|
|
1481
|
+
valid: false,
|
|
1482
|
+
error: { code: "EXPIRED", message: "Secure token has expired." }
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
return { ok: true, valid: true, payload };
|
|
1486
|
+
} catch {
|
|
1487
|
+
return {
|
|
1488
|
+
ok: false,
|
|
1489
|
+
valid: false,
|
|
1490
|
+
error: { code: "DECRYPTION_FAILED", message: "Secure token could not be verified." }
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async function decryptPayload(body, secret) {
|
|
1495
|
+
const encrypted = decode(body);
|
|
1496
|
+
if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
|
|
1497
|
+
return new Uint8Array(
|
|
1498
|
+
await cryptoApi().subtle.decrypt(
|
|
1499
|
+
{ name: "AES-GCM", iv: source(encrypted.slice(0, 12)) },
|
|
1500
|
+
await aesKey(secret),
|
|
1501
|
+
source(encrypted.slice(12))
|
|
1502
|
+
)
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1319
1506
|
// src/domain/i18n.ts
|
|
1320
1507
|
function resolveLocalizedText(text2, locale, fallbackLocale) {
|
|
1321
1508
|
if (text2 === void 0 || text2 === "") return "";
|
|
1322
1509
|
if (typeof text2 === "string") return text2;
|
|
1323
|
-
const fallback = fallbackLocale
|
|
1324
|
-
return text2[locale] ||
|
|
1510
|
+
const fallback = fallbackLocale === void 0 ? Object.values(text2).find((value) => typeof value === "string") : text2[fallbackLocale];
|
|
1511
|
+
return text2[locale] || fallback || "";
|
|
1325
1512
|
}
|
|
1326
1513
|
function toLocalizedString(text2) {
|
|
1327
1514
|
if (text2 === void 0) return { ja: "", en: "" };
|
|
@@ -1423,6 +1610,11 @@ function validateSpot(value, index, errors) {
|
|
|
1423
1610
|
add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be an array of IDs.`);
|
|
1424
1611
|
}
|
|
1425
1612
|
}
|
|
1613
|
+
for (const field of ["order", "orderIndex"]) {
|
|
1614
|
+
if (value[field] !== void 0 && (typeof value[field] !== "number" || !Number.isFinite(value[field]))) {
|
|
1615
|
+
add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be a finite number.`);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1426
1618
|
}
|
|
1427
1619
|
function validateReward(value, index, stampCount, errors) {
|
|
1428
1620
|
const path = `rewards[${index}]`;
|
|
@@ -1451,7 +1643,7 @@ function validateReward(value, index, stampCount, errors) {
|
|
|
1451
1643
|
if (value.validUntil !== void 0 && (typeof value.validUntil !== "string" || Number.isNaN(Date.parse(value.validUntil)))) {
|
|
1452
1644
|
add(errors, `${path}.validUntil`, "INVALID_DATE", "Reward expiry must be a valid ISO date.");
|
|
1453
1645
|
}
|
|
1454
|
-
for (const field of ["maxStock", "limitPerUser"]) {
|
|
1646
|
+
for (const field of ["maxStock", "limitPerUser", "stockLimit", "userClaimLimit"]) {
|
|
1455
1647
|
const count = value[field];
|
|
1456
1648
|
if (count !== void 0 && (typeof count !== "number" || !Number.isInteger(count) || count <= 0)) {
|
|
1457
1649
|
add(errors, `${path}.${field}`, "INVALID_REWARD", `${field} must be a positive integer.`);
|
|
@@ -1611,66 +1803,68 @@ function condition(value) {
|
|
|
1611
1803
|
}
|
|
1612
1804
|
}
|
|
1613
1805
|
function migrateSpot(value, index) {
|
|
1614
|
-
const
|
|
1615
|
-
const id = typeof
|
|
1616
|
-
const description = text(
|
|
1617
|
-
const hint = text(
|
|
1806
|
+
const source2 = isObject2(value) ? value : {};
|
|
1807
|
+
const id = typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : `spot-${index + 1}`;
|
|
1808
|
+
const description = text(source2.description);
|
|
1809
|
+
const hint = text(source2.hint);
|
|
1618
1810
|
return {
|
|
1619
1811
|
id,
|
|
1620
|
-
name: text(
|
|
1812
|
+
name: text(source2.name) ?? `Spot ${index + 1}`,
|
|
1621
1813
|
...description === void 0 ? {} : { description },
|
|
1622
1814
|
...hint === void 0 ? {} : { hint },
|
|
1623
1815
|
condition: condition(
|
|
1624
|
-
|
|
1816
|
+
source2.condition ?? (typeof source2.token === "string" ? { type: "token", token: source2.token } : void 0)
|
|
1625
1817
|
),
|
|
1626
|
-
...typeof
|
|
1627
|
-
...typeof
|
|
1628
|
-
...typeof
|
|
1629
|
-
...typeof
|
|
1630
|
-
...typeof
|
|
1631
|
-
...typeof
|
|
1632
|
-
...typeof
|
|
1633
|
-
...typeof
|
|
1634
|
-
...isObject2(
|
|
1635
|
-
...Array.isArray(
|
|
1818
|
+
...typeof source2.orderIndex === "number" ? { orderIndex: source2.orderIndex } : typeof source2.order === "number" ? { order: source2.order } : {},
|
|
1819
|
+
...typeof source2.deckId === "string" ? { deckId: source2.deckId } : {},
|
|
1820
|
+
...typeof source2.groupId === "string" ? { groupId: source2.groupId } : {},
|
|
1821
|
+
...typeof source2.guideId === "string" ? { guideId: source2.guideId } : {},
|
|
1822
|
+
...typeof source2.iconUrl === "string" ? { iconUrl: source2.iconUrl } : {},
|
|
1823
|
+
...typeof source2.imageUrl === "string" ? { imageUrl: source2.imageUrl } : {},
|
|
1824
|
+
...typeof source2.externalUrl === "string" ? { externalUrl: source2.externalUrl } : {},
|
|
1825
|
+
...typeof source2.redirectUrlAfterClaim === "string" ? { redirectUrlAfterClaim: source2.redirectUrlAfterClaim } : {},
|
|
1826
|
+
...isObject2(source2.metadata) ? { metadata: source2.metadata } : {},
|
|
1827
|
+
...Array.isArray(source2.dependsOn) ? { dependsOn: source2.dependsOn.filter((item) => typeof item === "string") } : {}
|
|
1636
1828
|
};
|
|
1637
1829
|
}
|
|
1638
1830
|
function migrateReward(value, index) {
|
|
1639
|
-
const
|
|
1831
|
+
const source2 = isObject2(value) ? value : {};
|
|
1640
1832
|
return {
|
|
1641
|
-
id: typeof
|
|
1642
|
-
title: text(
|
|
1643
|
-
description: text(
|
|
1644
|
-
type:
|
|
1645
|
-
redemptionMethod:
|
|
1646
|
-
requiredStampCount: typeof
|
|
1647
|
-
...typeof
|
|
1648
|
-
...typeof
|
|
1649
|
-
...typeof
|
|
1650
|
-
...typeof
|
|
1651
|
-
...typeof
|
|
1652
|
-
...typeof
|
|
1833
|
+
id: typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : `reward-${index + 1}`,
|
|
1834
|
+
title: text(source2.title) ?? `Reward ${index + 1}`,
|
|
1835
|
+
description: text(source2.description) ?? "",
|
|
1836
|
+
type: source2.type === "digital" ? "digital" : "in_person",
|
|
1837
|
+
redemptionMethod: source2.redemptionMethod === "staff_passcode" || source2.redemptionMethod === "view_only" || source2.redemptionMethod === "server_claim" ? source2.redemptionMethod : "manual_slide",
|
|
1838
|
+
requiredStampCount: typeof source2.requiredStampCount === "number" && Number.isFinite(source2.requiredStampCount) ? Math.max(0, Math.trunc(source2.requiredStampCount)) : 0,
|
|
1839
|
+
...typeof source2.digitalContentUrl === "string" ? { digitalContentUrl: source2.digitalContentUrl } : {},
|
|
1840
|
+
...typeof source2.staffPasscode === "string" ? { staffPasscode: source2.staffPasscode } : {},
|
|
1841
|
+
...typeof source2.validUntil === "string" ? { validUntil: source2.validUntil } : {},
|
|
1842
|
+
...typeof source2.maxStock === "number" ? { maxStock: source2.maxStock } : {},
|
|
1843
|
+
...typeof source2.limitPerUser === "number" ? { limitPerUser: source2.limitPerUser } : {},
|
|
1844
|
+
...typeof source2.stockLimit === "number" ? { stockLimit: source2.stockLimit } : {},
|
|
1845
|
+
...typeof source2.userClaimLimit === "number" ? { userClaimLimit: source2.userClaimLimit } : {},
|
|
1846
|
+
...typeof source2.claimTicketNumber === "string" ? { claimTicketNumber: source2.claimTicketNumber } : {}
|
|
1653
1847
|
};
|
|
1654
1848
|
}
|
|
1655
1849
|
function migrateRallyConfig(raw) {
|
|
1656
|
-
const
|
|
1657
|
-
const rawStamps = Array.isArray(
|
|
1658
|
-
const rawRewards = Array.isArray(
|
|
1659
|
-
const id = typeof
|
|
1660
|
-
const title = text(
|
|
1661
|
-
const description = text(
|
|
1662
|
-
const theme = isObject2(
|
|
1850
|
+
const source2 = isObject2(raw) ? raw : {};
|
|
1851
|
+
const rawStamps = Array.isArray(source2.stamps) ? source2.stamps : Array.isArray(source2.spots) ? source2.spots : [];
|
|
1852
|
+
const rawRewards = Array.isArray(source2.rewards) ? source2.rewards : [];
|
|
1853
|
+
const id = typeof source2.id === "string" && source2.id.trim() !== "" ? source2.id.trim() : "migrated-rally";
|
|
1854
|
+
const title = text(source2.title);
|
|
1855
|
+
const description = text(source2.description);
|
|
1856
|
+
const theme = isObject2(source2.theme) ? source2.theme : void 0;
|
|
1663
1857
|
return {
|
|
1664
1858
|
id,
|
|
1665
1859
|
...title === void 0 ? {} : { title },
|
|
1666
1860
|
...description === void 0 ? {} : { description },
|
|
1667
1861
|
stamps: rawStamps.map(migrateSpot),
|
|
1668
1862
|
...rawRewards.length === 0 ? {} : { rewards: rawRewards.map(migrateReward) },
|
|
1669
|
-
...typeof
|
|
1863
|
+
...typeof source2.isSequential === "boolean" ? { isSequential: source2.isSequential } : {},
|
|
1670
1864
|
...theme === void 0 ? {} : { theme },
|
|
1671
1865
|
version: CURRENT_RALLY_CONFIG_VERSION,
|
|
1672
|
-
...typeof
|
|
1673
|
-
...typeof
|
|
1866
|
+
...typeof source2.startDate === "string" ? { startDate: source2.startDate } : typeof source2.startsAt === "string" ? { startDate: source2.startsAt } : {},
|
|
1867
|
+
...typeof source2.endDate === "string" ? { endDate: source2.endDate } : typeof source2.endsAt === "string" ? { endDate: source2.endsAt } : {}
|
|
1674
1868
|
};
|
|
1675
1869
|
}
|
|
1676
1870
|
|
|
@@ -1795,22 +1989,22 @@ var THEME_PRESETS = [
|
|
|
1795
1989
|
];
|
|
1796
1990
|
|
|
1797
1991
|
// src/security/snapshotToken.ts
|
|
1798
|
-
var
|
|
1799
|
-
function
|
|
1992
|
+
var encoder2 = new TextEncoder();
|
|
1993
|
+
function cryptoApi2() {
|
|
1800
1994
|
if (globalThis.crypto === void 0 || globalThis.crypto.subtle === void 0) {
|
|
1801
1995
|
throw new Error("Web Crypto API is unavailable in this environment.");
|
|
1802
1996
|
}
|
|
1803
1997
|
return globalThis.crypto;
|
|
1804
1998
|
}
|
|
1805
1999
|
function secretBytes(secretKey) {
|
|
1806
|
-
return typeof secretKey === "string" ?
|
|
2000
|
+
return typeof secretKey === "string" ? encoder2.encode(secretKey) : new Uint8Array(secretKey);
|
|
1807
2001
|
}
|
|
1808
|
-
function webCryptoBytes(
|
|
1809
|
-
return
|
|
2002
|
+
function webCryptoBytes(bytes2) {
|
|
2003
|
+
return bytes2.buffer;
|
|
1810
2004
|
}
|
|
1811
|
-
function base64Url(
|
|
2005
|
+
function base64Url(bytes2) {
|
|
1812
2006
|
let binary = "";
|
|
1813
|
-
for (const byte of
|
|
2007
|
+
for (const byte of bytes2) binary += String.fromCharCode(byte);
|
|
1814
2008
|
return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
1815
2009
|
}
|
|
1816
2010
|
function fromBase64Url(value) {
|
|
@@ -1820,7 +2014,7 @@ function fromBase64Url(value) {
|
|
|
1820
2014
|
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
1821
2015
|
}
|
|
1822
2016
|
async function importHmacKey(secret) {
|
|
1823
|
-
return
|
|
2017
|
+
return cryptoApi2().subtle.importKey(
|
|
1824
2018
|
"raw",
|
|
1825
2019
|
webCryptoBytes(secret),
|
|
1826
2020
|
{ name: "HMAC", hash: "SHA-256" },
|
|
@@ -1829,18 +2023,18 @@ async function importHmacKey(secret) {
|
|
|
1829
2023
|
);
|
|
1830
2024
|
}
|
|
1831
2025
|
async function importAesKey(secret) {
|
|
1832
|
-
const digest = await
|
|
1833
|
-
return
|
|
2026
|
+
const digest = await cryptoApi2().subtle.digest("SHA-256", webCryptoBytes(secret));
|
|
2027
|
+
return cryptoApi2().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
1834
2028
|
}
|
|
1835
2029
|
function isExpired(payload, now) {
|
|
1836
2030
|
if (typeof payload.exp === "number" && now >= payload.exp * 1e3) return true;
|
|
1837
2031
|
return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now;
|
|
1838
2032
|
}
|
|
1839
2033
|
async function createSignedSnapshotToken(payload, secretKey) {
|
|
1840
|
-
const api =
|
|
2034
|
+
const api = cryptoApi2();
|
|
1841
2035
|
const secret = secretBytes(secretKey);
|
|
1842
2036
|
const iv = api.getRandomValues(new Uint8Array(12));
|
|
1843
|
-
const plaintext =
|
|
2037
|
+
const plaintext = encoder2.encode(JSON.stringify(payload));
|
|
1844
2038
|
const encrypted = new Uint8Array(
|
|
1845
2039
|
await api.subtle.encrypt(
|
|
1846
2040
|
{ name: "AES-GCM", iv: webCryptoBytes(iv) },
|
|
@@ -1850,7 +2044,7 @@ async function createSignedSnapshotToken(payload, secretKey) {
|
|
|
1850
2044
|
);
|
|
1851
2045
|
const body = base64Url(new Uint8Array([...iv, ...encrypted]));
|
|
1852
2046
|
const signature = new Uint8Array(
|
|
1853
|
-
await api.subtle.sign("HMAC", await importHmacKey(secret),
|
|
2047
|
+
await api.subtle.sign("HMAC", await importHmacKey(secret), encoder2.encode(body))
|
|
1854
2048
|
);
|
|
1855
2049
|
return `sr2.${body}.${base64Url(signature)}`;
|
|
1856
2050
|
}
|
|
@@ -1873,11 +2067,11 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1873
2067
|
};
|
|
1874
2068
|
}
|
|
1875
2069
|
const secret = secretBytes(secretKey);
|
|
1876
|
-
const validSignature = await
|
|
2070
|
+
const validSignature = await cryptoApi2().subtle.verify(
|
|
1877
2071
|
"HMAC",
|
|
1878
2072
|
await importHmacKey(secret),
|
|
1879
2073
|
webCryptoBytes(fromBase64Url(encodedSignature)),
|
|
1880
|
-
webCryptoBytes(
|
|
2074
|
+
webCryptoBytes(encoder2.encode(body))
|
|
1881
2075
|
);
|
|
1882
2076
|
if (!validSignature) {
|
|
1883
2077
|
return {
|
|
@@ -1888,7 +2082,7 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1888
2082
|
}
|
|
1889
2083
|
const encrypted = fromBase64Url(body);
|
|
1890
2084
|
if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
|
|
1891
|
-
const payloadText = await
|
|
2085
|
+
const payloadText = await cryptoApi2().subtle.decrypt(
|
|
1892
2086
|
{ name: "AES-GCM", iv: webCryptoBytes(encrypted.slice(0, 12)) },
|
|
1893
2087
|
await importAesKey(secret),
|
|
1894
2088
|
webCryptoBytes(encrypted.slice(12))
|
|
@@ -1925,7 +2119,9 @@ exports.calculateDistanceMeters = calculateDistanceMeters;
|
|
|
1925
2119
|
exports.calculateProgress = calculateProgress;
|
|
1926
2120
|
exports.consumeReward = consumeReward;
|
|
1927
2121
|
exports.createClaimTicketNumber = createClaimTicketNumber;
|
|
2122
|
+
exports.createSecureToken = createSecureToken;
|
|
1928
2123
|
exports.createSignedSnapshotToken = createSignedSnapshotToken;
|
|
2124
|
+
exports.createUniqueClaimTicketNumber = createUniqueClaimTicketNumber;
|
|
1929
2125
|
exports.evaluateCheckIn = evaluateCheckIn;
|
|
1930
2126
|
exports.evaluateCondition = evaluateCondition;
|
|
1931
2127
|
exports.evaluateConditionDetailed = evaluateConditionDetailed;
|
|
@@ -1949,6 +2145,7 @@ exports.stripSensitiveConfig = stripSensitiveConfig;
|
|
|
1949
2145
|
exports.toLocalizedString = toLocalizedString;
|
|
1950
2146
|
exports.validateRallyConfig = validateRallyConfig;
|
|
1951
2147
|
exports.verifyPasscode = verifyPasscode;
|
|
2148
|
+
exports.verifySecureToken = verifySecureToken;
|
|
1952
2149
|
exports.verifySnapshotToken = verifySnapshotToken;
|
|
1953
2150
|
//# sourceMappingURL=index.cjs.map
|
|
1954
2151
|
//# sourceMappingURL=index.cjs.map
|