@stamprally/server 0.17.0 → 0.19.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @stamprally/server v0.17.0
1
+ # @stamprally/server v0.19.0
2
2
 
3
3
  Web Standard `Request` / `Response` handlers for server-authoritative check-ins and reward claims.
4
4
 
package/dist/index.cjs CHANGED
@@ -4,9 +4,13 @@ var core = require('@stamprally/core');
4
4
 
5
5
  // src/examples/transaction.ts
6
6
  async function executeClaimRewardTransaction(database, store, params2, mutation2) {
7
+ if (params2.secondaryStockKey !== void 0 && store.writeSecondaryStock === void 0)
8
+ return { success: false, error: "SECONDARY_STOCK_UNSUPPORTED" };
7
9
  try {
8
10
  return await database.transaction(async (transaction) => {
9
11
  const current = await store.readContext(transaction, params2);
12
+ if (params2.secondaryStockKey !== void 0 && current.secondaryStock === void 0)
13
+ return { success: false, error: "SECONDARY_STOCK_UNSUPPORTED" };
10
14
  const secondaryStock = current.secondaryStock ?? null;
11
15
  const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : current.stock;
12
16
  const next = mutation2({
@@ -78,6 +82,7 @@ async function executeRedisTransaction(redis, queue) {
78
82
  // src/persistence.ts
79
83
  var InMemoryServerPersistenceAdapter = class {
80
84
  supportsRewardStock = true;
85
+ supportsSecondaryStock = true;
81
86
  #locks = /* @__PURE__ */ new Map();
82
87
  #idempotent = /* @__PURE__ */ new Map();
83
88
  #states = /* @__PURE__ */ new Map();
@@ -597,6 +602,40 @@ function validateSyncRequest(value) {
597
602
  return errors({ path: "$", message: "Expected an object.", code: "INVALID_TYPE" });
598
603
  if (value.userId !== void 0 && !nonEmpty(value.userId))
599
604
  return errors({ path: "userId", message: "userId must be non-empty.", code: "INVALID_TYPE" });
605
+ if (value.operations !== void 0) {
606
+ if (!Array.isArray(value.operations))
607
+ return errors({
608
+ path: "operations",
609
+ message: "operations must be an array.",
610
+ code: "INVALID_TYPE"
611
+ });
612
+ const operationErrors = [];
613
+ value.operations.forEach((operation, index) => {
614
+ const path = `operations[${index}]`;
615
+ if (!record(operation)) {
616
+ operationErrors.push({
617
+ path,
618
+ message: "Expected an operation object.",
619
+ code: "INVALID_TYPE"
620
+ });
621
+ return;
622
+ }
623
+ if (operation.kind !== "checkIn" && operation.kind !== "claimReward") {
624
+ operationErrors.push({
625
+ path: `${path}.kind`,
626
+ message: "Unknown sync operation.",
627
+ code: "INVALID_ENUM"
628
+ });
629
+ return;
630
+ }
631
+ const result = operation.kind === "checkIn" ? validateCheckInRequest(operation.request) : validateClaimRewardRequest(operation.request);
632
+ if (!result.success)
633
+ operationErrors.push(
634
+ ...result.errors.map((error) => ({ ...error, path: `${path}.request.${error.path}` }))
635
+ );
636
+ });
637
+ if (operationErrors.length > 0) return errors(...operationErrors);
638
+ }
600
639
  return {
601
640
  success: true,
602
641
  data: value
@@ -642,26 +681,41 @@ function initialState(config, userId, timestamp) {
642
681
  updatedAt: timestamp
643
682
  };
644
683
  }
645
- function rewardStock(config, rewardId, stockLimit) {
646
- const configured = config.inventory?.[rewardId];
684
+ function rewardStock(config, reward) {
685
+ const stockLimit = reward.stockLimit;
686
+ const key = reward.stockKey ?? reward.id;
687
+ const configured = key === "__shared__" ? config.inventory?.sharedStock : config.inventory?.[key];
647
688
  if (stockLimit === void 0) return configured ?? null;
648
689
  if (configured === void 0) return stockLimit;
649
690
  return Math.min(stockLimit, configured);
650
691
  }
651
692
  function sharedStock(config) {
652
- return config.inventory?.sharedStock ?? config.inventory?.global ?? null;
693
+ return config.inventory?.sharedStock ?? null;
653
694
  }
654
695
  function inventoryPlan(config, rewardId, stockLimit) {
655
- const individual = rewardStock(config, rewardId, stockLimit);
696
+ const reward = config.rewards.find((item) => item.id === rewardId);
697
+ const individual = reward === void 0 ? stockLimit ?? null : rewardStock(config, reward);
656
698
  const shared = sharedStock(config);
699
+ const explicitPrimaryKey = reward?.stockKey;
700
+ const explicitSecondaryKey = reward?.secondaryStockKey;
657
701
  if (config.inventoryMode === "shared" && shared !== null) {
658
702
  return {
659
- primaryKey: "__shared__",
660
- primaryInitial: shared,
661
- ...individual === null ? {} : { secondaryKey: rewardId, secondaryInitial: individual }
703
+ primaryKey: explicitPrimaryKey ?? "__shared__",
704
+ primaryInitial: explicitPrimaryKey === void 0 ? shared : individual,
705
+ ...explicitSecondaryKey !== void 0 ? {
706
+ secondaryKey: explicitSecondaryKey,
707
+ secondaryInitial: config.inventory?.[explicitSecondaryKey] ?? individual
708
+ } : individual === null ? {} : { secondaryKey: rewardId, secondaryInitial: individual }
662
709
  };
663
710
  }
664
- return { primaryKey: rewardId, primaryInitial: individual };
711
+ return {
712
+ primaryKey: explicitPrimaryKey ?? rewardId,
713
+ primaryInitial: individual,
714
+ ...explicitSecondaryKey === void 0 ? {} : {
715
+ secondaryKey: explicitSecondaryKey,
716
+ secondaryInitial: config.inventory?.[explicitSecondaryKey] ?? null
717
+ }
718
+ };
665
719
  }
666
720
  function getProof(context) {
667
721
  return context.type === "qr" ? context.token : context.type === "passcode" ? context.code : context.type === "gps" ? { latitude: context.latitude, longitude: context.longitude } : context.type === "nfc" ? context.tagId : context.value;
@@ -823,14 +877,21 @@ var StampRallyServer = class {
823
877
  401
824
878
  );
825
879
  const sessionId = request.headers.get("x-anonymous-session-id");
880
+ const authContext = {
881
+ authenticatedUserId: userId,
882
+ ...sessionId === null ? {} : { isAnonymous: true, sessionId }
883
+ };
826
884
  try {
827
885
  return json({
828
886
  ok: true,
829
- state: await this.syncProgress({
830
- rallyId: body.data.rallyId,
831
- userId,
832
- ...sessionId === null ? {} : { anonymousSessionId: sessionId }
833
- })
887
+ state: await this.syncProgress(
888
+ {
889
+ rallyId: body.data.rallyId,
890
+ ...body.data.operations === void 0 ? {} : { operations: body.data.operations },
891
+ ...sessionId === null ? {} : { anonymousSessionId: sessionId }
892
+ },
893
+ authContext
894
+ )
834
895
  });
835
896
  } catch (error) {
836
897
  if (error instanceof RequestValidationException) return validationResponse(error.errors);
@@ -990,6 +1051,8 @@ var StampRallyServer = class {
990
1051
  now(this.#options)
991
1052
  );
992
1053
  const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
1054
+ if (plan.secondaryKey !== void 0 && this.#persistence.supportsSecondaryStock !== true)
1055
+ throw new Error("SECONDARY_STOCK_UNSUPPORTED");
993
1056
  const inventoryEnabled = plan.primaryInitial !== null || plan.secondaryInitial !== void 0 && plan.secondaryInitial !== null;
994
1057
  if (inventoryEnabled && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function" || typeof this.#persistence.executeClaimRewardTransaction !== "function"))
995
1058
  return this.#rememberClaim(
@@ -1024,7 +1087,7 @@ var StampRallyServer = class {
1024
1087
  rewardId: reward.id,
1025
1088
  stockKey: plan.primaryKey,
1026
1089
  ...plan.secondaryKey === void 0 ? {} : { secondaryStockKey: plan.secondaryKey },
1027
- rewardStockLimit: rewardStock(this.#config, reward.id, reward.stockLimit),
1090
+ rewardStockLimit: rewardStock(this.#config, reward),
1028
1091
  sharedStockLimit: this.#config.inventoryMode === "shared" ? sharedStock(this.#config) : null,
1029
1092
  initialStock: plan.primaryInitial,
1030
1093
  ...plan.secondaryInitial === void 0 ? {} : { initialSecondaryStock: plan.secondaryInitial },
@@ -1156,21 +1219,28 @@ var StampRallyServer = class {
1156
1219
  await this.#persistence.releaseLock(request.rallyId, lockKey);
1157
1220
  }
1158
1221
  }
1159
- async sync(rallyId, userId) {
1222
+ async sync(rallyId, identity) {
1223
+ const userId = typeof identity === "string" ? identity : identity.authenticatedUserId;
1160
1224
  assertValidSyncParams({ rallyId, userId }, this.#config);
1161
1225
  const state2 = await this.#persistence.getUserState(rallyId, userId) ?? initialState(this.#config, userId, now(this.#options));
1162
1226
  return this.#attachInventory(state2);
1163
1227
  }
1164
- async syncProgress(request) {
1165
- const directRequest = withDirectIdentity(request);
1228
+ async syncProgress(request, authContext) {
1229
+ const directRequest = withDirectIdentity(request, authContext);
1166
1230
  assertValidSyncParams(directRequest, this.#config);
1167
- return this.sync(directRequest.rallyId, directRequest.userId);
1231
+ for (const operation of request.operations ?? []) {
1232
+ if (operation.kind === "checkIn") await this.checkIn(operation.request, authContext);
1233
+ else await this.claimReward(operation.request, authContext);
1234
+ }
1235
+ return this.sync(directRequest.rallyId, authContext);
1168
1236
  }
1169
1237
  async #attachInventory(state2) {
1170
1238
  const rewardRemaining = {};
1171
1239
  for (const reward of this.#config.rewards) {
1172
1240
  const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
1173
1241
  if (plan.secondaryKey !== void 0) {
1242
+ if (this.#persistence.supportsSecondaryStock !== true)
1243
+ throw new Error("SECONDARY_STOCK_UNSUPPORTED");
1174
1244
  const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.secondaryKey);
1175
1245
  const remaining = stock ?? plan.secondaryInitial ?? null;
1176
1246
  if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);