dgc-core 2.0.7 → 2.0.9

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 CHANGED
@@ -97,3 +97,24 @@ The shared RevenueCat catalog maps Rookie, Collector and Elite entitlement IDs f
97
97
  `createRevenueCatClient` accepts server-provided credentials and request limits. `verifyCreditGiveawayEntitlement` checks all eligible tiers using one paginated customer lookup. Null expiry represents non-expiring access; expired entitlements are excluded. Network, rate-limit and malformed-response failures throw a sanitized 503 without exposing credentials. The active entitlements endpoint has no environment filter; configure RevenueCat Sandbox Testing Access and test-account policy for the selected project.
98
98
 
99
99
  Credit winner selection, confirmation and replacement require an injected `checkCreditEligibility(userId)` callback in the optional third `GiveawayRepository` constructor argument. Missing verification is a 503, never a fallback to saved membership. Product operations do not call the verifier. Checks occur outside transactions; reviewed revisions, candidate identity and unexpired evidence are validated at commit. Random selection picks one candidate first, then checks their entitlement. Inactive candidates are retried without replacement up to `creditWinnerMaxAttempts` (default 5; integer 1-100). It never checks the whole entrant pool in advance. The 30-second draw budget remains in place. Transient RevenueCat network/429/5xx failures retry the same user and page up to three attempts with bounded backoff and Retry-After handling, sharing the existing total lookup deadline. Authorization, missing-resource and malformed-response failures are not retried or treated as inactive access. Evidence is stored on selections and winners. Existing confirmed claims and idempotent rewards do not recheck access.
100
+
101
+ ## Public mobile API contracts
102
+
103
+ PriceChartingApiCard and PriceChartingVariantApiCard are defined in src/types/api.ts and remain available through dgc-core/types.
104
+
105
+ The browser-safe types entry exports the public card, set, pricing, image-search,
106
+ population and grading-response contracts from src/types/api.ts. They reuse
107
+ the existing response envelopes and PriceCharting types while keeping nullable
108
+ image-search fields separate from catalog cards. ApiCardResponse, ApiCardsResponse,
109
+ ApiMarketHistoryResponse and the other response aliases model the actual HTTP
110
+ envelopes without exposing stored database records.
111
+
112
+ The dgc-web documentation generator reads these TypeScript contracts and preserves
113
+ its client-facing descriptions as separate annotations. Change shared model fields
114
+ here, then sync the dashboard's generated documentation. The package export tests
115
+ and mobile API compile tests verify public availability, nullability, literal
116
+ statuses and the browser import boundary.
117
+
118
+ These additions follow the existing package release process. Publish a new core
119
+ version before registry consumers import the new aliases; documentation can be
120
+ generated from the reviewed local checkout before that release.
@@ -61,9 +61,9 @@ export declare class GiveawayRepository {
61
61
  private advanceGiveaway;
62
62
  submitEntry(account: UserProfile, id: string, raw: unknown): Promise<{
63
63
  _id: string;
64
+ name: string;
64
65
  email: string;
65
66
  _collectorType: UserCollectorType | null;
66
- name: string;
67
67
  giveawayId: import("../types").GiveawayObjectId;
68
68
  country?: string | undefined;
69
69
  enteredAt: string;
package/dist/index.d.ts CHANGED
@@ -2,8 +2,6 @@ export * from './types';
2
2
  export * from './db/mongo';
3
3
  export * from './db/mongo2';
4
4
  export * from './db/mongo3';
5
- export * from './tw/utils';
6
- export * from './tw/wallet';
7
5
  export * from './auth';
8
6
  export * from './chunk';
9
7
  export * from './env';
package/dist/index.js CHANGED
@@ -18,8 +18,6 @@ __exportStar(require("./types"), exports);
18
18
  __exportStar(require("./db/mongo"), exports);
19
19
  __exportStar(require("./db/mongo2"), exports);
20
20
  __exportStar(require("./db/mongo3"), exports);
21
- __exportStar(require("./tw/utils"), exports);
22
- __exportStar(require("./tw/wallet"), exports);
23
21
  __exportStar(require("./auth"), exports);
24
22
  __exportStar(require("./chunk"), exports);
25
23
  __exportStar(require("./env"), exports);
package/dist/pub.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.publishMessage = publishMessage;
4
4
  // src/pub.ts
5
5
  const pubsub_1 = require("@google-cloud/pubsub");
6
+ const logger_1 = require("./logger");
6
7
  let cachedPubSub = null;
7
8
  function getPubSub(projectId) {
8
9
  if (!projectId)
@@ -26,11 +27,11 @@ async function publishMessage(projectId, topicName, envelope) {
26
27
  const messageId = await topic.publishMessage({
27
28
  data
28
29
  });
29
- console.info(`INFO: Pub/Sub publish topic=${topicName} msg_id=${messageId}`);
30
+ (0, logger_1.log)(logger_1.LogLevel.INFO, 'Pub/Sub publish', { topicName, messageId });
30
31
  return messageId;
31
32
  }
32
33
  catch (err) {
33
- console.error('ERROR: publish failed:', err);
34
+ (0, logger_1.log)(logger_1.LogLevel.ERROR, 'publish failed', { error: err });
34
35
  throw err;
35
36
  }
36
37
  }
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,3 +1,5 @@
1
+ import type { CardType } from '../enums/card';
2
+ import type { PriceChartingImages, PriceChartingPrice } from './pricecharting';
1
3
  /** Successful non-paginated DGC API response. */
2
4
  export type ApiResponse<T> = {
3
5
  data: T;
@@ -10,3 +12,187 @@ export type PaginatedApiResponse<T> = {
10
12
  totalCount: number;
11
13
  data: T[];
12
14
  };
15
+ export type PriceChartingApiCard = {
16
+ priceChartingId: number;
17
+ name: string;
18
+ set: string;
19
+ number: string | null;
20
+ releaseDate?: string;
21
+ price: PriceChartingPrice;
22
+ images?: PriceChartingImages;
23
+ type: CardType;
24
+ version: string;
25
+ };
26
+ export type PriceChartingVariantApiCard = PriceChartingApiCard & {
27
+ variant: string;
28
+ };
29
+ /** An unsuccessful API response. Use the HTTP status and message together. */
30
+ export type ApiError = {
31
+ /** Message explaining why the request failed. */
32
+ error: string;
33
+ };
34
+ /** A grading change was accepted. Read the card again to show its current state. */
35
+ export type ApiSuccess = {
36
+ success: true;
37
+ };
38
+ /** Available card image URLs. Missing image sizes are omitted. */
39
+ export type ApiImages = NonNullable<PriceChartingApiCard['images']>;
40
+ /** Prices in cents. Unavailable prices are null; missing price fields are omitted. */
41
+ export type ApiPrice = Partial<PriceChartingPrice>;
42
+ /** Card details for catalog screens. Optional fields are omitted when unavailable. */
43
+ export type ApiCardCore = Partial<Omit<PriceChartingApiCard, 'price' | 'type'>> & {
44
+ /** Available prices in cents. */
45
+ price?: ApiPrice;
46
+ /** Card type, such as pokemon or baseball. */
47
+ type?: string;
48
+ };
49
+ /** Card details with an optional link to its set. */
50
+ export type ApiCard = ApiCardCore & {
51
+ /** Set ID to use with /sets/{id}/cards. */
52
+ setId?: string;
53
+ };
54
+ /** A batch card result with its printing label when available. */
55
+ export type ApiCardWithVariant = ApiCard & Partial<Pick<PriceChartingVariantApiCard, 'variant'>>;
56
+ /** A card variant returned for a set, without a separate setId field. */
57
+ export type ApiSetCardVariant = ApiCardCore & Partial<Pick<PriceChartingVariantApiCard, 'variant'>>;
58
+ /** Paginated card search results. totalCount is capped at 1000. */
59
+ export type ApiCardPage = PaginatedApiResponse<ApiCard>;
60
+ /** Paginated cards in a set. totalCount is the full matching count. */
61
+ export type ApiSetCardPage = PaginatedApiResponse<ApiCardCore>;
62
+ /** Card IDs for a batch detail request. Supply at least one numeric ID. */
63
+ export type ApiBatchRequest = {
64
+ ids: number[];
65
+ };
66
+ /** One legacy trading-card price observation. */
67
+ export type ApiLegacyHistoryEntry = {
68
+ /** Price date, normally YYYY-MM-DD. */
69
+ date: string;
70
+ /** averageSellPrice is in dollars; the other values are zero placeholders. */
71
+ cardmarket: {
72
+ averageSellPrice?: number;
73
+ avg1?: 0;
74
+ avg30?: 0;
75
+ avg7?: 0;
76
+ germanProLow?: 0;
77
+ lowPrice?: 0;
78
+ lowPriceExPlus?: 0;
79
+ reverseHoloAvg1?: 0;
80
+ reverseHoloAvg30?: 0;
81
+ reverseHoloAvg7?: 0;
82
+ reverseHoloLow?: 0;
83
+ reverseHoloSell?: 0;
84
+ reverseHoloTrend?: 0;
85
+ suggestedPrice?: 0;
86
+ trendPrice?: 0;
87
+ };
88
+ };
89
+ /** One market-price observation for a UTC date. Prices are cents or null. */
90
+ export type ApiMarketHistoryEntry = {
91
+ date: string;
92
+ price: Partial<Pick<PriceChartingPrice, 'loose' | 'retailLooseBuy' | 'retailLooseSell'>>;
93
+ };
94
+ /** The selected card, condition, printing and language for a price chart. */
95
+ export type ApiConditionHistoryMeta = {
96
+ /** Requested ID: a number for numeric IDs, or a string for legacy Pokemon IDs. */
97
+ assetId: number | string;
98
+ priceChartingId: number;
99
+ tcgplayerId: string;
100
+ /** ID of the pricing variant used for this chart. */
101
+ id: string;
102
+ /** Full condition name, such as Near Mint. */
103
+ condition: string;
104
+ printing: string;
105
+ language: string;
106
+ /** Current price in cents, or null when unavailable. */
107
+ price: number | null;
108
+ /** Last price update as Unix time in seconds, or null when unavailable. */
109
+ lastUpdated: number | null;
110
+ };
111
+ /** Condition-specific prices, ordered oldest to newest, and chart labels. */
112
+ export type ApiConditionHistoryResponse = ApiResponse<{
113
+ /** UTC price date in YYYY-MM-DD format. */
114
+ date: string;
115
+ /** Price in cents, or null when unavailable. */
116
+ price: Pick<PriceChartingPrice, 'loose'>;
117
+ }[]> & {
118
+ meta: ApiConditionHistoryMeta;
119
+ };
120
+ /** A master set available for browsing. */
121
+ export type ApiMasterSet = {
122
+ /** 24-character set ID to use with /sets/{id}/cards. */
123
+ id: string;
124
+ name: string;
125
+ /** Card type, such as pokemon. */
126
+ type: string;
127
+ releaseDate?: string;
128
+ logo?: string;
129
+ };
130
+ /** Paginated set search results. totalCount includes matches outside this page. */
131
+ export type ApiMasterSetPage = PaginatedApiResponse<ApiMasterSet>;
132
+ /** The master set available for one requested card ID. */
133
+ export type ApiMasterRelationship = {
134
+ assetId: number;
135
+ /** Master-set ID, or null when no master set is available. */
136
+ masterIdentifier: string | null;
137
+ isExist: boolean;
138
+ };
139
+ /** A catalog group and its master sets. Empty groups have an empty sets array. */
140
+ export type ApiGroup = {
141
+ name: string;
142
+ sets: ApiMasterSet[];
143
+ };
144
+ type MaterializedApiImages = {
145
+ [Key in keyof ApiImages]: ApiImages[Key];
146
+ };
147
+ interface ApiImageMetadata extends MaterializedApiImages {
148
+ [key: string]: unknown;
149
+ }
150
+ /** Card identified from a photo. Required nullable fields remain present as null. */
151
+ export type ApiImageCard = Pick<PriceChartingApiCard, 'priceChartingId' | 'name' | 'number' | 'version'> & {
152
+ set: string | null;
153
+ releaseDate: string | null;
154
+ /** Available prices in cents, or null when unavailable. */
155
+ price: Record<string, unknown> | null;
156
+ images: ApiImageMetadata | null;
157
+ type: string | null;
158
+ /** Set ID to use with /sets/{id}/cards. Omitted when unavailable. */
159
+ setId?: string;
160
+ };
161
+ /** Photo search result. Open data only when matched is true. */
162
+ export type ApiImageMatch = {
163
+ matched: boolean;
164
+ reason?: string;
165
+ score: number | null;
166
+ gap: number | null;
167
+ data: ApiImageCard | null;
168
+ };
169
+ /** Card count and percentage for one grade group. */
170
+ export type ApiPopulationBucket = {
171
+ /** Grade from 0.5 to 10 in half-point steps. */
172
+ score: number;
173
+ count: number;
174
+ /** Percentage of the report total, rounded to two decimal places. */
175
+ percent: number;
176
+ };
177
+ /** Exactly 20 grade groups, ordered from 0.5 to 10, including empty groups. */
178
+ export type ApiPopulation = {
179
+ status: 'success';
180
+ data: ApiPopulationBucket[];
181
+ };
182
+ /** Card details in the standard data envelope. */
183
+ export type ApiCardResponse = ApiResponse<ApiCard>;
184
+ /** Batch card details with variant labels in the standard data envelope. */
185
+ export type ApiCardsResponse = ApiResponse<ApiCardWithVariant[]>;
186
+ /** Legacy price observations in the priceData envelope. */
187
+ export type ApiLegacyHistoryResponse = {
188
+ priceData: ApiLegacyHistoryEntry[];
189
+ };
190
+ /** Market-price observations in the standard data envelope. */
191
+ export type ApiMarketHistoryResponse = ApiResponse<ApiMarketHistoryEntry[]>;
192
+ /** Card variants in the standard data envelope. */
193
+ export type ApiVariantsResponse = ApiResponse<ApiSetCardVariant[]>;
194
+ /** Master-set relationships in the standard data envelope. */
195
+ export type ApiMasterRelationshipsResponse = ApiResponse<ApiMasterRelationship[]>;
196
+ /** Catalog groups and their master sets in the standard data envelope. */
197
+ export type ApiGroupsResponse = ApiResponse<ApiGroup[]>;
198
+ export {};
@@ -27,7 +27,7 @@ export type SalesVolumeEntry = {
27
27
  date: string;
28
28
  salesVolume: SalesVolume;
29
29
  };
30
- type Image = {
30
+ export type PriceChartingImages = {
31
31
  tiny?: string;
32
32
  small?: string;
33
33
  normal?: string;
@@ -46,7 +46,7 @@ export type PriceChartingDbCard = {
46
46
  priceHistory: PriceChartingPriceEntry[];
47
47
  salesVolume: SalesVolume;
48
48
  salesVolumeHistory: SalesVolumeEntry[];
49
- images?: Image;
49
+ images?: PriceChartingImages;
50
50
  uploadStatus?: {
51
51
  imagesUploaded?: boolean;
52
52
  noImage?: boolean;
@@ -62,20 +62,6 @@ export type PriceChartingDbCardDto = Omit<PriceChartingDbCard, 'createdAt' | 'up
62
62
  createdAt?: string;
63
63
  updatedAt: string;
64
64
  };
65
- export type PriceChartingApiCard = {
66
- priceChartingId: number;
67
- name: string;
68
- set: string;
69
- number: string | null;
70
- releaseDate?: string;
71
- price: PriceChartingPrice;
72
- images?: Image;
73
- type: CardType;
74
- version: string;
75
- };
76
- export type PriceChartingVariantApiCard = PriceChartingApiCard & {
77
- variant: string;
78
- };
79
65
  export type PriceChartingDbSet = {
80
66
  type: CardType;
81
67
  consoleName: string;
@@ -83,4 +69,3 @@ export type PriceChartingDbSet = {
83
69
  createdAt: Date;
84
70
  updatedAt: Date;
85
71
  };
86
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dgc-core",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
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
- }