@carddb/client 0.3.15 → 0.4.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 CHANGED
@@ -11,7 +11,7 @@ This package provides the core client implementation shared between browser and
11
11
  - **CardDBClient**: Main client class with resource accessors
12
12
  - **Connection**: HTTP/GraphQL connection handling
13
13
  - **Configuration**: Client configuration management
14
- - **Resources**: Publisher, Game, Dataset, Record, Import Format, Import, Export, File, Deck, and Rule resource classes
14
+ - **Resources**: Publisher, Game, Dataset, Record, Import Format, Import, Export, File, Scan, Scan Template, Deck, and Rule resource classes
15
15
  - **Cache utilities**: Cache key generation and read/write helpers
16
16
 
17
17
  ## Usage
package/dist/index.cjs CHANGED
@@ -680,6 +680,25 @@ var GamesResource = class extends BaseResource {
680
680
  const result = await this.connection.execute(query, core.QueryBuilder.gameUpdateVariables(id, input));
681
681
  return result["gameUpdate"];
682
682
  }
683
+ /**
684
+ * Get publisher-defined game region metadata for scan-capable clients.
685
+ */
686
+ async getRegions(options = {}) {
687
+ const publisherSlug = this.resolvePublisher(options.publisherSlug);
688
+ const gameKey = this.resolveGame(options.gameKey);
689
+ this.validateAccess(publisherSlug, gameKey);
690
+ const variables = core.QueryBuilder.gameScanRegionsVariables(
691
+ publisherSlug,
692
+ gameKey,
693
+ options.includeInactive
694
+ );
695
+ const key = this.cacheKey("games", "getRegions", variables);
696
+ return this.withCache(key, "games", options, async () => {
697
+ const { query } = core.QueryBuilder.gameScanRegions();
698
+ const result = await this.connection.execute(query, variables);
699
+ return result["gameScanRegions"] ?? [];
700
+ });
701
+ }
683
702
  /**
684
703
  * Search for games
685
704
  *
@@ -2524,6 +2543,27 @@ var FilesResource = class extends BaseResource {
2524
2543
  const result = await this.connection.execute(query, { id });
2525
2544
  return result["fileUploadConfirm"];
2526
2545
  }
2546
+ /**
2547
+ * Request a presigned upload URL, PUT bytes directly to storage, then confirm the file.
2548
+ */
2549
+ async upload(input) {
2550
+ const size = input.size ?? inferUploadSize(input.body);
2551
+ if (!size || size <= 0) {
2552
+ throw new Error("files.upload requires a positive size or a body with an inferable size");
2553
+ }
2554
+ const upload = await this.requestUpload({
2555
+ filename: input.filename,
2556
+ contentType: input.contentType,
2557
+ size,
2558
+ isPublic: input.isPublic ?? false,
2559
+ entityType: input.entityType ?? entityTypeForPurpose(input.purpose),
2560
+ entityId: input.entityId,
2561
+ publisherId: input.publisherId,
2562
+ datasetId: input.datasetId
2563
+ });
2564
+ await putUpload(upload.uploadUrl, input.contentType, input.body);
2565
+ return this.confirmUpload(upload.file.id);
2566
+ }
2527
2567
  /**
2528
2568
  * Delete a file. Requires a server-side secret credential.
2529
2569
  */
@@ -2534,6 +2574,294 @@ var FilesResource = class extends BaseResource {
2534
2574
  return Boolean(result["fileDelete"]);
2535
2575
  }
2536
2576
  };
2577
+ async function putUpload(uploadUrl, contentType, body) {
2578
+ const uploadFetch = globalThis.fetch;
2579
+ if (!uploadFetch) throw new Error("files.upload requires a runtime with fetch support");
2580
+ const response = await uploadFetch(uploadUrl, {
2581
+ method: "PUT",
2582
+ headers: { "Content-Type": contentType },
2583
+ body
2584
+ });
2585
+ if (!response.ok) {
2586
+ const responseBody = await response.text().catch(() => "");
2587
+ throw new Error(
2588
+ `File upload failed with ${response.status} ${response.statusText}${responseBody ? `: ${responseBody}` : ""}`
2589
+ );
2590
+ }
2591
+ }
2592
+ function inferUploadSize(body) {
2593
+ if (typeof body === "string") return new TextEncoder().encode(body).byteLength;
2594
+ if (body instanceof ArrayBuffer) return body.byteLength;
2595
+ if (ArrayBuffer.isView(body)) return body.byteLength;
2596
+ const sized = body;
2597
+ if (typeof sized?.size === "number") return sized.size;
2598
+ if (typeof sized?.byteLength === "number") return sized.byteLength;
2599
+ if (typeof sized?.length === "number") return sized.length;
2600
+ return void 0;
2601
+ }
2602
+ function entityTypeForPurpose(purpose) {
2603
+ if (!purpose || purpose === "scan_image") return void 0;
2604
+ if (purpose === "import") return "import";
2605
+ if (purpose === "scan_template_example") return "scan_template_example";
2606
+ return purpose;
2607
+ }
2608
+ var ScansResource = class extends BaseResource {
2609
+ constructor(connection, config) {
2610
+ super(connection, config);
2611
+ }
2612
+ /**
2613
+ * Create a scan job for an already uploaded image file.
2614
+ */
2615
+ async createJob(input) {
2616
+ const scopedInput = this.resolveCreateJobInput(input);
2617
+ const { query } = core.QueryBuilder.createScanJob();
2618
+ const result = await this.connection.execute(
2619
+ query,
2620
+ core.QueryBuilder.createScanJobVariables(scopedInput)
2621
+ );
2622
+ return result["createScanJob"];
2623
+ }
2624
+ /**
2625
+ * Get a scan job and its latest processing result.
2626
+ */
2627
+ async getJob(id, options = {}) {
2628
+ const key = this.cacheKey("scans", "getJob", { id });
2629
+ return this.withCache(key, "scans", options, async () => {
2630
+ const { query } = core.QueryBuilder.scanJob();
2631
+ const result = await this.connection.execute(query, { id });
2632
+ return result["scanJob"] ?? null;
2633
+ });
2634
+ }
2635
+ /**
2636
+ * Poll a scan job until it reaches a terminal status.
2637
+ */
2638
+ async pollJob(id, options = {}) {
2639
+ const timeoutMs = options.timeoutMs ?? 3e5;
2640
+ const backoffFactor = Math.max(options.backoffFactor ?? 1.5, 1);
2641
+ const maxIntervalMs = options.maxIntervalMs ?? 1e4;
2642
+ const startedAt = Date.now();
2643
+ let intervalMs = Math.max(options.intervalMs ?? 1e3, 1);
2644
+ while (true) {
2645
+ throwIfAborted(options.signal);
2646
+ const job = await this.getJob(id, { cache: false });
2647
+ if (!job) throw new Error(`Scan job '${id}' was not found`);
2648
+ if (isTerminalScanStatus(job.status)) return job;
2649
+ const elapsedMs = Date.now() - startedAt;
2650
+ const remainingMs = timeoutMs - elapsedMs;
2651
+ if (remainingMs <= 0) {
2652
+ throw new Error(`Timed out waiting for scan job '${id}'`);
2653
+ }
2654
+ await sleep(Math.min(intervalMs, remainingMs));
2655
+ intervalMs = Math.min(Math.ceil(intervalMs * backoffFactor), maxIntervalMs);
2656
+ }
2657
+ }
2658
+ /**
2659
+ * Submit correctness feedback for a completed scan job.
2660
+ */
2661
+ async submitFeedback(input) {
2662
+ const { query } = core.QueryBuilder.submitScanFeedback();
2663
+ const result = await this.connection.execute(
2664
+ query,
2665
+ core.QueryBuilder.submitScanFeedbackVariables(input)
2666
+ );
2667
+ return result["submitScanFeedback"];
2668
+ }
2669
+ /**
2670
+ * Aggregate scan metrics for a publisher/game/dataset scope.
2671
+ */
2672
+ async metrics(input) {
2673
+ const scopedInput = this.resolveMetricsInput(input);
2674
+ const { query } = core.QueryBuilder.scanMetrics();
2675
+ const result = await this.connection.execute(
2676
+ query,
2677
+ core.QueryBuilder.scanMetricsVariables(scopedInput)
2678
+ );
2679
+ return result["scanMetrics"];
2680
+ }
2681
+ /**
2682
+ * Verify an `X-CardDB-Signature` value for a webhook body.
2683
+ */
2684
+ async verifyWebhookSignature(input) {
2685
+ const subtle = getSubtleCrypto();
2686
+ const key = await subtle.importKey(
2687
+ "raw",
2688
+ encodeUtf8(input.secret),
2689
+ { name: "HMAC", hash: "SHA-256" },
2690
+ false,
2691
+ ["sign"]
2692
+ );
2693
+ const digest = await subtle.sign("HMAC", key, bodyToBytes(input.body));
2694
+ const expected = `sha256=${toHex(new Uint8Array(digest))}`;
2695
+ return constantTimeEqual(input.signature.trim().toLowerCase(), expected);
2696
+ }
2697
+ resolveCreateJobInput(input) {
2698
+ const publisherSlug = this.resolvePublisher(input.publisherSlug);
2699
+ const gameKey = this.resolveGame(input.gameKey);
2700
+ this.validateAccess(publisherSlug, gameKey);
2701
+ return { ...input, publisherSlug, gameKey };
2702
+ }
2703
+ resolveMetricsInput(input) {
2704
+ const publisherSlug = this.resolvePublisher(input.publisherSlug);
2705
+ const gameKey = this.resolveGame(input.gameKey);
2706
+ this.validateAccess(publisherSlug, gameKey);
2707
+ return { ...input, publisherSlug, gameKey };
2708
+ }
2709
+ };
2710
+ function isTerminalScanStatus(status) {
2711
+ const normalized = status.toLowerCase();
2712
+ return normalized === "completed" || normalized === "failed" || normalized === "cancelled";
2713
+ }
2714
+ async function sleep(ms) {
2715
+ return new Promise((resolve) => setTimeout(resolve, ms));
2716
+ }
2717
+ function throwIfAborted(signal) {
2718
+ if (!signal) return;
2719
+ if (signal.throwIfAborted) signal.throwIfAborted();
2720
+ if (signal.aborted) throw new Error("Scan job polling aborted");
2721
+ }
2722
+ function getSubtleCrypto() {
2723
+ const cryptoLike = globalThis;
2724
+ const subtle = cryptoLike.crypto?.subtle;
2725
+ if (!subtle) {
2726
+ throw new Error("Web Crypto HMAC support is not available in this runtime");
2727
+ }
2728
+ return subtle;
2729
+ }
2730
+ function bodyToBytes(body) {
2731
+ if (typeof body === "string") return encodeUtf8(body);
2732
+ if (body instanceof ArrayBuffer) return new Uint8Array(body);
2733
+ return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
2734
+ }
2735
+ function encodeUtf8(value) {
2736
+ return new TextEncoder().encode(value);
2737
+ }
2738
+ function toHex(bytes) {
2739
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
2740
+ }
2741
+ function constantTimeEqual(left, right) {
2742
+ const maxLength = Math.max(left.length, right.length);
2743
+ let diff = left.length ^ right.length;
2744
+ for (let index = 0; index < maxLength; index++) {
2745
+ diff |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
2746
+ }
2747
+ return diff === 0;
2748
+ }
2749
+ var ScanTemplatesResource = class extends BaseResource {
2750
+ constructor(connection, config) {
2751
+ super(connection, config);
2752
+ }
2753
+ /**
2754
+ * Resolve the active scan template for a game or dataset scope.
2755
+ */
2756
+ async resolve(input) {
2757
+ const scopedInput = this.resolveTemplateScope(input);
2758
+ const key = this.cacheKey("scanTemplates", "resolve", { ...scopedInput });
2759
+ return this.withCache(key, "scanTemplates", input, async () => {
2760
+ const { query } = core.QueryBuilder.resolveScanTemplate();
2761
+ const result = await this.connection.execute(
2762
+ query,
2763
+ core.QueryBuilder.resolveScanTemplateVariables(scopedInput)
2764
+ );
2765
+ return result["resolveScanTemplate"];
2766
+ });
2767
+ }
2768
+ /**
2769
+ * List publisher-managed scan templates for a game or dataset scope.
2770
+ */
2771
+ async list(input) {
2772
+ const scopedInput = this.resolveListScope(input);
2773
+ const key = this.cacheKey("scanTemplates", "list", { ...scopedInput });
2774
+ return this.withCache(key, "scanTemplates", input, async () => {
2775
+ const { query } = core.QueryBuilder.scanTemplates();
2776
+ const result = await this.connection.execute(
2777
+ query,
2778
+ core.QueryBuilder.scanTemplatesVariables(scopedInput)
2779
+ );
2780
+ const payload = result["scanTemplates"];
2781
+ return payload?.templates ?? [];
2782
+ });
2783
+ }
2784
+ /**
2785
+ * Create a publisher-managed scan template. Requires a server-side secret credential.
2786
+ */
2787
+ async create(input) {
2788
+ this.config.requireSecretCredential("scanTemplates.create");
2789
+ const scopedInput = this.resolveSaveInput(input);
2790
+ const { query } = core.QueryBuilder.createScanTemplate();
2791
+ const result = await this.connection.execute(
2792
+ query,
2793
+ core.QueryBuilder.createScanTemplateVariables(scopedInput)
2794
+ );
2795
+ return result["createScanTemplate"];
2796
+ }
2797
+ /**
2798
+ * Replace a publisher-managed scan template. Requires a server-side secret credential.
2799
+ */
2800
+ async update(idOrInput, input) {
2801
+ this.config.requireSecretCredential("scanTemplates.update");
2802
+ const id = typeof idOrInput === "string" ? idOrInput : idOrInput.id;
2803
+ const templateInput = typeof idOrInput === "string" ? input : withoutId(idOrInput);
2804
+ if (!templateInput) throw new Error("scan template input is required");
2805
+ const scopedInput = this.resolveSaveInput(templateInput);
2806
+ const { query } = core.QueryBuilder.updateScanTemplate();
2807
+ const result = await this.connection.execute(
2808
+ query,
2809
+ core.QueryBuilder.updateScanTemplateVariables(id, scopedInput)
2810
+ );
2811
+ return result["updateScanTemplate"];
2812
+ }
2813
+ /**
2814
+ * Validate a draft scan template and normalize provided region OCR hints.
2815
+ */
2816
+ async preview(input) {
2817
+ const scopedInput = this.resolvePreviewInput(input);
2818
+ const { query } = core.QueryBuilder.previewScanTemplate();
2819
+ const result = await this.connection.execute(
2820
+ query,
2821
+ core.QueryBuilder.previewScanTemplateVariables(scopedInput)
2822
+ );
2823
+ return result["previewScanTemplate"];
2824
+ }
2825
+ resolveTemplateScope(input) {
2826
+ const publisherSlug = this.resolvePublisher(input.publisherSlug);
2827
+ const gameKey = this.resolveGame(input.gameKey);
2828
+ this.validateAccess(publisherSlug, gameKey);
2829
+ return {
2830
+ publisherSlug,
2831
+ gameKey,
2832
+ datasetKey: input.datasetKey,
2833
+ templateId: input.templateId
2834
+ };
2835
+ }
2836
+ resolveListScope(input) {
2837
+ const publisherSlug = this.resolvePublisher(input.publisherSlug);
2838
+ const gameKey = this.resolveGame(input.gameKey);
2839
+ this.validateAccess(publisherSlug, gameKey);
2840
+ return {
2841
+ publisherSlug,
2842
+ gameKey,
2843
+ datasetKey: input.datasetKey,
2844
+ includeArchived: input.includeArchived
2845
+ };
2846
+ }
2847
+ resolveSaveInput(input) {
2848
+ const publisherSlug = this.resolvePublisher(input.publisherSlug);
2849
+ const gameKey = this.resolveGame(input.gameKey);
2850
+ this.validateAccess(publisherSlug, gameKey);
2851
+ return { ...input, publisherSlug, gameKey };
2852
+ }
2853
+ resolvePreviewInput(input) {
2854
+ const publisherSlug = this.resolvePublisher(input.publisherSlug);
2855
+ const gameKey = this.resolveGame(input.gameKey);
2856
+ this.validateAccess(publisherSlug, gameKey);
2857
+ return { ...input, publisherSlug, gameKey };
2858
+ }
2859
+ };
2860
+ function withoutId(input) {
2861
+ const templateInput = { ...input };
2862
+ delete templateInput.id;
2863
+ return templateInput;
2864
+ }
2537
2865
 
2538
2866
  // src/client.ts
2539
2867
  var CardDBClient = class _CardDBClient {
@@ -2559,6 +2887,10 @@ var CardDBClient = class _CardDBClient {
2559
2887
  exports;
2560
2888
  /** File upload helper resource */
2561
2889
  files;
2890
+ /** Scan job helper resource */
2891
+ scans;
2892
+ /** Scan template helper resource */
2893
+ scanTemplates;
2562
2894
  /**
2563
2895
  * Create a new CardDB client
2564
2896
  *
@@ -2577,6 +2909,8 @@ var CardDBClient = class _CardDBClient {
2577
2909
  this.imports = new ImportsResource(this.connection, this.config);
2578
2910
  this.exports = new ExportsResource(this.connection, this.config);
2579
2911
  this.files = new FilesResource(this.connection, this.config);
2912
+ this.scans = new ScansResource(this.connection, this.config);
2913
+ this.scanTemplates = new ScanTemplatesResource(this.connection, this.config);
2580
2914
  }
2581
2915
  /**
2582
2916
  * Get information about the current API key
@@ -2672,6 +3006,8 @@ exports.ImportsResource = ImportsResource;
2672
3006
  exports.PublishersResource = PublishersResource;
2673
3007
  exports.RecordsResource = RecordsResource;
2674
3008
  exports.RulesResource = RulesResource;
3009
+ exports.ScanTemplatesResource = ScanTemplatesResource;
3010
+ exports.ScansResource = ScansResource;
2675
3011
  exports.cacheKey = cacheKey;
2676
3012
  exports.getRateLimitInfo = getRateLimitInfo;
2677
3013
  exports.getSDKVersion = getSDKVersion;