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