@stamprally/server 0.16.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 +1 -1
- package/dist/index.cjs +101 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -27
- package/dist/index.d.ts +33 -27
- package/dist/index.js +101 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
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
|
|
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: "
|
|
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)
|
|
@@ -69,6 +78,7 @@ async function executeRedisTransaction(redis, queue) {
|
|
|
69
78
|
// src/persistence.ts
|
|
70
79
|
var InMemoryServerPersistenceAdapter = class {
|
|
71
80
|
supportsRewardStock = true;
|
|
81
|
+
supportsSecondaryStock = true;
|
|
72
82
|
#locks = /* @__PURE__ */ new Map();
|
|
73
83
|
#idempotent = /* @__PURE__ */ new Map();
|
|
74
84
|
#states = /* @__PURE__ */ new Map();
|
|
@@ -146,9 +156,16 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
146
156
|
params2.stockKey ?? params2.rewardId
|
|
147
157
|
);
|
|
148
158
|
const storedSecondaryStock = params2.secondaryStockKey === void 0 ? null : await this.getRewardStock(params2.rallyId, params2.secondaryStockKey);
|
|
159
|
+
const stock = storedStock ?? initialStock;
|
|
160
|
+
const secondaryStock = storedSecondaryStock ?? initialSecondaryStock;
|
|
161
|
+
const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : stock;
|
|
162
|
+
const sharedStock2 = params2.stockKey === "__shared__" ? stock : null;
|
|
149
163
|
const mutationResult = mutation2({
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
rewardStock: rewardStock2,
|
|
165
|
+
sharedStock: sharedStock2,
|
|
166
|
+
primaryStock: rewardStock2,
|
|
167
|
+
secondaryStock,
|
|
168
|
+
stock,
|
|
152
169
|
claimCount: await this.getUserClaimCount(params2.rallyId, params2.userId, params2.rewardId),
|
|
153
170
|
userState
|
|
154
171
|
});
|
|
@@ -626,26 +643,41 @@ function initialState(config, userId, timestamp) {
|
|
|
626
643
|
updatedAt: timestamp
|
|
627
644
|
};
|
|
628
645
|
}
|
|
629
|
-
function rewardStock(config,
|
|
630
|
-
const
|
|
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];
|
|
631
650
|
if (stockLimit === void 0) return configured ?? null;
|
|
632
651
|
if (configured === void 0) return stockLimit;
|
|
633
652
|
return Math.min(stockLimit, configured);
|
|
634
653
|
}
|
|
635
654
|
function sharedStock(config) {
|
|
636
|
-
return config.inventory?.sharedStock ??
|
|
655
|
+
return config.inventory?.sharedStock ?? null;
|
|
637
656
|
}
|
|
638
657
|
function inventoryPlan(config, rewardId, stockLimit) {
|
|
639
|
-
const
|
|
658
|
+
const reward = config.rewards.find((item) => item.id === rewardId);
|
|
659
|
+
const individual = reward === void 0 ? stockLimit ?? null : rewardStock(config, reward);
|
|
640
660
|
const shared = sharedStock(config);
|
|
661
|
+
const explicitPrimaryKey = reward?.stockKey;
|
|
662
|
+
const explicitSecondaryKey = reward?.secondaryStockKey;
|
|
641
663
|
if (config.inventoryMode === "shared" && shared !== null) {
|
|
642
664
|
return {
|
|
643
|
-
primaryKey: "__shared__",
|
|
644
|
-
primaryInitial: shared,
|
|
645
|
-
...
|
|
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 }
|
|
646
671
|
};
|
|
647
672
|
}
|
|
648
|
-
return {
|
|
673
|
+
return {
|
|
674
|
+
primaryKey: explicitPrimaryKey ?? rewardId,
|
|
675
|
+
primaryInitial: individual,
|
|
676
|
+
...explicitSecondaryKey === void 0 ? {} : {
|
|
677
|
+
secondaryKey: explicitSecondaryKey,
|
|
678
|
+
secondaryInitial: config.inventory?.[explicitSecondaryKey] ?? null
|
|
679
|
+
}
|
|
680
|
+
};
|
|
649
681
|
}
|
|
650
682
|
function getProof(context) {
|
|
651
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;
|
|
@@ -680,7 +712,18 @@ function operationStatus(result) {
|
|
|
680
712
|
if (result.ok) return "ACCEPTED";
|
|
681
713
|
return result.code === "CONFLICT" || result.code === "PERSISTENCE_FAILED" ? "RETRYABLE_ERROR" : "REJECTED_PERMANENT";
|
|
682
714
|
}
|
|
683
|
-
function withDirectIdentity(request) {
|
|
715
|
+
function withDirectIdentity(request, authContext) {
|
|
716
|
+
if (authContext !== void 0) {
|
|
717
|
+
if (authContext.authenticatedUserId.trim() === "")
|
|
718
|
+
throw new RequestValidationException([
|
|
719
|
+
{
|
|
720
|
+
path: "authContext.authenticatedUserId",
|
|
721
|
+
message: "An authenticated userId is required.",
|
|
722
|
+
code: "REQUIRED"
|
|
723
|
+
}
|
|
724
|
+
]);
|
|
725
|
+
return { ...request, userId: authContext.authenticatedUserId };
|
|
726
|
+
}
|
|
684
727
|
if (request.userId !== void 0) return { ...request, userId: request.userId };
|
|
685
728
|
if (request.anonymousSessionId !== void 0 && isUuidV4(request.anonymousSessionId))
|
|
686
729
|
return { ...request, userId: request.anonymousSessionId };
|
|
@@ -796,22 +839,28 @@ var StampRallyServer = class {
|
|
|
796
839
|
401
|
|
797
840
|
);
|
|
798
841
|
const sessionId = request.headers.get("x-anonymous-session-id");
|
|
842
|
+
const authContext = {
|
|
843
|
+
authenticatedUserId: userId,
|
|
844
|
+
...sessionId === null ? {} : { isAnonymous: true, sessionId }
|
|
845
|
+
};
|
|
799
846
|
try {
|
|
800
847
|
return json({
|
|
801
848
|
ok: true,
|
|
802
|
-
state: await this.syncProgress(
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
849
|
+
state: await this.syncProgress(
|
|
850
|
+
{
|
|
851
|
+
rallyId: body.data.rallyId,
|
|
852
|
+
...sessionId === null ? {} : { anonymousSessionId: sessionId }
|
|
853
|
+
},
|
|
854
|
+
authContext
|
|
855
|
+
)
|
|
807
856
|
});
|
|
808
857
|
} catch (error) {
|
|
809
858
|
if (error instanceof RequestValidationException) return validationResponse(error.errors);
|
|
810
859
|
throw error;
|
|
811
860
|
}
|
|
812
861
|
}
|
|
813
|
-
async checkIn(request) {
|
|
814
|
-
const directRequest = withDirectIdentity(request);
|
|
862
|
+
async checkIn(request, authContext) {
|
|
863
|
+
const directRequest = withDirectIdentity(request, authContext);
|
|
815
864
|
assertValidCheckInParams(directRequest, this.#config);
|
|
816
865
|
const { userId } = directRequest;
|
|
817
866
|
const key = `check-in:${request.rallyId}:${userId}:${request.idempotencyKey}`;
|
|
@@ -944,8 +993,8 @@ var StampRallyServer = class {
|
|
|
944
993
|
await this.#persistence.releaseLock(request.rallyId, lockKey);
|
|
945
994
|
}
|
|
946
995
|
}
|
|
947
|
-
async claimReward(request) {
|
|
948
|
-
const directRequest = withDirectIdentity(request);
|
|
996
|
+
async claimReward(request, authContext) {
|
|
997
|
+
const directRequest = withDirectIdentity(request, authContext);
|
|
949
998
|
assertValidClaimParams(directRequest, this.#config);
|
|
950
999
|
const { userId } = directRequest;
|
|
951
1000
|
const key = `claim:${request.rallyId}:${userId}:${request.rewardId}:${request.idempotencyKey}`;
|
|
@@ -963,13 +1012,16 @@ var StampRallyServer = class {
|
|
|
963
1012
|
now(this.#options)
|
|
964
1013
|
);
|
|
965
1014
|
const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
|
|
966
|
-
if (
|
|
1015
|
+
if (plan.secondaryKey !== void 0 && this.#persistence.supportsSecondaryStock !== true)
|
|
1016
|
+
throw new Error("SECONDARY_STOCK_UNSUPPORTED");
|
|
1017
|
+
const inventoryEnabled = plan.primaryInitial !== null || plan.secondaryInitial !== void 0 && plan.secondaryInitial !== null;
|
|
1018
|
+
if (inventoryEnabled && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function" || typeof this.#persistence.executeClaimRewardTransaction !== "function"))
|
|
967
1019
|
return this.#rememberClaim(
|
|
968
1020
|
key,
|
|
969
1021
|
{
|
|
970
1022
|
ok: false,
|
|
971
|
-
code: "
|
|
972
|
-
message: "This persistence adapter cannot store
|
|
1023
|
+
code: "INVENTORY_STORAGE_NOT_IMPLEMENTED",
|
|
1024
|
+
message: "This persistence adapter cannot atomically store inventory."
|
|
973
1025
|
},
|
|
974
1026
|
directRequest,
|
|
975
1027
|
now(this.#options)
|
|
@@ -996,7 +1048,7 @@ var StampRallyServer = class {
|
|
|
996
1048
|
rewardId: reward.id,
|
|
997
1049
|
stockKey: plan.primaryKey,
|
|
998
1050
|
...plan.secondaryKey === void 0 ? {} : { secondaryStockKey: plan.secondaryKey },
|
|
999
|
-
rewardStockLimit: rewardStock(this.#config, reward
|
|
1051
|
+
rewardStockLimit: rewardStock(this.#config, reward),
|
|
1000
1052
|
sharedStockLimit: this.#config.inventoryMode === "shared" ? sharedStock(this.#config) : null,
|
|
1001
1053
|
initialStock: plan.primaryInitial,
|
|
1002
1054
|
...plan.secondaryInitial === void 0 ? {} : { initialSecondaryStock: plan.secondaryInitial },
|
|
@@ -1100,11 +1152,11 @@ var StampRallyServer = class {
|
|
|
1100
1152
|
const response = responseHolder.value;
|
|
1101
1153
|
if (!result.success) {
|
|
1102
1154
|
if (response !== null && !response.ok && response.code === result.error) return response;
|
|
1103
|
-
if (result.error === "
|
|
1155
|
+
if (result.error === "INVENTORY_STORAGE_NOT_IMPLEMENTED")
|
|
1104
1156
|
return {
|
|
1105
1157
|
ok: false,
|
|
1106
|
-
code: "
|
|
1107
|
-
message: "This persistence adapter cannot store
|
|
1158
|
+
code: "INVENTORY_STORAGE_NOT_IMPLEMENTED",
|
|
1159
|
+
message: "This persistence adapter cannot atomically store inventory."
|
|
1108
1160
|
};
|
|
1109
1161
|
return {
|
|
1110
1162
|
ok: false,
|
|
@@ -1128,21 +1180,24 @@ var StampRallyServer = class {
|
|
|
1128
1180
|
await this.#persistence.releaseLock(request.rallyId, lockKey);
|
|
1129
1181
|
}
|
|
1130
1182
|
}
|
|
1131
|
-
async sync(rallyId,
|
|
1183
|
+
async sync(rallyId, identity) {
|
|
1184
|
+
const userId = typeof identity === "string" ? identity : identity.authenticatedUserId;
|
|
1132
1185
|
assertValidSyncParams({ rallyId, userId }, this.#config);
|
|
1133
1186
|
const state2 = await this.#persistence.getUserState(rallyId, userId) ?? initialState(this.#config, userId, now(this.#options));
|
|
1134
1187
|
return this.#attachInventory(state2);
|
|
1135
1188
|
}
|
|
1136
|
-
async syncProgress(request) {
|
|
1137
|
-
const directRequest = withDirectIdentity(request);
|
|
1189
|
+
async syncProgress(request, authContext) {
|
|
1190
|
+
const directRequest = withDirectIdentity(request, authContext);
|
|
1138
1191
|
assertValidSyncParams(directRequest, this.#config);
|
|
1139
|
-
return this.sync(directRequest.rallyId,
|
|
1192
|
+
return this.sync(directRequest.rallyId, authContext);
|
|
1140
1193
|
}
|
|
1141
1194
|
async #attachInventory(state2) {
|
|
1142
1195
|
const rewardRemaining = {};
|
|
1143
1196
|
for (const reward of this.#config.rewards) {
|
|
1144
1197
|
const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
|
|
1145
1198
|
if (plan.secondaryKey !== void 0) {
|
|
1199
|
+
if (this.#persistence.supportsSecondaryStock !== true)
|
|
1200
|
+
throw new Error("SECONDARY_STOCK_UNSUPPORTED");
|
|
1146
1201
|
const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.secondaryKey);
|
|
1147
1202
|
const remaining = stock ?? plan.secondaryInitial ?? null;
|
|
1148
1203
|
if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);
|
|
@@ -1312,6 +1367,15 @@ async function runPersistenceAdapterComplianceTests(createAdapter) {
|
|
|
1312
1367
|
await adapter.getRewardStock("compliance-rally", "reward") === 0,
|
|
1313
1368
|
"per-reward stock was not decremented atomically"
|
|
1314
1369
|
);
|
|
1370
|
+
const exhausted = await adapter.executeClaimRewardTransaction(
|
|
1371
|
+
params("carol", "boundary"),
|
|
1372
|
+
mutation
|
|
1373
|
+
);
|
|
1374
|
+
assert(!exhausted.success, "a zero-stock claim was accepted");
|
|
1375
|
+
assert(
|
|
1376
|
+
await adapter.getRewardStock("compliance-rally", "__shared__") === 0 && await adapter.getRewardStock("compliance-rally", "reward") === 0,
|
|
1377
|
+
"zero-stock rejection did not preserve both stock boundaries"
|
|
1378
|
+
);
|
|
1315
1379
|
const idempotentAdapter = await createAdapter();
|
|
1316
1380
|
const idempotentParams = params("alice", "same-key");
|
|
1317
1381
|
const firstClaim = await idempotentAdapter.executeClaimRewardTransaction(
|
|
@@ -1327,6 +1391,10 @@ async function runPersistenceAdapterComplianceTests(createAdapter) {
|
|
|
1327
1391
|
await idempotentAdapter.getRewardStock("compliance-rally", "__shared__") === 0,
|
|
1328
1392
|
"idempotent retry decremented shared stock twice"
|
|
1329
1393
|
);
|
|
1394
|
+
assert(
|
|
1395
|
+
await idempotentAdapter.getRewardStock("compliance-rally", "reward") === 0,
|
|
1396
|
+
"idempotent retry decremented per-reward stock twice"
|
|
1397
|
+
);
|
|
1330
1398
|
const rollbackAdapter = await createAdapter();
|
|
1331
1399
|
const rollbackParams = params("alice", "rollback");
|
|
1332
1400
|
const rollback = await rollbackAdapter.executeClaimRewardTransaction(rollbackParams, () => {
|