@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/dist/index.d.cts CHANGED
@@ -93,6 +93,8 @@ interface ServerPersistenceAdapter {
93
93
  }>;
94
94
  /** Set false when the adapter cannot persist per-reward inventory atomically. */
95
95
  readonly supportsRewardStock?: boolean;
96
+ /** Must be true when a claim plan uses `secondaryStockKey`. */
97
+ readonly supportsSecondaryStock?: boolean;
96
98
  acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
97
99
  releaseLock(rallyId: string, lockKey: string): Promise<void>;
98
100
  getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
@@ -117,6 +119,7 @@ interface InMemoryServerPersistenceOptions {
117
119
  declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapter {
118
120
  #private;
119
121
  readonly supportsRewardStock: boolean;
122
+ readonly supportsSecondaryStock: boolean;
120
123
  constructor(options?: InMemoryServerPersistenceOptions);
121
124
  acquireLock(rallyId: string, key: string, ttlMs: number): Promise<boolean>;
122
125
  releaseLock(rallyId: string, key: string): Promise<void>;
@@ -271,12 +274,12 @@ declare class StampRallyServer {
271
274
  handleSync(request: Request): Promise<Response>;
272
275
  checkIn(request: CheckInRequest, authContext?: TrustedAuthContext): Promise<CheckInResponse>;
273
276
  claimReward(request: ClaimRewardRequest, authContext?: TrustedAuthContext): Promise<ClaimResponse>;
274
- sync(rallyId: string, userId: string): Promise<UserRallyState>;
277
+ sync(rallyId: string, identity: string | TrustedAuthContext): Promise<UserRallyState>;
275
278
  syncProgress(request: {
276
279
  readonly rallyId: string;
277
280
  readonly userId?: string;
278
281
  readonly anonymousSessionId?: string;
279
- }): Promise<UserRallyState>;
282
+ }, authContext: TrustedAuthContext): Promise<UserRallyState>;
280
283
  }
281
284
 
282
285
  /**
package/dist/index.d.ts CHANGED
@@ -93,6 +93,8 @@ interface ServerPersistenceAdapter {
93
93
  }>;
94
94
  /** Set false when the adapter cannot persist per-reward inventory atomically. */
95
95
  readonly supportsRewardStock?: boolean;
96
+ /** Must be true when a claim plan uses `secondaryStockKey`. */
97
+ readonly supportsSecondaryStock?: boolean;
96
98
  acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
97
99
  releaseLock(rallyId: string, lockKey: string): Promise<void>;
98
100
  getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
@@ -117,6 +119,7 @@ interface InMemoryServerPersistenceOptions {
117
119
  declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapter {
118
120
  #private;
119
121
  readonly supportsRewardStock: boolean;
122
+ readonly supportsSecondaryStock: boolean;
120
123
  constructor(options?: InMemoryServerPersistenceOptions);
121
124
  acquireLock(rallyId: string, key: string, ttlMs: number): Promise<boolean>;
122
125
  releaseLock(rallyId: string, key: string): Promise<void>;
@@ -271,12 +274,12 @@ declare class StampRallyServer {
271
274
  handleSync(request: Request): Promise<Response>;
272
275
  checkIn(request: CheckInRequest, authContext?: TrustedAuthContext): Promise<CheckInResponse>;
273
276
  claimReward(request: ClaimRewardRequest, authContext?: TrustedAuthContext): Promise<ClaimResponse>;
274
- sync(rallyId: string, userId: string): Promise<UserRallyState>;
277
+ sync(rallyId: string, identity: string | TrustedAuthContext): Promise<UserRallyState>;
275
278
  syncProgress(request: {
276
279
  readonly rallyId: string;
277
280
  readonly userId?: string;
278
281
  readonly anonymousSessionId?: string;
279
- }): Promise<UserRallyState>;
282
+ }, authContext: TrustedAuthContext): Promise<UserRallyState>;
280
283
  }
281
284
 
282
285
  /**
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ async function executeRedisTransaction(redis, queue) {
76
76
  // src/persistence.ts
77
77
  var InMemoryServerPersistenceAdapter = class {
78
78
  supportsRewardStock = true;
79
+ supportsSecondaryStock = true;
79
80
  #locks = /* @__PURE__ */ new Map();
80
81
  #idempotent = /* @__PURE__ */ new Map();
81
82
  #states = /* @__PURE__ */ new Map();
@@ -640,26 +641,41 @@ function initialState(config, userId, timestamp) {
640
641
  updatedAt: timestamp
641
642
  };
642
643
  }
643
- function rewardStock(config, rewardId, stockLimit) {
644
- const configured = config.inventory?.[rewardId];
644
+ function rewardStock(config, reward) {
645
+ const stockLimit = reward.stockLimit;
646
+ const key = reward.stockKey ?? reward.id;
647
+ const configured = key === "__shared__" ? config.inventory?.sharedStock : config.inventory?.[key];
645
648
  if (stockLimit === void 0) return configured ?? null;
646
649
  if (configured === void 0) return stockLimit;
647
650
  return Math.min(stockLimit, configured);
648
651
  }
649
652
  function sharedStock(config) {
650
- return config.inventory?.sharedStock ?? config.inventory?.global ?? null;
653
+ return config.inventory?.sharedStock ?? null;
651
654
  }
652
655
  function inventoryPlan(config, rewardId, stockLimit) {
653
- const individual = rewardStock(config, rewardId, stockLimit);
656
+ const reward = config.rewards.find((item) => item.id === rewardId);
657
+ const individual = reward === void 0 ? stockLimit ?? null : rewardStock(config, reward);
654
658
  const shared = sharedStock(config);
659
+ const explicitPrimaryKey = reward?.stockKey;
660
+ const explicitSecondaryKey = reward?.secondaryStockKey;
655
661
  if (config.inventoryMode === "shared" && shared !== null) {
656
662
  return {
657
- primaryKey: "__shared__",
658
- primaryInitial: shared,
659
- ...individual === null ? {} : { secondaryKey: rewardId, secondaryInitial: individual }
663
+ primaryKey: explicitPrimaryKey ?? "__shared__",
664
+ primaryInitial: explicitPrimaryKey === void 0 ? shared : individual,
665
+ ...explicitSecondaryKey !== void 0 ? {
666
+ secondaryKey: explicitSecondaryKey,
667
+ secondaryInitial: config.inventory?.[explicitSecondaryKey] ?? individual
668
+ } : individual === null ? {} : { secondaryKey: rewardId, secondaryInitial: individual }
660
669
  };
661
670
  }
662
- return { primaryKey: rewardId, primaryInitial: individual };
671
+ return {
672
+ primaryKey: explicitPrimaryKey ?? rewardId,
673
+ primaryInitial: individual,
674
+ ...explicitSecondaryKey === void 0 ? {} : {
675
+ secondaryKey: explicitSecondaryKey,
676
+ secondaryInitial: config.inventory?.[explicitSecondaryKey] ?? null
677
+ }
678
+ };
663
679
  }
664
680
  function getProof(context) {
665
681
  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;
@@ -821,14 +837,20 @@ var StampRallyServer = class {
821
837
  401
822
838
  );
823
839
  const sessionId = request.headers.get("x-anonymous-session-id");
840
+ const authContext = {
841
+ authenticatedUserId: userId,
842
+ ...sessionId === null ? {} : { isAnonymous: true, sessionId }
843
+ };
824
844
  try {
825
845
  return json({
826
846
  ok: true,
827
- state: await this.syncProgress({
828
- rallyId: body.data.rallyId,
829
- userId,
830
- ...sessionId === null ? {} : { anonymousSessionId: sessionId }
831
- })
847
+ state: await this.syncProgress(
848
+ {
849
+ rallyId: body.data.rallyId,
850
+ ...sessionId === null ? {} : { anonymousSessionId: sessionId }
851
+ },
852
+ authContext
853
+ )
832
854
  });
833
855
  } catch (error) {
834
856
  if (error instanceof RequestValidationException) return validationResponse(error.errors);
@@ -988,6 +1010,8 @@ var StampRallyServer = class {
988
1010
  now(this.#options)
989
1011
  );
990
1012
  const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
1013
+ if (plan.secondaryKey !== void 0 && this.#persistence.supportsSecondaryStock !== true)
1014
+ throw new Error("SECONDARY_STOCK_UNSUPPORTED");
991
1015
  const inventoryEnabled = plan.primaryInitial !== null || plan.secondaryInitial !== void 0 && plan.secondaryInitial !== null;
992
1016
  if (inventoryEnabled && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function" || typeof this.#persistence.executeClaimRewardTransaction !== "function"))
993
1017
  return this.#rememberClaim(
@@ -1022,7 +1046,7 @@ var StampRallyServer = class {
1022
1046
  rewardId: reward.id,
1023
1047
  stockKey: plan.primaryKey,
1024
1048
  ...plan.secondaryKey === void 0 ? {} : { secondaryStockKey: plan.secondaryKey },
1025
- rewardStockLimit: rewardStock(this.#config, reward.id, reward.stockLimit),
1049
+ rewardStockLimit: rewardStock(this.#config, reward),
1026
1050
  sharedStockLimit: this.#config.inventoryMode === "shared" ? sharedStock(this.#config) : null,
1027
1051
  initialStock: plan.primaryInitial,
1028
1052
  ...plan.secondaryInitial === void 0 ? {} : { initialSecondaryStock: plan.secondaryInitial },
@@ -1154,21 +1178,24 @@ var StampRallyServer = class {
1154
1178
  await this.#persistence.releaseLock(request.rallyId, lockKey);
1155
1179
  }
1156
1180
  }
1157
- async sync(rallyId, userId) {
1181
+ async sync(rallyId, identity) {
1182
+ const userId = typeof identity === "string" ? identity : identity.authenticatedUserId;
1158
1183
  assertValidSyncParams({ rallyId, userId }, this.#config);
1159
1184
  const state2 = await this.#persistence.getUserState(rallyId, userId) ?? initialState(this.#config, userId, now(this.#options));
1160
1185
  return this.#attachInventory(state2);
1161
1186
  }
1162
- async syncProgress(request) {
1163
- const directRequest = withDirectIdentity(request);
1187
+ async syncProgress(request, authContext) {
1188
+ const directRequest = withDirectIdentity(request, authContext);
1164
1189
  assertValidSyncParams(directRequest, this.#config);
1165
- return this.sync(directRequest.rallyId, directRequest.userId);
1190
+ return this.sync(directRequest.rallyId, authContext);
1166
1191
  }
1167
1192
  async #attachInventory(state2) {
1168
1193
  const rewardRemaining = {};
1169
1194
  for (const reward of this.#config.rewards) {
1170
1195
  const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
1171
1196
  if (plan.secondaryKey !== void 0) {
1197
+ if (this.#persistence.supportsSecondaryStock !== true)
1198
+ throw new Error("SECONDARY_STOCK_UNSUPPORTED");
1172
1199
  const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.secondaryKey);
1173
1200
  const remaining = stock ?? plan.secondaryInitial ?? null;
1174
1201
  if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);