@stamprally/server 0.19.0 → 0.20.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 +26 -1
- package/dist/index.cjs +82 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +82 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @stamprally/server v0.
|
|
1
|
+
# @stamprally/server v0.20.0
|
|
2
2
|
|
|
3
3
|
Web Standard `Request` / `Response` handlers for server-authoritative check-ins and reward claims.
|
|
4
4
|
|
|
@@ -26,6 +26,31 @@ Configure `anonymousPolicy: "session_scoped"` to use the UUID v4 from the
|
|
|
26
26
|
HTTP 401 when no authenticated identity is present. Request validation failures use
|
|
27
27
|
HTTP 400 with `{ error: "VALIDATION_FAILED", details: [...] }`.
|
|
28
28
|
|
|
29
|
+
## Batch sync and trusted authentication
|
|
30
|
+
|
|
31
|
+
`syncProgress(request, authContext)` evaluates `operations` by their request
|
|
32
|
+
timestamp, preserving FIFO order for equal timestamps, and returns
|
|
33
|
+
`{ results, currentState, syncTimestamp }`. A permanently rejected
|
|
34
|
+
operation does not stop unrelated operations; a later operation depending on that
|
|
35
|
+
resource receives `REJECTED_PREREQUISITE_FAILED`. Retryable failures are returned
|
|
36
|
+
as `FAILED_RETRYABLE` so the client can retain them for the next request.
|
|
37
|
+
|
|
38
|
+
Direct server calls require a verified `TrustedAuthContext`:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const progress = await server.syncProgress(
|
|
42
|
+
{ rallyId: "city-tour", operations },
|
|
43
|
+
{ authenticatedUserId: session.userId, claims: session.claims },
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The context identity is authoritative and overrides any caller-supplied `userId`.
|
|
48
|
+
For inventory, `stockKey` selects the primary bucket and `secondaryStockKey`
|
|
49
|
+
selects an additional bucket that must be decremented in the same transaction.
|
|
50
|
+
An adapter using a secondary key must expose `supportsSecondaryStock: true` and
|
|
51
|
+
persist both buckets atomically; otherwise the claim fails closed with
|
|
52
|
+
`SECONDARY_STOCK_UNSUPPORTED`.
|
|
53
|
+
|
|
29
54
|
Hono can mount the handler directly because it accepts the same Web Standard request and response types.
|
|
30
55
|
|
|
31
56
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -663,6 +663,15 @@ function timestampMillis(timestamp) {
|
|
|
663
663
|
const value = Date.parse(timestamp);
|
|
664
664
|
return Number.isFinite(value) ? value : Date.now();
|
|
665
665
|
}
|
|
666
|
+
function operationTimestamp(operation) {
|
|
667
|
+
const value = operation.request.now;
|
|
668
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
669
|
+
if (typeof value === "string") {
|
|
670
|
+
const parsed = Date.parse(value);
|
|
671
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
672
|
+
}
|
|
673
|
+
return Number.MAX_SAFE_INTEGER;
|
|
674
|
+
}
|
|
666
675
|
function validationResponse(errors2) {
|
|
667
676
|
return json(
|
|
668
677
|
{
|
|
@@ -750,6 +759,38 @@ function operationStatus(result) {
|
|
|
750
759
|
if (result.ok) return "ACCEPTED";
|
|
751
760
|
return result.code === "CONFLICT" || result.code === "PERSISTENCE_FAILED" ? "RETRYABLE_ERROR" : "REJECTED_PERMANENT";
|
|
752
761
|
}
|
|
762
|
+
function syncOperationId(operation, userId) {
|
|
763
|
+
return `${operation.kind}:${operation.request.rallyId}:${userId}:${operation.request.idempotencyKey}`;
|
|
764
|
+
}
|
|
765
|
+
function syncOperationResult(operation, userId, result) {
|
|
766
|
+
const operationId = syncOperationId(operation, userId);
|
|
767
|
+
const resourceId = operation.kind === "checkIn" ? operation.request.spotId : operation.request.rewardId;
|
|
768
|
+
const action = operation.kind === "checkIn" ? "CHECK_IN" : "CLAIM_REWARD";
|
|
769
|
+
const status = operationStatus(result);
|
|
770
|
+
if (status === "ACCEPTED") {
|
|
771
|
+
return {
|
|
772
|
+
operationId,
|
|
773
|
+
status,
|
|
774
|
+
resourceId,
|
|
775
|
+
action,
|
|
776
|
+
appliedAt: timestampMillis(result.ok ? result.state.updatedAt : "")
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
if (status === "RETRYABLE_ERROR")
|
|
780
|
+
return {
|
|
781
|
+
operationId,
|
|
782
|
+
status: "FAILED_RETRYABLE",
|
|
783
|
+
resourceId,
|
|
784
|
+
error: result.ok ? "The operation can be retried." : result.message
|
|
785
|
+
};
|
|
786
|
+
return {
|
|
787
|
+
operationId,
|
|
788
|
+
status,
|
|
789
|
+
resourceId,
|
|
790
|
+
errorCode: result.ok ? "REJECTED_PERMANENT" : result.code,
|
|
791
|
+
reason: result.ok ? "The operation was rejected." : result.message
|
|
792
|
+
};
|
|
793
|
+
}
|
|
753
794
|
function withDirectIdentity(request, authContext) {
|
|
754
795
|
if (authContext !== void 0) {
|
|
755
796
|
if (authContext.authenticatedUserId.trim() === "")
|
|
@@ -882,16 +923,17 @@ var StampRallyServer = class {
|
|
|
882
923
|
...sessionId === null ? {} : { isAnonymous: true, sessionId }
|
|
883
924
|
};
|
|
884
925
|
try {
|
|
926
|
+
const progress = await this.syncProgress(
|
|
927
|
+
{
|
|
928
|
+
rallyId: body.data.rallyId,
|
|
929
|
+
...body.data.operations === void 0 ? {} : { operations: body.data.operations },
|
|
930
|
+
...sessionId === null ? {} : { anonymousSessionId: sessionId }
|
|
931
|
+
},
|
|
932
|
+
authContext
|
|
933
|
+
);
|
|
885
934
|
return json({
|
|
886
935
|
ok: true,
|
|
887
|
-
|
|
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
|
-
)
|
|
936
|
+
...progress
|
|
895
937
|
});
|
|
896
938
|
} catch (error) {
|
|
897
939
|
if (error instanceof RequestValidationException) return validationResponse(error.errors);
|
|
@@ -1228,11 +1270,39 @@ var StampRallyServer = class {
|
|
|
1228
1270
|
async syncProgress(request, authContext) {
|
|
1229
1271
|
const directRequest = withDirectIdentity(request, authContext);
|
|
1230
1272
|
assertValidSyncParams(directRequest, this.#config);
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1273
|
+
const results = [];
|
|
1274
|
+
const rejectedCheckIns = /* @__PURE__ */ new Set();
|
|
1275
|
+
const operations = [...request.operations ?? []].map((operation, index) => ({ operation, index })).sort(
|
|
1276
|
+
(left, right) => operationTimestamp(left.operation) - operationTimestamp(right.operation) || left.index - right.index
|
|
1277
|
+
).map(({ operation }) => operation);
|
|
1278
|
+
for (const operation of operations) {
|
|
1279
|
+
const resourceId = operation.kind === "checkIn" ? operation.request.spotId : operation.request.rewardId;
|
|
1280
|
+
if (operation.kind === "checkIn") {
|
|
1281
|
+
const spot = this.#config.spots.find((candidate) => candidate.id === resourceId);
|
|
1282
|
+
if (spot?.prerequisites?.some((prerequisite) => rejectedCheckIns.has(prerequisite))) {
|
|
1283
|
+
results.push({
|
|
1284
|
+
operationId: syncOperationId(operation, directRequest.userId),
|
|
1285
|
+
status: "REJECTED_PERMANENT",
|
|
1286
|
+
resourceId,
|
|
1287
|
+
errorCode: "REJECTED_PREREQUISITE_FAILED",
|
|
1288
|
+
reason: "A prerequisite operation was rejected by the server."
|
|
1289
|
+
});
|
|
1290
|
+
rejectedCheckIns.add(resourceId);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const result = operation.kind === "checkIn" ? await this.checkIn(operation.request, authContext) : await this.claimReward(operation.request, authContext);
|
|
1295
|
+
const operationResult = syncOperationResult(operation, directRequest.userId, result);
|
|
1296
|
+
results.push(operationResult);
|
|
1297
|
+
if (operation.kind === "checkIn" && operationResult.status === "REJECTED_PERMANENT")
|
|
1298
|
+
rejectedCheckIns.add(resourceId);
|
|
1234
1299
|
}
|
|
1235
|
-
|
|
1300
|
+
const currentState = await this.sync(directRequest.rallyId, authContext);
|
|
1301
|
+
return {
|
|
1302
|
+
results,
|
|
1303
|
+
currentState,
|
|
1304
|
+
syncTimestamp: timestampMillis(now(this.#options))
|
|
1305
|
+
};
|
|
1236
1306
|
}
|
|
1237
1307
|
async #attachInventory(state2) {
|
|
1238
1308
|
const rewardRemaining = {};
|