@jskit-ai/google-rewarded-core 0.1.1

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.
Files changed (42) hide show
  1. package/README.md +98 -0
  2. package/docs/protecting-server-actions.md +190 -0
  3. package/package.descriptor.mjs +186 -0
  4. package/package.json +10 -0
  5. package/src/server/GoogleRewardedCoreProvider.js +51 -0
  6. package/src/server/actions.js +102 -0
  7. package/src/server/inputSchemas.js +394 -0
  8. package/src/server/providerConfigs/GoogleRewardedProviderConfigsProvider.js +98 -0
  9. package/src/server/providerConfigs/actions.js +157 -0
  10. package/src/server/providerConfigs/registerRoutes.js +176 -0
  11. package/src/server/providerConfigs/repository.js +107 -0
  12. package/src/server/providerConfigs/service.js +61 -0
  13. package/src/server/registerRoutes.js +169 -0
  14. package/src/server/rules/GoogleRewardedRulesProvider.js +98 -0
  15. package/src/server/rules/actions.js +157 -0
  16. package/src/server/rules/registerRoutes.js +176 -0
  17. package/src/server/rules/repository.js +107 -0
  18. package/src/server/rules/service.js +61 -0
  19. package/src/server/service.js +552 -0
  20. package/src/server/support/requireGoogleRewardedUnlock.js +142 -0
  21. package/src/server/unlockReceipts/GoogleRewardedUnlockReceiptsProvider.js +98 -0
  22. package/src/server/unlockReceipts/actions.js +157 -0
  23. package/src/server/unlockReceipts/registerRoutes.js +176 -0
  24. package/src/server/unlockReceipts/repository.js +107 -0
  25. package/src/server/unlockReceipts/service.js +61 -0
  26. package/src/server/watchSessions/GoogleRewardedWatchSessionsProvider.js +98 -0
  27. package/src/server/watchSessions/actions.js +157 -0
  28. package/src/server/watchSessions/registerRoutes.js +176 -0
  29. package/src/server/watchSessions/repository.js +107 -0
  30. package/src/server/watchSessions/service.js +61 -0
  31. package/src/shared/googleRewardedProviderConfigResource.js +94 -0
  32. package/src/shared/googleRewardedRuleResource.js +136 -0
  33. package/src/shared/googleRewardedUnlockReceiptResource.js +113 -0
  34. package/src/shared/googleRewardedWatchSessionResource.js +139 -0
  35. package/src/shared/index.js +12 -0
  36. package/templates/migrations/google_rewarded_provider_configs_initial.cjs +27 -0
  37. package/templates/migrations/google_rewarded_rules_initial.cjs +31 -0
  38. package/templates/migrations/google_rewarded_unlock_receipts_initial.cjs +35 -0
  39. package/templates/migrations/google_rewarded_watch_sessions_initial.cjs +36 -0
  40. package/test/requireGoogleRewardedUnlock.test.js +274 -0
  41. package/test/routes.test.js +248 -0
  42. package/test/service.test.js +315 -0
@@ -0,0 +1,552 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import { simplifyJsonApiDocument } from "@jskit-ai/http-runtime/shared";
4
+
5
+ const GOOGLE_REWARDED_SURFACE = "app";
6
+
7
+ function resolveActionUser(context, input) {
8
+ const payload = normalizeObject(input);
9
+ const request = context?.requestMeta?.request || null;
10
+ return payload.user || request?.user || context?.actor || null;
11
+ }
12
+
13
+ function normalizeRecordList(document = null) {
14
+ const simplified = simplifyJsonApiDocument(document);
15
+ return Array.isArray(simplified) ? simplified.filter(Boolean) : [];
16
+ }
17
+
18
+ function normalizeRecord(document = null) {
19
+ const simplified = simplifyJsonApiDocument(document);
20
+ return simplified && typeof simplified === "object" && !Array.isArray(simplified) ? simplified : null;
21
+ }
22
+
23
+ function normalizeGateKey(value = "") {
24
+ return normalizeText(value);
25
+ }
26
+
27
+ function normalizeSurface(value = "", { fallback = GOOGLE_REWARDED_SURFACE } = {}) {
28
+ const normalized = normalizeText(value).toLowerCase();
29
+ return normalized || fallback;
30
+ }
31
+
32
+ function toIsoOrNull(value = null) {
33
+ if (!value) {
34
+ return null;
35
+ }
36
+ const date = value instanceof Date ? value : new Date(value);
37
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
38
+ }
39
+
40
+ function parseDate(value = null) {
41
+ if (!value) {
42
+ return null;
43
+ }
44
+ const date = value instanceof Date ? value : new Date(value);
45
+ return Number.isNaN(date.getTime()) ? null : date;
46
+ }
47
+
48
+ function addMinutes(date, minutes) {
49
+ const amount = Number(minutes || 0);
50
+ return new Date(date.getTime() + Math.max(0, amount) * 60 * 1000);
51
+ }
52
+
53
+ function startOfUtcDay(date) {
54
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
55
+ }
56
+
57
+ function requireActor(context, input) {
58
+ const actor = resolveActionUser(context, input);
59
+ const actorId = actor?.id == null ? "" : String(actor.id).trim();
60
+ if (!actorId) {
61
+ throw new AppError(401, "Authentication required.");
62
+ }
63
+ return actor;
64
+ }
65
+
66
+ function requireWorkspaceSlug(input = {}) {
67
+ const workspaceSlug = normalizeText(input?.workspaceSlug).toLowerCase();
68
+ if (!workspaceSlug) {
69
+ throw new AppError(400, "workspaceSlug is required.");
70
+ }
71
+ return workspaceSlug;
72
+ }
73
+
74
+ function requireGateKey(input = {}) {
75
+ const gateKey = normalizeGateKey(input?.gateKey);
76
+ if (!gateKey) {
77
+ throw new AppError(400, "gateKey is required.");
78
+ }
79
+ return gateKey;
80
+ }
81
+
82
+ function formatRule(rule = null) {
83
+ if (!rule) {
84
+ return null;
85
+ }
86
+
87
+ return {
88
+ id: rule.id || null,
89
+ gateKey: normalizeGateKey(rule.gateKey),
90
+ surface: normalizeSurface(rule.surface, { fallback: GOOGLE_REWARDED_SURFACE }),
91
+ enabled: rule.enabled === true,
92
+ unlockMinutes: Number(rule.unlockMinutes || 0),
93
+ cooldownMinutes: Number(rule.cooldownMinutes || 0),
94
+ dailyLimit: rule.dailyLimit == null ? null : Number(rule.dailyLimit),
95
+ title: normalizeText(rule.title),
96
+ description: normalizeText(rule.description)
97
+ };
98
+ }
99
+
100
+ function formatProviderConfig(record = null) {
101
+ if (!record) {
102
+ return null;
103
+ }
104
+
105
+ return {
106
+ id: record.id || null,
107
+ surface: normalizeSurface(record.surface, { fallback: GOOGLE_REWARDED_SURFACE }),
108
+ enabled: record.enabled === true,
109
+ adUnitPath: normalizeText(record.adUnitPath),
110
+ scriptMode: normalizeText(record.scriptMode) || "gpt_rewarded"
111
+ };
112
+ }
113
+
114
+ function formatWatchSession(record = null) {
115
+ if (!record) {
116
+ return null;
117
+ }
118
+
119
+ return {
120
+ id: record.id || null,
121
+ gateKey: normalizeGateKey(record.gateKey),
122
+ providerConfigId: record.providerConfigId || null,
123
+ status: normalizeSurface(record.status, { fallback: "started" }),
124
+ startedAt: toIsoOrNull(record.startedAt),
125
+ rewardedAt: toIsoOrNull(record.rewardedAt),
126
+ completedAt: toIsoOrNull(record.completedAt),
127
+ closedAt: toIsoOrNull(record.closedAt)
128
+ };
129
+ }
130
+
131
+ function formatUnlockReceipt(record = null) {
132
+ if (!record) {
133
+ return null;
134
+ }
135
+
136
+ return {
137
+ id: record.id || null,
138
+ gateKey: normalizeGateKey(record.gateKey),
139
+ providerConfigId: record.providerConfigId || null,
140
+ watchSessionId: record.watchSessionId || null,
141
+ grantedAt: toIsoOrNull(record.grantedAt),
142
+ unlockedUntil: toIsoOrNull(record.unlockedUntil)
143
+ };
144
+ }
145
+
146
+ function isFutureDate(value = null, now = new Date()) {
147
+ const date = parseDate(value);
148
+ return !!date && date.getTime() > now.getTime();
149
+ }
150
+
151
+ function createService({
152
+ googleRewardedRulesRepository,
153
+ googleRewardedProviderConfigsRepository,
154
+ googleRewardedWatchSessionsRepository,
155
+ googleRewardedUnlockReceiptsRepository
156
+ } = {}) {
157
+ if (!googleRewardedRulesRepository) {
158
+ throw new TypeError("createService requires googleRewardedRulesRepository.");
159
+ }
160
+ if (!googleRewardedProviderConfigsRepository) {
161
+ throw new TypeError("createService requires googleRewardedProviderConfigsRepository.");
162
+ }
163
+ if (!googleRewardedWatchSessionsRepository) {
164
+ throw new TypeError("createService requires googleRewardedWatchSessionsRepository.");
165
+ }
166
+ if (!googleRewardedUnlockReceiptsRepository) {
167
+ throw new TypeError("createService requires googleRewardedUnlockReceiptsRepository.");
168
+ }
169
+
170
+ async function queryFirst(repository, query = {}, options = {}) {
171
+ const rows = normalizeRecordList(await repository.queryDocuments(query, options));
172
+ return rows[0] || null;
173
+ }
174
+
175
+ async function listRecords(repository, query = {}, options = {}) {
176
+ return normalizeRecordList(await repository.queryDocuments(query, options));
177
+ }
178
+
179
+ async function resolveRule({ gateKey, surface, context, trx }) {
180
+ const filters = {
181
+ gateKey,
182
+ enabled: "true"
183
+ };
184
+ if (surface) {
185
+ filters.surface = surface;
186
+ }
187
+ return queryFirst(googleRewardedRulesRepository, {
188
+ ...filters,
189
+ sort: ["-updatedAt"],
190
+ limit: 1
191
+ }, {
192
+ context,
193
+ trx
194
+ });
195
+ }
196
+
197
+ async function resolveProviderConfig({ surface, context, trx }) {
198
+ return queryFirst(googleRewardedProviderConfigsRepository, {
199
+ surface,
200
+ enabled: "true",
201
+ sort: ["-updatedAt"],
202
+ limit: 1
203
+ }, {
204
+ context,
205
+ trx
206
+ });
207
+ }
208
+
209
+ async function resolveUnlockReceipts({ gateKey, context, trx, limit = 25 }) {
210
+ return listRecords(googleRewardedUnlockReceiptsRepository, {
211
+ gateKey,
212
+ sort: ["-grantedAt"],
213
+ limit
214
+ }, {
215
+ context,
216
+ trx
217
+ });
218
+ }
219
+
220
+ async function evaluateGate(input = {}, options = {}) {
221
+ requireActor(options?.context || null, input);
222
+ const workspaceSlug = requireWorkspaceSlug(input);
223
+ const gateKey = requireGateKey(input);
224
+ const surface = GOOGLE_REWARDED_SURFACE;
225
+ const now = new Date();
226
+
227
+ const ruleRecord = await resolveRule({
228
+ gateKey,
229
+ surface,
230
+ context: options?.context || null,
231
+ trx: options?.trx || null
232
+ });
233
+
234
+ if (!ruleRecord) {
235
+ return {
236
+ gateKey,
237
+ workspaceSlug,
238
+ surface,
239
+ enabled: false,
240
+ available: false,
241
+ blocked: false,
242
+ reason: "rule-not-configured",
243
+ rule: null,
244
+ providerConfig: null,
245
+ unlock: null,
246
+ cooldownUntil: null,
247
+ dailyLimitRemaining: null
248
+ };
249
+ }
250
+
251
+ const providerConfigRecord = await resolveProviderConfig({
252
+ surface,
253
+ context: options?.context || null,
254
+ trx: options?.trx || null
255
+ });
256
+ if (!providerConfigRecord) {
257
+ return {
258
+ gateKey,
259
+ workspaceSlug,
260
+ surface,
261
+ enabled: false,
262
+ available: false,
263
+ blocked: false,
264
+ reason: "provider-not-configured",
265
+ rule: formatRule(ruleRecord),
266
+ providerConfig: null,
267
+ unlock: null,
268
+ cooldownUntil: null,
269
+ dailyLimitRemaining: null
270
+ };
271
+ }
272
+
273
+ const rule = formatRule(ruleRecord);
274
+ const providerConfig = formatProviderConfig(providerConfigRecord);
275
+ const receipts = await resolveUnlockReceipts({
276
+ gateKey,
277
+ context: options?.context || null,
278
+ trx: options?.trx || null
279
+ });
280
+ const activeReceiptRecord = receipts.find((entry) => isFutureDate(entry?.unlockedUntil, now)) || null;
281
+ const activeUnlock = formatUnlockReceipt(activeReceiptRecord);
282
+
283
+ if (activeUnlock) {
284
+ return {
285
+ gateKey,
286
+ workspaceSlug,
287
+ surface,
288
+ enabled: true,
289
+ available: true,
290
+ blocked: false,
291
+ reason: "already-unlocked",
292
+ rule,
293
+ providerConfig,
294
+ unlock: activeUnlock,
295
+ cooldownUntil: null,
296
+ dailyLimitRemaining: rule.dailyLimit
297
+ };
298
+ }
299
+
300
+ const latestReceipt = receipts[0] || null;
301
+ let cooldownUntil = null;
302
+ if (latestReceipt && Number(rule.cooldownMinutes || 0) > 0) {
303
+ cooldownUntil = addMinutes(parseDate(latestReceipt.grantedAt) || now, rule.cooldownMinutes);
304
+ if (cooldownUntil.getTime() > now.getTime()) {
305
+ return {
306
+ gateKey,
307
+ workspaceSlug,
308
+ surface,
309
+ enabled: true,
310
+ available: false,
311
+ blocked: false,
312
+ reason: "cooldown-active",
313
+ rule,
314
+ providerConfig,
315
+ unlock: null,
316
+ cooldownUntil: cooldownUntil.toISOString(),
317
+ dailyLimitRemaining: rule.dailyLimit
318
+ };
319
+ }
320
+ }
321
+
322
+ if (rule.dailyLimit != null) {
323
+ const dayStart = startOfUtcDay(now);
324
+ const todaysCount = receipts.filter((entry) => {
325
+ const grantedAt = parseDate(entry?.grantedAt);
326
+ return grantedAt && grantedAt.getTime() >= dayStart.getTime();
327
+ }).length;
328
+ const remaining = Math.max(0, Number(rule.dailyLimit) - todaysCount);
329
+ if (remaining < 1) {
330
+ return {
331
+ gateKey,
332
+ workspaceSlug,
333
+ surface,
334
+ enabled: true,
335
+ available: false,
336
+ blocked: false,
337
+ reason: "daily-limit-reached",
338
+ rule,
339
+ providerConfig,
340
+ unlock: null,
341
+ cooldownUntil: null,
342
+ dailyLimitRemaining: 0
343
+ };
344
+ }
345
+
346
+ return {
347
+ gateKey,
348
+ workspaceSlug,
349
+ surface,
350
+ enabled: true,
351
+ available: true,
352
+ blocked: true,
353
+ reason: "reward-required",
354
+ rule,
355
+ providerConfig,
356
+ unlock: null,
357
+ cooldownUntil: null,
358
+ dailyLimitRemaining: remaining
359
+ };
360
+ }
361
+
362
+ return {
363
+ gateKey,
364
+ workspaceSlug,
365
+ surface,
366
+ enabled: true,
367
+ available: true,
368
+ blocked: true,
369
+ reason: "reward-required",
370
+ rule,
371
+ providerConfig,
372
+ unlock: null,
373
+ cooldownUntil: null,
374
+ dailyLimitRemaining: null
375
+ };
376
+ }
377
+
378
+ async function getCurrentState(input = {}, options = {}) {
379
+ return evaluateGate(input, options);
380
+ }
381
+
382
+ async function startGate(input = {}, options = {}) {
383
+ const state = await evaluateGate(input, options);
384
+ if (!state.enabled || !state.available || !state.blocked) {
385
+ return {
386
+ ...state,
387
+ session: null
388
+ };
389
+ }
390
+
391
+ const createdSession = normalizeRecord(await googleRewardedWatchSessionsRepository.createDocument(
392
+ {
393
+ gateKey: state.gateKey,
394
+ providerConfigId: state.providerConfig?.id || null,
395
+ status: "started",
396
+ startedAt: new Date()
397
+ },
398
+ {
399
+ context: options?.context || null,
400
+ trx: options?.trx || null
401
+ }
402
+ ));
403
+
404
+ return {
405
+ ...state,
406
+ session: formatWatchSession(createdSession)
407
+ };
408
+ }
409
+
410
+ async function grantReward(input = {}, options = {}) {
411
+ requireActor(options?.context || null, input);
412
+ const workspaceSlug = requireWorkspaceSlug(input);
413
+ const sessionId = input?.sessionId == null ? "" : String(input.sessionId).trim();
414
+ if (!sessionId) {
415
+ throw new AppError(400, "sessionId is required.");
416
+ }
417
+
418
+ return googleRewardedWatchSessionsRepository.withTransaction(async (trx) => {
419
+ const sessionRecord = normalizeRecord(await googleRewardedWatchSessionsRepository.getDocumentById(sessionId, {
420
+ context: options?.context || null,
421
+ trx
422
+ }));
423
+ if (!sessionRecord) {
424
+ throw new AppError(404, "Watch session not found.");
425
+ }
426
+
427
+ if (normalizeText(sessionRecord.status).toLowerCase() === "closed") {
428
+ throw new AppError(409, "Watch session is already closed.");
429
+ }
430
+ if (sessionRecord.rewardedAt || normalizeText(sessionRecord.status).toLowerCase() === "rewarded") {
431
+ const receiptRecord = await queryFirst(googleRewardedUnlockReceiptsRepository, {
432
+ watchSessionId: sessionId,
433
+ sort: ["-grantedAt"],
434
+ limit: 1
435
+ }, {
436
+ context: options?.context || null,
437
+ trx
438
+ });
439
+ return {
440
+ unlocked: true,
441
+ workspaceSlug,
442
+ gateKey: normalizeGateKey(sessionRecord.gateKey),
443
+ unlock: formatUnlockReceipt(receiptRecord),
444
+ session: formatWatchSession(sessionRecord)
445
+ };
446
+ }
447
+
448
+ const ruleRecord = await resolveRule({
449
+ gateKey: normalizeGateKey(sessionRecord.gateKey),
450
+ surface: GOOGLE_REWARDED_SURFACE,
451
+ context: options?.context || null,
452
+ trx
453
+ });
454
+ if (!ruleRecord) {
455
+ throw new AppError(409, "No rewarded rule is configured for this session.");
456
+ }
457
+
458
+ const now = new Date();
459
+ const unlockedUntil = addMinutes(now, Number(ruleRecord.unlockMinutes || 0));
460
+
461
+ const updatedSession = normalizeRecord(await googleRewardedWatchSessionsRepository.patchDocumentById(
462
+ sessionId,
463
+ {
464
+ status: "rewarded",
465
+ rewardedAt: now,
466
+ completedAt: now
467
+ },
468
+ {
469
+ context: options?.context || null,
470
+ trx
471
+ }
472
+ ));
473
+
474
+ const createdReceipt = normalizeRecord(await googleRewardedUnlockReceiptsRepository.createDocument(
475
+ {
476
+ gateKey: normalizeGateKey(sessionRecord.gateKey),
477
+ providerConfigId: sessionRecord.providerConfigId || null,
478
+ watchSessionId: sessionId,
479
+ grantedAt: now,
480
+ unlockedUntil
481
+ },
482
+ {
483
+ context: options?.context || null,
484
+ trx
485
+ }
486
+ ));
487
+
488
+ return {
489
+ unlocked: true,
490
+ workspaceSlug,
491
+ gateKey: normalizeGateKey(sessionRecord.gateKey),
492
+ unlock: formatUnlockReceipt(createdReceipt),
493
+ session: formatWatchSession(updatedSession)
494
+ };
495
+ });
496
+ }
497
+
498
+ async function closeSession(input = {}, options = {}) {
499
+ requireActor(options?.context || null, input);
500
+ const workspaceSlug = requireWorkspaceSlug(input);
501
+ const sessionId = input?.sessionId == null ? "" : String(input.sessionId).trim();
502
+ if (!sessionId) {
503
+ throw new AppError(400, "sessionId is required.");
504
+ }
505
+
506
+ const sessionRecord = normalizeRecord(await googleRewardedWatchSessionsRepository.getDocumentById(sessionId, {
507
+ context: options?.context || null,
508
+ trx: options?.trx || null
509
+ }));
510
+ if (!sessionRecord) {
511
+ throw new AppError(404, "Watch session not found.");
512
+ }
513
+
514
+ if (sessionRecord.rewardedAt || normalizeText(sessionRecord.status).toLowerCase() === "rewarded") {
515
+ return {
516
+ closed: false,
517
+ workspaceSlug,
518
+ gateKey: normalizeGateKey(sessionRecord.gateKey),
519
+ session: formatWatchSession(sessionRecord),
520
+ reason: "already-rewarded"
521
+ };
522
+ }
523
+
524
+ const closedSession = normalizeRecord(await googleRewardedWatchSessionsRepository.patchDocumentById(
525
+ sessionId,
526
+ {
527
+ status: "closed",
528
+ closedAt: new Date()
529
+ },
530
+ {
531
+ context: options?.context || null,
532
+ trx: options?.trx || null
533
+ }
534
+ ));
535
+
536
+ return {
537
+ closed: true,
538
+ workspaceSlug,
539
+ gateKey: normalizeGateKey(closedSession?.gateKey || sessionRecord.gateKey),
540
+ session: formatWatchSession(closedSession)
541
+ };
542
+ }
543
+
544
+ return Object.freeze({
545
+ getCurrentState,
546
+ startGate,
547
+ grantReward,
548
+ closeSession
549
+ });
550
+ }
551
+
552
+ export { createService };
@@ -0,0 +1,142 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+
4
+ const GOOGLE_REWARDED_SURFACE = "app";
5
+
6
+ const REWARDED_GATE_CONFIGURATION_REASONS = new Set([
7
+ "rule-not-configured",
8
+ "provider-not-configured"
9
+ ]);
10
+ const REWARDED_GATE_NON_BLOCKING_REASONS = new Set([
11
+ "already-unlocked",
12
+ "cooldown-active",
13
+ "daily-limit-reached",
14
+ ...REWARDED_GATE_CONFIGURATION_REASONS
15
+ ]);
16
+
17
+ const REWARDED_GATE_REASON_ERROR_MAP = Object.freeze({
18
+ "reward-required": {
19
+ status: 423,
20
+ code: "google_rewarded_unlock_required",
21
+ message: "Rewarded unlock required."
22
+ },
23
+ "cooldown-active": {
24
+ status: 423,
25
+ code: "google_rewarded_cooldown_active",
26
+ message: "Rewarded unlock is cooling down."
27
+ },
28
+ "daily-limit-reached": {
29
+ status: 423,
30
+ code: "google_rewarded_daily_limit_reached",
31
+ message: "Rewarded unlock limit reached."
32
+ },
33
+ "rule-not-configured": {
34
+ status: 503,
35
+ code: "google_rewarded_not_configured",
36
+ message: "Rewarded gate is not configured."
37
+ },
38
+ "provider-not-configured": {
39
+ status: 503,
40
+ code: "google_rewarded_not_configured",
41
+ message: "Rewarded gate is not configured."
42
+ }
43
+ });
44
+
45
+ function normalizeReason(value = "") {
46
+ return normalizeText(value).toLowerCase();
47
+ }
48
+
49
+ function isUnlockSatisfied(gateState = null) {
50
+ const reason = normalizeReason(gateState?.reason);
51
+ return Boolean(gateState?.unlock) || reason === "already-unlocked";
52
+ }
53
+
54
+ function hasBooleanGateFlag(gateState, key) {
55
+ return gateState && typeof gateState === "object" && typeof gateState[key] === "boolean";
56
+ }
57
+
58
+ function assertWellFormedGateState(gateState = null) {
59
+ if (hasBooleanGateFlag(gateState, "enabled") && hasBooleanGateFlag(gateState, "blocked")) {
60
+ if (
61
+ gateState.blocked === true ||
62
+ isUnlockSatisfied(gateState) ||
63
+ REWARDED_GATE_NON_BLOCKING_REASONS.has(normalizeReason(gateState?.reason))
64
+ ) {
65
+ return;
66
+ }
67
+ }
68
+
69
+ throw new AppError(503, "Rewarded gate returned an invalid state.", {
70
+ code: "google_rewarded_gate_state_invalid",
71
+ details: {
72
+ rewardedGate: gateState
73
+ }
74
+ });
75
+ }
76
+
77
+ function isConfigurationBypassAllowed(gateState = null, requireConfigured = false) {
78
+ if (requireConfigured === true) {
79
+ return false;
80
+ }
81
+
82
+ if (gateState?.enabled === true) {
83
+ return false;
84
+ }
85
+
86
+ const reason = normalizeReason(gateState?.reason);
87
+ return REWARDED_GATE_CONFIGURATION_REASONS.has(reason) || !reason;
88
+ }
89
+
90
+ function createRewardedGateError(gateState = null, {
91
+ errorCode = "",
92
+ errorMessage = ""
93
+ } = {}) {
94
+ const reason = normalizeReason(gateState?.reason);
95
+ const defaults = REWARDED_GATE_REASON_ERROR_MAP[reason] || {
96
+ status: 423,
97
+ code: "google_rewarded_unlock_blocked",
98
+ message: "Rewarded unlock is required before this action can continue."
99
+ };
100
+
101
+ return new AppError(defaults.status, errorMessage || defaults.message, {
102
+ code: errorCode || defaults.code,
103
+ details: {
104
+ rewardedGate: gateState
105
+ }
106
+ });
107
+ }
108
+
109
+ async function requireGoogleRewardedUnlock(googleRewardedService, input = {}, {
110
+ context = null,
111
+ requireConfigured = false,
112
+ errorCode = "",
113
+ errorMessage = ""
114
+ } = {}) {
115
+ if (!googleRewardedService || typeof googleRewardedService.getCurrentState !== "function") {
116
+ throw new TypeError("requireGoogleRewardedUnlock requires googleRewardedService.getCurrentState().");
117
+ }
118
+
119
+ const gateState = await googleRewardedService.getCurrentState(
120
+ {
121
+ ...input,
122
+ surface: GOOGLE_REWARDED_SURFACE
123
+ },
124
+ {
125
+ context
126
+ }
127
+ );
128
+ assertWellFormedGateState(gateState);
129
+
130
+ if (isUnlockSatisfied(gateState) || isConfigurationBypassAllowed(gateState, requireConfigured)) {
131
+ return gateState;
132
+ }
133
+
134
+ throw createRewardedGateError(gateState, {
135
+ errorCode,
136
+ errorMessage
137
+ });
138
+ }
139
+
140
+ export {
141
+ requireGoogleRewardedUnlock
142
+ };