@frihet/mcp-server 1.17.0 → 2.0.1

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 (87) hide show
  1. package/README.md +106 -348
  2. package/dist/agent-onboarding.d.ts +1 -1
  3. package/dist/agent-onboarding.js +3 -3
  4. package/dist/canary/mcp-harness.d.ts +1 -1
  5. package/dist/canary/mcp-harness.js +4 -1
  6. package/dist/capability-truth.js +20 -12
  7. package/dist/claude-candidate-contract.d.ts +482 -0
  8. package/dist/claude-candidate-contract.js +87 -0
  9. package/dist/claude-output-privacy.d.ts +17 -0
  10. package/dist/claude-output-privacy.js +249 -0
  11. package/dist/claude-profile.d.ts +18 -0
  12. package/dist/claude-profile.js +260 -0
  13. package/dist/client-interface.d.ts +26 -20
  14. package/dist/client.d.ts +65 -29
  15. package/dist/client.js +278 -167
  16. package/dist/demo-client.d.ts +28 -21
  17. package/dist/demo-client.js +32 -14
  18. package/dist/deposit-contract.d.ts +29 -0
  19. package/dist/deposit-contract.js +76 -0
  20. package/dist/fetch-no-redirect.d.ts +3 -0
  21. package/dist/fetch-no-redirect.js +16 -0
  22. package/dist/fiscal-aliases.d.ts +2 -2
  23. package/dist/fiscal-aliases.js +2 -2
  24. package/dist/fiscal-period.d.ts +26 -0
  25. package/dist/fiscal-period.js +31 -0
  26. package/dist/google-profile.d.ts +19 -0
  27. package/dist/google-profile.js +122 -0
  28. package/dist/index.js +31 -7
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +2 -2
  31. package/dist/muse-profile.d.ts +19 -0
  32. package/dist/muse-profile.js +122 -0
  33. package/dist/observability.js +3 -2
  34. package/dist/openai-profile.d.ts +2 -2
  35. package/dist/openai-profile.js +37 -11
  36. package/dist/openai-review-contract.d.ts +1 -1
  37. package/dist/openai-review-contract.js +4 -5
  38. package/dist/openai-review-oauth.d.ts +107 -7
  39. package/dist/openai-review-oauth.js +161 -17
  40. package/dist/prompts/register-all.js +32 -78
  41. package/dist/redaction.d.ts +5 -0
  42. package/dist/redaction.js +9 -0
  43. package/dist/resources/register-all.js +150 -54
  44. package/dist/retry-after.d.ts +19 -0
  45. package/dist/retry-after.js +84 -0
  46. package/dist/server-composition.d.ts +6 -2
  47. package/dist/server-composition.js +46 -5
  48. package/dist/tool-exposure.d.ts +7 -5
  49. package/dist/tool-exposure.js +21 -13
  50. package/dist/tools/audit_gl.js +8 -8
  51. package/dist/tools/backend-availability.d.ts +8 -29
  52. package/dist/tools/backend-availability.js +31 -30
  53. package/dist/tools/bank_rules.js +66 -15
  54. package/dist/tools/banking.js +5 -13
  55. package/dist/tools/clients.js +254 -6
  56. package/dist/tools/deposits.js +107 -97
  57. package/dist/tools/einvoice.d.ts +17 -3
  58. package/dist/tools/einvoice.js +59 -12
  59. package/dist/tools/fiscal.d.ts +3 -4
  60. package/dist/tools/fiscal.js +93 -60
  61. package/dist/tools/gestoria.js +15 -7
  62. package/dist/tools/hr.js +41 -11
  63. package/dist/tools/igic.d.ts +9 -8
  64. package/dist/tools/igic.js +34 -64
  65. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  66. package/dist/tools/impuesto_sociedades.js +16 -37
  67. package/dist/tools/intelligence.js +164 -10
  68. package/dist/tools/invoices.js +109 -42
  69. package/dist/tools/kitchen.js +19 -9
  70. package/dist/tools/quotes.js +86 -25
  71. package/dist/tools/recurring.js +26 -25
  72. package/dist/tools/register-all.d.ts +2 -2
  73. package/dist/tools/register-all.js +9 -6
  74. package/dist/tools/search.d.ts +19 -0
  75. package/dist/tools/search.js +84 -0
  76. package/dist/tools/shared.d.ts +18 -38
  77. package/dist/tools/shared.js +96 -30
  78. package/dist/tools/team.js +8 -16
  79. package/dist/tools/time.js +4 -12
  80. package/dist/tools/vendors.js +242 -6
  81. package/dist/types.d.ts +3 -1
  82. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  83. package/dist/workflows/daily-operational-brief.js +881 -0
  84. package/docs/agent-onboarding.json +15 -5
  85. package/package.json +26 -12
  86. package/dist/tools/onboarding.d.ts +0 -19
  87. package/dist/tools/onboarding.js +0 -64
package/dist/client.js CHANGED
@@ -11,17 +11,20 @@
11
11
  * (`req.query.cursor`, functions/src/publicApi.ts in Frihet-ERP). `after`
12
12
  * is never sent on the wire. See src/__tests__/pagination-cursor-param.test.ts.
13
13
  */
14
- import { sanitizeServerRemediation } from "./redaction.js";
14
+ import { rejectRedirectResponse } from "./fetch-no-redirect.js";
15
+ import { safeRequestId, sanitizeServerRemediation } from "./redaction.js";
15
16
  import { logApiCall, logRetry } from "./logger.js";
17
+ import { parseRetryAfter } from "./retry-after.js";
18
+ import { fiscalModeloQuery } from "./fiscal-period.js";
19
+ import { normalizeApplyDepositBody, normalizeDepositBody } from "./deposit-contract.js";
16
20
  const BASE_URL = "https://api.frihet.io/v1";
17
- const OAUTH_CLOUD_FUNCTION_BASE_URL = "https://europe-west1-gen-lang-client-0335716041.cloudfunctions.net/publicApi/api/v1";
18
- const OAUTH_SERVICE_TRUSTED_BASE_URLS = new Set([
19
- BASE_URL,
20
- OAUTH_CLOUD_FUNCTION_BASE_URL,
21
- ]);
22
21
  const MAX_RETRIES = 3;
23
22
  const DEFAULT_RETRY_DELAY_MS = 1000;
24
23
  const REQUEST_TIMEOUT_MS = 30000;
24
+ /** Total time one logical call may spend sleeping on 429s before it is deferred to the caller. */
25
+ const RETRY_BUDGET_MS = 30000;
26
+ /** setTimeout fires immediately above this (32-bit signed); a larger wait must defer, never sleep. */
27
+ const MAX_TIMER_MS = 2 ** 31 - 1;
25
28
  /**
26
29
  * Hard size caps for document responses. Enforced TWICE: precheck on
27
30
  * `Content-Length` (so an honest server doesn't waste bandwidth) and after
@@ -39,6 +42,52 @@ const REQUEST_TIMEOUT_MS = 30000;
39
42
  export const MAX_PDF_BYTES = 25 * 1024 * 1024;
40
43
  export const MAX_XML_BYTES = 5 * 1024 * 1024;
41
44
  const MAX_ERROR_BYTES = 64 * 1024;
45
+ /** One exhaustive translation at the HTTP boundary. Never send the MCP's
46
+ * lowercase convenience values to the strict API. Factur-X Extended, Basic
47
+ * and Minimum are intentionally absent: collapsing them to EN16931 would lie
48
+ * about the generated profile. */
49
+ const E_INVOICE_EXPORT_API_FORMAT = {
50
+ facturae: "Facturae",
51
+ "xrechnung-cii": "XRechnung-CII",
52
+ "xrechnung-ubl": "XRechnung-UBL",
53
+ "facturx-en16931": "Factur-X",
54
+ fatturapa: "FatturaPA",
55
+ "peppol-bis-3": "PEPPOL-BIS-3",
56
+ "fa-2-ksef": "FA-2-KSeF",
57
+ ubl: "UBL",
58
+ cii: "CII",
59
+ };
60
+ // Only the existing guards below can attest that a request was never sent.
61
+ // Provider error codes, copied errors and public properties cannot forge this
62
+ // process-local provenance. Both runtimes use this fetch-only client.
63
+ const LOCAL_VALIDATION_ERRORS = new WeakSet();
64
+ export function isLocalInputValidationError(error) {
65
+ return typeof error === "object" && error !== null && LOCAL_VALIDATION_ERRORS.has(error);
66
+ }
67
+ export function toApiEInvoiceExportFormat(format) {
68
+ if (!Object.hasOwn(E_INVOICE_EXPORT_API_FORMAT, format)) {
69
+ const error = new FrihetApiError(400, "unsupported_einvoice_export_format", "Unsupported e-invoice export format. Factur-X export supports the EN16931 profile only.");
70
+ LOCAL_VALIDATION_ERRORS.add(error);
71
+ throw error;
72
+ }
73
+ return E_INVOICE_EXPORT_API_FORMAT[format];
74
+ }
75
+ /**
76
+ * Encode one caller-supplied path segment. `encodeURIComponent` leaves "." and
77
+ * ".." unchanged and URL parsing resolves them as dot-segments, so either value
78
+ * would change the route a request reaches; an empty value would drop the
79
+ * segment. Every id interpolated into a request path goes through here, and
80
+ * those three values are refused before any request is sent.
81
+ */
82
+ function pathSegment(value) {
83
+ const segment = String(value);
84
+ if (segment === "" || segment === "." || segment === "..") {
85
+ const error = new FrihetApiError(400, "invalid_path_segment", 'Invalid identifier: an id cannot be empty, "." or "..".');
86
+ LOCAL_VALIDATION_ERRORS.add(error);
87
+ throw error;
88
+ }
89
+ return encodeURIComponent(segment);
90
+ }
42
91
  /**
43
92
  * HTTP methods for which the backend treats the request as a mutation and
44
93
  * therefore accepts (and for some endpoints REQUIRES) an `Idempotency-Key`
@@ -172,13 +221,17 @@ export class FrihetApiError extends Error {
172
221
  statusCode;
173
222
  errorCode;
174
223
  detail;
175
- constructor(statusCode, errorCode, message, detail) {
224
+ requestId;
225
+ constructor(statusCode, errorCode, message, detail,
226
+ /** Backend correlation id (`meta.requestId` or `X-Request-Id`); carried for diagnostics, never echoed to agents. */
227
+ requestId) {
176
228
  super(statusCode === 403
177
229
  ? sanitizeServerRemediation(message, errorCode) ?? errorCode
178
230
  : message ?? errorCode);
179
231
  this.statusCode = statusCode;
180
232
  this.errorCode = errorCode;
181
233
  this.detail = detail;
234
+ this.requestId = requestId;
182
235
  this.name = "FrihetApiError";
183
236
  }
184
237
  }
@@ -222,6 +275,12 @@ function normalizeApiError(value, statusCode, statusText) {
222
275
  const record = typeof value === "object" && value !== null
223
276
  ? value
224
277
  : undefined;
278
+ // publicApi error envelopes carry `meta: { requestId }` (Frihet-ERP
279
+ // functions/src/publicApi.ts, e.g. the 401/404 branches).
280
+ const meta = typeof record?.meta === "object" && record.meta !== null
281
+ ? record.meta
282
+ : undefined;
283
+ const requestId = safeRequestId(meta?.requestId);
225
284
  const error = typeof record?.error === "string" && record.error.trim()
226
285
  ? record.error
227
286
  : fallbackError;
@@ -232,13 +291,16 @@ function normalizeApiError(value, statusCode, statusText) {
232
291
  error,
233
292
  message,
234
293
  ...(typeof record?.detail === "string" ? { detail: record.detail } : {}),
294
+ ...(requestId ? { requestId } : {}),
235
295
  };
236
296
  }
237
297
  export class FrihetClient {
238
298
  apiKey;
239
299
  baseUrl;
240
300
  timeoutMs;
241
- oauthServiceSecret;
301
+ retryBudgetMs;
302
+ sleepFn;
303
+ nowFn;
242
304
  constructor(apiKey, baseUrl, options) {
243
305
  if (!apiKey) {
244
306
  throw new Error("FRIHET_API_KEY is required. Set it as an environment variable or pass it to the constructor.");
@@ -247,24 +309,18 @@ export class FrihetClient {
247
309
  this.apiKey = apiKey;
248
310
  this.baseUrl = resolvedBaseUrl;
249
311
  this.timeoutMs = options?.timeoutMs ?? REQUEST_TIMEOUT_MS;
250
- if (options?.oauthServiceSecret !== undefined) {
251
- if (new TextEncoder().encode(options.oauthServiceSecret).byteLength < 32
252
- || /[\r\n\0]/u.test(options.oauthServiceSecret)) {
253
- throw new Error("OAuth API-key service authentication is not configured");
254
- }
255
- // A dashboard/API-key caller may still use a custom API base. The
256
- // internal OAuth second factor may not: pin its authority at the sink so
257
- // a future caller cannot exfiltrate both credentials by changing only
258
- // constructor input while leaving the approved fetch fingerprint intact.
259
- if (!OAUTH_SERVICE_TRUSTED_BASE_URLS.has(resolvedBaseUrl)) {
260
- throw new Error("OAuth API-key service authority is not trusted");
261
- }
262
- this.oauthServiceSecret = options.oauthServiceSecret;
263
- }
312
+ const budget = options?.retryBudgetMs;
313
+ // NaN/negative would make every `> budget` comparison pass or fail silently.
314
+ this.retryBudgetMs =
315
+ typeof budget === "number" && Number.isFinite(budget) && budget >= 0
316
+ ? Math.min(budget, MAX_TIMER_MS)
317
+ : RETRY_BUDGET_MS;
318
+ this.sleepFn = options?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
319
+ this.nowFn = options?.now ?? Date.now;
264
320
  }
265
321
  // ------------------------------------------------------------------ HTTP
266
322
  // ------------------------------------------------------------------
267
- async request(method, path, body, query, retryCount = 0, idempotencyKey) {
323
+ async request(method, path, body, query, retryCount = 0, idempotencyKey, waitedMs = 0) {
268
324
  const url = new URL(`${this.baseUrl}${path}`);
269
325
  // Resolved once, at the top of the call chain: a caller-supplied key wins,
270
326
  // otherwise mutations get a freshly minted one. The resolved value is then
@@ -287,9 +343,6 @@ export class FrihetClient {
287
343
  "X-Frihet-Source": SOURCE_MARKER,
288
344
  "User-Agent": SOURCE_USER_AGENT,
289
345
  };
290
- if (this.oauthServiceSecret) {
291
- headers["x-frihet-oauth-key"] = this.oauthServiceSecret;
292
- }
293
346
  if (resolvedIdempotencyKey) {
294
347
  headers["Idempotency-Key"] = resolvedIdempotencyKey;
295
348
  }
@@ -300,11 +353,11 @@ export class FrihetClient {
300
353
  try {
301
354
  response = await fetch(url.toString(), {
302
355
  method,
356
+ redirect: "manual",
303
357
  headers,
304
358
  body: body ? JSON.stringify(body) : undefined,
305
359
  signal: controller.signal,
306
- redirect: "error",
307
- });
360
+ }).then(rejectRedirectResponse);
308
361
  }
309
362
  catch (error) {
310
363
  clearTimeout(timeoutId);
@@ -324,13 +377,12 @@ export class FrihetClient {
324
377
  if (retryCount >= MAX_RETRIES) {
325
378
  throw new FrihetApiError(429, "rate_limit_exceeded", "Rate limit exceeded after multiple retries. Please try again later.");
326
379
  }
327
- const retryAfter = response.headers.get("Retry-After");
328
- const delayMs = retryAfter
329
- ? parseInt(retryAfter, 10) * 1000
330
- : DEFAULT_RETRY_DELAY_MS * Math.pow(2, retryCount);
331
- logRetry(method, path, retryCount, delayMs);
332
- await this.sleep(delayMs);
333
- return this.request(method, path, body, query, retryCount + 1, resolvedIdempotencyKey);
380
+ // Release the discarded 429 body before waiting so the socket is freed.
381
+ // (requestDocument has already drained it in readBoundedBody.)
382
+ await response.body?.cancel().catch(() => undefined);
383
+ const delayMs = this.rateLimitDelayMs(response.headers.get("Retry-After"), retryCount, waitedMs, method, path);
384
+ await this.sleepFn(delayMs);
385
+ return this.request(method, path, body, query, retryCount + 1, resolvedIdempotencyKey, waitedMs + delayMs);
334
386
  }
335
387
  // Error responses
336
388
  if (!response.ok) {
@@ -343,7 +395,7 @@ export class FrihetClient {
343
395
  errorBody = normalizeApiError(undefined, response.status, response.statusText);
344
396
  }
345
397
  clearTimeout(timeoutId);
346
- throw new FrihetApiError(response.status, errorBody.error, errorBody.message ?? errorBody.error, typeof errorBody.detail === "string" ? errorBody.detail : undefined);
398
+ throw new FrihetApiError(response.status, errorBody.error, errorBody.message ?? errorBody.error, typeof errorBody.detail === "string" ? errorBody.detail : undefined, errorBody.requestId ?? safeRequestId(response.headers.get("x-request-id")));
347
399
  }
348
400
  logApiCall(method, path, response.status, durationMs);
349
401
  // 204 No Content (e.g. DELETE)
@@ -395,19 +447,16 @@ export class FrihetClient {
395
447
  "X-Frihet-Source": SOURCE_MARKER,
396
448
  "User-Agent": SOURCE_USER_AGENT,
397
449
  };
398
- if (this.oauthServiceSecret) {
399
- headers["x-frihet-oauth-key"] = this.oauthServiceSecret;
400
- }
401
450
  if (resolvedIdempotencyKey) {
402
451
  headers["Idempotency-Key"] = resolvedIdempotencyKey;
403
452
  }
404
453
  return fetch(url.toString(), {
405
454
  method,
455
+ redirect: "manual",
406
456
  headers,
407
457
  body: body ? JSON.stringify(body) : undefined,
408
458
  signal,
409
- redirect: "error",
410
- });
459
+ }).then(rejectRedirectResponse);
411
460
  }
412
461
  /** Consume one response body without ever retaining more than `maxBytes`. */
413
462
  async readBoundedBody(response, maxBytes, controller) {
@@ -476,7 +525,7 @@ export class FrihetClient {
476
525
  * active until the body is complete, and a normal success performs one GET.
477
526
  * A 429 may retry, matching the generic JSON client path.
478
527
  */
479
- async requestDocument(method, path, body, query, idempotencyKey, maxBytesForContentType, retryCount = 0) {
528
+ async requestDocument(method, path, body, query, idempotencyKey, maxBytesForContentType, retryCount = 0, waitedMs = 0) {
480
529
  const controller = new AbortController();
481
530
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
482
531
  const startedAt = Date.now();
@@ -491,15 +540,10 @@ export class FrihetClient {
491
540
  if (retryCount >= MAX_RETRIES) {
492
541
  throw new FrihetApiError(429, "rate_limit_exceeded", "Rate limit exceeded after multiple retries. Please try again later.");
493
542
  }
494
- const retryAfter = response.headers.get("Retry-After");
495
- const parsedRetryAfter = retryAfter ? Number.parseInt(retryAfter, 10) : Number.NaN;
496
- const delayMs = Number.isFinite(parsedRetryAfter)
497
- ? parsedRetryAfter * 1000
498
- : DEFAULT_RETRY_DELAY_MS * Math.pow(2, retryCount);
499
- logRetry(method, path, retryCount, delayMs);
500
543
  clearTimeout(timeoutId);
501
- await this.sleep(delayMs);
502
- return this.requestDocument(method, path, body, query, idempotencyKey, maxBytesForContentType, retryCount + 1);
544
+ const delayMs = this.rateLimitDelayMs(response.headers.get("Retry-After"), retryCount, waitedMs, method, path);
545
+ await this.sleepFn(delayMs);
546
+ return this.requestDocument(method, path, body, query, idempotencyKey, maxBytesForContentType, retryCount + 1, waitedMs + delayMs);
503
547
  }
504
548
  if (!response.ok) {
505
549
  logApiCall(method, path, response.status, durationMs);
@@ -511,7 +555,7 @@ export class FrihetClient {
511
555
  if (error instanceof FrihetApiError && error.errorCode !== "invalid_response")
512
556
  throw error;
513
557
  }
514
- throw new FrihetApiError(response.status, errorBody.error, errorBody.message ?? errorBody.error, typeof errorBody.detail === "string" ? errorBody.detail : undefined);
558
+ throw new FrihetApiError(response.status, errorBody.error, errorBody.message ?? errorBody.error, typeof errorBody.detail === "string" ? errorBody.detail : undefined, errorBody.requestId ?? safeRequestId(response.headers.get("x-request-id")));
515
559
  }
516
560
  logApiCall(method, path, response.status, durationMs);
517
561
  const filename = attachmentFilename(response.headers.get("content-disposition"));
@@ -538,8 +582,22 @@ export class FrihetClient {
538
582
  clearTimeout(timeoutId);
539
583
  }
540
584
  }
541
- sleep(ms) {
542
- return new Promise((resolve) => setTimeout(resolve, ms));
585
+ /**
586
+ * Wait before the next 429 retry: the server's `Retry-After` when it parses
587
+ * (RFC 9110 §10.2.3), otherwise exponential backoff. Either wait that does
588
+ * not fit the remaining cumulative budget is deferred to the caller; a server
589
+ * wait is never shortened into an early retry.
590
+ */
591
+ rateLimitDelayMs(retryAfter, retryCount, waitedMs, method, path) {
592
+ const serverMs = parseRetryAfter(retryAfter, this.nowFn());
593
+ const delayMs = serverMs ?? DEFAULT_RETRY_DELAY_MS * Math.pow(2, retryCount);
594
+ if (waitedMs + delayMs > this.retryBudgetMs) {
595
+ throw new FrihetApiError(429, "rate_limit_deferred", serverMs !== null
596
+ ? `Rate limited: the server asks to wait ${Math.ceil(serverMs / 1000)} seconds, which exceeds this operation's retry budget. Retry after that time; nothing was retried early.`
597
+ : "Rate limited: the next backoff wait exceeds this operation's retry budget. Retry later.");
598
+ }
599
+ logRetry(method, path, retryCount, delayMs, serverMs === null ? "backoff" : "retry_after");
600
+ return delayMs;
543
601
  }
544
602
  /**
545
603
  * Wrapper for endpoints whose backend wraps the payload in a `{ data, meta }`
@@ -569,7 +627,7 @@ export class FrihetClient {
569
627
  * Only unwraps when the body is an object carrying a `data` property that is
570
628
  * itself a (non-array) object — i.e. a genuine single-object envelope. It is
571
629
  * deliberately distinct from {@link requestPaginated}, which keeps the
572
- * `{ data: [...] , meta }` shape intact for list endpoints. If the body is
630
+ * list rows and pagination fields while omitting transport metadata. If the body is
573
631
  * not an envelope (legacy endpoints that return the item directly), it is
574
632
  * returned unchanged so existing callers keep working.
575
633
  */
@@ -589,13 +647,21 @@ export class FrihetClient {
589
647
  }
590
648
  return raw;
591
649
  }
592
- /** Wrapper for paginated endpoints — validates response shape has `data` array. */
650
+ /** Validate list rows and emit only the declared MCP pagination envelope. */
593
651
  async requestPaginated(method, path, body, query) {
594
652
  const result = await this.request(method, path, body, query);
595
653
  if (!result || !Array.isArray(result.data)) {
596
654
  throw new FrihetApiError(200, 'invalid_response', 'API returned invalid paginated response');
597
655
  }
598
- return result;
656
+ // API transport metadata is not part of the closed MCP page contract.
657
+ // Keep rows untouched and preserve every declared pagination field.
658
+ return {
659
+ data: result.data,
660
+ total: result.total,
661
+ limit: result.limit,
662
+ offset: result.offset,
663
+ ...(result.nextCursor !== undefined ? { nextCursor: result.nextCursor } : {}),
664
+ };
599
665
  }
600
666
  // ---------------------------------------------------------------- Public
601
667
  // ----------------------------------------------------------------
@@ -615,13 +681,13 @@ export class FrihetClient {
615
681
  });
616
682
  }
617
683
  async getInvoice(id) {
618
- return this.requestUnwrapped("GET", `/invoices/${encodeURIComponent(id)}`);
684
+ return this.requestUnwrapped("GET", `/invoices/${pathSegment(id)}`);
619
685
  }
620
686
  async createInvoice(data) {
621
687
  return this.requestUnwrapped("POST", "/invoices", data);
622
688
  }
623
689
  async updateInvoice(id, data) {
624
- return this.requestUnwrapped("PATCH", `/invoices/${encodeURIComponent(id)}`, data);
690
+ return this.requestUnwrapped("PATCH", `/invoices/${pathSegment(id)}`, data);
625
691
  }
626
692
  /**
627
693
  * DELETE /invoices/{id} — NOT an unconditional destroy.
@@ -638,7 +704,7 @@ export class FrihetClient {
638
704
  * Resolves to the unwrapped soft-cancel payload on 200, `undefined` on 204.
639
705
  */
640
706
  async deleteInvoice(id) {
641
- return this.requestUnwrapped("DELETE", `/invoices/${encodeURIComponent(id)}`);
707
+ return this.requestUnwrapped("DELETE", `/invoices/${pathSegment(id)}`);
642
708
  }
643
709
  async searchInvoices(query, params) {
644
710
  return this.requestPaginated("GET", "/invoices", undefined, {
@@ -667,16 +733,16 @@ export class FrihetClient {
667
733
  });
668
734
  }
669
735
  async getExpense(id) {
670
- return this.requestUnwrapped("GET", `/expenses/${encodeURIComponent(id)}`);
736
+ return this.requestUnwrapped("GET", `/expenses/${pathSegment(id)}`);
671
737
  }
672
738
  async createExpense(data) {
673
739
  return this.requestUnwrapped("POST", "/expenses", data);
674
740
  }
675
741
  async updateExpense(id, data) {
676
- return this.requestUnwrapped("PATCH", `/expenses/${encodeURIComponent(id)}`, data);
742
+ return this.requestUnwrapped("PATCH", `/expenses/${pathSegment(id)}`, data);
677
743
  }
678
744
  async deleteExpense(id) {
679
- return this.request("DELETE", `/expenses/${encodeURIComponent(id)}`);
745
+ return this.request("DELETE", `/expenses/${pathSegment(id)}`);
680
746
  }
681
747
  // ---------------------------------------------------------------- Clients
682
748
  // ----------------------------------------------------------------
@@ -691,16 +757,16 @@ export class FrihetClient {
691
757
  });
692
758
  }
693
759
  async getClient(id) {
694
- return this.requestUnwrapped("GET", `/clients/${encodeURIComponent(id)}`);
760
+ return this.requestUnwrapped("GET", `/clients/${pathSegment(id)}`);
695
761
  }
696
762
  async createClient(data) {
697
763
  return this.requestUnwrapped("POST", "/clients", data);
698
764
  }
699
765
  async updateClient(id, data) {
700
- return this.requestUnwrapped("PATCH", `/clients/${encodeURIComponent(id)}`, data);
766
+ return this.requestUnwrapped("PATCH", `/clients/${pathSegment(id)}`, data);
701
767
  }
702
768
  async deleteClient(id) {
703
- return this.request("DELETE", `/clients/${encodeURIComponent(id)}`);
769
+ return this.request("DELETE", `/clients/${pathSegment(id)}`);
704
770
  }
705
771
  // ---------------------------------------------------------------- Products
706
772
  // ----------------------------------------------------------------
@@ -717,16 +783,16 @@ export class FrihetClient {
717
783
  });
718
784
  }
719
785
  async getProduct(id) {
720
- return this.requestUnwrapped("GET", `/products/${encodeURIComponent(id)}`);
786
+ return this.requestUnwrapped("GET", `/products/${pathSegment(id)}`);
721
787
  }
722
788
  async createProduct(data) {
723
789
  return this.requestUnwrapped("POST", "/products", data);
724
790
  }
725
791
  async updateProduct(id, data) {
726
- return this.requestUnwrapped("PATCH", `/products/${encodeURIComponent(id)}`, data);
792
+ return this.requestUnwrapped("PATCH", `/products/${pathSegment(id)}`, data);
727
793
  }
728
794
  async deleteProduct(id) {
729
- return this.request("DELETE", `/products/${encodeURIComponent(id)}`);
795
+ return this.request("DELETE", `/products/${pathSegment(id)}`);
730
796
  }
731
797
  // ---------------------------------------------------------------- Quotes
732
798
  // ----------------------------------------------------------------
@@ -744,13 +810,13 @@ export class FrihetClient {
744
810
  });
745
811
  }
746
812
  async getQuote(id) {
747
- return this.requestUnwrapped("GET", `/quotes/${encodeURIComponent(id)}`);
813
+ return this.requestUnwrapped("GET", `/quotes/${pathSegment(id)}`);
748
814
  }
749
815
  async createQuote(data) {
750
816
  return this.requestUnwrapped("POST", "/quotes", data);
751
817
  }
752
818
  async updateQuote(id, data) {
753
- return this.requestUnwrapped("PATCH", `/quotes/${encodeURIComponent(id)}`, data);
819
+ return this.requestUnwrapped("PATCH", `/quotes/${pathSegment(id)}`, data);
754
820
  }
755
821
  /**
756
822
  * DELETE /quotes/{id} — same two-outcome contract as {@link deleteInvoice}:
@@ -759,7 +825,7 @@ export class FrihetClient {
759
825
  * and refuses a protected draft with 409.
760
826
  */
761
827
  async deleteQuote(id) {
762
- return this.requestUnwrapped("DELETE", `/quotes/${encodeURIComponent(id)}`);
828
+ return this.requestUnwrapped("DELETE", `/quotes/${pathSegment(id)}`);
763
829
  }
764
830
  // ---------------------------------------------------------------- Vendors
765
831
  // ----------------------------------------------------------------
@@ -773,42 +839,52 @@ export class FrihetClient {
773
839
  });
774
840
  }
775
841
  async getVendor(id) {
776
- return this.requestUnwrapped("GET", `/vendors/${encodeURIComponent(id)}`);
842
+ return this.requestUnwrapped("GET", `/vendors/${pathSegment(id)}`);
777
843
  }
778
844
  async createVendor(data) {
779
845
  return this.requestUnwrapped("POST", "/vendors", data);
780
846
  }
781
847
  async updateVendor(id, data) {
782
- return this.requestUnwrapped("PATCH", `/vendors/${encodeURIComponent(id)}`, data);
848
+ return this.requestUnwrapped("PATCH", `/vendors/${pathSegment(id)}`, data);
783
849
  }
784
850
  async deleteVendor(id) {
785
- return this.request("DELETE", `/vendors/${encodeURIComponent(id)}`);
851
+ return this.request("DELETE", `/vendors/${pathSegment(id)}`);
786
852
  }
787
853
  // ---------------------------------------------------------------- Invoice Actions
788
854
  // ----------------------------------------------------------------
789
855
  async sendInvoice(id, to) {
790
- return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(id)}/send`, to ? { recipientEmail: to } : undefined);
856
+ return this.requestUnwrapped("POST", `/invoices/${pathSegment(id)}/send`, to ? { recipientEmail: to } : undefined);
791
857
  }
792
858
  async markInvoicePaid(id, paidDate) {
793
- return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(id)}/paid`, paidDate ? { paidDate } : undefined);
859
+ return this.requestUnwrapped("POST", `/invoices/${pathSegment(id)}/paid`, paidDate ? { paidDate } : undefined);
794
860
  }
795
861
  async getInvoicePdf(id) {
796
862
  // One fetch, one Response: validate MIME and signature only after the
797
- // bounded stream completes. The ERP endpoint has always returned bytes;
798
- // there is no pre-signed-URL JSON success contract to emulate.
799
- const url = `/invoices/${encodeURIComponent(id)}/pdf`;
800
- const doc = await this.requestDocument("GET", url, undefined, undefined, undefined, (contentType) => normalizedContentType(contentType) === "application/pdf"
801
- ? MAX_PDF_BYTES
802
- : MAX_ERROR_BYTES);
803
- if (normalizedContentType(doc.contentType) !== "application/pdf") {
863
+ // bounded stream completes. The ERP endpoint has always returned raw bytes
864
+ // (functions/src/publicApi.ts:6149-6167); there is no pre-signed-URL or JSON
865
+ // success contract to emulate.
866
+ const url = `/invoices/${pathSegment(id)}/pdf`;
867
+ const doc = await this.requestDocument("GET", url, undefined, undefined, undefined, (contentType) => {
868
+ const norm = normalizedContentType(contentType);
869
+ return norm === "application/pdf"
870
+ || norm === "application/octet-stream"
871
+ || norm === "binary/octet-stream"
872
+ ? MAX_PDF_BYTES
873
+ : MAX_ERROR_BYTES;
874
+ });
875
+ const normType = normalizedContentType(doc.contentType);
876
+ if (normType !== "application/pdf" && normType !== "application/octet-stream" && normType !== "binary/octet-stream") {
804
877
  throw new FrihetApiError(200, "invalid_response", `Invoice PDF endpoint returned unexpected Content-Type: ${doc.contentType}`);
805
878
  }
806
879
  if (!hasPdfSignature(doc.bytes)) {
807
880
  throw new FrihetApiError(200, "invalid_response", "Invoice PDF response is malformed");
808
881
  }
882
+ if (doc.sizeBytes > MAX_PDF_BYTES) {
883
+ throw new FrihetApiError(413, "payload_too_large", `Invoice PDF exceeds maximum allowed size of ${MAX_PDF_BYTES} bytes`);
884
+ }
809
885
  return {
810
886
  id,
811
- contentType: doc.contentType,
887
+ contentType: "application/pdf",
812
888
  sizeBytes: doc.sizeBytes,
813
889
  base64: encodeBase64(doc.bytes),
814
890
  ...(doc.filename ? { filename: doc.filename } : {}),
@@ -817,7 +893,7 @@ export class FrihetClient {
817
893
  async getInvoiceEInvoice(invoiceId) {
818
894
  // Stored e-invoices are either XML or Factur-X PDF. Select the size cap
819
895
  // from the actual MIME before reading and preserve the request identity.
820
- const url = `/invoices/${encodeURIComponent(invoiceId)}/xml`;
896
+ const url = `/invoices/${pathSegment(invoiceId)}/xml`;
821
897
  const doc = await this.requestDocument("GET", url, undefined, undefined, undefined, (contentType) => {
822
898
  const normalized = normalizedContentType(contentType);
823
899
  if (normalized === "application/pdf")
@@ -867,15 +943,15 @@ export class FrihetClient {
867
943
  * which is the property that matters here.
868
944
  */
869
945
  async createCreditNote(invoiceId, data, idempotencyKey) {
870
- return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(invoiceId)}/credit-note`, data, undefined, idempotencyKey);
946
+ return this.requestUnwrapped("POST", `/invoices/${pathSegment(invoiceId)}/credit-note`, data, undefined, idempotencyKey);
871
947
  }
872
948
  async applyLateFee(invoiceId, data) {
873
- return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(invoiceId)}/late-fee`, data ?? {});
949
+ return this.requestUnwrapped("POST", `/invoices/${pathSegment(invoiceId)}/late-fee`, data ?? {});
874
950
  }
875
951
  // ---------------------------------------------------------------- Quote Actions
876
952
  // ----------------------------------------------------------------
877
953
  async sendQuote(id, to) {
878
- return this.requestUnwrapped("POST", `/quotes/${encodeURIComponent(id)}/send`, to ? { recipientEmail: to } : undefined);
954
+ return this.requestUnwrapped("POST", `/quotes/${pathSegment(id)}/send`, to ? { recipientEmail: to } : undefined);
879
955
  }
880
956
  // ---------------------------------------------------------------- Webhooks
881
957
  // ----------------------------------------------------------------
@@ -897,7 +973,7 @@ export class FrihetClient {
897
973
  };
898
974
  }
899
975
  async getWebhook(id) {
900
- const result = await this.requestUnwrapped("GET", `/webhooks/${encodeURIComponent(id)}`);
976
+ const result = await this.requestUnwrapped("GET", `/webhooks/${pathSegment(id)}`);
901
977
  return normalizeWebhookRecord(result);
902
978
  }
903
979
  async createWebhook(data) {
@@ -905,50 +981,79 @@ export class FrihetClient {
905
981
  return normalizeWebhookRecord(result, typeof data.secret === "string" ? data.secret : undefined);
906
982
  }
907
983
  async updateWebhook(id, data) {
908
- const result = await this.requestUnwrapped("PATCH", `/webhooks/${encodeURIComponent(id)}`, data);
984
+ const result = await this.requestUnwrapped("PATCH", `/webhooks/${pathSegment(id)}`, data);
909
985
  return normalizeWebhookRecord(result);
910
986
  }
911
987
  async deleteWebhook(id) {
912
- return this.request("DELETE", `/webhooks/${encodeURIComponent(id)}`);
988
+ return this.request("DELETE", `/webhooks/${pathSegment(id)}`);
989
+ }
990
+ // ---------------------------------------------------------------- Cross-resource search
991
+ // ----------------------------------------------------------------
992
+ //
993
+ // GET /v1/search/global?q=<term>&types=a,b,c&limit=&offset= (publicApi.ts:4476)
994
+ //
995
+ // Read-only fan-out across invoices / expenses / vendors / clients / products.
996
+ // All five collections are workspace-scoped via users/{userId}/..., so the
997
+ // tenant boundary is preserved by Firestore subcollection paths — same as
998
+ // every other list/search endpoint on the server. No mutation counterpart;
999
+ // no auth boundary different from any other read tool.
1000
+ async globalSearch(params) {
1001
+ const typesCsv = params.types && params.types.length > 0 ? params.types.join(",") : undefined;
1002
+ const result = await this.request("GET", "/search/global", undefined, {
1003
+ q: params.q,
1004
+ types: typesCsv,
1005
+ limit: params.limit,
1006
+ offset: params.offset,
1007
+ });
1008
+ return {
1009
+ data: result.data,
1010
+ total: result.total,
1011
+ limit: result.limit,
1012
+ offset: result.offset,
1013
+ hasMore: result.hasMore,
1014
+ query: result.query,
1015
+ types: result.types,
1016
+ ...(result.truncated !== undefined ? { truncated: result.truncated } : {}),
1017
+ };
913
1018
  }
914
1019
  // ---------------------------------------------------------------- CRM: Contacts
915
1020
  // ----------------------------------------------------------------
916
1021
  async listClientContacts(clientId, params) {
917
- return this.requestPaginated("GET", `/clients/${encodeURIComponent(clientId)}/contacts`, undefined, {
1022
+ return this.requestPaginated("GET", `/clients/${pathSegment(clientId)}/contacts`, undefined, {
918
1023
  limit: params?.limit,
919
1024
  offset: params?.offset,
920
1025
  });
921
1026
  }
922
1027
  async createClientContact(clientId, data) {
923
- return this.requestUnwrapped("POST", `/clients/${encodeURIComponent(clientId)}/contacts`, data);
1028
+ return this.requestUnwrapped("POST", `/clients/${pathSegment(clientId)}/contacts`, data);
924
1029
  }
925
1030
  async deleteClientContact(clientId, contactId) {
926
- return this.request("DELETE", `/clients/${encodeURIComponent(clientId)}/contacts/${encodeURIComponent(contactId)}`);
1031
+ return this.request("DELETE", `/clients/${pathSegment(clientId)}/contacts/${pathSegment(contactId)}`);
927
1032
  }
928
1033
  // ---------------------------------------------------------------- CRM: Activities
929
1034
  // ----------------------------------------------------------------
930
1035
  async listClientActivities(clientId, params) {
931
- return this.requestPaginated("GET", `/clients/${encodeURIComponent(clientId)}/activities`, undefined, {
1036
+ return this.requestPaginated("GET", `/clients/${pathSegment(clientId)}/activities`, undefined, {
932
1037
  limit: params?.limit,
933
1038
  offset: params?.offset,
934
1039
  });
935
1040
  }
936
1041
  async logClientActivity(clientId, data) {
937
- return this.requestUnwrapped("POST", `/clients/${encodeURIComponent(clientId)}/activities`, data);
1042
+ return this.requestUnwrapped("POST", `/clients/${pathSegment(clientId)}/activities`, data);
938
1043
  }
939
1044
  // ---------------------------------------------------------------- CRM: Notes
940
1045
  // ----------------------------------------------------------------
941
1046
  async listClientNotes(clientId, params) {
942
- return this.requestPaginated("GET", `/clients/${encodeURIComponent(clientId)}/notes`, undefined, {
1047
+ return this.requestPaginated("GET", `/clients/${pathSegment(clientId)}/notes`, undefined, {
943
1048
  limit: params?.limit,
944
1049
  offset: params?.offset,
945
1050
  });
946
1051
  }
947
1052
  async createClientNote(clientId, data) {
948
- return this.requestUnwrapped("POST", `/clients/${encodeURIComponent(clientId)}/notes`, data);
1053
+ return this.requestUnwrapped("POST", `/clients/${pathSegment(clientId)}/notes`, data);
949
1054
  }
950
1055
  async deleteClientNote(clientId, noteId) {
951
- return this.request("DELETE", `/clients/${encodeURIComponent(clientId)}/notes/${encodeURIComponent(noteId)}`);
1056
+ return this.request("DELETE", `/clients/${pathSegment(clientId)}/notes/${pathSegment(noteId)}`);
952
1057
  }
953
1058
  // ---------------------------------------------------------------- Deposits
954
1059
  // ----------------------------------------------------------------
@@ -965,22 +1070,22 @@ export class FrihetClient {
965
1070
  });
966
1071
  }
967
1072
  async getDeposit(id) {
968
- return this.requestUnwrapped("GET", `/deposits/${encodeURIComponent(id)}`);
1073
+ return this.requestUnwrapped("GET", `/deposits/${pathSegment(id)}`);
969
1074
  }
970
1075
  async createDeposit(data) {
971
- return this.requestUnwrapped("POST", "/deposits", data);
1076
+ return this.requestUnwrapped("POST", "/deposits", normalizeDepositBody(data, false));
972
1077
  }
973
1078
  async updateDeposit(id, data) {
974
- return this.requestUnwrapped("PATCH", `/deposits/${encodeURIComponent(id)}`, data);
1079
+ return this.requestUnwrapped("PATCH", `/deposits/${pathSegment(id)}`, normalizeDepositBody(data, true));
975
1080
  }
976
1081
  async deleteDeposit(id) {
977
- return this.request("DELETE", `/deposits/${encodeURIComponent(id)}`);
1082
+ return this.request("DELETE", `/deposits/${pathSegment(id)}`);
978
1083
  }
979
1084
  async applyDeposit(id, data) {
980
- return this.requestUnwrapped("POST", `/deposits/${encodeURIComponent(id)}/apply`, data ?? {});
1085
+ return this.requestUnwrapped("POST", `/deposits/${pathSegment(id)}/apply`, normalizeApplyDepositBody(data));
981
1086
  }
982
1087
  async refundDeposit(id, data) {
983
- return this.requestUnwrapped("POST", `/deposits/${encodeURIComponent(id)}/refund`, data ?? {});
1088
+ return this.requestUnwrapped("POST", `/deposits/${pathSegment(id)}/refund`, data ?? {});
984
1089
  }
985
1090
  // ---------------------------------------------------------------- E-Invoicing
986
1091
  // ----------------------------------------------------------------
@@ -991,7 +1096,7 @@ export class FrihetClient {
991
1096
  // get_einvoice_status's outputSchema (eInvoiceStatusOutput) expects the
992
1097
  // flat status object, same single-object envelope convention as the rest
993
1098
  // of the /v1 REST surface.
994
- return this.requestUnwrapped("GET", `/einvoice/status/${encodeURIComponent(workflowRunId)}`);
1099
+ return this.requestUnwrapped("GET", `/einvoice/status/${pathSegment(workflowRunId)}`);
995
1100
  }
996
1101
  async validateEInvoiceXml(params) {
997
1102
  return this.requestUnwrapped("POST", "/einvoice/validate", params);
@@ -1004,22 +1109,26 @@ export class FrihetClient {
1004
1109
  // New per-invoice endpoints: /v1/invoices/:id/einvoice/export, /face/*, /ticketbai/*
1005
1110
  // 404 responses propagate to tool handlers which fall back to stub responses.
1006
1111
  async exportEInvoice(params) {
1007
- const { invoiceId, ...body } = params;
1008
- return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(invoiceId)}/einvoice/export`, body);
1112
+ const { invoiceId, format, signed } = params;
1113
+ const body = {
1114
+ format: toApiEInvoiceExportFormat(format),
1115
+ ...(signed === undefined ? {} : { signed }),
1116
+ };
1117
+ return this.requestUnwrapped("POST", `/invoices/${pathSegment(invoiceId)}/einvoice/export`, body);
1009
1118
  }
1010
1119
  async faceSubmit(params) {
1011
1120
  const { invoiceId, mode } = params;
1012
- return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(invoiceId)}/face/submit`, { mode });
1121
+ return this.requestUnwrapped("POST", `/invoices/${pathSegment(invoiceId)}/face/submit`, { mode });
1013
1122
  }
1014
1123
  async faceStatus(params) {
1015
- return this.request("GET", `/invoices/${encodeURIComponent(params.invoiceId)}/face/status`);
1124
+ return this.request("GET", `/invoices/${pathSegment(params.invoiceId)}/face/status`);
1016
1125
  }
1017
1126
  async ticketbaiSubmit(params) {
1018
1127
  const { invoiceId, sandbox } = params;
1019
- return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(invoiceId)}/ticketbai/submit`, { sandbox });
1128
+ return this.requestUnwrapped("POST", `/invoices/${pathSegment(invoiceId)}/ticketbai/submit`, { sandbox });
1020
1129
  }
1021
1130
  async ticketbaiStatus(params) {
1022
- return this.request("GET", `/invoices/${encodeURIComponent(params.invoiceId)}/ticketbai/status`);
1131
+ return this.request("GET", `/invoices/${pathSegment(params.invoiceId)}/ticketbai/status`);
1023
1132
  }
1024
1133
  // kSeFSubmit intentionally omitted — ksef_submit tool is always-stub (public KSeF endpoint not yet exposed; transport infra-ready in Frihet-ERP).
1025
1134
  // ---------------------------------------------------------------- Stay (Vacation Rental)
@@ -1040,7 +1149,7 @@ export class FrihetClient {
1040
1149
  });
1041
1150
  }
1042
1151
  async getReservation(id) {
1043
- return this.requestUnwrapped("GET", `/stay/reservations/${encodeURIComponent(id)}`);
1152
+ return this.requestUnwrapped("GET", `/stay/reservations/${pathSegment(id)}`);
1044
1153
  }
1045
1154
  async createReservation(data) {
1046
1155
  return this.requestUnwrapped("POST", "/stay/reservations", data);
@@ -1056,7 +1165,7 @@ export class FrihetClient {
1056
1165
  });
1057
1166
  }
1058
1167
  async syncChannel(channelId, direction) {
1059
- return this.requestUnwrapped("POST", `/stay/channels/${encodeURIComponent(channelId)}/sync`, { direction });
1168
+ return this.requestUnwrapped("POST", `/stay/channels/${pathSegment(channelId)}/sync`, { direction });
1060
1169
  }
1061
1170
  // ---------------------------------------------------------------- POS (Point of Sale)
1062
1171
  // ----------------------------------------------------------------
@@ -1069,7 +1178,7 @@ export class FrihetClient {
1069
1178
  });
1070
1179
  }
1071
1180
  async getSale(id) {
1072
- return this.requestUnwrapped("GET", `/pos/sales/${encodeURIComponent(id)}`);
1181
+ return this.requestUnwrapped("GET", `/pos/sales/${pathSegment(id)}`);
1073
1182
  }
1074
1183
  async listSales(params) {
1075
1184
  return this.requestPaginated("GET", "/pos/sales", undefined, {
@@ -1083,7 +1192,7 @@ export class FrihetClient {
1083
1192
  });
1084
1193
  }
1085
1194
  async refundSale(id, data) {
1086
- return this.requestUnwrapped("POST", `/pos/sales/${encodeURIComponent(id)}/refund`, data ?? {});
1195
+ return this.requestUnwrapped("POST", `/pos/sales/${pathSegment(id)}/refund`, data ?? {});
1087
1196
  }
1088
1197
  // ---------------------------------------------------------------- Kitchen (KDS)
1089
1198
  // ----------------------------------------------------------------
@@ -1098,10 +1207,13 @@ export class FrihetClient {
1098
1207
  });
1099
1208
  }
1100
1209
  async getKitchenTicket(id) {
1101
- return this.requestUnwrapped("GET", `/kitchen/tickets/${encodeURIComponent(id)}`);
1210
+ return this.requestUnwrapped("GET", `/kitchen/tickets/${pathSegment(id)}`);
1102
1211
  }
1103
1212
  async updateKitchenTicket(id, data) {
1104
- return this.requestUnwrapped("PATCH", `/kitchen/tickets/${encodeURIComponent(id)}`, data);
1213
+ // The public tool accepts one station; the strict REST contract stores an array.
1214
+ const { stationId, ...patch } = data;
1215
+ const body = stationId === undefined ? patch : { ...patch, stationIds: [stationId] };
1216
+ return this.requestUnwrapped("PATCH", `/kitchen/tickets/${pathSegment(id)}`, body);
1105
1217
  }
1106
1218
  async listKitchenStations(params) {
1107
1219
  return this.requestPaginated("GET", "/kitchen/stations", undefined, {
@@ -1144,7 +1256,7 @@ export class FrihetClient {
1144
1256
  });
1145
1257
  }
1146
1258
  async getBankAccount(id) {
1147
- return this.requestUnwrapped("GET", `/banking/accounts/${encodeURIComponent(id)}`);
1259
+ return this.requestUnwrapped("GET", `/banking/accounts/${pathSegment(id)}`);
1148
1260
  }
1149
1261
  async listTransactions(params) {
1150
1262
  return this.requestPaginated("GET", "/banking/transactions", undefined, {
@@ -1159,10 +1271,10 @@ export class FrihetClient {
1159
1271
  });
1160
1272
  }
1161
1273
  async categorizeTransaction(id, data) {
1162
- return this.requestUnwrapped("PATCH", `/banking/transactions/${encodeURIComponent(id)}/categorize`, data);
1274
+ return this.requestUnwrapped("PATCH", `/banking/transactions/${pathSegment(id)}/categorize`, data);
1163
1275
  }
1164
1276
  async matchTransactionToDocument(transactionId, data) {
1165
- return this.requestUnwrapped("POST", `/banking/transactions/${encodeURIComponent(transactionId)}/match`, data);
1277
+ return this.requestUnwrapped("POST", `/banking/transactions/${pathSegment(transactionId)}/match`, data);
1166
1278
  }
1167
1279
  // ---------------------------------------------------------------- Fiscal
1168
1280
  // RESOLVED (was: "planned — 404 propagates until backend ships"). The
@@ -1174,18 +1286,19 @@ export class FrihetClient {
1174
1286
  // NOTE: /v1/fiscal/verifactu/* + /ticketbai/* below remain pending and 404
1175
1287
  // until those backends ship (they keep the raw `request` path for now).
1176
1288
  async getFiscalModeloSummary(modeloCode, period) {
1177
- return this.requestUnwrapped("GET", `/fiscal/modelo/${encodeURIComponent(modeloCode)}`, undefined, {
1178
- period,
1179
- });
1289
+ // The backend reads `?quarter=` (303/130) or `?year=` (390/347) and
1290
+ // silently answers the CURRENT period for any other param, so the param
1291
+ // name comes from the shared rule table, never a generic `?period=`.
1292
+ return this.requestUnwrapped("GET", `/fiscal/modelo/${pathSegment(modeloCode)}`, undefined, fiscalModeloQuery(modeloCode, period));
1180
1293
  }
1181
1294
  async getVerifactuStatus(invoiceId) {
1182
- return this.requestUnwrapped("GET", `/fiscal/verifactu/${encodeURIComponent(invoiceId)}/status`);
1295
+ return this.requestUnwrapped("GET", `/fiscal/verifactu/${pathSegment(invoiceId)}/status`);
1183
1296
  }
1184
1297
  async resubmitVerifactu(invoiceId) {
1185
- return this.requestUnwrapped("POST", `/fiscal/verifactu/${encodeURIComponent(invoiceId)}/resubmit`, {});
1298
+ return this.requestUnwrapped("POST", `/fiscal/verifactu/${pathSegment(invoiceId)}/resubmit`, {});
1186
1299
  }
1187
1300
  async getTicketbaiStatus(invoiceId) {
1188
- return this.request("GET", `/fiscal/ticketbai/${encodeURIComponent(invoiceId)}/status`);
1301
+ return this.request("GET", `/fiscal/ticketbai/${pathSegment(invoiceId)}/status`);
1189
1302
  }
1190
1303
  // ---------------------------------------------------------------- Time Tracking
1191
1304
  // Backend: /v1/time/* endpoints live as of Wave 4-A (Frihet-ERP functions/src/publicApi.ts).
@@ -1202,16 +1315,16 @@ export class FrihetClient {
1202
1315
  });
1203
1316
  }
1204
1317
  async getTimeEntry(id) {
1205
- return this.requestUnwrapped("GET", `/time/entries/${encodeURIComponent(id)}`);
1318
+ return this.requestUnwrapped("GET", `/time/entries/${pathSegment(id)}`);
1206
1319
  }
1207
1320
  async createTimeEntry(data) {
1208
1321
  return this.requestUnwrapped("POST", "/time/entries", data);
1209
1322
  }
1210
1323
  async updateTimeEntry(id, data) {
1211
- return this.requestUnwrapped("PATCH", `/time/entries/${encodeURIComponent(id)}`, data);
1324
+ return this.requestUnwrapped("PATCH", `/time/entries/${pathSegment(id)}`, data);
1212
1325
  }
1213
1326
  async deleteTimeEntry(id) {
1214
- return this.request("DELETE", `/time/entries/${encodeURIComponent(id)}`);
1327
+ return this.request("DELETE", `/time/entries/${pathSegment(id)}`);
1215
1328
  }
1216
1329
  async getTimeSummary(params) {
1217
1330
  return this.requestUnwrapped("GET", "/time/summary", undefined, {
@@ -1232,25 +1345,25 @@ export class FrihetClient {
1232
1345
  });
1233
1346
  }
1234
1347
  async getRecurringInvoice(id) {
1235
- return this.requestUnwrapped("GET", `/recurring/invoices/${encodeURIComponent(id)}`);
1348
+ return this.requestUnwrapped("GET", `/recurring/invoices/${pathSegment(id)}`);
1236
1349
  }
1237
1350
  async createRecurringInvoice(data) {
1238
1351
  return this.requestUnwrapped("POST", "/recurring/invoices", data);
1239
1352
  }
1240
1353
  async updateRecurringInvoice(id, data) {
1241
- return this.requestUnwrapped("PATCH", `/recurring/invoices/${encodeURIComponent(id)}`, data);
1354
+ return this.requestUnwrapped("PATCH", `/recurring/invoices/${pathSegment(id)}`, data);
1242
1355
  }
1243
1356
  async pauseRecurringInvoice(id) {
1244
- return this.requestUnwrapped("POST", `/recurring/invoices/${encodeURIComponent(id)}/pause`, {});
1357
+ return this.requestUnwrapped("POST", `/recurring/invoices/${pathSegment(id)}/pause`, {});
1245
1358
  }
1246
1359
  async resumeRecurringInvoice(id) {
1247
- return this.requestUnwrapped("POST", `/recurring/invoices/${encodeURIComponent(id)}/resume`, {});
1360
+ return this.requestUnwrapped("POST", `/recurring/invoices/${pathSegment(id)}/resume`, {});
1248
1361
  }
1249
1362
  async deleteRecurringInvoice(id) {
1250
- return this.request("DELETE", `/recurring/invoices/${encodeURIComponent(id)}`);
1363
+ return this.request("DELETE", `/recurring/invoices/${pathSegment(id)}`);
1251
1364
  }
1252
1365
  async runRecurringNow(templateId, options) {
1253
- return this.requestUnwrapped("POST", `/recurring/invoices/${encodeURIComponent(templateId)}/run`, {
1366
+ return this.requestUnwrapped("POST", `/recurring/invoices/${pathSegment(templateId)}/run`, {
1254
1367
  draftOnly: options?.draftOnly ?? true,
1255
1368
  });
1256
1369
  }
@@ -1268,10 +1381,10 @@ export class FrihetClient {
1268
1381
  return this.requestUnwrapped("POST", "/team/members/invite", data);
1269
1382
  }
1270
1383
  async updateTeamMemberRole(memberId, role) {
1271
- return this.requestUnwrapped("PATCH", `/team/members/${encodeURIComponent(memberId)}/role`, { role });
1384
+ return this.requestUnwrapped("PATCH", `/team/members/${pathSegment(memberId)}/role`, { role });
1272
1385
  }
1273
1386
  async removeTeamMember(memberId) {
1274
- return this.request("DELETE", `/team/members/${encodeURIComponent(memberId)}`);
1387
+ return this.request("DELETE", `/team/members/${pathSegment(memberId)}`);
1275
1388
  }
1276
1389
  // ---------------------------------------------------------------- Gestoria (Wave Fase 1)
1277
1390
  // Backend: /v1/gestoria/* endpoints land with Frihet-ERP Wave Fase 1 closure.
@@ -1297,20 +1410,22 @@ export class FrihetClient {
1297
1410
  return this.requestUnwrapped("POST", "/gestoria/templates/bulk-send", data);
1298
1411
  }
1299
1412
  async getGestoriaAgingConsolidated(params) {
1300
- return this.request("GET", "/gestoria/aging/consolidated", undefined, {
1301
- ownerUid: params?.ownerUid,
1413
+ return this.requestUnwrapped("POST", "/gestoria/aging", {
1414
+ workspaceIds: params?.workspaceIds,
1415
+ asOf: params?.asOf,
1416
+ bustCache: params?.bustCache,
1302
1417
  });
1303
1418
  }
1304
1419
  // ---------------------------------------------------------------- Audit GL
1305
1420
  // NOTE: /v1/gl/* endpoints proxy Firebase callables (PR #395). 404 until REST shell ships.
1306
1421
  async approveGLEntry(entryId, notes) {
1307
- return this.requestUnwrapped("POST", `/gl/entries/${encodeURIComponent(entryId)}/approve`, { notes });
1422
+ return this.requestUnwrapped("POST", `/gl/entries/${pathSegment(entryId)}/approve`, { notes });
1308
1423
  }
1309
- async rejectGLEntry(entryId, reason) {
1310
- return this.requestUnwrapped("POST", `/gl/entries/${encodeURIComponent(entryId)}/reject`, { reason });
1424
+ async rejectGLEntry(entryId, note) {
1425
+ return this.requestUnwrapped("POST", `/gl/entries/${pathSegment(entryId)}/reject`, { note });
1311
1426
  }
1312
1427
  async getGLEntryAuditLog(entryId) {
1313
- return this.request("GET", `/gl/entries/${encodeURIComponent(entryId)}/audit-log`);
1428
+ return this.request("GET", `/gl/entries/${pathSegment(entryId)}/audit-log`);
1314
1429
  }
1315
1430
  // ---------------------------------------------------------------- White-label Portal Domain
1316
1431
  // NOTE: /v1/portal/domain/* endpoints proxy Firebase callables (PR #397). 404 until REST shell ships.
@@ -1318,10 +1433,10 @@ export class FrihetClient {
1318
1433
  return this.requestUnwrapped("POST", "/portal/domain", data);
1319
1434
  }
1320
1435
  async verifyCustomPortalDomain(data) {
1321
- return this.requestUnwrapped("POST", `/portal/domain/${encodeURIComponent(data.domain)}/verify`, {});
1436
+ return this.requestUnwrapped("POST", `/portal/domain/${pathSegment(data.domain)}/verify`, {});
1322
1437
  }
1323
1438
  async removeCustomPortalDomain(data) {
1324
- return this.request("DELETE", `/portal/domain/${encodeURIComponent(data.domain)}`);
1439
+ return this.request("DELETE", `/portal/domain/${pathSegment(data.domain)}`);
1325
1440
  }
1326
1441
  // ---------------------------------------------------------------- Self-onboard + VIES
1327
1442
  // NOTE: /v1/portal/onboard/* endpoints proxy Firebase callables (PR #398). 404 until REST shell ships.
@@ -1337,7 +1452,7 @@ export class FrihetClient {
1337
1452
  // ---------------------------------------------------------------- IGIC (Canary Islands)
1338
1453
  // NOTE: /v1/igic/* endpoints (PR #390). 404 until REST shell ships.
1339
1454
  async getIgicModeloSummary(modeloCode, params) {
1340
- return this.request("GET", `/igic/modelo/${encodeURIComponent(modeloCode)}`, undefined, {
1455
+ return this.request("GET", `/igic/modelo/${pathSegment(modeloCode)}`, undefined, {
1341
1456
  year: params?.year,
1342
1457
  period: params?.period,
1343
1458
  });
@@ -1348,7 +1463,7 @@ export class FrihetClient {
1348
1463
  // ---------------------------------------------------------------- Impuesto Sociedades (IS)
1349
1464
  // NOTE: /v1/is/* endpoints (PR #392). 404 until REST shell ships.
1350
1465
  async getISSummary(modeloCode, params) {
1351
- return this.request("GET", `/is/modelo/${encodeURIComponent(modeloCode)}`, undefined, {
1466
+ return this.request("GET", `/is/modelo/${pathSegment(modeloCode)}`, undefined, {
1352
1467
  year: params?.year,
1353
1468
  installment: params?.installment,
1354
1469
  });
@@ -1356,7 +1471,7 @@ export class FrihetClient {
1356
1471
  // ---------------------------------------------------------------- Bank Rules
1357
1472
  // NOTE: /v1/banking/rules Q3-flagged (PR #394). 404 until callable wrapper ships.
1358
1473
  async listBankRules(params) {
1359
- return this.request("GET", "/banking/rules", undefined, {
1474
+ return this.requestPaginated("GET", "/banking/rules", undefined, {
1360
1475
  isActive: params?.isActive !== undefined ? String(params.isActive) : undefined,
1361
1476
  limit: params?.limit,
1362
1477
  offset: params?.offset,
@@ -1383,19 +1498,19 @@ export class FrihetClient {
1383
1498
  return this.requestUnwrapped("POST", "/leaves", data);
1384
1499
  }
1385
1500
  async approveLeave(leaveId, data) {
1386
- return this.requestUnwrapped("POST", `/leaves/${encodeURIComponent(leaveId)}/approve`, data ?? {});
1501
+ return this.requestUnwrapped("POST", `/leaves/${pathSegment(leaveId)}/approve`, data ?? {});
1387
1502
  }
1388
1503
  async rejectLeave(leaveId, data) {
1389
- return this.requestUnwrapped("POST", `/leaves/${encodeURIComponent(leaveId)}/reject`, data);
1504
+ return this.requestUnwrapped("POST", `/leaves/${pathSegment(leaveId)}/reject`, data);
1390
1505
  }
1391
1506
  async cancelLeave(leaveId) {
1392
- return this.requestUnwrapped("POST", `/leaves/${encodeURIComponent(leaveId)}/cancel`, {});
1507
+ return this.requestUnwrapped("POST", `/leaves/${pathSegment(leaveId)}/cancel`, {});
1393
1508
  }
1394
- async attendanceClockIn(data) {
1395
- return this.requestUnwrapped("POST", "/time-entries/clock-in", data);
1509
+ async attendanceClockIn(data, idempotencyKey) {
1510
+ return this.requestUnwrapped("POST", "/time-entries/clock-in", data, undefined, idempotencyKey);
1396
1511
  }
1397
- async attendanceClockOut(entryId) {
1398
- return this.requestUnwrapped("PATCH", `/time-entries/${encodeURIComponent(entryId)}/clock-out`, {});
1512
+ async attendanceClockOut(entryId, idempotencyKey) {
1513
+ return this.requestUnwrapped("PATCH", `/time-entries/${pathSegment(entryId)}/clock-out`, {}, undefined, idempotencyKey);
1399
1514
  }
1400
1515
  async getOvertimeReport(params) {
1401
1516
  return this.requestUnwrapped("GET", "/time-entries/overtime", undefined, {
@@ -1416,7 +1531,7 @@ export class FrihetClient {
1416
1531
  // ---------------------------------------------------------------- Webhook Trust-Area Extensions
1417
1532
  // NOTE: /v1/webhooks/:id/test — D4-A parallel deploy. 404 propagates until backend ships.
1418
1533
  async testWebhook(id, data) {
1419
- return this.requestUnwrapped("POST", `/webhooks/${encodeURIComponent(id)}/test`, data ?? {});
1534
+ return this.requestUnwrapped("POST", `/webhooks/${pathSegment(id)}/test`, data ?? {});
1420
1535
  }
1421
1536
  // ---------------------------------------------------------------- Payroll
1422
1537
  // These live read routes return normalized payroll-preparation records in
@@ -1432,17 +1547,13 @@ export class FrihetClient {
1432
1547
  month: params.month,
1433
1548
  });
1434
1549
  }
1435
- // ---------------------------------------------------------------- Onboarding
1436
- // NOTE: /v1/onboarding/* — D4-A parallel deploy. 404 propagates until backend ships.
1437
- async getOnboardingStatus() {
1438
- return this.request("GET", "/onboarding/status");
1439
- }
1440
- async setOnboardingPersona(data) {
1441
- return this.requestUnwrapped("PATCH", "/onboarding/persona", data);
1442
- }
1443
1550
  // ---------------------------------------------------------------- Permissions
1444
1551
  // These are standard ERP family responses: unwrap their single-object
1445
1552
  // `{ data, meta }` envelope before handing the payload to MCP output schemas.
1553
+ //
1554
+ // Note: /v1/onboarding/* methods (getOnboardingStatus, setOnboardingPersona)
1555
+ // were removed when the onboarding family was withdrawn via issue #124.
1556
+ // See src/tools/igic.ts for the rationale and the inline-stub precedent.
1446
1557
  async getPermissionsMatrix() {
1447
1558
  return this.requestUnwrapped("GET", "/permissions/matrix");
1448
1559
  }
@@ -1454,7 +1565,7 @@ export class FrihetClient {
1454
1565
  // honest ERP 501 responses and are intentionally unchanged here.
1455
1566
  async getCurrentPeriod(params) {
1456
1567
  if (params?.fiscalYear) {
1457
- return this.requestUnwrapped("GET", `/periods/${encodeURIComponent(params.fiscalYear)}`);
1568
+ return this.requestUnwrapped("GET", `/periods/${pathSegment(params.fiscalYear)}`);
1458
1569
  }
1459
1570
  return this.requestUnwrapped("GET", "/periods/current");
1460
1571
  }
@@ -1462,7 +1573,7 @@ export class FrihetClient {
1462
1573
  return this.requestUnwrapped("POST", "/periods/close", data);
1463
1574
  }
1464
1575
  async reopenPeriod(data) {
1465
- return this.requestUnwrapped("POST", `/periods/${encodeURIComponent(data.periodId)}/reopen`, { reason: data.reason });
1576
+ return this.requestUnwrapped("POST", `/periods/${pathSegment(data.periodId)}/reopen`, { reason: data.reason });
1466
1577
  }
1467
1578
  }
1468
1579
  //# sourceMappingURL=client.js.map