@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/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,16 @@ import * as _stamprally_core from '@stamprally/core';
|
|
|
2
2
|
import { UserRallyState, AdminRallyConfig } from '@stamprally/core';
|
|
3
3
|
export { UserRallyState } from '@stamprally/core';
|
|
4
4
|
|
|
5
|
+
/** Authentication already verified by the host application's middleware. */
|
|
6
|
+
interface TrustedAuthContext {
|
|
7
|
+
readonly authenticatedUserId: string;
|
|
8
|
+
readonly isAnonymous?: boolean;
|
|
9
|
+
readonly sessionId?: string;
|
|
10
|
+
readonly claims?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** @deprecated Use TrustedAuthContext. */
|
|
13
|
+
type AuthenticationContext = TrustedAuthContext;
|
|
14
|
+
|
|
5
15
|
interface UserClaimRecord {
|
|
6
16
|
readonly rallyId: string;
|
|
7
17
|
readonly userId: string;
|
|
@@ -39,6 +49,16 @@ interface ClaimRewardTransactionMutation {
|
|
|
39
49
|
/** A domain rejection is committed as an audit/idempotency record without mutations. */
|
|
40
50
|
readonly error?: string;
|
|
41
51
|
}
|
|
52
|
+
interface ClaimRewardMutationContext {
|
|
53
|
+
readonly rewardStock: number | null;
|
|
54
|
+
readonly sharedStock: number | null;
|
|
55
|
+
readonly primaryStock: number | null;
|
|
56
|
+
readonly secondaryStock: number | null;
|
|
57
|
+
/** @deprecated Use rewardStock. */
|
|
58
|
+
readonly stock: number | null;
|
|
59
|
+
readonly claimCount: number;
|
|
60
|
+
readonly userState: UserRallyState;
|
|
61
|
+
}
|
|
42
62
|
interface CheckInTransactionParams {
|
|
43
63
|
readonly rallyId: string;
|
|
44
64
|
readonly userId: string;
|
|
@@ -67,17 +87,14 @@ interface ServerPersistenceAdapter {
|
|
|
67
87
|
* Atomically commits stock, user state, claim count, audit log, and idempotency data.
|
|
68
88
|
* Adapters must roll back every write when the callback or any commit operation fails.
|
|
69
89
|
*/
|
|
70
|
-
executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: {
|
|
71
|
-
readonly stock: number | null;
|
|
72
|
-
readonly secondaryStock: number | null;
|
|
73
|
-
readonly claimCount: number;
|
|
74
|
-
readonly userState: UserRallyState;
|
|
75
|
-
}) => ClaimRewardTransactionMutation): Promise<{
|
|
90
|
+
executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: ClaimRewardMutationContext) => ClaimRewardTransactionMutation): Promise<{
|
|
76
91
|
readonly success: boolean;
|
|
77
92
|
readonly error?: string;
|
|
78
93
|
}>;
|
|
79
94
|
/** Set false when the adapter cannot persist per-reward inventory atomically. */
|
|
80
95
|
readonly supportsRewardStock?: boolean;
|
|
96
|
+
/** Must be true when a claim plan uses `secondaryStockKey`. */
|
|
97
|
+
readonly supportsSecondaryStock?: boolean;
|
|
81
98
|
acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
|
|
82
99
|
releaseLock(rallyId: string, lockKey: string): Promise<void>;
|
|
83
100
|
getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
|
|
@@ -101,7 +118,8 @@ interface InMemoryServerPersistenceOptions {
|
|
|
101
118
|
}
|
|
102
119
|
declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapter {
|
|
103
120
|
#private;
|
|
104
|
-
readonly supportsRewardStock
|
|
121
|
+
readonly supportsRewardStock: boolean;
|
|
122
|
+
readonly supportsSecondaryStock: boolean;
|
|
105
123
|
constructor(options?: InMemoryServerPersistenceOptions);
|
|
106
124
|
acquireLock(rallyId: string, key: string, ttlMs: number): Promise<boolean>;
|
|
107
125
|
releaseLock(rallyId: string, key: string): Promise<void>;
|
|
@@ -111,12 +129,7 @@ declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapt
|
|
|
111
129
|
remainingStock: number;
|
|
112
130
|
}>;
|
|
113
131
|
restoreRewardStock(rallyId: string, rewardId: string, count?: number): Promise<void>;
|
|
114
|
-
executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: {
|
|
115
|
-
readonly stock: number | null;
|
|
116
|
-
readonly secondaryStock: number | null;
|
|
117
|
-
readonly claimCount: number;
|
|
118
|
-
readonly userState: UserRallyState;
|
|
119
|
-
}) => ClaimRewardTransactionMutation): Promise<{
|
|
132
|
+
executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: ClaimRewardMutationContext) => ClaimRewardTransactionMutation): Promise<{
|
|
120
133
|
readonly success: boolean;
|
|
121
134
|
readonly error?: string;
|
|
122
135
|
}>;
|
|
@@ -175,11 +188,7 @@ interface SqlClaimRewardStore<Tx> {
|
|
|
175
188
|
* while unexpected write failures reject the transaction and roll everything
|
|
176
189
|
* back.
|
|
177
190
|
*/
|
|
178
|
-
declare function executeClaimRewardTransaction<Tx>(database: SqlTransactionDatabase<Tx>, store: SqlClaimRewardStore<Tx>, params: ClaimRewardTransactionParams, mutation: (current: {
|
|
179
|
-
readonly stock: number | null;
|
|
180
|
-
readonly claimCount: number;
|
|
181
|
-
readonly userState: UserRallyState;
|
|
182
|
-
}) => ClaimRewardTransactionMutation): Promise<{
|
|
191
|
+
declare function executeClaimRewardTransaction<Tx>(database: SqlTransactionDatabase<Tx>, store: SqlClaimRewardStore<Tx>, params: ClaimRewardTransactionParams, mutation: (current: ClaimRewardMutationContext) => ClaimRewardTransactionMutation): Promise<{
|
|
183
192
|
readonly success: boolean;
|
|
184
193
|
readonly error?: string;
|
|
185
194
|
}>;
|
|
@@ -263,14 +272,14 @@ declare class StampRallyServer {
|
|
|
263
272
|
handleCheckIn(request: Request): Promise<Response>;
|
|
264
273
|
handleClaimReward(request: Request): Promise<Response>;
|
|
265
274
|
handleSync(request: Request): Promise<Response>;
|
|
266
|
-
checkIn(request: CheckInRequest): Promise<CheckInResponse>;
|
|
267
|
-
claimReward(request: ClaimRewardRequest): Promise<ClaimResponse>;
|
|
268
|
-
sync(rallyId: string,
|
|
275
|
+
checkIn(request: CheckInRequest, authContext?: TrustedAuthContext): Promise<CheckInResponse>;
|
|
276
|
+
claimReward(request: ClaimRewardRequest, authContext?: TrustedAuthContext): Promise<ClaimResponse>;
|
|
277
|
+
sync(rallyId: string, identity: string | TrustedAuthContext): Promise<UserRallyState>;
|
|
269
278
|
syncProgress(request: {
|
|
270
279
|
readonly rallyId: string;
|
|
271
280
|
readonly userId?: string;
|
|
272
281
|
readonly anonymousSessionId?: string;
|
|
273
|
-
}): Promise<UserRallyState>;
|
|
282
|
+
}, authContext: TrustedAuthContext): Promise<UserRallyState>;
|
|
274
283
|
}
|
|
275
284
|
|
|
276
285
|
/**
|
|
@@ -323,13 +332,10 @@ type CheckInResponse = {
|
|
|
323
332
|
interface ServerOptions {
|
|
324
333
|
readonly lockTtlMs?: number;
|
|
325
334
|
readonly idempotencyTtlMs?: number;
|
|
326
|
-
readonly authenticate?: (request: Request) => Promise<string |
|
|
335
|
+
readonly authenticate?: (request: Request) => Promise<string | TrustedAuthContext | null> | string | TrustedAuthContext | null;
|
|
327
336
|
readonly customValidators?: Readonly<Record<string, _stamprally_core.Validator>>;
|
|
328
337
|
readonly now?: () => string;
|
|
329
338
|
readonly anonymousPolicy?: "session_scoped" | "reject" | "shared_global_opt_in_insecure";
|
|
330
339
|
}
|
|
331
|
-
interface AuthenticationContext {
|
|
332
|
-
readonly authenticatedUserId: string;
|
|
333
|
-
}
|
|
334
340
|
|
|
335
|
-
export { type AuditLog, type AuthenticationContext, type CheckInRequest, type CheckInResponse, type CheckInTransactionMutation, type CheckInTransactionParams, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type RedisMultiExecutor, type RequestValidationError, RequestValidationException, type RequestValidationResult, type ServerOptions, type ServerPersistenceAdapter, type SqlCheckInStore, type SqlClaimRewardStore, type SqlTransactionDatabase, StampRallyServer, type SyncOperationStatus, type UserClaimRecord, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
|
|
341
|
+
export { type AuditLog, type AuthenticationContext, type CheckInRequest, type CheckInResponse, type CheckInTransactionMutation, type CheckInTransactionParams, type ClaimRewardMutationContext, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type RedisMultiExecutor, type RequestValidationError, RequestValidationException, type RequestValidationResult, type ServerOptions, type ServerPersistenceAdapter, type SqlCheckInStore, type SqlClaimRewardStore, type SqlTransactionDatabase, StampRallyServer, type SyncOperationStatus, type TrustedAuthContext, type UserClaimRecord, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,16 @@ import * as _stamprally_core from '@stamprally/core';
|
|
|
2
2
|
import { UserRallyState, AdminRallyConfig } from '@stamprally/core';
|
|
3
3
|
export { UserRallyState } from '@stamprally/core';
|
|
4
4
|
|
|
5
|
+
/** Authentication already verified by the host application's middleware. */
|
|
6
|
+
interface TrustedAuthContext {
|
|
7
|
+
readonly authenticatedUserId: string;
|
|
8
|
+
readonly isAnonymous?: boolean;
|
|
9
|
+
readonly sessionId?: string;
|
|
10
|
+
readonly claims?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** @deprecated Use TrustedAuthContext. */
|
|
13
|
+
type AuthenticationContext = TrustedAuthContext;
|
|
14
|
+
|
|
5
15
|
interface UserClaimRecord {
|
|
6
16
|
readonly rallyId: string;
|
|
7
17
|
readonly userId: string;
|
|
@@ -39,6 +49,16 @@ interface ClaimRewardTransactionMutation {
|
|
|
39
49
|
/** A domain rejection is committed as an audit/idempotency record without mutations. */
|
|
40
50
|
readonly error?: string;
|
|
41
51
|
}
|
|
52
|
+
interface ClaimRewardMutationContext {
|
|
53
|
+
readonly rewardStock: number | null;
|
|
54
|
+
readonly sharedStock: number | null;
|
|
55
|
+
readonly primaryStock: number | null;
|
|
56
|
+
readonly secondaryStock: number | null;
|
|
57
|
+
/** @deprecated Use rewardStock. */
|
|
58
|
+
readonly stock: number | null;
|
|
59
|
+
readonly claimCount: number;
|
|
60
|
+
readonly userState: UserRallyState;
|
|
61
|
+
}
|
|
42
62
|
interface CheckInTransactionParams {
|
|
43
63
|
readonly rallyId: string;
|
|
44
64
|
readonly userId: string;
|
|
@@ -67,17 +87,14 @@ interface ServerPersistenceAdapter {
|
|
|
67
87
|
* Atomically commits stock, user state, claim count, audit log, and idempotency data.
|
|
68
88
|
* Adapters must roll back every write when the callback or any commit operation fails.
|
|
69
89
|
*/
|
|
70
|
-
executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: {
|
|
71
|
-
readonly stock: number | null;
|
|
72
|
-
readonly secondaryStock: number | null;
|
|
73
|
-
readonly claimCount: number;
|
|
74
|
-
readonly userState: UserRallyState;
|
|
75
|
-
}) => ClaimRewardTransactionMutation): Promise<{
|
|
90
|
+
executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: ClaimRewardMutationContext) => ClaimRewardTransactionMutation): Promise<{
|
|
76
91
|
readonly success: boolean;
|
|
77
92
|
readonly error?: string;
|
|
78
93
|
}>;
|
|
79
94
|
/** Set false when the adapter cannot persist per-reward inventory atomically. */
|
|
80
95
|
readonly supportsRewardStock?: boolean;
|
|
96
|
+
/** Must be true when a claim plan uses `secondaryStockKey`. */
|
|
97
|
+
readonly supportsSecondaryStock?: boolean;
|
|
81
98
|
acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
|
|
82
99
|
releaseLock(rallyId: string, lockKey: string): Promise<void>;
|
|
83
100
|
getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
|
|
@@ -101,7 +118,8 @@ interface InMemoryServerPersistenceOptions {
|
|
|
101
118
|
}
|
|
102
119
|
declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapter {
|
|
103
120
|
#private;
|
|
104
|
-
readonly supportsRewardStock
|
|
121
|
+
readonly supportsRewardStock: boolean;
|
|
122
|
+
readonly supportsSecondaryStock: boolean;
|
|
105
123
|
constructor(options?: InMemoryServerPersistenceOptions);
|
|
106
124
|
acquireLock(rallyId: string, key: string, ttlMs: number): Promise<boolean>;
|
|
107
125
|
releaseLock(rallyId: string, key: string): Promise<void>;
|
|
@@ -111,12 +129,7 @@ declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapt
|
|
|
111
129
|
remainingStock: number;
|
|
112
130
|
}>;
|
|
113
131
|
restoreRewardStock(rallyId: string, rewardId: string, count?: number): Promise<void>;
|
|
114
|
-
executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: {
|
|
115
|
-
readonly stock: number | null;
|
|
116
|
-
readonly secondaryStock: number | null;
|
|
117
|
-
readonly claimCount: number;
|
|
118
|
-
readonly userState: UserRallyState;
|
|
119
|
-
}) => ClaimRewardTransactionMutation): Promise<{
|
|
132
|
+
executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: ClaimRewardMutationContext) => ClaimRewardTransactionMutation): Promise<{
|
|
120
133
|
readonly success: boolean;
|
|
121
134
|
readonly error?: string;
|
|
122
135
|
}>;
|
|
@@ -175,11 +188,7 @@ interface SqlClaimRewardStore<Tx> {
|
|
|
175
188
|
* while unexpected write failures reject the transaction and roll everything
|
|
176
189
|
* back.
|
|
177
190
|
*/
|
|
178
|
-
declare function executeClaimRewardTransaction<Tx>(database: SqlTransactionDatabase<Tx>, store: SqlClaimRewardStore<Tx>, params: ClaimRewardTransactionParams, mutation: (current: {
|
|
179
|
-
readonly stock: number | null;
|
|
180
|
-
readonly claimCount: number;
|
|
181
|
-
readonly userState: UserRallyState;
|
|
182
|
-
}) => ClaimRewardTransactionMutation): Promise<{
|
|
191
|
+
declare function executeClaimRewardTransaction<Tx>(database: SqlTransactionDatabase<Tx>, store: SqlClaimRewardStore<Tx>, params: ClaimRewardTransactionParams, mutation: (current: ClaimRewardMutationContext) => ClaimRewardTransactionMutation): Promise<{
|
|
183
192
|
readonly success: boolean;
|
|
184
193
|
readonly error?: string;
|
|
185
194
|
}>;
|
|
@@ -263,14 +272,14 @@ declare class StampRallyServer {
|
|
|
263
272
|
handleCheckIn(request: Request): Promise<Response>;
|
|
264
273
|
handleClaimReward(request: Request): Promise<Response>;
|
|
265
274
|
handleSync(request: Request): Promise<Response>;
|
|
266
|
-
checkIn(request: CheckInRequest): Promise<CheckInResponse>;
|
|
267
|
-
claimReward(request: ClaimRewardRequest): Promise<ClaimResponse>;
|
|
268
|
-
sync(rallyId: string,
|
|
275
|
+
checkIn(request: CheckInRequest, authContext?: TrustedAuthContext): Promise<CheckInResponse>;
|
|
276
|
+
claimReward(request: ClaimRewardRequest, authContext?: TrustedAuthContext): Promise<ClaimResponse>;
|
|
277
|
+
sync(rallyId: string, identity: string | TrustedAuthContext): Promise<UserRallyState>;
|
|
269
278
|
syncProgress(request: {
|
|
270
279
|
readonly rallyId: string;
|
|
271
280
|
readonly userId?: string;
|
|
272
281
|
readonly anonymousSessionId?: string;
|
|
273
|
-
}): Promise<UserRallyState>;
|
|
282
|
+
}, authContext: TrustedAuthContext): Promise<UserRallyState>;
|
|
274
283
|
}
|
|
275
284
|
|
|
276
285
|
/**
|
|
@@ -323,13 +332,10 @@ type CheckInResponse = {
|
|
|
323
332
|
interface ServerOptions {
|
|
324
333
|
readonly lockTtlMs?: number;
|
|
325
334
|
readonly idempotencyTtlMs?: number;
|
|
326
|
-
readonly authenticate?: (request: Request) => Promise<string |
|
|
335
|
+
readonly authenticate?: (request: Request) => Promise<string | TrustedAuthContext | null> | string | TrustedAuthContext | null;
|
|
327
336
|
readonly customValidators?: Readonly<Record<string, _stamprally_core.Validator>>;
|
|
328
337
|
readonly now?: () => string;
|
|
329
338
|
readonly anonymousPolicy?: "session_scoped" | "reject" | "shared_global_opt_in_insecure";
|
|
330
339
|
}
|
|
331
|
-
interface AuthenticationContext {
|
|
332
|
-
readonly authenticatedUserId: string;
|
|
333
|
-
}
|
|
334
340
|
|
|
335
|
-
export { type AuditLog, type AuthenticationContext, type CheckInRequest, type CheckInResponse, type CheckInTransactionMutation, type CheckInTransactionParams, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type RedisMultiExecutor, type RequestValidationError, RequestValidationException, type RequestValidationResult, type ServerOptions, type ServerPersistenceAdapter, type SqlCheckInStore, type SqlClaimRewardStore, type SqlTransactionDatabase, StampRallyServer, type SyncOperationStatus, type UserClaimRecord, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
|
|
341
|
+
export { type AuditLog, type AuthenticationContext, type CheckInRequest, type CheckInResponse, type CheckInTransactionMutation, type CheckInTransactionParams, type ClaimRewardMutationContext, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type RedisMultiExecutor, type RequestValidationError, RequestValidationException, type RequestValidationResult, type ServerOptions, type ServerPersistenceAdapter, type SqlCheckInStore, type SqlClaimRewardStore, type SqlTransactionDatabase, StampRallyServer, type SyncOperationStatus, type TrustedAuthContext, type UserClaimRecord, assertValidCheckInParams, assertValidClaimParams, assertValidSyncParams, executeCheckInTransaction, executeClaimRewardTransaction, executeRedisTransaction, runPersistenceAdapterComplianceTests, validateCheckInRequest, validateClaimRewardRequest, validateSyncRequest };
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,16 @@ async function executeClaimRewardTransaction(database, store, params2, mutation2
|
|
|
5
5
|
try {
|
|
6
6
|
return await database.transaction(async (transaction) => {
|
|
7
7
|
const current = await store.readContext(transaction, params2);
|
|
8
|
-
const
|
|
8
|
+
const secondaryStock = current.secondaryStock ?? null;
|
|
9
|
+
const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : current.stock;
|
|
10
|
+
const next = mutation2({
|
|
11
|
+
...current,
|
|
12
|
+
rewardStock: rewardStock2,
|
|
13
|
+
sharedStock: params2.stockKey === "__shared__" ? current.stock : null,
|
|
14
|
+
primaryStock: rewardStock2,
|
|
15
|
+
secondaryStock,
|
|
16
|
+
stock: current.stock
|
|
17
|
+
});
|
|
9
18
|
if (next.error !== void 0) {
|
|
10
19
|
await store.writeAudit(transaction, next.auditLog);
|
|
11
20
|
if (params2.idempotencyKey !== void 0 && next.result !== void 0)
|
|
@@ -13,7 +22,7 @@ async function executeClaimRewardTransaction(database, store, params2, mutation2
|
|
|
13
22
|
return { success: false, error: next.error };
|
|
14
23
|
}
|
|
15
24
|
if (next.nextSecondaryStock !== void 0 && store.writeSecondaryStock === void 0)
|
|
16
|
-
return { success: false, error: "
|
|
25
|
+
return { success: false, error: "INVENTORY_STORAGE_NOT_IMPLEMENTED" };
|
|
17
26
|
if (next.nextStock !== null) await store.writeStock(transaction, params2, next.nextStock);
|
|
18
27
|
if (next.nextSecondaryStock !== void 0 && store.writeSecondaryStock !== void 0) {
|
|
19
28
|
if (next.nextSecondaryStock !== null)
|
|
@@ -67,6 +76,7 @@ async function executeRedisTransaction(redis, queue) {
|
|
|
67
76
|
// src/persistence.ts
|
|
68
77
|
var InMemoryServerPersistenceAdapter = class {
|
|
69
78
|
supportsRewardStock = true;
|
|
79
|
+
supportsSecondaryStock = true;
|
|
70
80
|
#locks = /* @__PURE__ */ new Map();
|
|
71
81
|
#idempotent = /* @__PURE__ */ new Map();
|
|
72
82
|
#states = /* @__PURE__ */ new Map();
|
|
@@ -144,9 +154,16 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
144
154
|
params2.stockKey ?? params2.rewardId
|
|
145
155
|
);
|
|
146
156
|
const storedSecondaryStock = params2.secondaryStockKey === void 0 ? null : await this.getRewardStock(params2.rallyId, params2.secondaryStockKey);
|
|
157
|
+
const stock = storedStock ?? initialStock;
|
|
158
|
+
const secondaryStock = storedSecondaryStock ?? initialSecondaryStock;
|
|
159
|
+
const rewardStock2 = params2.stockKey === "__shared__" ? secondaryStock : stock;
|
|
160
|
+
const sharedStock2 = params2.stockKey === "__shared__" ? stock : null;
|
|
147
161
|
const mutationResult = mutation2({
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
rewardStock: rewardStock2,
|
|
163
|
+
sharedStock: sharedStock2,
|
|
164
|
+
primaryStock: rewardStock2,
|
|
165
|
+
secondaryStock,
|
|
166
|
+
stock,
|
|
150
167
|
claimCount: await this.getUserClaimCount(params2.rallyId, params2.userId, params2.rewardId),
|
|
151
168
|
userState
|
|
152
169
|
});
|
|
@@ -624,26 +641,41 @@ function initialState(config, userId, timestamp) {
|
|
|
624
641
|
updatedAt: timestamp
|
|
625
642
|
};
|
|
626
643
|
}
|
|
627
|
-
function rewardStock(config,
|
|
628
|
-
const
|
|
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];
|
|
629
648
|
if (stockLimit === void 0) return configured ?? null;
|
|
630
649
|
if (configured === void 0) return stockLimit;
|
|
631
650
|
return Math.min(stockLimit, configured);
|
|
632
651
|
}
|
|
633
652
|
function sharedStock(config) {
|
|
634
|
-
return config.inventory?.sharedStock ??
|
|
653
|
+
return config.inventory?.sharedStock ?? null;
|
|
635
654
|
}
|
|
636
655
|
function inventoryPlan(config, rewardId, stockLimit) {
|
|
637
|
-
const
|
|
656
|
+
const reward = config.rewards.find((item) => item.id === rewardId);
|
|
657
|
+
const individual = reward === void 0 ? stockLimit ?? null : rewardStock(config, reward);
|
|
638
658
|
const shared = sharedStock(config);
|
|
659
|
+
const explicitPrimaryKey = reward?.stockKey;
|
|
660
|
+
const explicitSecondaryKey = reward?.secondaryStockKey;
|
|
639
661
|
if (config.inventoryMode === "shared" && shared !== null) {
|
|
640
662
|
return {
|
|
641
|
-
primaryKey: "__shared__",
|
|
642
|
-
primaryInitial: shared,
|
|
643
|
-
...
|
|
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 }
|
|
644
669
|
};
|
|
645
670
|
}
|
|
646
|
-
return {
|
|
671
|
+
return {
|
|
672
|
+
primaryKey: explicitPrimaryKey ?? rewardId,
|
|
673
|
+
primaryInitial: individual,
|
|
674
|
+
...explicitSecondaryKey === void 0 ? {} : {
|
|
675
|
+
secondaryKey: explicitSecondaryKey,
|
|
676
|
+
secondaryInitial: config.inventory?.[explicitSecondaryKey] ?? null
|
|
677
|
+
}
|
|
678
|
+
};
|
|
647
679
|
}
|
|
648
680
|
function getProof(context) {
|
|
649
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;
|
|
@@ -678,7 +710,18 @@ function operationStatus(result) {
|
|
|
678
710
|
if (result.ok) return "ACCEPTED";
|
|
679
711
|
return result.code === "CONFLICT" || result.code === "PERSISTENCE_FAILED" ? "RETRYABLE_ERROR" : "REJECTED_PERMANENT";
|
|
680
712
|
}
|
|
681
|
-
function withDirectIdentity(request) {
|
|
713
|
+
function withDirectIdentity(request, authContext) {
|
|
714
|
+
if (authContext !== void 0) {
|
|
715
|
+
if (authContext.authenticatedUserId.trim() === "")
|
|
716
|
+
throw new RequestValidationException([
|
|
717
|
+
{
|
|
718
|
+
path: "authContext.authenticatedUserId",
|
|
719
|
+
message: "An authenticated userId is required.",
|
|
720
|
+
code: "REQUIRED"
|
|
721
|
+
}
|
|
722
|
+
]);
|
|
723
|
+
return { ...request, userId: authContext.authenticatedUserId };
|
|
724
|
+
}
|
|
682
725
|
if (request.userId !== void 0) return { ...request, userId: request.userId };
|
|
683
726
|
if (request.anonymousSessionId !== void 0 && isUuidV4(request.anonymousSessionId))
|
|
684
727
|
return { ...request, userId: request.anonymousSessionId };
|
|
@@ -794,22 +837,28 @@ var StampRallyServer = class {
|
|
|
794
837
|
401
|
|
795
838
|
);
|
|
796
839
|
const sessionId = request.headers.get("x-anonymous-session-id");
|
|
840
|
+
const authContext = {
|
|
841
|
+
authenticatedUserId: userId,
|
|
842
|
+
...sessionId === null ? {} : { isAnonymous: true, sessionId }
|
|
843
|
+
};
|
|
797
844
|
try {
|
|
798
845
|
return json({
|
|
799
846
|
ok: true,
|
|
800
|
-
state: await this.syncProgress(
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
847
|
+
state: await this.syncProgress(
|
|
848
|
+
{
|
|
849
|
+
rallyId: body.data.rallyId,
|
|
850
|
+
...sessionId === null ? {} : { anonymousSessionId: sessionId }
|
|
851
|
+
},
|
|
852
|
+
authContext
|
|
853
|
+
)
|
|
805
854
|
});
|
|
806
855
|
} catch (error) {
|
|
807
856
|
if (error instanceof RequestValidationException) return validationResponse(error.errors);
|
|
808
857
|
throw error;
|
|
809
858
|
}
|
|
810
859
|
}
|
|
811
|
-
async checkIn(request) {
|
|
812
|
-
const directRequest = withDirectIdentity(request);
|
|
860
|
+
async checkIn(request, authContext) {
|
|
861
|
+
const directRequest = withDirectIdentity(request, authContext);
|
|
813
862
|
assertValidCheckInParams(directRequest, this.#config);
|
|
814
863
|
const { userId } = directRequest;
|
|
815
864
|
const key = `check-in:${request.rallyId}:${userId}:${request.idempotencyKey}`;
|
|
@@ -942,8 +991,8 @@ var StampRallyServer = class {
|
|
|
942
991
|
await this.#persistence.releaseLock(request.rallyId, lockKey);
|
|
943
992
|
}
|
|
944
993
|
}
|
|
945
|
-
async claimReward(request) {
|
|
946
|
-
const directRequest = withDirectIdentity(request);
|
|
994
|
+
async claimReward(request, authContext) {
|
|
995
|
+
const directRequest = withDirectIdentity(request, authContext);
|
|
947
996
|
assertValidClaimParams(directRequest, this.#config);
|
|
948
997
|
const { userId } = directRequest;
|
|
949
998
|
const key = `claim:${request.rallyId}:${userId}:${request.rewardId}:${request.idempotencyKey}`;
|
|
@@ -961,13 +1010,16 @@ var StampRallyServer = class {
|
|
|
961
1010
|
now(this.#options)
|
|
962
1011
|
);
|
|
963
1012
|
const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
|
|
964
|
-
if (
|
|
1013
|
+
if (plan.secondaryKey !== void 0 && this.#persistence.supportsSecondaryStock !== true)
|
|
1014
|
+
throw new Error("SECONDARY_STOCK_UNSUPPORTED");
|
|
1015
|
+
const inventoryEnabled = plan.primaryInitial !== null || plan.secondaryInitial !== void 0 && plan.secondaryInitial !== null;
|
|
1016
|
+
if (inventoryEnabled && (this.#persistence.supportsRewardStock === false || typeof this.#persistence.getRewardStock !== "function" || typeof this.#persistence.executeClaimRewardTransaction !== "function"))
|
|
965
1017
|
return this.#rememberClaim(
|
|
966
1018
|
key,
|
|
967
1019
|
{
|
|
968
1020
|
ok: false,
|
|
969
|
-
code: "
|
|
970
|
-
message: "This persistence adapter cannot store
|
|
1021
|
+
code: "INVENTORY_STORAGE_NOT_IMPLEMENTED",
|
|
1022
|
+
message: "This persistence adapter cannot atomically store inventory."
|
|
971
1023
|
},
|
|
972
1024
|
directRequest,
|
|
973
1025
|
now(this.#options)
|
|
@@ -994,7 +1046,7 @@ var StampRallyServer = class {
|
|
|
994
1046
|
rewardId: reward.id,
|
|
995
1047
|
stockKey: plan.primaryKey,
|
|
996
1048
|
...plan.secondaryKey === void 0 ? {} : { secondaryStockKey: plan.secondaryKey },
|
|
997
|
-
rewardStockLimit: rewardStock(this.#config, reward
|
|
1049
|
+
rewardStockLimit: rewardStock(this.#config, reward),
|
|
998
1050
|
sharedStockLimit: this.#config.inventoryMode === "shared" ? sharedStock(this.#config) : null,
|
|
999
1051
|
initialStock: plan.primaryInitial,
|
|
1000
1052
|
...plan.secondaryInitial === void 0 ? {} : { initialSecondaryStock: plan.secondaryInitial },
|
|
@@ -1098,11 +1150,11 @@ var StampRallyServer = class {
|
|
|
1098
1150
|
const response = responseHolder.value;
|
|
1099
1151
|
if (!result.success) {
|
|
1100
1152
|
if (response !== null && !response.ok && response.code === result.error) return response;
|
|
1101
|
-
if (result.error === "
|
|
1153
|
+
if (result.error === "INVENTORY_STORAGE_NOT_IMPLEMENTED")
|
|
1102
1154
|
return {
|
|
1103
1155
|
ok: false,
|
|
1104
|
-
code: "
|
|
1105
|
-
message: "This persistence adapter cannot store
|
|
1156
|
+
code: "INVENTORY_STORAGE_NOT_IMPLEMENTED",
|
|
1157
|
+
message: "This persistence adapter cannot atomically store inventory."
|
|
1106
1158
|
};
|
|
1107
1159
|
return {
|
|
1108
1160
|
ok: false,
|
|
@@ -1126,21 +1178,24 @@ var StampRallyServer = class {
|
|
|
1126
1178
|
await this.#persistence.releaseLock(request.rallyId, lockKey);
|
|
1127
1179
|
}
|
|
1128
1180
|
}
|
|
1129
|
-
async sync(rallyId,
|
|
1181
|
+
async sync(rallyId, identity) {
|
|
1182
|
+
const userId = typeof identity === "string" ? identity : identity.authenticatedUserId;
|
|
1130
1183
|
assertValidSyncParams({ rallyId, userId }, this.#config);
|
|
1131
1184
|
const state2 = await this.#persistence.getUserState(rallyId, userId) ?? initialState(this.#config, userId, now(this.#options));
|
|
1132
1185
|
return this.#attachInventory(state2);
|
|
1133
1186
|
}
|
|
1134
|
-
async syncProgress(request) {
|
|
1135
|
-
const directRequest = withDirectIdentity(request);
|
|
1187
|
+
async syncProgress(request, authContext) {
|
|
1188
|
+
const directRequest = withDirectIdentity(request, authContext);
|
|
1136
1189
|
assertValidSyncParams(directRequest, this.#config);
|
|
1137
|
-
return this.sync(directRequest.rallyId,
|
|
1190
|
+
return this.sync(directRequest.rallyId, authContext);
|
|
1138
1191
|
}
|
|
1139
1192
|
async #attachInventory(state2) {
|
|
1140
1193
|
const rewardRemaining = {};
|
|
1141
1194
|
for (const reward of this.#config.rewards) {
|
|
1142
1195
|
const plan = inventoryPlan(this.#config, reward.id, reward.stockLimit);
|
|
1143
1196
|
if (plan.secondaryKey !== void 0) {
|
|
1197
|
+
if (this.#persistence.supportsSecondaryStock !== true)
|
|
1198
|
+
throw new Error("SECONDARY_STOCK_UNSUPPORTED");
|
|
1144
1199
|
const stock = await this.#persistence.getRewardStock(state2.rallyId, plan.secondaryKey);
|
|
1145
1200
|
const remaining = stock ?? plan.secondaryInitial ?? null;
|
|
1146
1201
|
if (remaining !== null) rewardRemaining[reward.id] = Math.max(0, remaining);
|
|
@@ -1310,6 +1365,15 @@ async function runPersistenceAdapterComplianceTests(createAdapter) {
|
|
|
1310
1365
|
await adapter.getRewardStock("compliance-rally", "reward") === 0,
|
|
1311
1366
|
"per-reward stock was not decremented atomically"
|
|
1312
1367
|
);
|
|
1368
|
+
const exhausted = await adapter.executeClaimRewardTransaction(
|
|
1369
|
+
params("carol", "boundary"),
|
|
1370
|
+
mutation
|
|
1371
|
+
);
|
|
1372
|
+
assert(!exhausted.success, "a zero-stock claim was accepted");
|
|
1373
|
+
assert(
|
|
1374
|
+
await adapter.getRewardStock("compliance-rally", "__shared__") === 0 && await adapter.getRewardStock("compliance-rally", "reward") === 0,
|
|
1375
|
+
"zero-stock rejection did not preserve both stock boundaries"
|
|
1376
|
+
);
|
|
1313
1377
|
const idempotentAdapter = await createAdapter();
|
|
1314
1378
|
const idempotentParams = params("alice", "same-key");
|
|
1315
1379
|
const firstClaim = await idempotentAdapter.executeClaimRewardTransaction(
|
|
@@ -1325,6 +1389,10 @@ async function runPersistenceAdapterComplianceTests(createAdapter) {
|
|
|
1325
1389
|
await idempotentAdapter.getRewardStock("compliance-rally", "__shared__") === 0,
|
|
1326
1390
|
"idempotent retry decremented shared stock twice"
|
|
1327
1391
|
);
|
|
1392
|
+
assert(
|
|
1393
|
+
await idempotentAdapter.getRewardStock("compliance-rally", "reward") === 0,
|
|
1394
|
+
"idempotent retry decremented per-reward stock twice"
|
|
1395
|
+
);
|
|
1328
1396
|
const rollbackAdapter = await createAdapter();
|
|
1329
1397
|
const rollbackParams = params("alice", "rollback");
|
|
1330
1398
|
const rollback = await rollbackAdapter.executeClaimRewardTransaction(rollbackParams, () => {
|