dgc-core 2.0.5 → 2.0.8

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.
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RevenueCatLookupError = void 0;
7
+ exports.createRevenueCatClient = createRevenueCatClient;
8
+ exports.verifyCreditGiveawayEntitlement = verifyCreditGiveawayEntitlement;
9
+ const axios_1 = __importDefault(require("axios"));
10
+ const revenuecat_1 = require("./constants/revenuecat");
11
+ const user_1 = require("./enums/user");
12
+ const http_error_1 = require("./http-error");
13
+ /** Deliberately excludes the original HTTP error, which can contain the API key. */
14
+ class RevenueCatLookupError extends http_error_1.HttpError {
15
+ constructor() {
16
+ super('Unable to verify RevenueCat entitlement access. Please try again.', 503);
17
+ this.name = 'RevenueCatLookupError';
18
+ }
19
+ }
20
+ exports.RevenueCatLookupError = RevenueCatLookupError;
21
+ function positiveInteger(value, maximum) {
22
+ return Number.isInteger(value) && value > 0 && value <= maximum;
23
+ }
24
+ function validUserId(value) {
25
+ return (typeof value === 'string' &&
26
+ value.length > 0 &&
27
+ value.length <= 128 &&
28
+ value.trim() === value &&
29
+ !/[.#$[\]/]/.test(value) &&
30
+ !Array.from(value).some(character => {
31
+ const code = character.charCodeAt(0);
32
+ return code < 32 || code === 127;
33
+ }));
34
+ }
35
+ function parsePage(value) {
36
+ if (!value || typeof value !== 'object' || Array.isArray(value))
37
+ throw new RevenueCatLookupError();
38
+ const page = value;
39
+ if (!Array.isArray(page.items) ||
40
+ (page.next_page != null && (typeof page.next_page !== 'string' || !page.next_page)))
41
+ throw new RevenueCatLookupError();
42
+ const items = page.items.map((raw) => {
43
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
44
+ throw new RevenueCatLookupError();
45
+ const entitlement = raw;
46
+ if (typeof entitlement.entitlement_id !== 'string' ||
47
+ !/^entl[a-zA-Z0-9]+$/.test(entitlement.entitlement_id) ||
48
+ (entitlement.expires_at !== null &&
49
+ (typeof entitlement.expires_at !== 'number' ||
50
+ !Number.isSafeInteger(entitlement.expires_at) ||
51
+ entitlement.expires_at < 0)))
52
+ throw new RevenueCatLookupError();
53
+ return {
54
+ entitlementId: entitlement.entitlement_id,
55
+ expiresAt: entitlement.expires_at
56
+ };
57
+ });
58
+ return { items, nextPage: page.next_page ?? null };
59
+ }
60
+ function retryAfterMs(value) {
61
+ if (typeof value === 'number')
62
+ return Number.isFinite(value) && value >= 0 ? value * 1000 : null;
63
+ if (typeof value !== 'string' || !value.trim())
64
+ return null;
65
+ const text = value.trim();
66
+ if (/^\d+$/.test(text)) {
67
+ const seconds = Number(text);
68
+ return Number.isFinite(seconds) ? seconds * 1000 : null;
69
+ }
70
+ const date = Date.parse(text);
71
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : null;
72
+ }
73
+ /** Inspect only retry metadata; never retain or log the original transport error. */
74
+ function requestRetryDelay(error, attempt) {
75
+ if (!error || typeof error !== 'object')
76
+ return null;
77
+ const failure = error;
78
+ const response = failure.response && typeof failure.response === 'object'
79
+ ? failure.response
80
+ : undefined;
81
+ if (response) {
82
+ if (typeof response.status !== 'number' || ![429, 500, 502, 503, 504].includes(response.status))
83
+ return null;
84
+ }
85
+ else if (typeof failure.code !== 'string' ||
86
+ ![
87
+ 'ECONNRESET',
88
+ 'ETIMEDOUT',
89
+ 'ECONNABORTED',
90
+ 'EAI_AGAIN',
91
+ 'ENOTFOUND',
92
+ 'ERR_NETWORK',
93
+ 'EPIPE',
94
+ 'ECONNREFUSED'
95
+ ].includes(failure.code))
96
+ return null;
97
+ let headerDelay = null;
98
+ if (response?.headers && typeof response.headers === 'object') {
99
+ const headers = response.headers;
100
+ headerDelay = retryAfterMs(typeof headers.get === 'function'
101
+ ? headers.get('retry-after')
102
+ : (headers['retry-after'] ?? headers['Retry-After']));
103
+ }
104
+ const body = response?.data && typeof response.data === 'object'
105
+ ? response.data
106
+ : undefined;
107
+ const bodyDelay = typeof body?.backoff_ms === 'number' && Number.isFinite(body.backoff_ms) && body.backoff_ms >= 0
108
+ ? body.backoff_ms
109
+ : 0;
110
+ return Math.max(100 * 2 ** attempt, headerDelay ?? 0, bodyDelay);
111
+ }
112
+ async function requestPage(url, apiKey, timeoutMs, deadline) {
113
+ for (let attempt = 0; attempt < 3; attempt++) {
114
+ const remainingMs = deadline - Date.now();
115
+ if (remainingMs <= 0)
116
+ throw new RevenueCatLookupError();
117
+ try {
118
+ return await axios_1.default.get(url.href, {
119
+ headers: { Authorization: `Bearer ${apiKey}` },
120
+ timeout: Math.min(timeoutMs, remainingMs),
121
+ signal: AbortSignal.timeout(remainingMs),
122
+ maxRedirects: 0,
123
+ maxContentLength: 1024 * 1024
124
+ });
125
+ }
126
+ catch (error) {
127
+ const delay = attempt < 2 ? requestRetryDelay(error, attempt) : null;
128
+ if (delay === null || delay >= deadline - Date.now())
129
+ throw new RevenueCatLookupError();
130
+ await new Promise(resolve => setTimeout(resolve, delay));
131
+ }
132
+ }
133
+ throw new RevenueCatLookupError();
134
+ }
135
+ function createRevenueCatClient(options) {
136
+ const apiKey = options.apiKey?.trim();
137
+ const projectId = options.projectId?.trim();
138
+ const timeoutMs = options.timeoutMs ?? 15000;
139
+ const totalTimeoutMs = options.totalTimeoutMs ?? 15000;
140
+ const maxPages = options.maxPages ?? 20;
141
+ if (!apiKey || /[\r\n]/.test(apiKey))
142
+ throw new Error('Configure the RevenueCat API key');
143
+ if (!projectId || projectId.length > 255 || !/^[a-zA-Z0-9_-]+$/.test(projectId))
144
+ throw new Error('Configure a valid RevenueCat project ID');
145
+ if (!positiveInteger(timeoutMs, 60000) ||
146
+ !positiveInteger(totalTimeoutMs, 120000) ||
147
+ !positiveInteger(maxPages, 100))
148
+ throw new Error('Configure valid RevenueCat request limits');
149
+ return {
150
+ async getActiveEntitlements(userId) {
151
+ if (!validUserId(userId))
152
+ throw new http_error_1.HttpError('Invalid DGC user ID', 400);
153
+ const endpoint = new URL(`${revenuecat_1.REVENUECAT_API_V2}/projects/${encodeURIComponent(projectId)}/customers/${encodeURIComponent(userId)}/active_entitlements`);
154
+ endpoint.searchParams.set('limit', '100');
155
+ const deadline = Date.now() + totalTimeoutMs;
156
+ const visited = new Set();
157
+ const entitlements = [];
158
+ let url = endpoint;
159
+ try {
160
+ while (url) {
161
+ const remainingMs = deadline - Date.now();
162
+ if (remainingMs <= 0 ||
163
+ visited.size >= maxPages ||
164
+ url.origin !== endpoint.origin ||
165
+ url.pathname !== endpoint.pathname ||
166
+ url.username ||
167
+ url.password ||
168
+ url.hash ||
169
+ visited.has(url.href))
170
+ throw new RevenueCatLookupError();
171
+ visited.add(url.href);
172
+ const response = await requestPage(url, apiKey, timeoutMs, deadline);
173
+ const page = parsePage(response.data);
174
+ entitlements.push(...page.items);
175
+ url = page.nextPage ? new URL(page.nextPage, endpoint) : null;
176
+ }
177
+ const now = Date.now();
178
+ if (now >= deadline)
179
+ throw new RevenueCatLookupError();
180
+ return entitlements.filter(item => item.expiresAt === null || item.expiresAt > now);
181
+ }
182
+ catch {
183
+ // Do not attach request/config/cause or log the customer payload.
184
+ throw new RevenueCatLookupError();
185
+ }
186
+ }
187
+ };
188
+ }
189
+ /** Resolve any eligible entitlement from one customer lookup; never infer Free from API failures. */
190
+ async function verifyCreditGiveawayEntitlement(userId, client, entitlementIds) {
191
+ const ids = (0, revenuecat_1.createRevenueCatEntitlementIds)(entitlementIds);
192
+ const active = await client.getActiveEntitlements(userId);
193
+ const checkedAt = new Date().toISOString();
194
+ const now = Date.parse(checkedAt);
195
+ const tiers = [
196
+ [user_1.UserCollectorType.Elite, ids.elite],
197
+ [user_1.UserCollectorType.Collector, ids.collector],
198
+ [user_1.UserCollectorType.Rookie, ids.rookie]
199
+ ];
200
+ for (const [collectorType, entitlementId] of tiers) {
201
+ const match = active.find(item => item.entitlementId === entitlementId && (item.expiresAt === null || item.expiresAt > now));
202
+ if (match)
203
+ return { userId, checkedAt, collectorType, ...match };
204
+ }
205
+ return null;
206
+ }
package/dist/task.js CHANGED
@@ -4,6 +4,7 @@ exports.isRetryableCloudTasks = isRetryableCloudTasks;
4
4
  exports.createTask = createTask;
5
5
  // src/cloud-tasks.ts
6
6
  const tasks_1 = require("@google-cloud/tasks");
7
+ const logger_1 = require("./logger");
7
8
  const retry_1 = require("./retry");
8
9
  const tasksClient = new tasks_1.CloudTasksClient();
9
10
  // cloud-tasks errors can be gRPC-style (numeric code) or an Error with code string.
@@ -67,7 +68,11 @@ async function createTask(projectId, region, queueId, data, serviceAccount, audi
67
68
  timeoutMs: 15000,
68
69
  isRetryable: isRetryableCloudTasks,
69
70
  onRetry: (err, attempt, delay) => {
70
- console.warn(`createTask retry #${attempt} in ${delay}ms — ${String(err?.message ?? err)}`);
71
+ (0, logger_1.log)(logger_1.LogLevel.WARNING, 'createTask retry', {
72
+ attempt,
73
+ delayMs: delay,
74
+ error: String(err?.message ?? err)
75
+ });
71
76
  },
72
77
  ...opts.retryOptions // allow overrides
73
78
  };
@@ -93,11 +98,13 @@ async function createTask(projectId, region, queueId, data, serviceAccount, audi
93
98
  if (err?.original?.code === 6 ||
94
99
  err?.code === 6 ||
95
100
  String(err?.original?.code).toUpperCase().includes('ALREADY_EXISTS')) {
96
- console.warn('createTask — task already exists, returning existing name', expectedName);
101
+ (0, logger_1.log)(logger_1.LogLevel.WARNING, 'createTask — task already exists, returning existing name', {
102
+ taskName: expectedName
103
+ });
97
104
  return expectedName;
98
105
  }
99
106
  }
100
- console.error('createTask — final failure after retries', { error: err?.message ?? err });
107
+ (0, logger_1.log)(logger_1.LogLevel.ERROR, 'createTask — final failure after retries', { error: err?.message ?? err });
101
108
  throw err; // rethrow (or return null if you prefer)
102
109
  }
103
110
  }
@@ -1,6 +1,7 @@
1
1
  import type { GiveawayStatus, GiveawayCategory, GiveawaySelectionMethod, GiveawayWinnerStatus, GiveawayNotificationKind, GiveawayNotificationAudience, GiveawayNotificationStatus, GiveawayTicketResult, GiveawayAction, GiveawayNotificationChannel } from '../enums/giveaway';
2
2
  import type { FbUser } from './user';
3
3
  import type { UserCollectorType } from '../enums/user';
4
+ import type { GiveawayCreditEligibilityEvidence } from './revenuecat';
4
5
  /** API identities are strings. MongoDB ObjectIds serialize as 24 lowercase hexadecimal characters. */
5
6
  export type GiveawayObjectId = string;
6
7
  /** New tickets use DGC- followed by twelve uppercase letters or digits. Existing numeric IDs remain readable. */
@@ -16,6 +17,7 @@ export type GiveawaySelection = {
16
17
  reason: string;
17
18
  selectedBy: string;
18
19
  selectedAt: string;
20
+ creditEligibility?: GiveawayCreditEligibilityEvidence;
19
21
  };
20
22
  export type GiveawayPrize = {
21
23
  _id: GiveawayObjectId;
@@ -78,7 +80,7 @@ export type GiveawayEntry = {
78
80
  contactNumber?: string;
79
81
  /** Retained only for historical entry records; new entries collect country at claim. */
80
82
  country?: string;
81
- /** Null only for legacy entries whose exact collector tier was not recorded. */
83
+ /** Null when the account tier is missing or unsupported; product entries remain eligible. */
82
84
  _collectorType: UserCollectorType | null;
83
85
  enteredAt: string;
84
86
  rulesAcceptedAt: string;
@@ -96,6 +98,7 @@ export type GiveawayWinner = {
96
98
  status: GiveawayWinnerStatus;
97
99
  userId?: string;
98
100
  selection?: GiveawaySelection;
101
+ creditEligibility?: GiveawayCreditEligibilityEvidence;
99
102
  category: GiveawayCategory;
100
103
  creditAmount?: number;
101
104
  claimDeadline?: string;
@@ -127,6 +130,14 @@ export type GiveawayNotificationJob = {
127
130
  /** Stable event key supports replacements without resending a prior event. */
128
131
  eventKey?: string;
129
132
  audience?: GiveawayNotificationAudience;
133
+ /** Snapshots keep targeting and expiry stable throughout a broadcast. */
134
+ category?: GiveawayCategory;
135
+ expiresAt?: string;
136
+ dispatchAfter?: string;
137
+ dispatchGeneration?: number;
138
+ recipientsQueued?: number;
139
+ recipientsSkipped?: number;
140
+ dispatchCompletedAt?: string;
130
141
  status: GiveawayNotificationStatus;
131
142
  attempts: number;
132
143
  error?: string;
@@ -173,6 +184,12 @@ export type GiveawayWinnerRecord = GiveawayWinner & {
173
184
  };
174
185
  export type GiveawayNotificationRecord = Omit<GiveawayNotificationJob, 'claimToken'> & {
175
186
  giveawayTitle: string;
187
+ /** Queue acceptance counts, not confirmation of device delivery. */
188
+ deliveryCounts?: {
189
+ queued: number;
190
+ skipped: number;
191
+ pending: number;
192
+ };
176
193
  };
177
194
  export type GiveawayOverviewData = {
178
195
  days: {
@@ -182,8 +199,9 @@ export type GiveawayOverviewData = {
182
199
  liveGiveaways: GiveawaySummary[];
183
200
  needsSelection: Giveaway[];
184
201
  };
185
- export type GiveawayUserProfile = Pick<FbUser, 'email' | 'firstName' | 'lastName' | '_collectorType'> & {
202
+ export type GiveawayUserProfile = Pick<FbUser, 'email' | 'firstName' | 'lastName'> & {
186
203
  userId: string;
204
+ _collectorType?: UserCollectorType | null;
187
205
  };
188
206
  export type GiveawayPhysicalClaim = {
189
207
  fullName: string;
@@ -279,6 +297,8 @@ export type GiveawayWinnerPreview = {
279
297
  };
280
298
  /** Public app response; excludes admin settings, prize values and personal data. */
281
299
  export type GiveawayPublic = Pick<Giveaway, '_id' | 'title' | 'description' | 'image' | 'status' | 'startsAt' | 'closesAt' | 'countries' | 'rules' | 'category' | 'creditAmount' | 'deliveryInformation'> & {
300
+ /** Null permits all authenticated accounts regardless of their stored tier. */
301
+ eligibleCollectorTypes: UserCollectorType[] | null;
282
302
  entryCount: number;
283
303
  images: string[];
284
304
  prizes: Pick<GiveawayPrize, 'name' | 'quantity'>[];
@@ -19,3 +19,5 @@ export * from './user';
19
19
  export * from './x-credit';
20
20
  export * from './giveaway';
21
21
  export * from '../enums/giveaway';
22
+ export * from './revenuecat';
23
+ export * from '../constants/revenuecat';
@@ -35,3 +35,5 @@ __exportStar(require("./user"), exports);
35
35
  __exportStar(require("./x-credit"), exports);
36
36
  __exportStar(require("./giveaway"), exports);
37
37
  __exportStar(require("../enums/giveaway"), exports);
38
+ __exportStar(require("./revenuecat"), exports);
39
+ __exportStar(require("../constants/revenuecat"), exports);
@@ -0,0 +1,19 @@
1
+ import type { UserCollectorType } from '../enums/user';
2
+ export type RevenueCatActiveEntitlement = {
3
+ entitlementId: string;
4
+ /** RevenueCat uses milliseconds since epoch; null is non-expiring access. */
5
+ expiresAt: number | null;
6
+ };
7
+ export type RevenueCatEntitlementIds = Readonly<{
8
+ rookie: string;
9
+ collector: string;
10
+ elite: string;
11
+ }>;
12
+ export type RevenueCatClient = {
13
+ getActiveEntitlements(userId: string): Promise<RevenueCatActiveEntitlement[]>;
14
+ };
15
+ export type GiveawayCreditEligibilityEvidence = RevenueCatActiveEntitlement & {
16
+ userId: string;
17
+ checkedAt: string;
18
+ collectorType: UserCollectorType.Rookie | UserCollectorType.Collector | UserCollectorType.Elite;
19
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dgc-core",
3
- "version": "2.0.5",
3
+ "version": "2.0.8",
4
4
  "description": "Shared DGC contracts, business rules and server utilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1 +0,0 @@
1
- export declare function isValidWebhookSignature(secret: string, body: string, signature: string): boolean;
package/dist/tw/utils.js DELETED
@@ -1,19 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.isValidWebhookSignature = isValidWebhookSignature;
7
- const crypto_1 = __importDefault(require("crypto"));
8
- const generateSignature = (body, secret) => {
9
- return crypto_1.default.createHmac('sha256', secret).update(body).digest('hex');
10
- };
11
- function isValidWebhookSignature(secret, body, signature) {
12
- const expectedSignature = generateSignature(body, secret);
13
- const expectedBuffer = Buffer.from(expectedSignature);
14
- const signatureBuffer = Buffer.from(signature);
15
- if (expectedBuffer.length !== signatureBuffer.length) {
16
- return false;
17
- }
18
- return crypto_1.default.timingSafeEqual(expectedBuffer, signatureBuffer);
19
- }
@@ -1 +0,0 @@
1
- export declare function pregenerateInAppWallet(twClientId: string, twSecretKey: string, email: string): Promise<string | null>;
package/dist/tw/wallet.js DELETED
@@ -1,57 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.pregenerateInAppWallet = pregenerateInAppWallet;
7
- // src/tw/wallet.ts
8
- const axios_1 = __importDefault(require("axios"));
9
- const retry_1 = require("../retry");
10
- async function pregenerateInAppWallet(twClientId, twSecretKey, email) {
11
- if (!twClientId || !twSecretKey || !email) {
12
- console.error('Missing client id or secret or email');
13
- return null;
14
- }
15
- const doRequest = async () => {
16
- const res = await axios_1.default.post('https://in-app-wallet.thirdweb.com/api/v1/pregenerate', { strategy: 'email', email }, {
17
- headers: {
18
- 'Content-Type': 'application/json',
19
- 'x-client-id': twClientId,
20
- 'x-secret-key': twSecretKey
21
- },
22
- timeout: 10000
23
- });
24
- const wallet = res.data?.wallet;
25
- if (!wallet || !wallet.address) {
26
- // server returned 2xx but unexpected shape — don't retry, treat as final failure
27
- console.error('Invalid wallet response shape', { status: res.status, data: res.data });
28
- throw new Error('Invalid wallet response');
29
- }
30
- return wallet.address;
31
- };
32
- const opts = {
33
- retries: 3,
34
- baseDelayMs: 300,
35
- maxDelayMs: 2000,
36
- timeoutMs: 15000,
37
- isRetryable: retry_1.isRetryableAxios,
38
- onRetry: (err, attempt, delay) => {
39
- console.warn(`pregenerateInAppWallet: retry #${attempt} in ${delay}ms — ${String(err?.message)}`);
40
- }
41
- };
42
- try {
43
- const address = await (0, retry_1.retry)(() => doRequest(), opts);
44
- return address;
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- }
47
- catch (err) {
48
- // final failure after retries
49
- console.error('pregenerateInAppWallet: final failure', {
50
- message: err?.message,
51
- // include axios response data when possible
52
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
- responseData: err?.response?.data ?? err?.cause ?? err.original ?? null
54
- });
55
- return null;
56
- }
57
- }