euredact 0.6.0 → 0.7.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.
Files changed (57) hide show
  1. package/NOTICE +11 -0
  2. package/README.md +403 -39
  3. package/dist/cjs/cache.js +2 -8
  4. package/dist/cjs/cloud/account.js +180 -0
  5. package/dist/cjs/cloud/batches.js +469 -0
  6. package/dist/cjs/cloud/client.js +124 -14
  7. package/dist/cjs/cloud/config.js +28 -2
  8. package/dist/cjs/cloud/errors.js +39 -2
  9. package/dist/cjs/cloud/index.js +16 -1
  10. package/dist/cjs/index.js +9 -1
  11. package/dist/cjs/platform.browser.js +98 -0
  12. package/dist/cjs/platform.js +140 -0
  13. package/dist/cjs/rules/bicGleif.js +57 -0
  14. package/dist/cjs/rules/bicRegistry.js +37 -11
  15. package/dist/cjs/rules/countries/index.js +63 -10
  16. package/dist/cjs/rules/cues.js +29 -9
  17. package/dist/cjs/rules/engine.js +14 -6
  18. package/dist/cjs/rules/suppressors.js +292 -22
  19. package/dist/cjs/rules/validators.js +50 -0
  20. package/dist/cjs/sdk.js +18 -7
  21. package/dist/cjs/types.js +4 -1
  22. package/dist/esm/cache.js +2 -8
  23. package/dist/esm/cloud/account.d.ts +155 -0
  24. package/dist/esm/cloud/account.js +175 -0
  25. package/dist/esm/cloud/batches.d.ts +166 -0
  26. package/dist/esm/cloud/batches.js +464 -0
  27. package/dist/esm/cloud/client.d.ts +83 -1
  28. package/dist/esm/cloud/client.js +119 -22
  29. package/dist/esm/cloud/config.d.ts +6 -0
  30. package/dist/esm/cloud/config.js +27 -2
  31. package/dist/esm/cloud/errors.d.ts +26 -1
  32. package/dist/esm/cloud/errors.js +35 -1
  33. package/dist/esm/cloud/index.d.ts +4 -2
  34. package/dist/esm/cloud/index.js +4 -2
  35. package/dist/esm/index.d.ts +2 -2
  36. package/dist/esm/index.js +1 -1
  37. package/dist/esm/platform.browser.d.ts +27 -0
  38. package/dist/esm/platform.browser.js +92 -0
  39. package/dist/esm/platform.d.ts +20 -0
  40. package/dist/esm/platform.js +101 -0
  41. package/dist/esm/rules/bicGleif.d.ts +4 -0
  42. package/dist/esm/rules/bicGleif.js +53 -0
  43. package/dist/esm/rules/bicRegistry.d.ts +25 -11
  44. package/dist/esm/rules/bicRegistry.js +36 -11
  45. package/dist/esm/rules/countries/index.js +63 -10
  46. package/dist/esm/rules/cues.d.ts +1 -1
  47. package/dist/esm/rules/cues.js +29 -9
  48. package/dist/esm/rules/engine.js +14 -6
  49. package/dist/esm/rules/suppressors.d.ts +1 -0
  50. package/dist/esm/rules/suppressors.js +291 -21
  51. package/dist/esm/rules/validators.d.ts +14 -0
  52. package/dist/esm/rules/validators.js +47 -0
  53. package/dist/esm/sdk.d.ts +51 -4
  54. package/dist/esm/sdk.js +18 -9
  55. package/dist/esm/types.d.ts +58 -2
  56. package/dist/esm/types.js +4 -1
  57. package/package.json +12 -4
@@ -65,7 +65,21 @@ export declare function validateSpanishDni(candidate: string): boolean;
65
65
  export declare function validateSpanishNie(candidate: string): boolean;
66
66
  export declare function validatePortugueseNif(candidate: string): boolean;
67
67
  export declare function validatePolishPesel(candidate: string): boolean;
68
+ /** Polish domestic account number (NRB): the PL IBAN without its "PL", so the IBAN's mod-97 decides it (rules-engine#93). */
69
+ export declare function validatePolishNrb(candidate: string): boolean;
68
70
  export declare function validatePolishNip(candidate: string): boolean;
71
+ /**
72
+ * Polish identity card (dowód osobisty): 3 letters + 6 digits, the first digit
73
+ * a check digit. Letters A=10 … Z=35, weights 7,3,1,9,7,3,1,7,3: the weighted
74
+ * sum of all nine is 0 mod 10 exactly when the check digit is right (rules-engine#75).
75
+ */
76
+ export declare function validatePolishIdCard(candidate: string): boolean;
77
+ /**
78
+ * Polish REGON: 9 digits, or 14 for a local unit, each with a mod-11 check (a
79
+ * remainder of 10 counts as 0). A 14-digit REGON begins with its parent's valid
80
+ * 9-digit REGON (rules-engine#76).
81
+ */
82
+ export declare function validatePolishRegon(candidate: string): boolean;
69
83
  export declare function validateCzechBirthNumber(candidate: string): boolean;
70
84
  export declare function validateRomanianCnp(candidate: string): boolean;
71
85
  export declare function validateHungarianTaj(candidate: string): boolean;
@@ -492,6 +492,13 @@ export function validatePolishPesel(candidate) {
492
492
  total += d[i] * weights[i];
493
493
  return d[10] === (10 - (total % 10)) % 10;
494
494
  }
495
+ /** Polish domestic account number (NRB): the PL IBAN without its "PL", so the IBAN's mod-97 decides it (rules-engine#93). */
496
+ export function validatePolishNrb(candidate) {
497
+ const c = candidate.replace(/[\s\-]/g, "");
498
+ if (!/^\d{26}$/.test(c))
499
+ return false;
500
+ return validateIban("PL" + c);
501
+ }
495
502
  export function validatePolishNip(candidate) {
496
503
  const c = candidate.replace(/[\s\-]/g, "");
497
504
  if (c.length !== 10 || !/^\d{10}$/.test(c))
@@ -503,6 +510,43 @@ export function validatePolishNip(candidate) {
503
510
  total += d[i] * weights[i];
504
511
  return total % 11 === d[9];
505
512
  }
513
+ /**
514
+ * Polish identity card (dowód osobisty): 3 letters + 6 digits, the first digit
515
+ * a check digit. Letters A=10 … Z=35, weights 7,3,1,9,7,3,1,7,3: the weighted
516
+ * sum of all nine is 0 mod 10 exactly when the check digit is right (rules-engine#75).
517
+ */
518
+ export function validatePolishIdCard(candidate) {
519
+ const c = candidate.replace(/\s/g, "").toUpperCase();
520
+ if (!/^[A-Z]{3}\d{6}$/.test(c))
521
+ return false;
522
+ const weights = [7, 3, 1, 9, 7, 3, 1, 7, 3];
523
+ let total = 0;
524
+ for (let i = 0; i < 9; i++) {
525
+ const ch = c[i];
526
+ total += (/[A-Z]/.test(ch) ? ch.charCodeAt(0) - 55 : Number(ch)) * weights[i];
527
+ }
528
+ return total % 10 === 0;
529
+ }
530
+ /**
531
+ * Polish REGON: 9 digits, or 14 for a local unit, each with a mod-11 check (a
532
+ * remainder of 10 counts as 0). A 14-digit REGON begins with its parent's valid
533
+ * 9-digit REGON (rules-engine#76).
534
+ */
535
+ export function validatePolishRegon(candidate) {
536
+ const c = candidate.replace(/[\s\-]/g, "");
537
+ if (!/^\d+$/.test(c) || (c.length !== 9 && c.length !== 14))
538
+ return false;
539
+ const check = (digits, weights) => {
540
+ let total = 0;
541
+ for (let i = 0; i < weights.length; i++)
542
+ total += Number(digits[i]) * weights[i];
543
+ const rest = total % 11;
544
+ return (rest === 10 ? 0 : rest) === Number(digits[weights.length]);
545
+ };
546
+ if (!check(c.slice(0, 9), [8, 9, 2, 3, 4, 5, 6, 7]))
547
+ return false;
548
+ return c.length === 9 || check(c, [2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8]);
549
+ }
506
550
  // Days per month for the rodné číslo date check. February is 29 because the
507
551
  // century is not recoverable from a two-digit year, so a leap year cannot be
508
552
  // ruled out -- being permissive by one day is the right direction here.
@@ -753,6 +797,9 @@ export const VALIDATORS = {
753
797
  portuguese_nif: validatePortugueseNif,
754
798
  polish_pesel: validatePolishPesel,
755
799
  polish_nip: validatePolishNip,
800
+ polish_nrb: validatePolishNrb,
801
+ polish_id_card: validatePolishIdCard,
802
+ polish_regon: validatePolishRegon,
756
803
  czech_birth_number: validateCzechBirthNumber,
757
804
  romanian_cnp: validateRomanianCnp,
758
805
  hungarian_taj: validateHungarianTaj,
package/dist/esm/sdk.d.ts CHANGED
@@ -22,11 +22,51 @@ type LabelFor = (det: Detection, slice: string) => string;
22
22
  */
23
23
  export declare function applyReplacements(text: string, detections: Detection[], labelFor: LabelFor): string;
24
24
  /**
25
- * Characters a token suffix is drawn from. No vowels, so a suffix never spells
26
- * a word; no 0/1/I/O, so it survives being read back by a person; no
27
- * underscore, so the type prefix stays unambiguous.
25
+ * One label in the text sent to the cloud tier: where it sits in the masked
26
+ * text, and the extent of the original it replaced —
27
+ * `[maskedStart, maskedEnd, start, end]`.
28
28
  */
29
- export declare const TOKEN_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
29
+ export type WireLabel = [number, number, number, number];
30
+ /**
31
+ * Bracket-mask `text` and record where each label landed.
32
+ *
33
+ * Returns what is sent to the cloud tier, plus one `WireLabel` per label.
34
+ * Everything between labels is copied unchanged, so the labels are all it
35
+ * takes to carry an offset from the masked text back to the original.
36
+ *
37
+ * Always `[TYPE]`, whatever label scheme the caller asked for. That is the
38
+ * syntax the model was trained on, where a placeholder means "already handled,
39
+ * do not list it". A token or a numbered label would be read as ordinary text,
40
+ * so the format on the wire is a model contract and not an output option.
41
+ */
42
+ /** @internal Shared with cloud/batches.ts. */
43
+ export declare function maskForCloud(text: string, detections: Detection[]): [string, WireLabel[]];
44
+ /**
45
+ * Carry the service's spans from the masked text onto the original.
46
+ *
47
+ * The service only ever saw `masked`, so its offsets index that. An offset in
48
+ * copied text shifts by whatever the labels before it added or removed. An
49
+ * offset inside a label has no counterpart in the original, so it snaps
50
+ * outward to the edge of the value the label replaced: an address the model
51
+ * reports around `[POSTAL_CODE]` covers the postal code, never half of it.
52
+ * Over-masking is the safe direction.
53
+ *
54
+ * A span lying wholly inside one label describes the label, not the document.
55
+ * It is dropped; the local detection behind the label stands.
56
+ *
57
+ * Throws `Unplaceable` when a span does not match the text that was sent. Such
58
+ * a span cannot be placed, and masking where it points would cover the wrong
59
+ * characters and leave the right ones in the clear.
60
+ */
61
+ /** @internal Shared with cloud/batches.ts. */
62
+ export declare function ontoOriginal(spans: Detection[], masked: string, text: string, labels: WireLabel[], Unplaceable: new (message: string) => Error): Detection[];
63
+ /**
64
+ * Characters a token suffix is drawn from. No vowels (and no Y), so a suffix
65
+ * never spells a word; no 0/1/I/O, so it survives being read back by a person;
66
+ * no underscore, so the type prefix stays unambiguous. A, E, U and Y were in it
67
+ * until rules-engine#55, against this comment.
68
+ */
69
+ export declare const TOKEN_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ23456789";
30
70
  export declare const TOKEN_SUFFIX_LENGTH = 4;
31
71
  /**
32
72
  * Maps each distinct PII value in one call to a reversible token.
@@ -106,6 +146,13 @@ export interface RedactOptions {
106
146
  allowlistDomains?: string[] | null;
107
147
  detectDates?: boolean;
108
148
  cache?: boolean;
149
+ /**
150
+ * `mode: "cloud"` only. Sent as the request's `Idempotency-Key`: the same key
151
+ * returns the same job instead of running (and billing) the document again,
152
+ * which is what a caller retrying after its own timeout needs. One is
153
+ * generated per call when omitted.
154
+ */
155
+ idempotencyKey?: string;
109
156
  }
110
157
  export declare class EuRedact {
111
158
  private engine;
package/dist/esm/sdk.js CHANGED
@@ -1,4 +1,4 @@
1
- import { randomInt } from "node:crypto";
1
+ import { randomIndex } from "./platform.js";
2
2
  import { ResultCache } from "./cache.js";
3
3
  import { normalize, mapOffsetToOriginal } from "./normalizer.js";
4
4
  import { RuleEngine } from "./rules/engine.js";
@@ -60,7 +60,8 @@ export function applyReplacements(text, detections, labelFor) {
60
60
  * do not list it". A token or a numbered label would be read as ordinary text,
61
61
  * so the format on the wire is a model contract and not an output option.
62
62
  */
63
- function maskForCloud(text, detections) {
63
+ /** @internal Shared with cloud/batches.ts. */
64
+ export function maskForCloud(text, detections) {
64
65
  const parts = [];
65
66
  const labels = [];
66
67
  let pos = 0;
@@ -110,7 +111,8 @@ function labelBefore(labels, offset, strict) {
110
111
  * a span cannot be placed, and masking where it points would cover the wrong
111
112
  * characters and leave the right ones in the clear.
112
113
  */
113
- function ontoOriginal(spans, masked, text, labels, Unplaceable) {
114
+ /** @internal Shared with cloud/batches.ts. */
115
+ export function ontoOriginal(spans, masked, text, labels, Unplaceable) {
114
116
  const placed = [];
115
117
  for (const span of spans) {
116
118
  if (!(0 <= span.start && span.start <= span.end && span.end <= masked.length) ||
@@ -139,11 +141,12 @@ function ontoOriginal(spans, masked, text, labels, Unplaceable) {
139
141
  return placed;
140
142
  }
141
143
  /**
142
- * Characters a token suffix is drawn from. No vowels, so a suffix never spells
143
- * a word; no 0/1/I/O, so it survives being read back by a person; no
144
- * underscore, so the type prefix stays unambiguous.
144
+ * Characters a token suffix is drawn from. No vowels (and no Y), so a suffix
145
+ * never spells a word; no 0/1/I/O, so it survives being read back by a person;
146
+ * no underscore, so the type prefix stays unambiguous. A, E, U and Y were in it
147
+ * until rules-engine#55, against this comment.
145
148
  */
146
- export const TOKEN_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
149
+ export const TOKEN_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ23456789";
147
150
  export const TOKEN_SUFFIX_LENGTH = 4;
148
151
  const TOKEN_MAX_DRAWS = 100;
149
152
  function escapeRegExp(s) {
@@ -191,7 +194,7 @@ export class TokenMapper {
191
194
  }
192
195
  let suffix = "";
193
196
  for (let i = 0; i < TOKEN_SUFFIX_LENGTH; i++) {
194
- suffix += TOKEN_ALPHABET[randomInt(TOKEN_ALPHABET.length)];
197
+ suffix += TOKEN_ALPHABET[randomIndex(TOKEN_ALPHABET.length)];
195
198
  }
196
199
  token = prefix + suffix;
197
200
  } while (this.taken.has(token));
@@ -527,7 +530,9 @@ export class EuRedact {
527
530
  const [masked, labels] = maskForCloud(text, local.detections);
528
531
  // The client converts the service's code-point offsets to UTF-16 units of
529
532
  // the text it sent, so everything from here on is in one unit.
530
- const remote = await client.redact(masked, { country: countries[0] });
533
+ const remote = await client.redact(masked, {
534
+ country: countries[0], idempotencyKey: options.idempotencyKey,
535
+ });
531
536
  // A new array throughout: `local` may be the cached rules result.
532
537
  const [detections, exempted] = applyAllowlist(text, [...local.detections, ...ontoOriginal(remote.detections, masked, text, labels, CloudError)], allowed, domains);
533
538
  detections.sort((a, b) => a.start - b.start || b.end - a.end);
@@ -539,6 +544,7 @@ export class EuRedact {
539
544
  source: "cloud",
540
545
  tokens: tokenMapper ? tokenMapper.tokens : {},
541
546
  exempted,
547
+ ...(remote.cloud ? { cloud: remote.cloud } : {}),
542
548
  };
543
549
  }
544
550
  redact(text, options = {}) {
@@ -560,6 +566,9 @@ export class EuRedact {
560
566
  if (requestedMode !== "rules") {
561
567
  throw new Error(`unknown mode ${JSON.stringify(requestedMode)}: expected "rules" or "cloud"`);
562
568
  }
569
+ if (options.idempotencyKey !== undefined) {
570
+ throw new Error('idempotencyKey applies to mode: "cloud" only: a rules-only redaction makes no request to deduplicate');
571
+ }
563
572
  return this.redactRules(text, options, allowed, domains);
564
573
  }
565
574
  /** The local pipeline: normalise, detect, exempt, mask. */
@@ -69,7 +69,8 @@ export declare enum EntityType {
69
69
  BIOMETRIC_REF = "BIOMETRIC_REF",
70
70
  FINANCIAL_AMOUNT = "FINANCIAL_AMOUNT",
71
71
  QUASI_IDENTIFIER = "QUASI_IDENTIFIER",
72
- CREDENTIAL = "CREDENTIAL",
72
+ /** Legacy alias of SECRET: every credential, the model's included, is SECRET (rules-engine#111). */
73
+ CREDENTIAL = "SECRET",
73
74
  URL = "URL",
74
75
  OTHER = "OTHER"
75
76
  }
@@ -116,7 +117,13 @@ export interface Detection {
116
117
  text: string;
117
118
  source: DetectionSource;
118
119
  country: string | null;
119
- confidence: string;
120
+ /**
121
+ * How the type was arrived at: "high", "medium" or "low" for a rules
122
+ * detection. `null` for a span the cloud model found and reported no
123
+ * confidence for: the model emits none today, and "high" would claim the
124
+ * checksum-backed evidence a rules detection has (rules-engine#108).
125
+ */
126
+ confidence: string | null;
120
127
  /**
121
128
  * How strongly the document supports {@link Detection.country}, in [0, 1].
122
129
  *
@@ -151,6 +158,52 @@ export interface Exemption {
151
158
  /** `"value"` for the literal allowlist, `"domain"` for allowlistDomains. */
152
159
  ruleKind: "value" | "domain";
153
160
  }
161
+ /**
162
+ * One reason a cloud request cost what it did. `code` is one of the service's
163
+ * factor codes (`rules_only`, `document_length`, `instructions`,
164
+ * `dense_document`, `special_category_check`, `long_document`, `batch_rate`),
165
+ * kept as a string so a code added later on the service still arrives.
166
+ */
167
+ export interface UsageFactor {
168
+ code: string;
169
+ /** One sentence explaining the factor, written by the service. */
170
+ detail: string;
171
+ /** `special_category_check` only: the kinds of data that triggered it. */
172
+ types?: string[];
173
+ }
174
+ /**
175
+ * What a cloud request cost, and why (rules-engine#89). A total and its
176
+ * reasons, never a cost per step; `credits` is `round(tokens * billingRate)`,
177
+ * the formula the service debits by.
178
+ */
179
+ export interface Usage {
180
+ tokens: number;
181
+ billingRate: number;
182
+ credits: number;
183
+ factors: UsageFactor[];
184
+ }
185
+ /**
186
+ * Something the model found that could not be placed in the document: the
187
+ * service could not match it to a position in what it was sent, so nothing was
188
+ * masked for it. Reported so a caller can decide, rather than never being told.
189
+ */
190
+ export interface Unlocated {
191
+ text: string;
192
+ entityType: EntityType | string;
193
+ }
194
+ /** What the cloud service said about a request, beside its spans (rules-engine#89). */
195
+ export interface CloudInfo {
196
+ /** The service's job id: for support, for `Jobs.retrieve`, and to recognise an
197
+ * idempotent replay. `null` for a batch document. */
198
+ jobId: string | null;
199
+ /** The public name of the model that answered, `null` when only rules ran. */
200
+ modelVersion: string | null;
201
+ /** What the request cost and why. Absent when the service does not report it
202
+ * (older versions, batch documents) or reported it malformed. */
203
+ usage?: Usage;
204
+ /** What the model found but could not place in the text it was sent. */
205
+ unlocated: Unlocated[];
206
+ }
154
207
  export interface RedactResult {
155
208
  redactedText: string;
156
209
  detections: Detection[];
@@ -188,6 +241,9 @@ export interface RedactResult {
188
241
  /** Detections the allowlist kept out of the output, with the rule that
189
242
  * matched. Empty unless an allowlist was in force. */
190
243
  exempted: Exemption[];
244
+ /** What the cloud service reported beside the spans: job id, model, cost and
245
+ * anything it could not place. Absent on a rules-only result. */
246
+ cloud?: CloudInfo;
191
247
  }
192
248
  export interface PatternDef {
193
249
  entityType: EntityType | string;
package/dist/esm/types.js CHANGED
@@ -75,7 +75,8 @@ export var EntityType;
75
75
  EntityType["BIOMETRIC_REF"] = "BIOMETRIC_REF";
76
76
  EntityType["FINANCIAL_AMOUNT"] = "FINANCIAL_AMOUNT";
77
77
  EntityType["QUASI_IDENTIFIER"] = "QUASI_IDENTIFIER";
78
- EntityType["CREDENTIAL"] = "CREDENTIAL";
78
+ /** Legacy alias of SECRET: every credential, the model's included, is SECRET (rules-engine#111). */
79
+ EntityType["CREDENTIAL"] = "SECRET";
79
80
  EntityType["URL"] = "URL";
80
81
  EntityType["OTHER"] = "OTHER";
81
82
  })(EntityType || (EntityType = {}));
@@ -89,6 +90,8 @@ export const LEGACY_TYPE_ALIASES = {
89
90
  NAME: "PERSON_NAME",
90
91
  STREET_ADDRESS: "ADDRESS",
91
92
  NATIONALITY_ETHNICITY: "SENSITIVE_ATTRIBUTE",
93
+ // The model labels prose credentials CREDENTIAL; the SDK reports every credential as SECRET (rules-engine#111).
94
+ CREDENTIAL: "SECRET",
92
95
  };
93
96
  /**
94
97
  * Resolve a possibly-legacy type name to its canonical form.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euredact",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "European PII redaction SDK -- fast, zero-dependency, rule-based detection and redaction of personal data across 31 European countries",
5
5
  "homepage": "https://euredact.dev",
6
6
  "license": "Apache-2.0",
@@ -20,10 +20,15 @@
20
20
  }
21
21
  }
22
22
  },
23
+ "browser": {
24
+ "./dist/esm/platform.js": "./dist/esm/platform.browser.js",
25
+ "./dist/cjs/platform.js": "./dist/cjs/platform.browser.js"
26
+ },
23
27
  "files": [
24
28
  "dist",
25
29
  "README.md",
26
- "LICENSE"
30
+ "LICENSE",
31
+ "NOTICE"
27
32
  ],
28
33
  "engines": {
29
34
  "node": ">=16.0.0"
@@ -54,7 +59,7 @@
54
59
  "build:cjs": "tsc -p tsconfig.cjs.json",
55
60
  "postbuild:cjs": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
56
61
  "build": "rm -rf dist && npm run build:esm && npm run build:cjs",
57
- "test": "npm run test:spec && npm run test:inference && npm run test:cloud && npm run test:tokenize && npm run test:allowlist && npm run test:eval-scoring && npm run test:conformance",
62
+ "test": "npm run test:spec && npm run test:inference && npm run test:cloud && npm run test:cloud-api && npm run test:tokenize && npm run test:allowlist && npm run test:eval-scoring && npm run test:platform && npm run test:batches && npm run test:conformance",
58
63
  "benchmark": "node --import tsx src/__tests__/benchmark.ts",
59
64
  "prepublishOnly": "npm run build",
60
65
  "test:spec": "node --import tsx src/__tests__/spec.ts",
@@ -64,9 +69,12 @@
64
69
  "test:inference": "node --import tsx src/__tests__/inference.ts",
65
70
  "dump": "node --import tsx src/__tests__/dump.ts",
66
71
  "test:cloud": "node --import tsx src/__tests__/cloud.ts",
72
+ "test:cloud-api": "node --import tsx src/__tests__/cloudApi.ts",
67
73
  "test:tokenize": "node --import tsx src/__tests__/tokenize.ts",
68
74
  "test:allowlist": "node --import tsx src/__tests__/allowlist.ts",
69
- "test:eval-scoring": "node --import tsx src/__tests__/evalScoring.ts"
75
+ "test:eval-scoring": "node --import tsx src/__tests__/evalScoring.ts",
76
+ "test:platform": "node --import tsx src/__tests__/platform.ts",
77
+ "test:batches": "node --import tsx src/__tests__/batches.ts"
70
78
  },
71
79
  "devDependencies": {
72
80
  "@types/node": "22.10.2",