@stamprally/core 0.4.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 +243 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -3
- package/dist/index.d.ts +70 -3
- package/dist/index.js +242 -68
- 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
|
}
|
|
@@ -261,10 +261,10 @@ function hash(value) {
|
|
|
261
261
|
return result.toString(36).toUpperCase().padStart(7, "0");
|
|
262
262
|
}
|
|
263
263
|
function createRandomHash() {
|
|
264
|
-
const
|
|
265
|
-
if (
|
|
264
|
+
const cryptoApi3 = globalThis.crypto;
|
|
265
|
+
if (cryptoApi3 !== void 0 && typeof cryptoApi3.getRandomValues === "function") {
|
|
266
266
|
const values = new Uint32Array(2);
|
|
267
|
-
|
|
267
|
+
cryptoApi3.getRandomValues(values);
|
|
268
268
|
return Array.from(values, (value) => value.toString(36).toUpperCase().padStart(7, "0")).join(
|
|
269
269
|
""
|
|
270
270
|
);
|
|
@@ -633,7 +633,8 @@ function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now)
|
|
|
633
633
|
if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(now)) {
|
|
634
634
|
return { rewardId: reward.id, status: "EXPIRED" };
|
|
635
635
|
}
|
|
636
|
-
|
|
636
|
+
const stockLimit = reward.stockLimit ?? reward.maxStock;
|
|
637
|
+
if (stockLimit !== void 0 && (current?.redeemedCount ?? 0) >= stockLimit) {
|
|
637
638
|
return {
|
|
638
639
|
rewardId: reward.id,
|
|
639
640
|
status: "EXPIRED",
|
|
@@ -670,10 +671,12 @@ function consumeReward(params) {
|
|
|
670
671
|
if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(params.now)) {
|
|
671
672
|
return { ok: false, error: { code: "EXPIRED", reason: "EXPIRED", rewardId: reward.id } };
|
|
672
673
|
}
|
|
673
|
-
|
|
674
|
+
const stockLimit = reward.stockLimit ?? reward.maxStock;
|
|
675
|
+
const userClaimLimit = reward.userClaimLimit ?? reward.limitPerUser;
|
|
676
|
+
if (stockLimit !== void 0 && (currentState.redeemedCount ?? 0) >= stockLimit) {
|
|
674
677
|
return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward.id } };
|
|
675
678
|
}
|
|
676
|
-
if (
|
|
679
|
+
if (userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= userClaimLimit) {
|
|
677
680
|
return {
|
|
678
681
|
ok: false,
|
|
679
682
|
error: { code: "USER_LIMIT_REACHED", reason: "LIMIT_EXCEEDED", rewardId: reward.id }
|
|
@@ -702,7 +705,7 @@ function consumeReward(params) {
|
|
|
702
705
|
status: "CONSUMED",
|
|
703
706
|
consumedAt: params.now,
|
|
704
707
|
claimTicketNumber: createUniqueClaimTicketNumber(reward.id, params.now),
|
|
705
|
-
...
|
|
708
|
+
...stockLimit !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
|
|
706
709
|
...params.userId === void 0 ? {} : { userRedemptionCount: (currentState.userRedemptionCount ?? 0) + 1 },
|
|
707
710
|
...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
|
|
708
711
|
}
|
|
@@ -1199,6 +1202,7 @@ var IndexedDBAdapter = class {
|
|
|
1199
1202
|
var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1200
1203
|
var StampRallyClient = class {
|
|
1201
1204
|
#listeners = /* @__PURE__ */ new Set();
|
|
1205
|
+
#eventListeners = /* @__PURE__ */ new Set();
|
|
1202
1206
|
#config;
|
|
1203
1207
|
#storage;
|
|
1204
1208
|
#clock;
|
|
@@ -1216,12 +1220,38 @@ var StampRallyClient = class {
|
|
|
1216
1220
|
getConfig() {
|
|
1217
1221
|
return this.#config;
|
|
1218
1222
|
}
|
|
1219
|
-
subscribe(listener) {
|
|
1223
|
+
subscribe(listener, options = {}) {
|
|
1224
|
+
if (options.events === true) {
|
|
1225
|
+
this.#eventListeners.add(listener);
|
|
1226
|
+
return () => this.#eventListeners.delete(listener);
|
|
1227
|
+
}
|
|
1220
1228
|
this.#listeners.add(listener);
|
|
1221
1229
|
return () => {
|
|
1222
1230
|
this.#listeners.delete(listener);
|
|
1223
1231
|
};
|
|
1224
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
|
+
}
|
|
1225
1255
|
init() {
|
|
1226
1256
|
return this.initialize();
|
|
1227
1257
|
}
|
|
@@ -1247,11 +1277,13 @@ var StampRallyClient = class {
|
|
|
1247
1277
|
const currentState = await this.initialize();
|
|
1248
1278
|
const result = processStamp(currentState, this.#config, stampId, context, now);
|
|
1249
1279
|
if (!result.ok) {
|
|
1280
|
+
this.#emitEvent({ type: "error", error: result.error });
|
|
1250
1281
|
return result;
|
|
1251
1282
|
}
|
|
1252
1283
|
await this.#storage.save(result.value.nextState);
|
|
1253
1284
|
this.#currentState = result.value.nextState;
|
|
1254
1285
|
this.#emit(result.value.nextState);
|
|
1286
|
+
this.#emitEvent({ type: "checkIn", stampId, state: result.value.nextState });
|
|
1255
1287
|
return result;
|
|
1256
1288
|
});
|
|
1257
1289
|
}
|
|
@@ -1301,6 +1333,9 @@ var StampRallyClient = class {
|
|
|
1301
1333
|
listener(state);
|
|
1302
1334
|
}
|
|
1303
1335
|
}
|
|
1336
|
+
#emitEvent(event) {
|
|
1337
|
+
for (const listener of this.#eventListeners) listener(event);
|
|
1338
|
+
}
|
|
1304
1339
|
#createEmptyState(now) {
|
|
1305
1340
|
const state = {
|
|
1306
1341
|
rallyId: this.#config.id,
|
|
@@ -1336,12 +1371,144 @@ var StampRallyClient = class {
|
|
|
1336
1371
|
}
|
|
1337
1372
|
};
|
|
1338
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
|
+
|
|
1339
1506
|
// src/domain/i18n.ts
|
|
1340
1507
|
function resolveLocalizedText(text2, locale, fallbackLocale) {
|
|
1341
1508
|
if (text2 === void 0 || text2 === "") return "";
|
|
1342
1509
|
if (typeof text2 === "string") return text2;
|
|
1343
|
-
const fallback = fallbackLocale
|
|
1344
|
-
return text2[locale] ||
|
|
1510
|
+
const fallback = fallbackLocale === void 0 ? Object.values(text2).find((value) => typeof value === "string") : text2[fallbackLocale];
|
|
1511
|
+
return text2[locale] || fallback || "";
|
|
1345
1512
|
}
|
|
1346
1513
|
function toLocalizedString(text2) {
|
|
1347
1514
|
if (text2 === void 0) return { ja: "", en: "" };
|
|
@@ -1443,6 +1610,11 @@ function validateSpot(value, index, errors) {
|
|
|
1443
1610
|
add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be an array of IDs.`);
|
|
1444
1611
|
}
|
|
1445
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
|
+
}
|
|
1446
1618
|
}
|
|
1447
1619
|
function validateReward(value, index, stampCount, errors) {
|
|
1448
1620
|
const path = `rewards[${index}]`;
|
|
@@ -1471,7 +1643,7 @@ function validateReward(value, index, stampCount, errors) {
|
|
|
1471
1643
|
if (value.validUntil !== void 0 && (typeof value.validUntil !== "string" || Number.isNaN(Date.parse(value.validUntil)))) {
|
|
1472
1644
|
add(errors, `${path}.validUntil`, "INVALID_DATE", "Reward expiry must be a valid ISO date.");
|
|
1473
1645
|
}
|
|
1474
|
-
for (const field of ["maxStock", "limitPerUser"]) {
|
|
1646
|
+
for (const field of ["maxStock", "limitPerUser", "stockLimit", "userClaimLimit"]) {
|
|
1475
1647
|
const count = value[field];
|
|
1476
1648
|
if (count !== void 0 && (typeof count !== "number" || !Number.isInteger(count) || count <= 0)) {
|
|
1477
1649
|
add(errors, `${path}.${field}`, "INVALID_REWARD", `${field} must be a positive integer.`);
|
|
@@ -1631,66 +1803,68 @@ function condition(value) {
|
|
|
1631
1803
|
}
|
|
1632
1804
|
}
|
|
1633
1805
|
function migrateSpot(value, index) {
|
|
1634
|
-
const
|
|
1635
|
-
const id = typeof
|
|
1636
|
-
const description = text(
|
|
1637
|
-
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);
|
|
1638
1810
|
return {
|
|
1639
1811
|
id,
|
|
1640
|
-
name: text(
|
|
1812
|
+
name: text(source2.name) ?? `Spot ${index + 1}`,
|
|
1641
1813
|
...description === void 0 ? {} : { description },
|
|
1642
1814
|
...hint === void 0 ? {} : { hint },
|
|
1643
1815
|
condition: condition(
|
|
1644
|
-
|
|
1816
|
+
source2.condition ?? (typeof source2.token === "string" ? { type: "token", token: source2.token } : void 0)
|
|
1645
1817
|
),
|
|
1646
|
-
...typeof
|
|
1647
|
-
...typeof
|
|
1648
|
-
...typeof
|
|
1649
|
-
...typeof
|
|
1650
|
-
...typeof
|
|
1651
|
-
...typeof
|
|
1652
|
-
...typeof
|
|
1653
|
-
...typeof
|
|
1654
|
-
...isObject2(
|
|
1655
|
-
...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") } : {}
|
|
1656
1828
|
};
|
|
1657
1829
|
}
|
|
1658
1830
|
function migrateReward(value, index) {
|
|
1659
|
-
const
|
|
1831
|
+
const source2 = isObject2(value) ? value : {};
|
|
1660
1832
|
return {
|
|
1661
|
-
id: typeof
|
|
1662
|
-
title: text(
|
|
1663
|
-
description: text(
|
|
1664
|
-
type:
|
|
1665
|
-
redemptionMethod:
|
|
1666
|
-
requiredStampCount: typeof
|
|
1667
|
-
...typeof
|
|
1668
|
-
...typeof
|
|
1669
|
-
...typeof
|
|
1670
|
-
...typeof
|
|
1671
|
-
...typeof
|
|
1672
|
-
...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 } : {}
|
|
1673
1847
|
};
|
|
1674
1848
|
}
|
|
1675
1849
|
function migrateRallyConfig(raw) {
|
|
1676
|
-
const
|
|
1677
|
-
const rawStamps = Array.isArray(
|
|
1678
|
-
const rawRewards = Array.isArray(
|
|
1679
|
-
const id = typeof
|
|
1680
|
-
const title = text(
|
|
1681
|
-
const description = text(
|
|
1682
|
-
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;
|
|
1683
1857
|
return {
|
|
1684
1858
|
id,
|
|
1685
1859
|
...title === void 0 ? {} : { title },
|
|
1686
1860
|
...description === void 0 ? {} : { description },
|
|
1687
1861
|
stamps: rawStamps.map(migrateSpot),
|
|
1688
1862
|
...rawRewards.length === 0 ? {} : { rewards: rawRewards.map(migrateReward) },
|
|
1689
|
-
...typeof
|
|
1863
|
+
...typeof source2.isSequential === "boolean" ? { isSequential: source2.isSequential } : {},
|
|
1690
1864
|
...theme === void 0 ? {} : { theme },
|
|
1691
1865
|
version: CURRENT_RALLY_CONFIG_VERSION,
|
|
1692
|
-
...typeof
|
|
1693
|
-
...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 } : {}
|
|
1694
1868
|
};
|
|
1695
1869
|
}
|
|
1696
1870
|
|
|
@@ -1815,22 +1989,22 @@ var THEME_PRESETS = [
|
|
|
1815
1989
|
];
|
|
1816
1990
|
|
|
1817
1991
|
// src/security/snapshotToken.ts
|
|
1818
|
-
var
|
|
1819
|
-
function
|
|
1992
|
+
var encoder2 = new TextEncoder();
|
|
1993
|
+
function cryptoApi2() {
|
|
1820
1994
|
if (globalThis.crypto === void 0 || globalThis.crypto.subtle === void 0) {
|
|
1821
1995
|
throw new Error("Web Crypto API is unavailable in this environment.");
|
|
1822
1996
|
}
|
|
1823
1997
|
return globalThis.crypto;
|
|
1824
1998
|
}
|
|
1825
1999
|
function secretBytes(secretKey) {
|
|
1826
|
-
return typeof secretKey === "string" ?
|
|
2000
|
+
return typeof secretKey === "string" ? encoder2.encode(secretKey) : new Uint8Array(secretKey);
|
|
1827
2001
|
}
|
|
1828
|
-
function webCryptoBytes(
|
|
1829
|
-
return
|
|
2002
|
+
function webCryptoBytes(bytes2) {
|
|
2003
|
+
return bytes2.buffer;
|
|
1830
2004
|
}
|
|
1831
|
-
function base64Url(
|
|
2005
|
+
function base64Url(bytes2) {
|
|
1832
2006
|
let binary = "";
|
|
1833
|
-
for (const byte of
|
|
2007
|
+
for (const byte of bytes2) binary += String.fromCharCode(byte);
|
|
1834
2008
|
return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
1835
2009
|
}
|
|
1836
2010
|
function fromBase64Url(value) {
|
|
@@ -1840,7 +2014,7 @@ function fromBase64Url(value) {
|
|
|
1840
2014
|
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
1841
2015
|
}
|
|
1842
2016
|
async function importHmacKey(secret) {
|
|
1843
|
-
return
|
|
2017
|
+
return cryptoApi2().subtle.importKey(
|
|
1844
2018
|
"raw",
|
|
1845
2019
|
webCryptoBytes(secret),
|
|
1846
2020
|
{ name: "HMAC", hash: "SHA-256" },
|
|
@@ -1849,18 +2023,18 @@ async function importHmacKey(secret) {
|
|
|
1849
2023
|
);
|
|
1850
2024
|
}
|
|
1851
2025
|
async function importAesKey(secret) {
|
|
1852
|
-
const digest = await
|
|
1853
|
-
return
|
|
2026
|
+
const digest = await cryptoApi2().subtle.digest("SHA-256", webCryptoBytes(secret));
|
|
2027
|
+
return cryptoApi2().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
1854
2028
|
}
|
|
1855
2029
|
function isExpired(payload, now) {
|
|
1856
2030
|
if (typeof payload.exp === "number" && now >= payload.exp * 1e3) return true;
|
|
1857
2031
|
return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now;
|
|
1858
2032
|
}
|
|
1859
2033
|
async function createSignedSnapshotToken(payload, secretKey) {
|
|
1860
|
-
const api =
|
|
2034
|
+
const api = cryptoApi2();
|
|
1861
2035
|
const secret = secretBytes(secretKey);
|
|
1862
2036
|
const iv = api.getRandomValues(new Uint8Array(12));
|
|
1863
|
-
const plaintext =
|
|
2037
|
+
const plaintext = encoder2.encode(JSON.stringify(payload));
|
|
1864
2038
|
const encrypted = new Uint8Array(
|
|
1865
2039
|
await api.subtle.encrypt(
|
|
1866
2040
|
{ name: "AES-GCM", iv: webCryptoBytes(iv) },
|
|
@@ -1870,7 +2044,7 @@ async function createSignedSnapshotToken(payload, secretKey) {
|
|
|
1870
2044
|
);
|
|
1871
2045
|
const body = base64Url(new Uint8Array([...iv, ...encrypted]));
|
|
1872
2046
|
const signature = new Uint8Array(
|
|
1873
|
-
await api.subtle.sign("HMAC", await importHmacKey(secret),
|
|
2047
|
+
await api.subtle.sign("HMAC", await importHmacKey(secret), encoder2.encode(body))
|
|
1874
2048
|
);
|
|
1875
2049
|
return `sr2.${body}.${base64Url(signature)}`;
|
|
1876
2050
|
}
|
|
@@ -1893,11 +2067,11 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1893
2067
|
};
|
|
1894
2068
|
}
|
|
1895
2069
|
const secret = secretBytes(secretKey);
|
|
1896
|
-
const validSignature = await
|
|
2070
|
+
const validSignature = await cryptoApi2().subtle.verify(
|
|
1897
2071
|
"HMAC",
|
|
1898
2072
|
await importHmacKey(secret),
|
|
1899
2073
|
webCryptoBytes(fromBase64Url(encodedSignature)),
|
|
1900
|
-
webCryptoBytes(
|
|
2074
|
+
webCryptoBytes(encoder2.encode(body))
|
|
1901
2075
|
);
|
|
1902
2076
|
if (!validSignature) {
|
|
1903
2077
|
return {
|
|
@@ -1908,7 +2082,7 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1908
2082
|
}
|
|
1909
2083
|
const encrypted = fromBase64Url(body);
|
|
1910
2084
|
if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
|
|
1911
|
-
const payloadText = await
|
|
2085
|
+
const payloadText = await cryptoApi2().subtle.decrypt(
|
|
1912
2086
|
{ name: "AES-GCM", iv: webCryptoBytes(encrypted.slice(0, 12)) },
|
|
1913
2087
|
await importAesKey(secret),
|
|
1914
2088
|
webCryptoBytes(encrypted.slice(12))
|
|
@@ -1945,6 +2119,7 @@ exports.calculateDistanceMeters = calculateDistanceMeters;
|
|
|
1945
2119
|
exports.calculateProgress = calculateProgress;
|
|
1946
2120
|
exports.consumeReward = consumeReward;
|
|
1947
2121
|
exports.createClaimTicketNumber = createClaimTicketNumber;
|
|
2122
|
+
exports.createSecureToken = createSecureToken;
|
|
1948
2123
|
exports.createSignedSnapshotToken = createSignedSnapshotToken;
|
|
1949
2124
|
exports.createUniqueClaimTicketNumber = createUniqueClaimTicketNumber;
|
|
1950
2125
|
exports.evaluateCheckIn = evaluateCheckIn;
|
|
@@ -1970,6 +2145,7 @@ exports.stripSensitiveConfig = stripSensitiveConfig;
|
|
|
1970
2145
|
exports.toLocalizedString = toLocalizedString;
|
|
1971
2146
|
exports.validateRallyConfig = validateRallyConfig;
|
|
1972
2147
|
exports.verifyPasscode = verifyPasscode;
|
|
2148
|
+
exports.verifySecureToken = verifySecureToken;
|
|
1973
2149
|
exports.verifySnapshotToken = verifySnapshotToken;
|
|
1974
2150
|
//# sourceMappingURL=index.cjs.map
|
|
1975
2151
|
//# sourceMappingURL=index.cjs.map
|