@stamprally/server 0.16.0 → 0.17.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.16.0
1
+ # @stamprally/server v0.17.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
@@ -7,7 +7,16 @@ async function executeClaimRewardTransaction(database, store, params2, mutation2
7
7
  try {
8
8
  return await database.transaction(async (transaction) => {
9
9
  const current = await store.readContext(transaction, params2);
10
- const next = mutation2(current);
10
+ const secondaryStock = current.secondaryStock ?? null;
11
+ const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : current.stock;
12
+ const next = mutation2({
13
+ ...current,
14
+ rewardStock: rewardStock2,
15
+ sharedStock: params2.stockKey === "__shared__" ? current.stock : null,
16
+ primaryStock: rewardStock2,
17
+ secondaryStock,
18
+ stock: current.stock
19
+ });
11
20
  if (next.error !== void 0) {
12
21
  await store.writeAudit(transaction, next.auditLog);
13
22
  if (params2.idempotencyKey !== void 0 && next.result !== void 0)
@@ -15,7 +24,7 @@ async function executeClaimRewardTransaction(database, store, params2, mutation2
15
24
  return { success: false, error: next.error };
16
25
  }
17
26
  if (next.nextSecondaryStock !== void 0 && store.writeSecondaryStock === void 0)
18
- return { success: false, error: "INVENTORY_NOT_SUPPORTED" };
27
+ return { success: false, error: "INVENTORY_STORAGE_NOT_IMPLEMENTED" };
19
28
  if (next.nextStock !== null) await store.writeStock(transaction, params2, next.nextStock);
20
29
  if (next.nextSecondaryStock !== void 0 && store.writeSecondaryStock !== void 0) {
21
30
  if (next.nextSecondaryStock !== null)
@@ -146,9 +155,16 @@ var InMemoryServerPersistenceAdapter = class {
146
155
  params2.stockKey ?? params2.rewardId
147
156
  );
148
157
  const storedSecondaryStock = params2.secondaryStockKey === void 0 ? null : await this.getRewardStock(params2.rallyId, params2.secondaryStockKey);
158
+ const stock = storedStock ?? initialStock;
159
+ const secondaryStock = storedSecondaryStock ?? initialSecondaryStock;
160
+ const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : stock;
161
+ const sharedStock2 = params2.stockKey === "__shared__" ? stock : null;
149
162
  const mutationResult = mutation2({
150
- stock: storedStock ?? initialStock,
151
- secondaryStock: storedSecondaryStock ?? initialSecondaryStock,
163
+ rewardStock: rewardStock2,
164
+ sharedStock: sharedStock2,
165
+ primaryStock: rewardStock2,
166
+ secondaryStock,
167
+ stock,
152
168
  claimCount: await this.getUserClaimCount(params2.rallyId, params2.userId, params2.rewardId),
153
169
  userState
154
170
  });
@@ -680,7 +696,18 @@ function operationStatus(result) {
680
696
  if (result.ok) return "ACCEPTED";
681
697
  return result.code === "CONFLICT" || result.code === "PERSISTENCE_FAILED" ? "RETRYABLE_ERROR" : "REJECTED_PERMANENT";
682
698
  }
683
- function withDirectIdentity(request) {
699
+ function withDirectIdentity(request, authContext) {
700
+ if (authContext !== void 0) {
701
+ if (authContext.authenticatedUserId.trim() === "")
702
+ throw new RequestValidationException([
703
+ {
704
+ path: "authContext.authenticatedUserId",
705
+ message: "An authenticated userId is required.",
706
+ code: "REQUIRED"
707
+ }
708
+ ]);
709
+ return { ...request, userId: authContext.authenticatedUserId };
710
+ }
684
711
  if (request.userId !== void 0) return { ...request, userId: request.userId };
685
712
  if (request.anonymousSessionId !== void 0 && isUuidV4(request.anonymousSessionId))
686
713
  return { ...request, userId: request.anonymousSessionId };
@@ -810,8 +837,8 @@ var StampRallyServer = class {
810
837
  throw error;
811
838
  }
812
839
  }
813
- async checkIn(request) {
814
- const directRequest = withDirectIdentity(request);
840
+ async checkIn(request, authContext) {
841
+ const directRequest = withDirectIdentity(request, authContext);
815
842
  assertValidCheckInParams(directRequest, this.#config);
816
843
  const { userId } = directRequest;
817
844
  const key = `check-in:${request.rallyId}:${userId}:${request.idempotencyKey}`;
@@ -944,8 +971,8 @@ var StampRallyServer = class {
944
971
  await this.#persistence.releaseLock(request.rallyId, lockKey);
945
972
  }
946
973
  }
947
- async claimReward(request) {
948
- const directRequest = withDirectIdentity(request);
974
+ async claimReward(request, authContext) {
975
+ const directRequest = withDirectIdentity(request, authContext);
949
976
  assertValidClaimParams(directRequest, this.#config);
950
977
  const { userId } = directRequest;
951
978
  const key = `claim:${request.rallyId}:${userId}:${request.rewardId}:${request.idempotencyKey}`;
@@ -963,13 +990,14 @@ var StampRallyServer = class {
963
990
  now(this.#options)
964
991
  );
965
992
  const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
966
- if (rewardStock(this.#config, reward.id, reward.stockLimit) !== null && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function"))
993
+ const inventoryEnabled = plan.primaryInitial !== null || plan.secondaryInitial !== void 0 && plan.secondaryInitial !== null;
994
+ if (inventoryEnabled && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function" || typeof this.#persistence.executeClaimRewardTransaction !== "function"))
967
995
  return this.#rememberClaim(
968
996
  key,
969
997
  {
970
998
  ok: false,
971
- code: "INVENTORY_NOT_SUPPORTED",
972
- message: "This persistence adapter cannot store per-reward inventory."
999
+ code: "INVENTORY_STORAGE_NOT_IMPLEMENTED",
1000
+ message: "This persistence adapter cannot atomically store inventory."
973
1001
  },
974
1002
  directRequest,
975
1003
  now(this.#options)
@@ -1100,11 +1128,11 @@ var StampRallyServer = class {
1100
1128
  const response = responseHolder.value;
1101
1129
  if (!result.success) {
1102
1130
  if (response !== null && !response.ok && response.code === result.error) return response;
1103
- if (result.error === "INVENTORY_NOT_SUPPORTED")
1131
+ if (result.error === "INVENTORY_STORAGE_NOT_IMPLEMENTED")
1104
1132
  return {
1105
1133
  ok: false,
1106
- code: "INVENTORY_NOT_SUPPORTED",
1107
- message: "This persistence adapter cannot store per-reward inventory."
1134
+ code: "INVENTORY_STORAGE_NOT_IMPLEMENTED",
1135
+ message: "This persistence adapter cannot atomically store inventory."
1108
1136
  };
1109
1137
  return {
1110
1138
  ok: false,
@@ -1312,6 +1340,15 @@ async function runPersistenceAdapterComplianceTests(createAdapter) {
1312
1340
  await adapter.getRewardStock("compliance-rally", "reward") === 0,
1313
1341
  "per-reward stock was not decremented atomically"
1314
1342
  );
1343
+ const exhausted = await adapter.executeClaimRewardTransaction(
1344
+ params("carol", "boundary"),
1345
+ mutation
1346
+ );
1347
+ assert(!exhausted.success, "a zero-stock claim was accepted");
1348
+ assert(
1349
+ await adapter.getRewardStock("compliance-rally", "__shared__") === 0 && await adapter.getRewardStock("compliance-rally", "reward") === 0,
1350
+ "zero-stock rejection did not preserve both stock boundaries"
1351
+ );
1315
1352
  const idempotentAdapter = await createAdapter();
1316
1353
  const idempotentParams = params("alice", "same-key");
1317
1354
  const firstClaim = await idempotentAdapter.executeClaimRewardTransaction(
@@ -1327,6 +1364,10 @@ async function runPersistenceAdapterComplianceTests(createAdapter) {
1327
1364
  await idempotentAdapter.getRewardStock("compliance-rally", "__shared__") === 0,
1328
1365
  "idempotent retry decremented shared stock twice"
1329
1366
  );
1367
+ assert(
1368
+ await idempotentAdapter.getRewardStock("compliance-rally", "reward") === 0,
1369
+ "idempotent retry decremented per-reward stock twice"
1370
+ );
1330
1371
  const rollbackAdapter = await createAdapter();
1331
1372
  const rollbackParams = params("alice", "rollback");
1332
1373
  const rollback = await rollbackAdapter.executeClaimRewardTransaction(rollbackParams, () => {