@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
@@ -5,6 +5,44 @@
5
5
  export const OPENAI_REVIEW_ORIGIN = "https://openai-mcp.frihet.io";
6
6
  export const FULL_MCP_ORIGIN = "https://mcp.frihet.io";
7
7
  export const FRIHET_CONNECTOR_SCOPE = "frihet:workspace.manage";
8
+ export const OPENAI_REVIEW_OAUTH_SCOPES = Object.freeze([
9
+ "openid",
10
+ "email",
11
+ FRIHET_CONNECTOR_SCOPE,
12
+ ]);
13
+ /**
14
+ * RFC 8707 resource identifier of the reviewed MCP endpoint. Claude's
15
+ * connector guide requires protected-resource metadata to name the connector
16
+ * URL a user enters, including its path; OpenAI documents that ChatGPT takes
17
+ * the `resource` value it sends from that metadata.
18
+ */
19
+ export const OPENAI_REVIEW_MCP_RESOURCE = `${OPENAI_REVIEW_ORIGIN}/mcp`;
20
+ /** RFC 9728 section 3.1 path-inserted location of the metadata for
21
+ * OPENAI_REVIEW_MCP_RESOURCE. The 401 challenge points here. */
22
+ export const OPENAI_REVIEW_MCP_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/mcp";
23
+ /**
24
+ * The only RFC 8707 resource values the reviewed authorization boundary
25
+ * accepts. The canonical MCP URL comes first; the bare origin is the value the
26
+ * root metadata document still publishes (and the only one it published before
27
+ * the path-inserted document existed), so clients using it keep working. Both
28
+ * name this Worker only: the pinned provider matches a token audience by exact
29
+ * origin plus path prefix, so neither value can authorize the full host.
30
+ * The runtime checks that use this set are validateOAuthBoundary (below) and
31
+ * the /token rotation guard in workers/remote-mcp/src/oauth-token-family.ts.
32
+ * The inventory test in workers/remote-mcp/src/__tests__/openai-review-oauth.test.ts
33
+ * lists every runtime line that reads a field named `resource` or `audience`
34
+ * (dot access, string-literal bracket access, or single-line destructuring)
35
+ * and fails on any new such line, whatever it compares or calls. It does not
36
+ * follow a value after it is copied into another variable, multi-line
37
+ * destructuring, or computed property names.
38
+ * Grant props keep `oauthResource: OPENAI_REVIEW_ORIGIN` as the host-binding
39
+ * identity checked at session init and API-key provisioning; that field is not
40
+ * the RFC 8707 value, which the provider stores on the grant itself.
41
+ */
42
+ export const OPENAI_REVIEW_OAUTH_RESOURCES = Object.freeze([
43
+ OPENAI_REVIEW_MCP_RESOURCE,
44
+ OPENAI_REVIEW_ORIGIN,
45
+ ]);
8
46
  /** RFC 7636 S256 challenges are the 32-byte SHA-256 digest encoded as
9
47
  * unpadded base64url, which is always exactly 43 characters. */
10
48
  export function isValidS256CodeChallenge(value) {
@@ -33,32 +71,41 @@ export const OAUTH_PROVIDER_REVIEW_OPTIONS = {
33
71
  clientRegistrationEndpoint: "/register",
34
72
  // This provider does not implement per-tool OAuth authorization. Advertise
35
73
  // one honest connector-wide scope instead of implying that `read` prevents
36
- // writes. Every mutating reviewed tool still requires confirm=true.
37
- scopesSupported: [FRIHET_CONNECTOR_SCOPE],
74
+ // writes. Where a profile requires confirm=true, that is a client interlock,
75
+ // not an authorization boundary.
76
+ scopesSupported: [...OPENAI_REVIEW_OAUTH_SCOPES],
38
77
  accessTokenTTL: 3600,
39
78
  refreshTokenTTL: 2592000,
40
79
  allowPlainPKCE: false,
41
80
  resourceMetadata: {
42
81
  resource: OPENAI_REVIEW_ORIGIN,
43
82
  authorization_servers: [OPENAI_REVIEW_ORIGIN],
44
- scopes_supported: [FRIHET_CONNECTOR_SCOPE],
83
+ scopes_supported: [...OPENAI_REVIEW_OAUTH_SCOPES],
45
84
  bearer_methods_supported: ["header"],
46
- resource_name: "Frihet ChatGPT connector",
85
+ resource_name: "Frihet reviewed connector",
47
86
  },
48
87
  };
88
+ export function hasExactOpenAIReviewScopes(value) {
89
+ if (!Array.isArray(value) || value.length !== OPENAI_REVIEW_OAUTH_SCOPES.length) {
90
+ return false;
91
+ }
92
+ const scopes = new Set(value);
93
+ return scopes.size === OPENAI_REVIEW_OAUTH_SCOPES.length
94
+ && OPENAI_REVIEW_OAUTH_SCOPES.every((scope) => scopes.has(scope));
95
+ }
49
96
  /**
50
97
  * Validate the two OAuth values that form the reviewed-host authorization
51
- * boundary. RFC 8707 resource comparison is exact by design: accepting an
52
- * arbitrary absolute URI would let a token minted by one Frihet Worker target
53
- * the other Worker. Unknown or empty scopes are rejected instead of silently
54
- * downscoping to an empty token.
98
+ * boundary. RFC 8707 resource comparison is exact string membership by design:
99
+ * accepting an arbitrary absolute URI would let a token minted by one Frihet
100
+ * Worker target the other Worker. Unknown or empty scopes are rejected instead
101
+ * of silently downscoping to an empty token.
55
102
  */
56
- export function validateOAuthBoundary(parameters, expectedResource) {
103
+ export function validateOAuthBoundary(parameters, acceptedResources) {
57
104
  if (parameters.resource === undefined && !parameters.requireResource) {
58
105
  // The provider will inherit the exact resource stored on the grant.
59
106
  }
60
107
  else if (typeof parameters.resource !== "string"
61
- || parameters.resource !== expectedResource) {
108
+ || !acceptedResources.includes(parameters.resource)) {
62
109
  return {
63
110
  ok: false,
64
111
  error: "invalid_target",
@@ -72,11 +119,11 @@ export function validateOAuthBoundary(parameters, expectedResource) {
72
119
  : typeof parameters.scope === "string"
73
120
  ? parameters.scope.split(/\s+/u).filter(Boolean)
74
121
  : [];
75
- if (scopes.length !== 1 || scopes[0] !== FRIHET_CONNECTOR_SCOPE) {
122
+ if (!hasExactOpenAIReviewScopes(scopes)) {
76
123
  return {
77
124
  ok: false,
78
125
  error: "invalid_scope",
79
- description: `The only supported OAuth scope is ${FRIHET_CONNECTOR_SCOPE}.`,
126
+ description: `The required OAuth scopes are ${OPENAI_REVIEW_OAUTH_SCOPES.join(" ")}.`,
80
127
  };
81
128
  }
82
129
  return { ok: true };
@@ -85,22 +132,38 @@ function endpoint(origin, path) {
85
132
  return new URL(path, new URL(origin).origin).toString();
86
133
  }
87
134
  export function buildOpenAIUnauthorizedChallenge(origin = OPENAI_REVIEW_ORIGIN) {
88
- const resourceMetadataUrl = endpoint(new URL(origin).origin, "/.well-known/oauth-protected-resource");
135
+ const resourceMetadataUrl = endpoint(new URL(origin).origin, OPENAI_REVIEW_MCP_RESOURCE_METADATA_PATH);
89
136
  return `Bearer realm="OAuth", resource_metadata="${resourceMetadataUrl}", ` +
90
- `scope="${FRIHET_CONNECTOR_SCOPE}", error="invalid_token", ` +
137
+ `scope="${OPENAI_REVIEW_OAUTH_SCOPES.join(" ")}", error="invalid_token", ` +
91
138
  'error_description="Missing or invalid access token"';
92
139
  }
140
+ /**
141
+ * Path-inserted protected-resource metadata for the reviewed MCP endpoint,
142
+ * served by the Worker itself (the pinned provider only serves the root
143
+ * document). It differs from the root document only in `resource`.
144
+ */
145
+ export function buildReviewedMcpProtectedResourceMetadata(origin = OPENAI_REVIEW_ORIGIN) {
146
+ const normalizedOrigin = new URL(origin).origin;
147
+ return {
148
+ resource: endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.apiRoute),
149
+ authorization_servers: [normalizedOrigin],
150
+ scopes_supported: [...OAUTH_PROVIDER_REVIEW_OPTIONS.scopesSupported],
151
+ bearer_methods_supported: ["header"],
152
+ resource_name: OAUTH_PROVIDER_REVIEW_OPTIONS.resourceMetadata.resource_name,
153
+ };
154
+ }
93
155
  /**
94
156
  * Materialize the public metadata generated by workers-oauth-provider 0.3.0
95
- * from the same options used by the Worker. The dependency version is pinned
96
- * separately in the canonical descriptor snapshot.
157
+ * from the same options used by the Worker, plus the path-inserted document
158
+ * the Worker serves itself. The dependency version is pinned separately in
159
+ * the canonical descriptor snapshot.
97
160
  */
98
161
  export function buildOpenAIReviewOAuthContract(origin = OPENAI_REVIEW_ORIGIN) {
99
162
  const normalizedOrigin = new URL(origin).origin;
100
163
  const authorizationEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.authorizeEndpoint);
101
164
  const tokenEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.tokenEndpoint);
102
165
  const registrationEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.clientRegistrationEndpoint);
103
- const resourceMetadataUrl = endpoint(normalizedOrigin, "/.well-known/oauth-protected-resource");
166
+ const resourceMetadataUrl = endpoint(normalizedOrigin, OPENAI_REVIEW_MCP_RESOURCE_METADATA_PATH);
104
167
  return {
105
168
  authorizationServer: {
106
169
  issuer: normalizedOrigin,
@@ -127,10 +190,91 @@ export function buildOpenAIReviewOAuthContract(origin = OPENAI_REVIEW_ORIGIN) {
127
190
  bearer_methods_supported: ["header"],
128
191
  resource_name: OAUTH_PROVIDER_REVIEW_OPTIONS.resourceMetadata.resource_name,
129
192
  },
193
+ protectedResourceMcp: buildReviewedMcpProtectedResourceMetadata(normalizedOrigin),
194
+ openidConfiguration: {
195
+ issuer: normalizedOrigin,
196
+ authorization_endpoint: authorizationEndpoint,
197
+ token_endpoint: tokenEndpoint,
198
+ userinfo_endpoint: endpoint(normalizedOrigin, "/userinfo"),
199
+ registration_endpoint: registrationEndpoint,
200
+ scopes_supported: [...OPENAI_REVIEW_OAUTH_SCOPES],
201
+ response_types_supported: ["code"],
202
+ response_modes_supported: ["query"],
203
+ grant_types_supported: ["authorization_code", "refresh_token"],
204
+ subject_types_supported: ["public"],
205
+ token_endpoint_auth_methods_supported: [
206
+ "client_secret_basic",
207
+ "client_secret_post",
208
+ "none",
209
+ ],
210
+ code_challenge_methods_supported: ["S256"],
211
+ },
130
212
  wwwAuthenticate: {
131
213
  resourceMetadataUrl,
132
214
  missingTokenHeader: buildOpenAIUnauthorizedChallenge(normalizedOrigin),
133
215
  },
134
216
  };
135
217
  }
218
+ function isRecord(value) {
219
+ return value !== null && typeof value === "object" && !Array.isArray(value);
220
+ }
221
+ function isVerifiedEmail(email, verified) {
222
+ return verified === true
223
+ && typeof email === "string"
224
+ && email.length >= 3
225
+ && email.length <= 320
226
+ && email === email.trim()
227
+ && !/[\u0000-\u001f\u007f]/u.test(email)
228
+ && /^[^@\s]+@[^@\s]+$/u.test(email);
229
+ }
230
+ export function isVerifiedOpenAIIdentity(value) {
231
+ return typeof value.uid === "string"
232
+ && value.uid.length > 0
233
+ && value.uid.length <= 128
234
+ && isVerifiedEmail(value.email, value.emailVerified);
235
+ }
236
+ /**
237
+ * Validate the provider's opaque access-token summary before serving OIDC
238
+ * UserInfo. The provider already verifies the encrypted token and KV expiry;
239
+ * this second boundary deliberately rechecks every OpenAI-specific binding.
240
+ */
241
+ export function buildOpenAIUserInfo(token, nowEpochSeconds = Math.floor(Date.now() / 1000), boundary = {
242
+ accessProfile: "openai",
243
+ origin: OPENAI_REVIEW_ORIGIN,
244
+ acceptedResources: OPENAI_REVIEW_OAUTH_RESOURCES,
245
+ }) {
246
+ const audience = token?.audience;
247
+ if (!token
248
+ || !Number.isSafeInteger(token.expiresAt)
249
+ || token.expiresAt <= nowEpochSeconds
250
+ || typeof audience !== "string"
251
+ || !boundary.acceptedResources.includes(audience)
252
+ || !hasExactOpenAIReviewScopes(token.scope)
253
+ || !hasExactOpenAIReviewScopes(token.grant?.scope)
254
+ || !isRecord(token.grant?.props)) {
255
+ return undefined;
256
+ }
257
+ const props = token.grant.props;
258
+ const identity = {
259
+ uid: props.userId,
260
+ email: props.email,
261
+ emailVerified: props.emailVerified,
262
+ };
263
+ if (props.accessProfile !== boundary.accessProfile
264
+ || props.authMethod !== "oauth"
265
+ || props.oauthIssuer !== boundary.origin
266
+ || props.oauthResource !== boundary.origin
267
+ || props.oauthAudience !== audience
268
+ || props.oauthScope !== FRIHET_CONNECTOR_SCOPE
269
+ || !hasExactOpenAIReviewScopes(props.oauthScopes)
270
+ || token.userId !== props.userId
271
+ || !isVerifiedOpenAIIdentity(identity)) {
272
+ return undefined;
273
+ }
274
+ return {
275
+ sub: identity.uid,
276
+ email: identity.email,
277
+ email_verified: true,
278
+ };
279
+ }
136
280
  //# sourceMappingURL=openai-review-oauth.js.map
@@ -186,34 +186,21 @@ export function registerAllPrompts(server) {
186
186
  role: "user",
187
187
  content: {
188
188
  type: "text",
189
- text: `Help me follow up on overdue invoices.\n\n` +
190
- `Follow these steps:\n\n` +
191
- `1. FIND OVERDUE INVOICES\n` +
192
- ` - Use list_invoices to find all invoices with status "overdue" or "sent" past their due date\n` +
193
- ` - For each, note: invoice ID, client name, amount, due date, days overdue\n` +
194
- ` - Sort by days overdue (most overdue first)\n\n` +
195
- `2. GROUP BY CLIENT\n` +
196
- ` - Group overdue invoices by client\n` +
197
- ` - Calculate total outstanding per client\n` +
198
- ` - Use get_client to get contact details for each client\n\n` +
199
- `3. DRAFT FOLLOW-UP MESSAGES\n` +
200
- ` For each client, draft a professional payment reminder:\n` +
201
- ` - Tone: Firm but polite. Maintain the business relationship.\n` +
202
- ` - Include: Invoice number(s), amount(s), original due date(s)\n` +
203
- ` - 1-15 days overdue: Friendly reminder, assume it was overlooked\n` +
204
- ` - 16-30 days overdue: Firmer tone, request confirmation of payment date\n` +
205
- ` - 31-60 days overdue: Escalation notice, mention potential late fees\n` +
206
- ` - 60+ days overdue: Final notice, mention potential debt collection\n` +
207
- ` - Provide both English and Spanish versions\n\n` +
208
- `4. SUGGEST ACTIONS\n` +
209
- ` - Recommend which invoices to update to "overdue" status if still "sent"\n` +
210
- ` - Suggest setting up webhook notifications for future overdue invoices\n` +
211
- ` - Flag any clients with multiple overdue invoices (potential bad debt risk)\n\n` +
212
- `5. SUMMARY\n` +
213
- ` - Total overdue amount\n` +
214
- ` - Number of clients affected\n` +
215
- ` - Oldest unpaid invoice\n` +
216
- ` - Recommended priority order for follow-up`,
189
+ text: `Help me prepare overdue invoice follow-ups for review.
190
+
191
+ 1. GATHER RECORDS
192
+ - Call list_invoices without a status filter (it has no partial status) and select sent, partial and overdue records client-side from each returned status. Paginate every result set using the returned pagination fields until exhausted; deduplicate by invoice ID. If a page fails or results are incomplete, disclose that limitation.
193
+ - Use get_invoice when detail is missing. Record invoice ID/number, client, currency, due date and remaining unpaid balance after recorded payments and credits. Do not count full invoice totals as outstanding for partially paid records. If balance cannot be verified, flag it as unknown instead of assuming it.
194
+ - Confirm the as-of date and workspace timezone. Separate not-yet-due and due-today invoices from overdue invoices. Missing or invalid due dates need review, not an invented age. Invoice status alone does not prove delivery.
195
+
196
+ 2. GROUP AND DRAFT
197
+ - Group verified positive overdue balances by client and currency; never sum different currencies or infer conversion rates. Show the source invoices, balances and days overdue.
198
+ - Use get_client to verify contact details. Draft a neutral, professional reminder in English and Spanish, listing invoice numbers, due dates, remaining balances and currencies. Do not assume why payment is late or that a debt is undisputed.
199
+
200
+ 3. USER REVIEW
201
+ - Present drafts, recipients and supporting records for user approval before any actions, including sending messages, changing records or setting reminders.
202
+ - Do not automatically impose late fees, write off balances, stop work, threaten collection, assign collectible probabilities or make business decisions based on age alone. Ask the user to review disputes and their agreed terms.
203
+ - Summarize overdue balances per currency, clients affected, oldest verified overdue invoice and any missing evidence. Keep not-yet-due balances separate.`,
217
204
  },
218
205
  },
219
206
  ],
@@ -460,7 +447,7 @@ export function registerAllPrompts(server) {
460
447
  // -- invoice-aging-review --
461
448
  server.registerPrompt("invoice-aging-review", {
462
449
  title: "Invoice Aging Review",
463
- description: "Accounts receivable aging analysis: group unpaid invoices by aging bucket (0-30, 31-60, 61-90, 90+ days), " +
450
+ description: "Accounts receivable aging analysis: separate not-yet-due balances from overdue buckets by currency, " +
464
451
  "identify top debtors, and suggest collection actions. " +
465
452
  "/ Análisis de antigüedad de cuentas por cobrar: agrupar facturas impagadas por tramos, " +
466
453
  "identificar mayores deudores y sugerir acciones de cobro.",
@@ -470,55 +457,22 @@ export function registerAllPrompts(server) {
470
457
  role: "user",
471
458
  content: {
472
459
  type: "text",
473
- text: `Perform an accounts receivable aging analysis. Follow these steps:\n\n` +
474
- `1. GATHER UNPAID INVOICES\n` +
475
- ` - Use list_invoices to find all invoices with status "sent" or "overdue"\n` +
476
- ` - For each invoice, note: invoice ID/number, client name, amount (with tax),\n` +
477
- ` issue date, due date, and days since due date\n` +
478
- ` - If due date is in the future, calculate days until due (negative = not yet due)\n\n` +
479
- `2. AGING BUCKETS\n` +
480
- ` Group all unpaid invoices into these buckets:\n` +
481
- ` - CURRENT (0-30 days): Not yet due or less than 30 days overdue\n` +
482
- ` - 31-60 DAYS: Between 31 and 60 days overdue\n` +
483
- ` - 61-90 DAYS: Between 61 and 90 days overdue\n` +
484
- ` - 90+ DAYS: More than 90 days overdue (high risk)\n` +
485
- ` For each bucket, show:\n` +
486
- ` - Number of invoices\n` +
487
- ` - Total amount\n` +
488
- ` - Percentage of total outstanding receivables\n` +
489
- ` - List of invoices in that bucket\n\n` +
490
- `3. TOP DEBTORS\n` +
491
- ` - Use get_client for each client with unpaid invoices to get their details\n` +
492
- ` - Rank clients by total outstanding amount (highest first)\n` +
493
- ` - For each top debtor, show:\n` +
494
- ` - Client name and contact info\n` +
495
- ` - Number of unpaid invoices\n` +
496
- ` - Total outstanding amount\n` +
497
- ` - Oldest unpaid invoice date\n` +
498
- ` - Average days overdue\n\n` +
499
- `4. COLLECTION ACTIONS\n` +
500
- ` Suggest specific actions for each bucket:\n` +
501
- ` - CURRENT (0-30 days): No action needed, monitor normally\n` +
502
- ` - 31-60 DAYS: Send a friendly payment reminder (use overdue-followup prompt pattern)\n` +
503
- ` - 61-90 DAYS: Escalate — phone call + formal written reminder,\n` +
504
- ` consider pausing new work for this client\n` +
505
- ` - 90+ DAYS: Final notice before debt collection,\n` +
506
- ` evaluate write-off vs collection agency cost,\n` +
507
- ` stop all new work for this client\n\n` +
508
- `5. WRITE-OFF CANDIDATES\n` +
509
- ` - Flag any invoices 90+ days overdue with amounts under €100 — likely not worth pursuing\n` +
510
- ` - For larger amounts 90+ days, suggest formal demand letter before write-off\n` +
511
- ` - Note: written-off invoices may still be tax-deductible as bad debt\n` +
512
- ` (requires documentation of collection attempts)\n\n` +
513
- `6. SUMMARY DASHBOARD\n` +
514
- ` Present a clear overview:\n` +
515
- ` - Total accounts receivable (all unpaid invoices)\n` +
516
- ` - Breakdown by aging bucket (amount and % of total)\n` +
517
- ` - Weighted collection estimate:\n` +
518
- ` Current: 95% collectible, 31-60: 80%, 61-90: 50%, 90+: 20%\n` +
519
- ` - Expected collectible amount vs total outstanding\n` +
520
- ` - Number of clients with overdue invoices\n` +
521
- ` - Most urgent actions to take (prioritized list)`,
460
+ text: `Prepare an accounts receivable aging review.
461
+
462
+ 1. VERIFY THE DATA
463
+ - Call list_invoices without a status filter (it has no partial status) and select sent, partial and overdue records client-side from each returned status. Paginate every result set using the returned pagination fields until exhausted; deduplicate by invoice ID. Report any failed page or incomplete coverage.
464
+ - Use get_invoice when needed to verify currency, due date and remaining unpaid balance after recorded payments and credits. Do not count full invoice totals as outstanding for partially paid invoices. Mark unverifiable balances as unknown; do not invent totals.
465
+ - Confirm the as-of date and workspace timezone. Exclude settled, draft and cancelled records from receivables. Status alone does not establish delivery or an undisputed debt.
466
+
467
+ 2. AGE VERIFIED BALANCES
468
+ - Keep separate buckets: not-yet-due; due today; 1-30 days overdue; 31-60 days overdue; 61-90 days overdue; more than 90 days overdue. Missing or invalid due dates form an unknown-age bucket.
469
+ - Group by currency. For each bucket, show invoice IDs, count, remaining unpaid balance and percentage of that currency's verified receivables (only when the denominator is positive). Never combine currencies without a user-supplied conversion basis.
470
+ - Use get_client for client details and summarize positive balances by client within each currency, showing oldest due dates and missing evidence.
471
+
472
+ 3. REVIEW BEFORE ACTING
473
+ - Present the per-currency report and possible follow-up drafts using overdue-followup for user approval before any actions. A report is not permission to send, change status or create charges.
474
+ - Do not assign arbitrary collectible probabilities, automatically write off balances, impose late fees, stop work or make business decisions from age or amount alone. Ask for the user's terms, dispute context and decision before proposing an action.
475
+ - Keep not-yet-due amounts separate from overdue exposure and disclose unknown balances and incomplete pagination.`,
522
476
  },
523
477
  },
524
478
  ],
@@ -17,6 +17,11 @@
17
17
  */
18
18
  /** Sentinel left in place of a redacted value in cloned (tracing) payloads. */
19
19
  export declare const REDACTED = "[redacted]";
20
+ /**
21
+ * Return a backend request id only when it is a short opaque token that is
22
+ * safe to echo to an agent (no whitespace, no prose, no instructions).
23
+ */
24
+ export declare function safeRequestId(value: unknown): string | undefined;
20
25
  /**
21
26
  * Allowlist short, single-line human remediation prose for agent display.
22
27
  *
package/dist/redaction.js CHANGED
@@ -17,6 +17,15 @@
17
17
  */
18
18
  /** Sentinel left in place of a redacted value in cloned (tracing) payloads. */
19
19
  export const REDACTED = "[redacted]";
20
+ /** Opaque backend correlation id (publicApi `meta.requestId` / `X-Request-Id`). */
21
+ const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
22
+ /**
23
+ * Return a backend request id only when it is a short opaque token that is
24
+ * safe to echo to an agent (no whitespace, no prose, no instructions).
25
+ */
26
+ export function safeRequestId(value) {
27
+ return typeof value === "string" && REQUEST_ID_PATTERN.test(value) ? value : undefined;
28
+ }
20
29
  const MAX_SERVER_REMEDIATION_CHARS = 500;
21
30
  const SERVER_REMEDIATION_PROSE_ALPHABET = /^[\p{L}\p{N} .,;:!?'"()_/*+\-]+$/u;
22
31
  const LIVE_EINVOICE_SCOPE_REMEDIATION = "This API key requires the einvoice:read or einvoice:write scope to access e-invoicing endpoints. Add the scope when creating or updating the key.";