extractia-sdk 1.4.0 → 1.5.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
@@ -22,6 +22,7 @@ Works in Node.js ≥ 18 and modern browsers via the provided UMD build.
22
22
  - [AI Features](#ai-features)
23
23
  - [Exports](#exports)
24
24
  - [OCR Tools](#ocr-tools)
25
+ - [Execution History](#getocrtoolexecutionsoptions)
25
26
  - [Credits & Analytics](#credits--analytics)
26
27
  - [Sub-Users](#sub-users)
27
28
  6. [TypeScript](#typescript)
@@ -796,6 +797,74 @@ if (docType === "invoice") {
796
797
 
797
798
  ---
798
799
 
800
+ #### `getOcrToolExecutions(options?)`
801
+
802
+ Returns a paginated list of OCR tool execution history for the authenticated user.
803
+ Each record includes the original image (`imageBase64`), the AI answer, explanation, and run metadata.
804
+
805
+ > **Image data is included in every record.** Keep page sizes small when rendering thumbnails to avoid large payloads.
806
+
807
+ | Option | Type | Default | Description |
808
+ | -------- | -------- | ------- | -------------------------------------------------------- |
809
+ | `toolId` | `string` | — | Filter by tool ID. Omit to return executions for all tools |
810
+ | `page` | `number` | `0` | Zero-based page index |
811
+ | `size` | `number` | `20` | Page size — values above 50 are capped server-side |
812
+
813
+ ```js
814
+ // All executions (most recent first)
815
+ const history = await getOcrToolExecutions();
816
+ console.log(`${history.totalElements} total executions`);
817
+
818
+ for (const exec of history.content) {
819
+ console.log(exec.toolName, exec.answer, exec.status); // "Proof of Residence" "YES" "SUCCESS"
820
+ // exec.imageBase64 — the original image that was analyzed
821
+ // exec.createdAt — ISO 8601 UTC timestamp
822
+ }
823
+
824
+ // Filter by a specific tool
825
+ const residenceHistory = await getOcrToolExecutions({
826
+ toolId: "tool_residence_check",
827
+ page: 0,
828
+ size: 10,
829
+ });
830
+
831
+ // Check for failed executions
832
+ const failures = history.content.filter((e) => e.status === "FAILURE");
833
+ failures.forEach((e) => console.error(e.toolName, e.errorMessage));
834
+ ```
835
+
836
+ **Response shape:**
837
+
838
+ ```json
839
+ {
840
+ "content": [
841
+ {
842
+ "id": "exec_abc123",
843
+ "toolId": "tool_residence_check",
844
+ "toolName": "Proof of Residence",
845
+ "imageBase64": "...",
846
+ "mimeType": "image/jpeg",
847
+ "paramValues": null,
848
+ "answer": "YES",
849
+ "explanation": "Utility bill dated within 3 months.",
850
+ "booleanAnswer": true,
851
+ "status": "SUCCESS",
852
+ "processingTimeMs": 1240,
853
+ "createdAt": "2026-04-13T10:00:00Z"
854
+ }
855
+ ],
856
+ "totalElements": 42,
857
+ "totalPages": 3,
858
+ "size": 20,
859
+ "number": 0,
860
+ "first": true,
861
+ "last": false,
862
+ "empty": false
863
+ }
864
+ ```
865
+
866
+ ---
867
+
799
868
  ### Credits & Analytics
800
869
 
801
870
  #### `getCreditsBalance()`
@@ -833,8 +902,25 @@ history.content.forEach((entry) => {
833
902
 
834
903
  > Requires a **Pro or higher** plan. The plan determines the maximum number of sub-users allowed.
835
904
 
836
- Sub-users can log in to the web app and operate within the permissions you grant them.
837
- Available permissions: `"upload"` · `"view"` · `"template"` · `"settings"` · `"export"` · `"api"`
905
+ Sub-users can log in to the web app and operate within the permissions you grant them.
906
+
907
+ **Available permissions:**
908
+
909
+ | Permission | What it grants |
910
+ | -------------- | ---------------------------------------------------- |
911
+ | `"upload"` | Upload and process new documents |
912
+ | `"view"` | View all documents owned by the account |
913
+ | `"template"` | Create and edit form templates |
914
+ | `"settings"` | View and edit account settings |
915
+ | `"export"` | Export documents to CSV / Excel / JSON |
916
+ | `"api"` | Use the public API with their own API key |
917
+ | `"ocr_tools"` | Access the AI Agents / OCR Tools feature |
918
+ | `"gallery"` | Browse and clone templates from the public gallery |
919
+ | `"smart_scan"` | Use the Smart Scan camera capture flow |
920
+ | `"ia_agent"` | Use the IA Agent for intelligent document extraction |
921
+ | `"assistant"` | Use the built-in AI chatbot assistant |
922
+
923
+ **`allowedFormIds`** (optional): restrict which form templates the sub-user can access. Pass `null` or omit to grant access to all templates.
838
924
 
839
925
  ### Document History
840
926
 
@@ -862,13 +948,29 @@ const users = await getSubUsers();
862
948
  ```js
863
949
  import { createSubUser } from "extractia-sdk";
864
950
 
951
+ // Basic — access to all templates
865
952
  const sub = await createSubUser({
866
953
  username: "agent_carlos",
867
954
  password: "SecurePass1",
955
+ permissions: ["upload", "view", "ocr_tools"],
956
+ });
957
+
958
+ // Restricted to specific templates only
959
+ const restricted = await createSubUser({
960
+ username: "agent_maria",
961
+ password: "SecurePass2",
868
962
  permissions: ["upload", "view"],
963
+ allowedFormIds: ["tpl_invoices", "tpl_receipts"], // null = all templates
869
964
  });
870
965
  ```
871
966
 
967
+ | Parameter | Type | Required | Description |
968
+ | ---------------- | ---------- | -------- | --------------------------------------------------- |
969
+ | `username` | `string` | ✅ | Must not be an existing account email |
970
+ | `password` | `string` | ✅ | Must differ from the owner's password |
971
+ | `permissions` | `string[]` | ✅ | See permissions table above |
972
+ | `allowedFormIds` | `string[]` | — | Template IDs accessible to this sub-user (null = all) |
973
+
872
974
  **Error codes:**
873
975
  | Code | Reason |
874
976
  |------|--------|
@@ -876,16 +978,31 @@ const sub = await createSubUser({
876
978
  | `409` | Username already in use |
877
979
  | `400` | Missing fields or password matches the main account |
878
980
 
879
- ### Update Permissions or Password
981
+ ### Update Permissions, Template Access, or Password
880
982
 
881
- Only the fields you include are changed. Omit `password` to keep it unchanged.
983
+ Only the fields you include are changed. Omit any field to leave it unchanged.
882
984
 
883
985
  ```js
884
986
  import { updateSubUser } from "extractia-sdk";
885
987
 
988
+ // Change permissions
886
989
  await updateSubUser("agent_carlos", {
887
- permissions: ["upload", "view", "export"],
888
- // password: 'NewPass99', ← optional
990
+ permissions: ["upload", "view", "export", "ocr_tools"],
991
+ });
992
+
993
+ // Restrict to specific templates
994
+ await updateSubUser("agent_carlos", {
995
+ allowedFormIds: ["tpl_invoices"],
996
+ });
997
+
998
+ // Remove template restriction (grant access to all)
999
+ await updateSubUser("agent_carlos", {
1000
+ allowedFormIds: null,
1001
+ });
1002
+
1003
+ // Change password only
1004
+ await updateSubUser("agent_carlos", {
1005
+ password: "NewPass99",
889
1006
  });
890
1007
  ```
891
1008
 
@@ -914,15 +1031,36 @@ console.log(state.suspended); // true | false
914
1031
 
915
1032
  The SDK ships with a full `index.d.ts` declaration file — no `@types` package needed.
916
1033
 
1034
+ **Key exported types:**
1035
+
1036
+ | Type | Description |
1037
+ | --------------------- | -------------------------------------------------------- |
1038
+ | `FormTemplate` | A template with `id`, `label`, `fields`, `userId` |
1039
+ | `FormField` | A single field: `label`, `type`, `required`, `listLabel` |
1040
+ | `UserDocument` | A processed document with `rawJson`, `createdAt`, etc. |
1041
+ | `OcrToolConfig` | An OCR agent configuration |
1042
+ | `OcrRunResult` | Result of `runOcrTool`: `answer` + `explanation` |
1043
+ | `OcrToolExecution` | A single execution record from `getOcrToolExecutions` |
1044
+ | `OcrExecutionPage` | Paginated response of `OcrToolExecution` records |
1045
+ | `SubUser` | Sub-user with `username`, `permissions`, `allowedFormIds`, `suspended` |
1046
+ | `AppUserProfile` | User profile with quota, plan, and settings fields |
1047
+ | `DocumentAuditEntry` | An entry from `getDocumentHistory` |
1048
+ | `ExtractiaError` | Base error class with `status`, `code`, `userMessage` |
1049
+
917
1050
  ```ts
918
1051
  import {
919
1052
  setToken,
920
1053
  processImage,
921
1054
  runOcrTool,
1055
+ getOcrToolExecutions,
1056
+ createSubUser,
922
1057
  suggestFields,
923
1058
  exportDocumentsJson,
924
1059
  UserDocument,
925
1060
  OcrRunResult,
1061
+ OcrToolExecution,
1062
+ OcrExecutionPage,
1063
+ SubUser,
926
1064
  FormField,
927
1065
  TierError,
928
1066
  RateLimitError,
@@ -948,6 +1086,18 @@ async function classifyAndExtract(
948
1086
  // Extract with template
949
1087
  return processImage(templateId, base64);
950
1088
  }
1089
+
1090
+ // Typed execution history
1091
+ const page: OcrExecutionPage = await getOcrToolExecutions({ size: 5 });
1092
+ const recent: OcrToolExecution[] = page.content;
1093
+
1094
+ // Typed sub-user with allowedFormIds
1095
+ const sub: SubUser = await createSubUser({
1096
+ username: "agent_ts",
1097
+ password: "Secure1!",
1098
+ permissions: ["upload", "view", "ocr_tools"],
1099
+ allowedFormIds: ["tpl_invoices"],
1100
+ });
951
1101
  ```
952
1102
 
953
1103
  ---
@@ -968,6 +1118,13 @@ Purchase extra document packs or upgrade your plan from the dashboard to continu
968
1118
 
969
1119
  ## Changelog
970
1120
 
1121
+ ### v1.5.0
1122
+
1123
+ - **New:** `getOcrToolExecutions(opts?)` — paginated execution history for AI Agent runs, including the original image, answer, explanation, status, and processing time. Filterable by `toolId`.
1124
+ - **New:** `createSubUser` and `updateSubUser` now support `allowedFormIds` — restrict which form templates a sub-user can access (`null` = all templates).
1125
+ - **Expanded:** Sub-user permissions updated from 6 to 11: added `"ocr_tools"`, `"gallery"`, `"smart_scan"`, `"ia_agent"`, `"assistant"`.
1126
+ - **New TypeScript types:** `OcrToolExecution`, `OcrExecutionPage` interfaces; `SubUser` updated with `allowedFormIds` and all 11 permissions.
1127
+
971
1128
  ### v1.2.0
972
1129
 
973
1130
  - **New:** `getDocumentHistory(opts?)` — paginated log of all document processing events (successes and failures)
@@ -2785,7 +2785,10 @@ var ExtractiaSDK = (() => {
2785
2785
  const fields = (_b = body == null ? void 0 : body.fields) != null ? _b : Array.isArray(body == null ? void 0 : body.fieldErrors) ? Object.fromEntries(
2786
2786
  body.fieldErrors.map((f) => {
2787
2787
  var _a2;
2788
- return [f.field, (_a2 = f.message) != null ? _a2 : f.defaultMessage];
2788
+ return [
2789
+ f.field,
2790
+ (_a2 = f.message) != null ? _a2 : f.defaultMessage
2791
+ ];
2789
2792
  })
2790
2793
  ) : null;
2791
2794
  error = new ValidationError(detail != null ? detail : STATUS_MESSAGES[400], fields);
@@ -2813,12 +2816,18 @@ var ExtractiaSDK = (() => {
2813
2816
  case 429: {
2814
2817
  const retryAfterHeader = (_c = err.response.headers) == null ? void 0 : _c["retry-after"];
2815
2818
  const retryAfter = retryAfterHeader != null ? parseInt(retryAfterHeader, 10) : null;
2816
- error = new RateLimitError(detail != null ? detail : void 0, isNaN(retryAfter) ? null : retryAfter);
2819
+ error = new RateLimitError(
2820
+ detail != null ? detail : void 0,
2821
+ isNaN(retryAfter) ? null : retryAfter
2822
+ );
2817
2823
  break;
2818
2824
  }
2819
2825
  default:
2820
2826
  if (status >= 500) {
2821
- error = new ServerError((_d = detail != null ? detail : STATUS_MESSAGES[status]) != null ? _d : STATUS_MESSAGES[500], status);
2827
+ error = new ServerError(
2828
+ (_d = detail != null ? detail : STATUS_MESSAGES[status]) != null ? _d : STATUS_MESSAGES[500],
2829
+ status
2830
+ );
2822
2831
  } else {
2823
2832
  error = new ExtractiaError(
2824
2833
  detail != null ? detail : err.message,
@@ -3137,6 +3146,7 @@ var ExtractiaSDK = (() => {
3137
3146
  __export(ocrTools_exports, {
3138
3147
  createOcrTool: () => createOcrTool,
3139
3148
  deleteOcrTool: () => deleteOcrTool,
3149
+ getOcrToolExecutions: () => getOcrToolExecutions,
3140
3150
  getOcrTools: () => getOcrTools,
3141
3151
  runOcrTool: () => runOcrTool,
3142
3152
  updateOcrTool: () => updateOcrTool
@@ -3165,6 +3175,16 @@ var ExtractiaSDK = (() => {
3165
3175
  const res = await apiClient_default.post(`/ocr-tools/${id}/run`, body);
3166
3176
  return res.data;
3167
3177
  }
3178
+ async function getOcrToolExecutions({
3179
+ toolId,
3180
+ page = 0,
3181
+ size = 20
3182
+ } = {}) {
3183
+ const params = { page, size };
3184
+ if (toolId) params.toolId = toolId;
3185
+ const res = await apiClient_default.get("/ocr-tools/executions", { params });
3186
+ return res.data;
3187
+ }
3168
3188
 
3169
3189
  // src/subusers.js
3170
3190
  var subusers_exports = {};
@@ -3239,16 +3259,18 @@ var ExtractiaSDK = (() => {
3239
3259
  );
3240
3260
  reader.onerror = () => {
3241
3261
  var _a;
3242
- return reject(new Error(`fileToBase64: failed to read file "${(_a = file.name) != null ? _a : "unknown"}".`));
3262
+ return reject(
3263
+ new Error(
3264
+ `fileToBase64: failed to read file "${(_a = file.name) != null ? _a : "unknown"}".`
3265
+ )
3266
+ );
3243
3267
  };
3244
3268
  reader.readAsDataURL(file);
3245
3269
  });
3246
3270
  }
3247
3271
  function stripDataUrlPrefix(base64OrDataUrl) {
3248
3272
  if (typeof base64OrDataUrl !== "string") {
3249
- throw new TypeError(
3250
- "stripDataUrlPrefix: argument must be a string."
3251
- );
3273
+ throw new TypeError("stripDataUrlPrefix: argument must be a string.");
3252
3274
  }
3253
3275
  const idx = base64OrDataUrl.indexOf(";base64,");
3254
3276
  return idx !== -1 ? base64OrDataUrl.slice(idx + 8) : base64OrDataUrl;
@@ -122,6 +122,7 @@ __export(index_exports, {
122
122
  getDocumentsByTemplateId: () => getDocumentsByTemplateId,
123
123
  getMimeType: () => getMimeType,
124
124
  getMyProfile: () => getMyProfile,
125
+ getOcrToolExecutions: () => getOcrToolExecutions,
125
126
  getOcrTools: () => getOcrTools,
126
127
  getRecentDocuments: () => getRecentDocuments,
127
128
  getSubUsers: () => getSubUsers,
@@ -2849,7 +2850,10 @@ function mapAxiosError(err) {
2849
2850
  const fields = (_b = body == null ? void 0 : body.fields) != null ? _b : Array.isArray(body == null ? void 0 : body.fieldErrors) ? Object.fromEntries(
2850
2851
  body.fieldErrors.map((f) => {
2851
2852
  var _a2;
2852
- return [f.field, (_a2 = f.message) != null ? _a2 : f.defaultMessage];
2853
+ return [
2854
+ f.field,
2855
+ (_a2 = f.message) != null ? _a2 : f.defaultMessage
2856
+ ];
2853
2857
  })
2854
2858
  ) : null;
2855
2859
  error = new ValidationError(detail != null ? detail : STATUS_MESSAGES[400], fields);
@@ -2877,12 +2881,18 @@ function mapAxiosError(err) {
2877
2881
  case 429: {
2878
2882
  const retryAfterHeader = (_c = err.response.headers) == null ? void 0 : _c["retry-after"];
2879
2883
  const retryAfter = retryAfterHeader != null ? parseInt(retryAfterHeader, 10) : null;
2880
- error = new RateLimitError(detail != null ? detail : void 0, isNaN(retryAfter) ? null : retryAfter);
2884
+ error = new RateLimitError(
2885
+ detail != null ? detail : void 0,
2886
+ isNaN(retryAfter) ? null : retryAfter
2887
+ );
2881
2888
  break;
2882
2889
  }
2883
2890
  default:
2884
2891
  if (status >= 500) {
2885
- error = new ServerError((_d = detail != null ? detail : STATUS_MESSAGES[status]) != null ? _d : STATUS_MESSAGES[500], status);
2892
+ error = new ServerError(
2893
+ (_d = detail != null ? detail : STATUS_MESSAGES[status]) != null ? _d : STATUS_MESSAGES[500],
2894
+ status
2895
+ );
2886
2896
  } else {
2887
2897
  error = new ExtractiaError(
2888
2898
  detail != null ? detail : err.message,
@@ -3201,6 +3211,7 @@ var ocrTools_exports = {};
3201
3211
  __export(ocrTools_exports, {
3202
3212
  createOcrTool: () => createOcrTool,
3203
3213
  deleteOcrTool: () => deleteOcrTool,
3214
+ getOcrToolExecutions: () => getOcrToolExecutions,
3204
3215
  getOcrTools: () => getOcrTools,
3205
3216
  runOcrTool: () => runOcrTool,
3206
3217
  updateOcrTool: () => updateOcrTool
@@ -3229,6 +3240,16 @@ async function runOcrTool(id, base64Image, options = {}) {
3229
3240
  const res = await apiClient_default.post(`/ocr-tools/${id}/run`, body);
3230
3241
  return res.data;
3231
3242
  }
3243
+ async function getOcrToolExecutions({
3244
+ toolId,
3245
+ page = 0,
3246
+ size = 20
3247
+ } = {}) {
3248
+ const params = { page, size };
3249
+ if (toolId) params.toolId = toolId;
3250
+ const res = await apiClient_default.get("/ocr-tools/executions", { params });
3251
+ return res.data;
3252
+ }
3232
3253
 
3233
3254
  // src/subusers.js
3234
3255
  var subusers_exports = {};
@@ -3303,16 +3324,18 @@ function fileToBase64(file) {
3303
3324
  );
3304
3325
  reader.onerror = () => {
3305
3326
  var _a;
3306
- return reject(new Error(`fileToBase64: failed to read file "${(_a = file.name) != null ? _a : "unknown"}".`));
3327
+ return reject(
3328
+ new Error(
3329
+ `fileToBase64: failed to read file "${(_a = file.name) != null ? _a : "unknown"}".`
3330
+ )
3331
+ );
3307
3332
  };
3308
3333
  reader.readAsDataURL(file);
3309
3334
  });
3310
3335
  }
3311
3336
  function stripDataUrlPrefix(base64OrDataUrl) {
3312
3337
  if (typeof base64OrDataUrl !== "string") {
3313
- throw new TypeError(
3314
- "stripDataUrlPrefix: argument must be a string."
3315
- );
3338
+ throw new TypeError("stripDataUrlPrefix: argument must be a string.");
3316
3339
  }
3317
3340
  const idx = base64OrDataUrl.indexOf(";base64,");
3318
3341
  return idx !== -1 ? base64OrDataUrl.slice(idx + 8) : base64OrDataUrl;
@@ -2767,7 +2767,10 @@ function mapAxiosError(err) {
2767
2767
  const fields = (_b = body == null ? void 0 : body.fields) != null ? _b : Array.isArray(body == null ? void 0 : body.fieldErrors) ? Object.fromEntries(
2768
2768
  body.fieldErrors.map((f) => {
2769
2769
  var _a2;
2770
- return [f.field, (_a2 = f.message) != null ? _a2 : f.defaultMessage];
2770
+ return [
2771
+ f.field,
2772
+ (_a2 = f.message) != null ? _a2 : f.defaultMessage
2773
+ ];
2771
2774
  })
2772
2775
  ) : null;
2773
2776
  error = new ValidationError(detail != null ? detail : STATUS_MESSAGES[400], fields);
@@ -2795,12 +2798,18 @@ function mapAxiosError(err) {
2795
2798
  case 429: {
2796
2799
  const retryAfterHeader = (_c = err.response.headers) == null ? void 0 : _c["retry-after"];
2797
2800
  const retryAfter = retryAfterHeader != null ? parseInt(retryAfterHeader, 10) : null;
2798
- error = new RateLimitError(detail != null ? detail : void 0, isNaN(retryAfter) ? null : retryAfter);
2801
+ error = new RateLimitError(
2802
+ detail != null ? detail : void 0,
2803
+ isNaN(retryAfter) ? null : retryAfter
2804
+ );
2799
2805
  break;
2800
2806
  }
2801
2807
  default:
2802
2808
  if (status >= 500) {
2803
- error = new ServerError((_d = detail != null ? detail : STATUS_MESSAGES[status]) != null ? _d : STATUS_MESSAGES[500], status);
2809
+ error = new ServerError(
2810
+ (_d = detail != null ? detail : STATUS_MESSAGES[status]) != null ? _d : STATUS_MESSAGES[500],
2811
+ status
2812
+ );
2804
2813
  } else {
2805
2814
  error = new ExtractiaError(
2806
2815
  detail != null ? detail : err.message,
@@ -3119,6 +3128,7 @@ var ocrTools_exports = {};
3119
3128
  __export(ocrTools_exports, {
3120
3129
  createOcrTool: () => createOcrTool,
3121
3130
  deleteOcrTool: () => deleteOcrTool,
3131
+ getOcrToolExecutions: () => getOcrToolExecutions,
3122
3132
  getOcrTools: () => getOcrTools,
3123
3133
  runOcrTool: () => runOcrTool,
3124
3134
  updateOcrTool: () => updateOcrTool
@@ -3147,6 +3157,16 @@ async function runOcrTool(id, base64Image, options = {}) {
3147
3157
  const res = await apiClient_default.post(`/ocr-tools/${id}/run`, body);
3148
3158
  return res.data;
3149
3159
  }
3160
+ async function getOcrToolExecutions({
3161
+ toolId,
3162
+ page = 0,
3163
+ size = 20
3164
+ } = {}) {
3165
+ const params = { page, size };
3166
+ if (toolId) params.toolId = toolId;
3167
+ const res = await apiClient_default.get("/ocr-tools/executions", { params });
3168
+ return res.data;
3169
+ }
3150
3170
 
3151
3171
  // src/subusers.js
3152
3172
  var subusers_exports = {};
@@ -3221,16 +3241,18 @@ function fileToBase64(file) {
3221
3241
  );
3222
3242
  reader.onerror = () => {
3223
3243
  var _a;
3224
- return reject(new Error(`fileToBase64: failed to read file "${(_a = file.name) != null ? _a : "unknown"}".`));
3244
+ return reject(
3245
+ new Error(
3246
+ `fileToBase64: failed to read file "${(_a = file.name) != null ? _a : "unknown"}".`
3247
+ )
3248
+ );
3225
3249
  };
3226
3250
  reader.readAsDataURL(file);
3227
3251
  });
3228
3252
  }
3229
3253
  function stripDataUrlPrefix(base64OrDataUrl) {
3230
3254
  if (typeof base64OrDataUrl !== "string") {
3231
- throw new TypeError(
3232
- "stripDataUrlPrefix: argument must be a string."
3233
- );
3255
+ throw new TypeError("stripDataUrlPrefix: argument must be a string.");
3234
3256
  }
3235
3257
  const idx = base64OrDataUrl.indexOf(";base64,");
3236
3258
  return idx !== -1 ? base64OrDataUrl.slice(idx + 8) : base64OrDataUrl;
@@ -3358,6 +3380,7 @@ export {
3358
3380
  getDocumentsByTemplateId,
3359
3381
  getMimeType,
3360
3382
  getMyProfile,
3383
+ getOcrToolExecutions,
3361
3384
  getOcrTools,
3362
3385
  getRecentDocuments,
3363
3386
  getSubUsers,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // ─── Extractia SDK v1.1 — TypeScript type declarations ───────────────────────
1
+ // ─── Extractia SDK v1.5 — TypeScript type declarations ───────────────────────
2
2
  // Keep in sync with JavaScript source files.
3
3
 
4
4
  // ─── Primitives ───────────────────────────────────────────────────────────────
@@ -111,8 +111,14 @@ export interface DocumentAuditEntry {
111
111
  /** A sub-user belonging to an account. */
112
112
  export interface SubUser {
113
113
  username: string;
114
- /** Granted permission strings (e.g. "upload", "view", "template", "settings", "export", "api"). */
114
+ /**
115
+ * Granted permission strings.
116
+ * Valid values: `"upload"`, `"view"`, `"template"`, `"settings"`, `"export"`, `"api"`,
117
+ * `"ocr_tools"`, `"gallery"`, `"smart_scan"`, `"ia_agent"`, `"assistant"`.
118
+ */
115
119
  permissions: string[];
120
+ /** Optional list of form template IDs this sub-user may access. Null = access to all. */
121
+ allowedFormIds?: string[] | null;
116
122
  /** Whether the sub-user is currently suspended. */
117
123
  suspended: boolean;
118
124
  /** Last known geographic location ("lat,lng" format). */
@@ -202,6 +208,47 @@ export interface OcrRunResult {
202
208
  explanation: string;
203
209
  }
204
210
 
211
+ /** A single AI Agent execution record stored in `ocr_tool_executions`. */
212
+ export interface OcrToolExecution {
213
+ id: string;
214
+ toolId: string;
215
+ /** Snapshot of the tool name at execution time. */
216
+ toolName: string;
217
+ /** Original image encoded as base64 (with or without data-URL prefix). */
218
+ imageBase64: string;
219
+ mimeType: string;
220
+ /** Dynamic parameter values supplied at run time, keyed 1-based (e.g. `{ "1": "Main St" }`). */
221
+ paramValues?: Record<string, string> | null;
222
+ /** AI answer text (YES/NO, label, or free-form text). */
223
+ answer: string;
224
+ /** Short rationale explaining the answer. */
225
+ explanation: string;
226
+ /** Parsed boolean for `YES_NO` tools; `null` for `LABEL` and `TEXT` tools. */
227
+ booleanAnswer: boolean | null;
228
+ /** `"SUCCESS"` or `"FAILURE"`. */
229
+ status: 'SUCCESS' | 'FAILURE';
230
+ /** Error message when `status` is `"FAILURE"`. */
231
+ errorMessage?: string;
232
+ /** Wall-clock time in milliseconds for the AI provider call. */
233
+ processingTimeMs?: number;
234
+ /** ISO 8601 UTC timestamp. */
235
+ createdAt: string;
236
+ }
237
+
238
+ /** Spring-style paginated response wrapping a list of {@link OcrToolExecution} records. */
239
+ export interface OcrExecutionPage {
240
+ content: OcrToolExecution[];
241
+ totalElements: number;
242
+ totalPages: number;
243
+ /** Applied page size. */
244
+ size: number;
245
+ /** 0-based page index. */
246
+ number: number;
247
+ first: boolean;
248
+ last: boolean;
249
+ empty: boolean;
250
+ }
251
+
205
252
  // ─── Error classes ────────────────────────────────────────────────────────────
206
253
 
207
254
  /** Base error class for all Extractia SDK errors. */
@@ -730,6 +777,24 @@ export function deleteOcrTool(id: string): Promise<void>;
730
777
  */
731
778
  export function runOcrTool(id: string, base64Image: string, options?: OcrRunOptions): Promise<OcrRunResult>;
732
779
 
780
+ /**
781
+ * Returns a paginated list of OCR tool execution history for the authenticated user.
782
+ *
783
+ * Each record includes the original image, answer, explanation, and run metadata.
784
+ * Keep page sizes small when rendering image thumbnails.
785
+ *
786
+ * @param options.toolId Filter by tool ID. Omit to return executions for all tools.
787
+ * @param options.page 0-based page index (default `0`).
788
+ * @param options.size Page size, 1–50 (default `20`; values above 50 are capped server-side).
789
+ *
790
+ * @throws {AuthError} 401 if the user is not authenticated.
791
+ */
792
+ export function getOcrToolExecutions(options?: {
793
+ toolId?: string;
794
+ page?: number;
795
+ size?: number;
796
+ }): Promise<OcrExecutionPage>;
797
+
733
798
  // ─── Sub-Users ────────────────────────────────────────────────────────────────
734
799
 
735
800
  /**
@@ -741,7 +806,8 @@ export function getSubUsers(): Promise<SubUser[]>;
741
806
  /**
742
807
  * Creates a new sub-user.
743
808
  *
744
- * Available permissions: `"upload"`, `"view"`, `"template"`, `"settings"`, `"export"`, `"api"`.
809
+ * Available permissions: `"upload"`, `"view"`, `"template"`, `"settings"`, `"export"`, `"api"`,
810
+ * `"ocr_tools"`, `"gallery"`, `"smart_scan"`, `"ia_agent"`, `"assistant"`.
745
811
  *
746
812
  * @throws {ForbiddenError} 403 if the plan does not support sub-users or the limit is reached.
747
813
  * @throws {ExtractiaError} 409 if the username is already taken.
@@ -750,14 +816,17 @@ export function getSubUsers(): Promise<SubUser[]>;
750
816
  * await createSubUser({
751
817
  * username: 'agent_bob',
752
818
  * password: 'SecurePass1',
753
- * permissions: ['upload', 'view'],
819
+ * permissions: ['upload', 'view', 'ocr_tools'],
820
+ * allowedFormIds: ['tpl_123'], // optional — omit for access to all forms
754
821
  * });
755
822
  */
756
823
  export function createSubUser(subUser: {
757
824
  username: string;
758
825
  password: string;
759
826
  permissions: string[];
760
- }): Promise<{ username: string; permissions: string[] }>;
827
+ /** Restrict this sub-user to specific form templates. Omit or pass null for all forms. */
828
+ allowedFormIds?: string[] | null;
829
+ }): Promise<{ username: string; permissions: string[]; allowedFormIds?: string[] | null }>;
761
830
 
762
831
  /**
763
832
  * Deletes a sub-user by username.
@@ -767,7 +836,7 @@ export function createSubUser(subUser: {
767
836
  export function deleteSubUser(username: string): Promise<{ deleted: string }>;
768
837
 
769
838
  /**
770
- * Updates the permissions and/or password of a sub-user.
839
+ * Updates the permissions, allowed forms, and/or password of a sub-user.
771
840
  * Only the provided fields are changed.
772
841
  *
773
842
  * @param username The sub-user's username.
@@ -776,7 +845,12 @@ export function deleteSubUser(username: string): Promise<{ deleted: string }>;
776
845
  */
777
846
  export function updateSubUser(
778
847
  username: string,
779
- updates: { permissions?: string[]; password?: string }
848
+ updates: {
849
+ permissions?: string[];
850
+ password?: string;
851
+ /** New allowed form ID list (replaces existing). Pass empty array to remove all restrictions. */
852
+ allowedFormIds?: string[] | null;
853
+ }
780
854
  ): Promise<{ username: string; updated: boolean }>;
781
855
 
782
856
  /**
@@ -907,6 +981,7 @@ declare const extractia: {
907
981
  updateOcrTool: typeof updateOcrTool;
908
982
  deleteOcrTool: typeof deleteOcrTool;
909
983
  runOcrTool: typeof runOcrTool;
984
+ getOcrToolExecutions: typeof getOcrToolExecutions;
910
985
  // Sub-users
911
986
  getSubUsers: typeof getSubUsers;
912
987
  createSubUser: typeof createSubUser;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extractia-sdk",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "JavaScript SDK for the ExtractIA API — document extraction, OCR tools, AI summaries, templates & more",
5
5
  "type": "module",
6
6
  "author": "ExtractIA Team",
@@ -49,7 +49,8 @@
49
49
  "test": "vitest run",
50
50
  "test:watch": "vitest",
51
51
  "test:coverage": "vitest run --coverage",
52
- "test:ui": "vitest --ui"
52
+ "test:ui": "vitest --ui",
53
+ "test:integration": "vitest run --config vitest.integration.config.js"
53
54
  },
54
55
  "dependencies": {
55
56
  "axios": "^1.10.0"
package/src/apiClient.js CHANGED
@@ -194,7 +194,10 @@ export function configure(opts = {}) {
194
194
  if (opts.retryDelay != null) _config.retryDelay = opts.retryDelay;
195
195
  if (opts.debug != null) _config.debug = Boolean(opts.debug);
196
196
  if (opts.defaultHeaders) {
197
- _config.defaultHeaders = { ..._config.defaultHeaders, ...opts.defaultHeaders };
197
+ _config.defaultHeaders = {
198
+ ..._config.defaultHeaders,
199
+ ...opts.defaultHeaders,
200
+ };
198
201
  }
199
202
  if (opts.onBeforeRequest) _config.onBeforeRequest = opts.onBeforeRequest;
200
203
  if (opts.onAfterResponse) _config.onAfterResponse = opts.onAfterResponse;
package/src/errors.js CHANGED
@@ -32,12 +32,7 @@ export class ExtractiaError extends Error {
32
32
  * @param {string} [userMessage] — Human-friendly sentence shown to end users.
33
33
  * @param {string} [code] — Machine-readable error code.
34
34
  */
35
- constructor(
36
- message,
37
- status = 0,
38
- userMessage = null,
39
- code = "SDK_ERROR",
40
- ) {
35
+ constructor(message, status = 0, userMessage = null, code = "SDK_ERROR") {
41
36
  super(message);
42
37
  this.name = "ExtractiaError";
43
38
  this.status = status;
@@ -140,10 +135,7 @@ export class ValidationError extends ExtractiaError {
140
135
  * @param {string} [message]
141
136
  * @param {Record<string,string>|null} [fields] — Field-level errors, if the server provides them.
142
137
  */
143
- constructor(
144
- message = STATUS_MESSAGES[400],
145
- fields = null,
146
- ) {
138
+ constructor(message = STATUS_MESSAGES[400], fields = null) {
147
139
  super(message, 400, STATUS_MESSAGES[400], "VALIDATION_ERROR");
148
140
  this.name = "ValidationError";
149
141
  /** @type {Record<string,string>|null} */
@@ -256,7 +248,9 @@ export function mapAxiosError(err) {
256
248
  const detail = extractServerDetail(err.response.data);
257
249
  const userMessage =
258
250
  STATUS_MESSAGES[status] ??
259
- (status >= 500 ? STATUS_MESSAGES[500] : "Something went wrong. Please try again.");
251
+ (status >= 500
252
+ ? STATUS_MESSAGES[500]
253
+ : "Something went wrong. Please try again.");
260
254
 
261
255
  let error;
262
256
 
@@ -268,7 +262,10 @@ export function mapAxiosError(err) {
268
262
  body?.fields ??
269
263
  (Array.isArray(body?.fieldErrors)
270
264
  ? Object.fromEntries(
271
- body.fieldErrors.map((f) => [f.field, f.message ?? f.defaultMessage]),
265
+ body.fieldErrors.map((f) => [
266
+ f.field,
267
+ f.message ?? f.defaultMessage,
268
+ ]),
272
269
  )
273
270
  : null);
274
271
  error = new ValidationError(detail ?? STATUS_MESSAGES[400], fields);
@@ -303,12 +300,18 @@ export function mapAxiosError(err) {
303
300
  const retryAfterHeader = err.response.headers?.["retry-after"];
304
301
  const retryAfter =
305
302
  retryAfterHeader != null ? parseInt(retryAfterHeader, 10) : null;
306
- error = new RateLimitError(detail ?? undefined, isNaN(retryAfter) ? null : retryAfter);
303
+ error = new RateLimitError(
304
+ detail ?? undefined,
305
+ isNaN(retryAfter) ? null : retryAfter,
306
+ );
307
307
  break;
308
308
  }
309
309
  default:
310
310
  if (status >= 500) {
311
- error = new ServerError(detail ?? STATUS_MESSAGES[status] ?? STATUS_MESSAGES[500], status);
311
+ error = new ServerError(
312
+ detail ?? STATUS_MESSAGES[status] ?? STATUS_MESSAGES[500],
313
+ status,
314
+ );
312
315
  } else {
313
316
  error = new ExtractiaError(
314
317
  detail ?? err.message,
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // ─── Extractia SDK v1.1 — TypeScript type declarations ───────────────────────
1
+ // ─── Extractia SDK v1.5 — TypeScript type declarations ───────────────────────
2
2
  // Keep in sync with JavaScript source files.
3
3
 
4
4
  // ─── Primitives ───────────────────────────────────────────────────────────────
@@ -111,8 +111,14 @@ export interface DocumentAuditEntry {
111
111
  /** A sub-user belonging to an account. */
112
112
  export interface SubUser {
113
113
  username: string;
114
- /** Granted permission strings (e.g. "upload", "view", "template", "settings", "export", "api"). */
114
+ /**
115
+ * Granted permission strings.
116
+ * Valid values: `"upload"`, `"view"`, `"template"`, `"settings"`, `"export"`, `"api"`,
117
+ * `"ocr_tools"`, `"gallery"`, `"smart_scan"`, `"ia_agent"`, `"assistant"`.
118
+ */
115
119
  permissions: string[];
120
+ /** Optional list of form template IDs this sub-user may access. Null = access to all. */
121
+ allowedFormIds?: string[] | null;
116
122
  /** Whether the sub-user is currently suspended. */
117
123
  suspended: boolean;
118
124
  /** Last known geographic location ("lat,lng" format). */
@@ -202,6 +208,47 @@ export interface OcrRunResult {
202
208
  explanation: string;
203
209
  }
204
210
 
211
+ /** A single AI Agent execution record stored in `ocr_tool_executions`. */
212
+ export interface OcrToolExecution {
213
+ id: string;
214
+ toolId: string;
215
+ /** Snapshot of the tool name at execution time. */
216
+ toolName: string;
217
+ /** Original image encoded as base64 (with or without data-URL prefix). */
218
+ imageBase64: string;
219
+ mimeType: string;
220
+ /** Dynamic parameter values supplied at run time, keyed 1-based (e.g. `{ "1": "Main St" }`). */
221
+ paramValues?: Record<string, string> | null;
222
+ /** AI answer text (YES/NO, label, or free-form text). */
223
+ answer: string;
224
+ /** Short rationale explaining the answer. */
225
+ explanation: string;
226
+ /** Parsed boolean for `YES_NO` tools; `null` for `LABEL` and `TEXT` tools. */
227
+ booleanAnswer: boolean | null;
228
+ /** `"SUCCESS"` or `"FAILURE"`. */
229
+ status: 'SUCCESS' | 'FAILURE';
230
+ /** Error message when `status` is `"FAILURE"`. */
231
+ errorMessage?: string;
232
+ /** Wall-clock time in milliseconds for the AI provider call. */
233
+ processingTimeMs?: number;
234
+ /** ISO 8601 UTC timestamp. */
235
+ createdAt: string;
236
+ }
237
+
238
+ /** Spring-style paginated response wrapping a list of {@link OcrToolExecution} records. */
239
+ export interface OcrExecutionPage {
240
+ content: OcrToolExecution[];
241
+ totalElements: number;
242
+ totalPages: number;
243
+ /** Applied page size. */
244
+ size: number;
245
+ /** 0-based page index. */
246
+ number: number;
247
+ first: boolean;
248
+ last: boolean;
249
+ empty: boolean;
250
+ }
251
+
205
252
  // ─── Error classes ────────────────────────────────────────────────────────────
206
253
 
207
254
  /** Base error class for all Extractia SDK errors. */
@@ -730,6 +777,24 @@ export function deleteOcrTool(id: string): Promise<void>;
730
777
  */
731
778
  export function runOcrTool(id: string, base64Image: string, options?: OcrRunOptions): Promise<OcrRunResult>;
732
779
 
780
+ /**
781
+ * Returns a paginated list of OCR tool execution history for the authenticated user.
782
+ *
783
+ * Each record includes the original image, answer, explanation, and run metadata.
784
+ * Keep page sizes small when rendering image thumbnails.
785
+ *
786
+ * @param options.toolId Filter by tool ID. Omit to return executions for all tools.
787
+ * @param options.page 0-based page index (default `0`).
788
+ * @param options.size Page size, 1–50 (default `20`; values above 50 are capped server-side).
789
+ *
790
+ * @throws {AuthError} 401 if the user is not authenticated.
791
+ */
792
+ export function getOcrToolExecutions(options?: {
793
+ toolId?: string;
794
+ page?: number;
795
+ size?: number;
796
+ }): Promise<OcrExecutionPage>;
797
+
733
798
  // ─── Sub-Users ────────────────────────────────────────────────────────────────
734
799
 
735
800
  /**
@@ -741,7 +806,8 @@ export function getSubUsers(): Promise<SubUser[]>;
741
806
  /**
742
807
  * Creates a new sub-user.
743
808
  *
744
- * Available permissions: `"upload"`, `"view"`, `"template"`, `"settings"`, `"export"`, `"api"`.
809
+ * Available permissions: `"upload"`, `"view"`, `"template"`, `"settings"`, `"export"`, `"api"`,
810
+ * `"ocr_tools"`, `"gallery"`, `"smart_scan"`, `"ia_agent"`, `"assistant"`.
745
811
  *
746
812
  * @throws {ForbiddenError} 403 if the plan does not support sub-users or the limit is reached.
747
813
  * @throws {ExtractiaError} 409 if the username is already taken.
@@ -750,14 +816,17 @@ export function getSubUsers(): Promise<SubUser[]>;
750
816
  * await createSubUser({
751
817
  * username: 'agent_bob',
752
818
  * password: 'SecurePass1',
753
- * permissions: ['upload', 'view'],
819
+ * permissions: ['upload', 'view', 'ocr_tools'],
820
+ * allowedFormIds: ['tpl_123'], // optional — omit for access to all forms
754
821
  * });
755
822
  */
756
823
  export function createSubUser(subUser: {
757
824
  username: string;
758
825
  password: string;
759
826
  permissions: string[];
760
- }): Promise<{ username: string; permissions: string[] }>;
827
+ /** Restrict this sub-user to specific form templates. Omit or pass null for all forms. */
828
+ allowedFormIds?: string[] | null;
829
+ }): Promise<{ username: string; permissions: string[]; allowedFormIds?: string[] | null }>;
761
830
 
762
831
  /**
763
832
  * Deletes a sub-user by username.
@@ -767,7 +836,7 @@ export function createSubUser(subUser: {
767
836
  export function deleteSubUser(username: string): Promise<{ deleted: string }>;
768
837
 
769
838
  /**
770
- * Updates the permissions and/or password of a sub-user.
839
+ * Updates the permissions, allowed forms, and/or password of a sub-user.
771
840
  * Only the provided fields are changed.
772
841
  *
773
842
  * @param username The sub-user's username.
@@ -776,7 +845,12 @@ export function deleteSubUser(username: string): Promise<{ deleted: string }>;
776
845
  */
777
846
  export function updateSubUser(
778
847
  username: string,
779
- updates: { permissions?: string[]; password?: string }
848
+ updates: {
849
+ permissions?: string[];
850
+ password?: string;
851
+ /** New allowed form ID list (replaces existing). Pass empty array to remove all restrictions. */
852
+ allowedFormIds?: string[] | null;
853
+ }
780
854
  ): Promise<{ username: string; updated: boolean }>;
781
855
 
782
856
  /**
@@ -907,6 +981,7 @@ declare const extractia: {
907
981
  updateOcrTool: typeof updateOcrTool;
908
982
  deleteOcrTool: typeof deleteOcrTool;
909
983
  runOcrTool: typeof runOcrTool;
984
+ getOcrToolExecutions: typeof getOcrToolExecutions;
910
985
  // Sub-users
911
986
  getSubUsers: typeof getSubUsers;
912
987
  createSubUser: typeof createSubUser;
package/src/index.js CHANGED
@@ -20,12 +20,7 @@ export {
20
20
  TimeoutError,
21
21
  mapAxiosError,
22
22
  } from "./errors.js";
23
- export {
24
- getToken,
25
- hasToken,
26
- clearToken,
27
- getConfig,
28
- } from "./apiClient.js";
23
+ export { getToken, hasToken, clearToken, getConfig } from "./apiClient.js";
29
24
 
30
25
  import * as auth from "./auth.js";
31
26
  import * as templates from "./templates.js";
package/src/ocrTools.js CHANGED
@@ -82,3 +82,51 @@ export async function runOcrTool(id, base64Image, options = {}) {
82
82
  const res = await api.post(`/ocr-tools/${id}/run`, body);
83
83
  return res.data;
84
84
  }
85
+
86
+ /**
87
+ * Returns a paginated list of OCR tool execution history for the authenticated user.
88
+ *
89
+ * Each record contains the original image, the answer, explanation, and metadata about
90
+ * the run. Image data (`imageBase64`) is included, so pages should be kept small when
91
+ * rendering thumbnails.
92
+ *
93
+ * @param {Object} [options] - Query options.
94
+ * @param {string} [options.toolId] - Filter by tool ID. Omit to return executions for all tools.
95
+ * @param {number} [options.page=0] - 0-based page index.
96
+ * @param {number} [options.size=20] - Page size (1–50; values above 50 are capped server-side).
97
+ * @returns {Promise<{
98
+ * content: Array<{
99
+ * id: string,
100
+ * toolId: string,
101
+ * toolName: string,
102
+ * imageBase64: string,
103
+ * mimeType: string,
104
+ * paramValues: Record<string, string> | null,
105
+ * answer: string,
106
+ * explanation: string,
107
+ * booleanAnswer: boolean | null,
108
+ * status: 'SUCCESS' | 'FAILURE',
109
+ * errorMessage?: string,
110
+ * processingTimeMs: number,
111
+ * createdAt: string
112
+ * }>,
113
+ * totalElements: number,
114
+ * totalPages: number,
115
+ * size: number,
116
+ * number: number,
117
+ * first: boolean,
118
+ * last: boolean,
119
+ * empty: boolean
120
+ * }>}
121
+ * @throws {AuthError} 401 if the user is not authenticated.
122
+ */
123
+ export async function getOcrToolExecutions({
124
+ toolId,
125
+ page = 0,
126
+ size = 20,
127
+ } = {}) {
128
+ const params = { page, size };
129
+ if (toolId) params.toolId = toolId;
130
+ const res = await api.get("/ocr-tools/executions", { params });
131
+ return res.data;
132
+ }
package/src/subusers.js CHANGED
@@ -26,7 +26,7 @@ export async function getSubUsers() {
26
26
  * Creates a new sub-user for the authenticated account.
27
27
  *
28
28
  * Available permissions: `"upload"`, `"view"`, `"template"`, `"settings"`,
29
- * `"export"`, `"ocr_tools"`, `"gallery"`, `"smart_scan"`, `"ia_agent"`.
29
+ * `"export"`, `"api"`, `"ocr_tools"`, `"gallery"`, `"smart_scan"`, `"ia_agent"`, `"assistant"`.
30
30
  *
31
31
  * @param {Object} subUser - Sub-user definition.
32
32
  * @param {string} subUser.username - Unique username (must not be an existing account email).
package/src/utils.js CHANGED
@@ -30,7 +30,11 @@ export function fileToBase64(file) {
30
30
  const reader = new FileReader();
31
31
  reader.onload = () => resolve(/** @type {string} */ (reader.result));
32
32
  reader.onerror = () =>
33
- reject(new Error(`fileToBase64: failed to read file "${file.name ?? "unknown"}".`));
33
+ reject(
34
+ new Error(
35
+ `fileToBase64: failed to read file "${file.name ?? "unknown"}".`,
36
+ ),
37
+ );
34
38
  reader.readAsDataURL(file);
35
39
  });
36
40
  }
@@ -45,9 +49,7 @@ export function fileToBase64(file) {
45
49
  */
46
50
  export function stripDataUrlPrefix(base64OrDataUrl) {
47
51
  if (typeof base64OrDataUrl !== "string") {
48
- throw new TypeError(
49
- "stripDataUrlPrefix: argument must be a string.",
50
- );
52
+ throw new TypeError("stripDataUrlPrefix: argument must be a string.");
51
53
  }
52
54
  const idx = base64OrDataUrl.indexOf(";base64,");
53
55
  return idx !== -1 ? base64OrDataUrl.slice(idx + 8) : base64OrDataUrl;
@@ -76,7 +78,9 @@ export function isBase64(str) {
76
78
  if (typeof str !== "string" || !str.trim()) return false;
77
79
  const raw = stripDataUrlPrefix(str);
78
80
  // Must be divisible by 4 and only contain valid base64 characters
79
- return raw.length > 0 && raw.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(raw);
81
+ return (
82
+ raw.length > 0 && raw.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(raw)
83
+ );
80
84
  }
81
85
 
82
86
  /**
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ /**
4
+ * Vitest config for integration tests against the real ExtractIA API.
5
+ *
6
+ * Required env vars:
7
+ * EXTRACTIA_API_TOKEN — valid bearer token (all integration tests)
8
+ * EXTRACTIA_ALLOW_WRITE — "true" to enable create/update/delete tests
9
+ * EXTRACTIA_ALLOW_CREDITS — "true" to enable tests that consume doc/AI credits
10
+ *
11
+ * Run:
12
+ * EXTRACTIA_API_TOKEN=sk_xxx EXTRACTIA_ALLOW_WRITE=true EXTRACTIA_ALLOW_CREDITS=true \
13
+ * npm run test:integration
14
+ */
15
+ export default defineConfig({
16
+ test: {
17
+ globals: true,
18
+ environment: "node",
19
+ include: ["tests/integration/**/*.integration.test.js"],
20
+ // Real HTTP calls — 30 s per test, 30 s for setup/teardown hooks
21
+ testTimeout: 30_000,
22
+ hookTimeout: 30_000,
23
+ // Sequential: avoid hammering the API and triggering rate-limits
24
+ sequence: { concurrent: false },
25
+ reporters: ["verbose"],
26
+ },
27
+ });