@stamprally/server 0.17.0 → 0.18.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.18.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
@@ -78,6 +78,7 @@ async function executeRedisTransaction(redis, queue) {
78
78
  // src/persistence.ts
79
79
  var InMemoryServerPersistenceAdapter = class {
80
80
  supportsRewardStock = true;
81
+ supportsSecondaryStock = true;
81
82
  #locks = /* @__PURE__ */ new Map();
82
83
  #idempotent = /* @__PURE__ */ new Map();
83
84
  #states = /* @__PURE__ */ new Map();
@@ -642,26 +643,41 @@ function initialState(config, userId, timestamp) {
642
643
  updatedAt: timestamp
643
644
  };
644
645
  }
645
- function rewardStock(config, rewardId, stockLimit) {
646
- const configured = config.inventory?.[rewardId];
646
+ function rewardStock(config, reward) {
647
+ const stockLimit = reward.stockLimit;
648
+ const key = reward.stockKey ?? reward.id;
649
+ const configured = key === "__shared__" ? config.inventory?.sharedStock : config.inventory?.[key];
647
650
  if (stockLimit === void 0) return configured ?? null;
648
651
  if (configured === void 0) return stockLimit;
649
652
  return Math.min(stockLimit, configured);
650
653
  }
651
654
  function sharedStock(config) {
652
- return config.inventory?.sharedStock ?? config.inventory?.global ?? null;
655
+ return config.inventory?.sharedStock ?? null;
653
656
  }
654
657
  function inventoryPlan(config, rewardId, stockLimit) {
655
- const individual = rewardStock(config, rewardId, stockLimit);
658
+ const reward = config.rewards.find((item) => item.id === rewardId);
659
+ const individual = reward === void 0 ? stockLimit ?? null : rewardStock(config, reward);
656
660
  const shared = sharedStock(config);
661
+ const explicitPrimaryKey = reward?.stockKey;
662
+ const explicitSecondaryKey = reward?.secondaryStockKey;
657
663
  if (config.inventoryMode === "shared" && shared !== null) {
658
664
  return {
659
- primaryKey: "__shared__",
660
- primaryInitial: shared,
661
- ...individual === null ? {} : { secondaryKey: rewardId, secondaryInitial: individual }
665
+ primaryKey: explicitPrimaryKey ?? "__shared__",
666
+ primaryInitial: explicitPrimaryKey === void 0 ? shared : individual,
667
+ ...explicitSecondaryKey !== void 0 ? {
668
+ secondaryKey: explicitSecondaryKey,
669
+ secondaryInitial: config.inventory?.[explicitSecondaryKey] ?? individual
670
+ } : individual === null ? {} : { secondaryKey: rewardId, secondaryInitial: individual }
662
671
  };
663
672
  }
664
- return { primaryKey: rewardId, primaryInitial: individual };
673
+ return {
674
+ primaryKey: explicitPrimaryKey ?? rewardId,
675
+ primaryInitial: individual,
676
+ ...explicitSecondaryKey === void 0 ? {} : {
677
+ secondaryKey: explicitSecondaryKey,
678
+ secondaryInitial: config.inventory?.[explicitSecondaryKey] ?? null
679
+ }
680
+ };
665
681
  }
666
682
  function getProof(context) {
667
683
  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 +839,20 @@ var StampRallyServer = class {
823
839
  401
824
840
  );
825
841
  const sessionId = request.headers.get("x-anonymous-session-id");
842
+ const authContext = {
843
+ authenticatedUserId: userId,
844
+ ...sessionId === null ? {} : { isAnonymous: true, sessionId }
845
+ };
826
846
  try {
827
847
  return json({
828
848
  ok: true,
829
- state: await this.syncProgress({
830
- rallyId: body.data.rallyId,
831
- userId,
832
- ...sessionId === null ? {} : { anonymousSessionId: sessionId }
833
- })
849
+ state: await this.syncProgress(
850
+ {
851
+ rallyId: body.data.rallyId,
852
+ ...sessionId === null ? {} : { anonymousSessionId: sessionId }
853
+ },
854
+ authContext
855
+ )
834
856
  });
835
857
  } catch (error) {
836
858
  if (error instanceof RequestValidationException) return validationResponse(error.errors);
@@ -990,6 +1012,8 @@ var StampRallyServer = class {
990
1012
  now(this.#options)
991
1013
  );
992
1014
  const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
1015
+ if (plan.secondaryKey !== void 0 && this.#persistence.supportsSecondaryStock !== true)
1016
+ throw new Error("SECONDARY_STOCK_UNSUPPORTED");
993
1017
  const inventoryEnabled = plan.primaryInitial !== null || plan.secondaryInitial !== void 0 && plan.secondaryInitial !== null;
994
1018
  if (inventoryEnabled && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function" || typeof this.#persistence.executeClaimRewardTransaction !== "function"))
995
1019
  return this.#rememberClaim(
@@ -1024,7 +1048,7 @@ var StampRallyServer = class {
1024
1048
  rewardId: reward.id,
1025
1049
  stockKey: plan.primaryKey,
1026
1050
  ...plan.secondaryKey === void 0 ? {} : { secondaryStockKey: plan.secondaryKey },
1027
- rewardStockLimit: rewardStock(this.#config, reward.id, reward.stockLimit),
1051
+ rewardStockLimit: rewardStock(this.#config, reward),
1028
1052
  sharedStockLimit: this.#config.inventoryMode === "shared" ? sharedStock(this.#config) : null,
1029
1053
  initialStock: plan.primaryInitial,
1030
1054
  ...plan.secondaryInitial === void 0 ? {} : { initialSecondaryStock: plan.secondaryInitial },
@@ -1156,21 +1180,24 @@ var StampRallyServer = class {
1156
1180
  await this.#persistence.releaseLock(request.rallyId, lockKey);
1157
1181
  }
1158
1182
  }
1159
- async sync(rallyId, userId) {
1183
+ async sync(rallyId, identity) {
1184
+ const userId = typeof identity === "string" ? identity : identity.authenticatedUserId;
1160
1185
  assertValidSyncParams({ rallyId, userId }, this.#config);
1161
1186
  const state2 = await this.#persistence.getUserState(rallyId, userId) ?? initialState(this.#config, userId, now(this.#options));
1162
1187
  return this.#attachInventory(state2);
1163
1188
  }
1164
- async syncProgress(request) {
1165
- const directRequest = withDirectIdentity(request);
1189
+ async syncProgress(request, authContext) {
1190
+ const directRequest = withDirectIdentity(request, authContext);
1166
1191
  assertValidSyncParams(directRequest, this.#config);
1167
- return this.sync(directRequest.rallyId, directRequest.userId);
1192
+ return this.sync(directRequest.rallyId, authContext);
1168
1193
  }
1169
1194
  async #attachInventory(state2) {
1170
1195
  const rewardRemaining = {};
1171
1196
  for (const reward of this.#config.rewards) {
1172
1197
  const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
1173
1198
  if (plan.secondaryKey !== void 0) {
1199
+ if (this.#persistence.supportsSecondaryStock !== true)
1200
+ throw new Error("SECONDARY_STOCK_UNSUPPORTED");
1174
1201
  const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.secondaryKey);
1175
1202
  const remaining = stock ?? plan.secondaryInitial ?? null;
1176
1203
  if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);