@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.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
|
}
|
|
@@ -259,10 +259,10 @@ function hash(value) {
|
|
|
259
259
|
return result.toString(36).toUpperCase().padStart(7, "0");
|
|
260
260
|
}
|
|
261
261
|
function createRandomHash() {
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
262
|
+
const cryptoApi3 = globalThis.crypto;
|
|
263
|
+
if (cryptoApi3 !== void 0 && typeof cryptoApi3.getRandomValues === "function") {
|
|
264
264
|
const values = new Uint32Array(2);
|
|
265
|
-
|
|
265
|
+
cryptoApi3.getRandomValues(values);
|
|
266
266
|
return Array.from(values, (value) => value.toString(36).toUpperCase().padStart(7, "0")).join(
|
|
267
267
|
""
|
|
268
268
|
);
|
|
@@ -631,7 +631,8 @@ function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now)
|
|
|
631
631
|
if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(now)) {
|
|
632
632
|
return { rewardId: reward.id, status: "EXPIRED" };
|
|
633
633
|
}
|
|
634
|
-
|
|
634
|
+
const stockLimit = reward.stockLimit ?? reward.maxStock;
|
|
635
|
+
if (stockLimit !== void 0 && (current?.redeemedCount ?? 0) >= stockLimit) {
|
|
635
636
|
return {
|
|
636
637
|
rewardId: reward.id,
|
|
637
638
|
status: "EXPIRED",
|
|
@@ -668,10 +669,12 @@ function consumeReward(params) {
|
|
|
668
669
|
if (reward.validUntil !== void 0 && Date.parse(reward.validUntil) <= Date.parse(params.now)) {
|
|
669
670
|
return { ok: false, error: { code: "EXPIRED", reason: "EXPIRED", rewardId: reward.id } };
|
|
670
671
|
}
|
|
671
|
-
|
|
672
|
+
const stockLimit = reward.stockLimit ?? reward.maxStock;
|
|
673
|
+
const userClaimLimit = reward.userClaimLimit ?? reward.limitPerUser;
|
|
674
|
+
if (stockLimit !== void 0 && (currentState.redeemedCount ?? 0) >= stockLimit) {
|
|
672
675
|
return { ok: false, error: { code: "OUT_OF_STOCK", rewardId: reward.id } };
|
|
673
676
|
}
|
|
674
|
-
if (
|
|
677
|
+
if (userClaimLimit !== void 0 && (params.userRedemptionCount ?? currentState.userRedemptionCount ?? 0) >= userClaimLimit) {
|
|
675
678
|
return {
|
|
676
679
|
ok: false,
|
|
677
680
|
error: { code: "USER_LIMIT_REACHED", reason: "LIMIT_EXCEEDED", rewardId: reward.id }
|
|
@@ -700,7 +703,7 @@ function consumeReward(params) {
|
|
|
700
703
|
status: "CONSUMED",
|
|
701
704
|
consumedAt: params.now,
|
|
702
705
|
claimTicketNumber: createUniqueClaimTicketNumber(reward.id, params.now),
|
|
703
|
-
...
|
|
706
|
+
...stockLimit !== void 0 || params.userId !== void 0 || params.userRedemptionCount !== void 0 ? { redeemedCount: (currentState.redeemedCount ?? 0) + 1 } : {},
|
|
704
707
|
...params.userId === void 0 ? {} : { userRedemptionCount: (currentState.userRedemptionCount ?? 0) + 1 },
|
|
705
708
|
...params.staffId === void 0 ? {} : { consumedByStaffId: params.staffId }
|
|
706
709
|
}
|
|
@@ -1197,6 +1200,7 @@ var IndexedDBAdapter = class {
|
|
|
1197
1200
|
var systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1198
1201
|
var StampRallyClient = class {
|
|
1199
1202
|
#listeners = /* @__PURE__ */ new Set();
|
|
1203
|
+
#eventListeners = /* @__PURE__ */ new Set();
|
|
1200
1204
|
#config;
|
|
1201
1205
|
#storage;
|
|
1202
1206
|
#clock;
|
|
@@ -1214,12 +1218,38 @@ var StampRallyClient = class {
|
|
|
1214
1218
|
getConfig() {
|
|
1215
1219
|
return this.#config;
|
|
1216
1220
|
}
|
|
1217
|
-
subscribe(listener) {
|
|
1221
|
+
subscribe(listener, options = {}) {
|
|
1222
|
+
if (options.events === true) {
|
|
1223
|
+
this.#eventListeners.add(listener);
|
|
1224
|
+
return () => this.#eventListeners.delete(listener);
|
|
1225
|
+
}
|
|
1218
1226
|
this.#listeners.add(listener);
|
|
1219
1227
|
return () => {
|
|
1220
1228
|
this.#listeners.delete(listener);
|
|
1221
1229
|
};
|
|
1222
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
|
+
}
|
|
1223
1253
|
init() {
|
|
1224
1254
|
return this.initialize();
|
|
1225
1255
|
}
|
|
@@ -1245,11 +1275,13 @@ var StampRallyClient = class {
|
|
|
1245
1275
|
const currentState = await this.initialize();
|
|
1246
1276
|
const result = processStamp(currentState, this.#config, stampId, context, now);
|
|
1247
1277
|
if (!result.ok) {
|
|
1278
|
+
this.#emitEvent({ type: "error", error: result.error });
|
|
1248
1279
|
return result;
|
|
1249
1280
|
}
|
|
1250
1281
|
await this.#storage.save(result.value.nextState);
|
|
1251
1282
|
this.#currentState = result.value.nextState;
|
|
1252
1283
|
this.#emit(result.value.nextState);
|
|
1284
|
+
this.#emitEvent({ type: "checkIn", stampId, state: result.value.nextState });
|
|
1253
1285
|
return result;
|
|
1254
1286
|
});
|
|
1255
1287
|
}
|
|
@@ -1299,6 +1331,9 @@ var StampRallyClient = class {
|
|
|
1299
1331
|
listener(state);
|
|
1300
1332
|
}
|
|
1301
1333
|
}
|
|
1334
|
+
#emitEvent(event) {
|
|
1335
|
+
for (const listener of this.#eventListeners) listener(event);
|
|
1336
|
+
}
|
|
1302
1337
|
#createEmptyState(now) {
|
|
1303
1338
|
const state = {
|
|
1304
1339
|
rallyId: this.#config.id,
|
|
@@ -1334,12 +1369,144 @@ var StampRallyClient = class {
|
|
|
1334
1369
|
}
|
|
1335
1370
|
};
|
|
1336
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
|
+
|
|
1337
1504
|
// src/domain/i18n.ts
|
|
1338
1505
|
function resolveLocalizedText(text2, locale, fallbackLocale) {
|
|
1339
1506
|
if (text2 === void 0 || text2 === "") return "";
|
|
1340
1507
|
if (typeof text2 === "string") return text2;
|
|
1341
|
-
const fallback = fallbackLocale
|
|
1342
|
-
return text2[locale] ||
|
|
1508
|
+
const fallback = fallbackLocale === void 0 ? Object.values(text2).find((value) => typeof value === "string") : text2[fallbackLocale];
|
|
1509
|
+
return text2[locale] || fallback || "";
|
|
1343
1510
|
}
|
|
1344
1511
|
function toLocalizedString(text2) {
|
|
1345
1512
|
if (text2 === void 0) return { ja: "", en: "" };
|
|
@@ -1441,6 +1608,11 @@ function validateSpot(value, index, errors) {
|
|
|
1441
1608
|
add(errors, `${path}.${field}`, "INVALID_TYPE", `${field} must be an array of IDs.`);
|
|
1442
1609
|
}
|
|
1443
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
|
+
}
|
|
1444
1616
|
}
|
|
1445
1617
|
function validateReward(value, index, stampCount, errors) {
|
|
1446
1618
|
const path = `rewards[${index}]`;
|
|
@@ -1469,7 +1641,7 @@ function validateReward(value, index, stampCount, errors) {
|
|
|
1469
1641
|
if (value.validUntil !== void 0 && (typeof value.validUntil !== "string" || Number.isNaN(Date.parse(value.validUntil)))) {
|
|
1470
1642
|
add(errors, `${path}.validUntil`, "INVALID_DATE", "Reward expiry must be a valid ISO date.");
|
|
1471
1643
|
}
|
|
1472
|
-
for (const field of ["maxStock", "limitPerUser"]) {
|
|
1644
|
+
for (const field of ["maxStock", "limitPerUser", "stockLimit", "userClaimLimit"]) {
|
|
1473
1645
|
const count = value[field];
|
|
1474
1646
|
if (count !== void 0 && (typeof count !== "number" || !Number.isInteger(count) || count <= 0)) {
|
|
1475
1647
|
add(errors, `${path}.${field}`, "INVALID_REWARD", `${field} must be a positive integer.`);
|
|
@@ -1629,66 +1801,68 @@ function condition(value) {
|
|
|
1629
1801
|
}
|
|
1630
1802
|
}
|
|
1631
1803
|
function migrateSpot(value, index) {
|
|
1632
|
-
const
|
|
1633
|
-
const id = typeof
|
|
1634
|
-
const description = text(
|
|
1635
|
-
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);
|
|
1636
1808
|
return {
|
|
1637
1809
|
id,
|
|
1638
|
-
name: text(
|
|
1810
|
+
name: text(source2.name) ?? `Spot ${index + 1}`,
|
|
1639
1811
|
...description === void 0 ? {} : { description },
|
|
1640
1812
|
...hint === void 0 ? {} : { hint },
|
|
1641
1813
|
condition: condition(
|
|
1642
|
-
|
|
1814
|
+
source2.condition ?? (typeof source2.token === "string" ? { type: "token", token: source2.token } : void 0)
|
|
1643
1815
|
),
|
|
1644
|
-
...typeof
|
|
1645
|
-
...typeof
|
|
1646
|
-
...typeof
|
|
1647
|
-
...typeof
|
|
1648
|
-
...typeof
|
|
1649
|
-
...typeof
|
|
1650
|
-
...typeof
|
|
1651
|
-
...typeof
|
|
1652
|
-
...isObject2(
|
|
1653
|
-
...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") } : {}
|
|
1654
1826
|
};
|
|
1655
1827
|
}
|
|
1656
1828
|
function migrateReward(value, index) {
|
|
1657
|
-
const
|
|
1829
|
+
const source2 = isObject2(value) ? value : {};
|
|
1658
1830
|
return {
|
|
1659
|
-
id: typeof
|
|
1660
|
-
title: text(
|
|
1661
|
-
description: text(
|
|
1662
|
-
type:
|
|
1663
|
-
redemptionMethod:
|
|
1664
|
-
requiredStampCount: typeof
|
|
1665
|
-
...typeof
|
|
1666
|
-
...typeof
|
|
1667
|
-
...typeof
|
|
1668
|
-
...typeof
|
|
1669
|
-
...typeof
|
|
1670
|
-
...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 } : {}
|
|
1671
1845
|
};
|
|
1672
1846
|
}
|
|
1673
1847
|
function migrateRallyConfig(raw) {
|
|
1674
|
-
const
|
|
1675
|
-
const rawStamps = Array.isArray(
|
|
1676
|
-
const rawRewards = Array.isArray(
|
|
1677
|
-
const id = typeof
|
|
1678
|
-
const title = text(
|
|
1679
|
-
const description = text(
|
|
1680
|
-
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;
|
|
1681
1855
|
return {
|
|
1682
1856
|
id,
|
|
1683
1857
|
...title === void 0 ? {} : { title },
|
|
1684
1858
|
...description === void 0 ? {} : { description },
|
|
1685
1859
|
stamps: rawStamps.map(migrateSpot),
|
|
1686
1860
|
...rawRewards.length === 0 ? {} : { rewards: rawRewards.map(migrateReward) },
|
|
1687
|
-
...typeof
|
|
1861
|
+
...typeof source2.isSequential === "boolean" ? { isSequential: source2.isSequential } : {},
|
|
1688
1862
|
...theme === void 0 ? {} : { theme },
|
|
1689
1863
|
version: CURRENT_RALLY_CONFIG_VERSION,
|
|
1690
|
-
...typeof
|
|
1691
|
-
...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 } : {}
|
|
1692
1866
|
};
|
|
1693
1867
|
}
|
|
1694
1868
|
|
|
@@ -1813,22 +1987,22 @@ var THEME_PRESETS = [
|
|
|
1813
1987
|
];
|
|
1814
1988
|
|
|
1815
1989
|
// src/security/snapshotToken.ts
|
|
1816
|
-
var
|
|
1817
|
-
function
|
|
1990
|
+
var encoder2 = new TextEncoder();
|
|
1991
|
+
function cryptoApi2() {
|
|
1818
1992
|
if (globalThis.crypto === void 0 || globalThis.crypto.subtle === void 0) {
|
|
1819
1993
|
throw new Error("Web Crypto API is unavailable in this environment.");
|
|
1820
1994
|
}
|
|
1821
1995
|
return globalThis.crypto;
|
|
1822
1996
|
}
|
|
1823
1997
|
function secretBytes(secretKey) {
|
|
1824
|
-
return typeof secretKey === "string" ?
|
|
1998
|
+
return typeof secretKey === "string" ? encoder2.encode(secretKey) : new Uint8Array(secretKey);
|
|
1825
1999
|
}
|
|
1826
|
-
function webCryptoBytes(
|
|
1827
|
-
return
|
|
2000
|
+
function webCryptoBytes(bytes2) {
|
|
2001
|
+
return bytes2.buffer;
|
|
1828
2002
|
}
|
|
1829
|
-
function base64Url(
|
|
2003
|
+
function base64Url(bytes2) {
|
|
1830
2004
|
let binary = "";
|
|
1831
|
-
for (const byte of
|
|
2005
|
+
for (const byte of bytes2) binary += String.fromCharCode(byte);
|
|
1832
2006
|
return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
1833
2007
|
}
|
|
1834
2008
|
function fromBase64Url(value) {
|
|
@@ -1838,7 +2012,7 @@ function fromBase64Url(value) {
|
|
|
1838
2012
|
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
1839
2013
|
}
|
|
1840
2014
|
async function importHmacKey(secret) {
|
|
1841
|
-
return
|
|
2015
|
+
return cryptoApi2().subtle.importKey(
|
|
1842
2016
|
"raw",
|
|
1843
2017
|
webCryptoBytes(secret),
|
|
1844
2018
|
{ name: "HMAC", hash: "SHA-256" },
|
|
@@ -1847,18 +2021,18 @@ async function importHmacKey(secret) {
|
|
|
1847
2021
|
);
|
|
1848
2022
|
}
|
|
1849
2023
|
async function importAesKey(secret) {
|
|
1850
|
-
const digest = await
|
|
1851
|
-
return
|
|
2024
|
+
const digest = await cryptoApi2().subtle.digest("SHA-256", webCryptoBytes(secret));
|
|
2025
|
+
return cryptoApi2().subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
1852
2026
|
}
|
|
1853
2027
|
function isExpired(payload, now) {
|
|
1854
2028
|
if (typeof payload.exp === "number" && now >= payload.exp * 1e3) return true;
|
|
1855
2029
|
return payload.expiresAt !== void 0 && Date.parse(payload.expiresAt) <= now;
|
|
1856
2030
|
}
|
|
1857
2031
|
async function createSignedSnapshotToken(payload, secretKey) {
|
|
1858
|
-
const api =
|
|
2032
|
+
const api = cryptoApi2();
|
|
1859
2033
|
const secret = secretBytes(secretKey);
|
|
1860
2034
|
const iv = api.getRandomValues(new Uint8Array(12));
|
|
1861
|
-
const plaintext =
|
|
2035
|
+
const plaintext = encoder2.encode(JSON.stringify(payload));
|
|
1862
2036
|
const encrypted = new Uint8Array(
|
|
1863
2037
|
await api.subtle.encrypt(
|
|
1864
2038
|
{ name: "AES-GCM", iv: webCryptoBytes(iv) },
|
|
@@ -1868,7 +2042,7 @@ async function createSignedSnapshotToken(payload, secretKey) {
|
|
|
1868
2042
|
);
|
|
1869
2043
|
const body = base64Url(new Uint8Array([...iv, ...encrypted]));
|
|
1870
2044
|
const signature = new Uint8Array(
|
|
1871
|
-
await api.subtle.sign("HMAC", await importHmacKey(secret),
|
|
2045
|
+
await api.subtle.sign("HMAC", await importHmacKey(secret), encoder2.encode(body))
|
|
1872
2046
|
);
|
|
1873
2047
|
return `sr2.${body}.${base64Url(signature)}`;
|
|
1874
2048
|
}
|
|
@@ -1891,11 +2065,11 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1891
2065
|
};
|
|
1892
2066
|
}
|
|
1893
2067
|
const secret = secretBytes(secretKey);
|
|
1894
|
-
const validSignature = await
|
|
2068
|
+
const validSignature = await cryptoApi2().subtle.verify(
|
|
1895
2069
|
"HMAC",
|
|
1896
2070
|
await importHmacKey(secret),
|
|
1897
2071
|
webCryptoBytes(fromBase64Url(encodedSignature)),
|
|
1898
|
-
webCryptoBytes(
|
|
2072
|
+
webCryptoBytes(encoder2.encode(body))
|
|
1899
2073
|
);
|
|
1900
2074
|
if (!validSignature) {
|
|
1901
2075
|
return {
|
|
@@ -1906,7 +2080,7 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1906
2080
|
}
|
|
1907
2081
|
const encrypted = fromBase64Url(body);
|
|
1908
2082
|
if (encrypted.length <= 12) throw new Error("Invalid encrypted payload.");
|
|
1909
|
-
const payloadText = await
|
|
2083
|
+
const payloadText = await cryptoApi2().subtle.decrypt(
|
|
1910
2084
|
{ name: "AES-GCM", iv: webCryptoBytes(encrypted.slice(0, 12)) },
|
|
1911
2085
|
await importAesKey(secret),
|
|
1912
2086
|
webCryptoBytes(encrypted.slice(12))
|
|
@@ -1931,6 +2105,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
1931
2105
|
}
|
|
1932
2106
|
}
|
|
1933
2107
|
|
|
1934
|
-
export { CURRENT_RALLY_CONFIG_VERSION, DEFAULT_SHEET_THEME, InMemoryStorage, IndexedDBAdapter, LocalStorageAdapter, StampRallyClient, StorageAdapterError, THEME_PRESETS, calculateDistanceMeters, calculateProgress, consumeReward, createClaimTicketNumber, 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, 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 };
|
|
1935
2109
|
//# sourceMappingURL=index.js.map
|
|
1936
2110
|
//# sourceMappingURL=index.js.map
|