@slates/provider 1.0.0-rc.21 → 1.0.0-rc.22

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/dist/index.cjs CHANGED
@@ -1051,6 +1051,91 @@ var SlateActionBuilder = class {
1051
1051
  // src/action/getFileUrlTool.ts
1052
1052
  var import_zod = require("zod");
1053
1053
 
1054
+ // src/auth/redact.ts
1055
+ var AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = "$$MT$secret$authConfig$";
1056
+ var AuthConfigSecretRedactor = class {
1057
+ secretToPlaceholder = /* @__PURE__ */ new Map();
1058
+ constructor(authConfig) {
1059
+ if (authConfig) this.buildSecretMap(authConfig);
1060
+ }
1061
+ redact(value) {
1062
+ return this.transform(value, (stringValue) => {
1063
+ return this.secretToPlaceholder.get(stringValue) ?? stringValue;
1064
+ });
1065
+ }
1066
+ redactEmbedded(value) {
1067
+ if (this.secretToPlaceholder.size === 0) return value;
1068
+ let pattern = new RegExp(
1069
+ [...this.secretToPlaceholder.keys()].sort((a, b) => b.length - a.length).map((secret) => secret.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"),
1070
+ "g"
1071
+ );
1072
+ return this.transform(value, (stringValue) => {
1073
+ let exactPlaceholder = this.secretToPlaceholder.get(stringValue);
1074
+ if (exactPlaceholder) return exactPlaceholder;
1075
+ return stringValue.replace(
1076
+ pattern,
1077
+ (secret) => `${this.secretToPlaceholder.get(secret)}$$`
1078
+ );
1079
+ });
1080
+ }
1081
+ buildSecretMap(value, path = []) {
1082
+ if (typeof value === "string") {
1083
+ if (value.length === 0) return;
1084
+ this.secretToPlaceholder.set(
1085
+ value,
1086
+ `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path.join(".")}`
1087
+ );
1088
+ return;
1089
+ }
1090
+ if (Array.isArray(value)) {
1091
+ value.forEach((item, index) => {
1092
+ this.buildSecretMap(item, [...path, String(index)]);
1093
+ });
1094
+ return;
1095
+ }
1096
+ if (this.isPlainObject(value)) {
1097
+ Object.entries(value).forEach(([key, nestedValue]) => {
1098
+ this.buildSecretMap(nestedValue, [...path, key]);
1099
+ });
1100
+ }
1101
+ }
1102
+ transform(value, transformString) {
1103
+ if (typeof value === "string") {
1104
+ return transformString(value);
1105
+ }
1106
+ if (Array.isArray(value)) {
1107
+ return value.map((item) => this.transform(item, transformString));
1108
+ }
1109
+ if (this.isPlainObject(value)) {
1110
+ let result = {};
1111
+ Object.entries(value).forEach(([key, nestedValue]) => {
1112
+ result[key] = this.transform(nestedValue, transformString);
1113
+ });
1114
+ return result;
1115
+ }
1116
+ return value;
1117
+ }
1118
+ isPlainObject(value) {
1119
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1120
+ }
1121
+ };
1122
+ var redactUrlAttachmentSecrets = (attachments, authConfig) => {
1123
+ if (!authConfig || !attachments) return attachments;
1124
+ let redactor = new AuthConfigSecretRedactor(authConfig);
1125
+ return attachments.map((attachment) => {
1126
+ if (attachment.content.type !== "url") return attachment;
1127
+ if (!attachment.content.headers && !attachment.content.query) return attachment;
1128
+ return {
1129
+ ...attachment,
1130
+ content: {
1131
+ ...attachment.content,
1132
+ headers: attachment.content.headers ? redactor.redactEmbedded(attachment.content.headers) : void 0,
1133
+ query: attachment.content.query ? redactor.redactEmbedded(attachment.content.query) : void 0
1134
+ }
1135
+ };
1136
+ });
1137
+ };
1138
+
1054
1139
  // src/action/tool.ts
1055
1140
  var SlateTool = class _SlateTool extends SlateAction {
1056
1141
  #handleInvocation;
@@ -1089,10 +1174,18 @@ var getFileUrlTool = (spec2, handleGetFileUrl) => tool(spec2, {
1089
1174
  key: GET_FILE_URL_TOOL_ID,
1090
1175
  name: "Get File URL",
1091
1176
  description: "Reserved: refreshes an expiring attachment download URL. Hub-internal only."
1092
- }).input(getFileUrlToolInputSchema).output(getFileUrlToolOutputSchema).handleInvocation(async (ctx) => ({
1093
- output: await handleGetFileUrl(ctx),
1094
- message: "ok"
1095
- })).build();
1177
+ }).input(getFileUrlToolInputSchema).output(getFileUrlToolOutputSchema).handleInvocation(async (ctx) => {
1178
+ let output = await handleGetFileUrl(ctx);
1179
+ let redactor = new AuthConfigSecretRedactor(ctx._getAuthConfigForRedaction());
1180
+ return {
1181
+ output: {
1182
+ ...output,
1183
+ ...output.headers ? { headers: redactor.redactEmbedded(output.headers) } : {},
1184
+ ...output.query ? { query: redactor.redactEmbedded(output.query) } : {}
1185
+ },
1186
+ message: "ok"
1187
+ };
1188
+ }).build();
1096
1189
 
1097
1190
  // src/action/publicTool.ts
1098
1191
  var SlatePublicTool = class {
@@ -1215,7 +1308,9 @@ var bufferAttachmentBody = async (body, maxSizeBytes) => {
1215
1308
  if (body.byteLength > maxSizeBytes) {
1216
1309
  throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
1217
1310
  }
1218
- return body;
1311
+ let buffered2 = new ArrayBuffer(body.byteLength);
1312
+ new Uint8Array(buffered2).set(body);
1313
+ return buffered2;
1219
1314
  }
1220
1315
  let reader = body.getReader();
1221
1316
  let chunks = [];
@@ -1231,10 +1326,11 @@ var bufferAttachmentBody = async (body, maxSizeBytes) => {
1231
1326
  }
1232
1327
  chunks.push(value);
1233
1328
  }
1234
- let buffered = new Uint8Array(sizeBytes);
1329
+ let buffered = new ArrayBuffer(sizeBytes);
1330
+ let bufferedView = new Uint8Array(buffered);
1235
1331
  let offset = 0;
1236
1332
  for (let chunk of chunks) {
1237
- buffered.set(chunk, offset);
1333
+ bufferedView.set(chunk, offset);
1238
1334
  offset += chunk.byteLength;
1239
1335
  }
1240
1336
  return buffered;
@@ -1555,76 +1651,6 @@ var normalizeOAuthTokenResponse = (data, options = {}) => {
1555
1651
  };
1556
1652
  };
1557
1653
 
1558
- // src/auth/redact.ts
1559
- var AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = "$$MT$secret$authConfig$";
1560
- var AuthConfigSecretRedactor = class {
1561
- secretToPlaceholder = /* @__PURE__ */ new Map();
1562
- constructor(authConfig) {
1563
- if (authConfig) this.buildSecretMap(authConfig);
1564
- }
1565
- redact(value) {
1566
- return this.transform(value, (stringValue) => {
1567
- return this.secretToPlaceholder.get(stringValue) ?? stringValue;
1568
- });
1569
- }
1570
- buildSecretMap(value, path = []) {
1571
- if (typeof value === "string") {
1572
- if (value.length === 0) return;
1573
- this.secretToPlaceholder.set(
1574
- value,
1575
- `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path.join(".")}`
1576
- );
1577
- return;
1578
- }
1579
- if (Array.isArray(value)) {
1580
- value.forEach((item, index) => {
1581
- this.buildSecretMap(item, [...path, String(index)]);
1582
- });
1583
- return;
1584
- }
1585
- if (this.isPlainObject(value)) {
1586
- Object.entries(value).forEach(([key, nestedValue]) => {
1587
- this.buildSecretMap(nestedValue, [...path, key]);
1588
- });
1589
- }
1590
- }
1591
- transform(value, transformString) {
1592
- if (typeof value === "string") {
1593
- return transformString(value);
1594
- }
1595
- if (Array.isArray(value)) {
1596
- return value.map((item) => this.transform(item, transformString));
1597
- }
1598
- if (this.isPlainObject(value)) {
1599
- let result = {};
1600
- Object.entries(value).forEach(([key, nestedValue]) => {
1601
- result[key] = this.transform(nestedValue, transformString);
1602
- });
1603
- return result;
1604
- }
1605
- return value;
1606
- }
1607
- isPlainObject(value) {
1608
- return typeof value === "object" && value !== null && !Array.isArray(value);
1609
- }
1610
- };
1611
- var redactUrlAttachmentSecrets = (attachments, authConfig) => {
1612
- if (!authConfig || !attachments) return attachments;
1613
- let redactor = new AuthConfigSecretRedactor(authConfig);
1614
- return attachments.map((attachment) => {
1615
- if (attachment.content.type !== "url") return attachment;
1616
- if (!attachment.content.headers && !attachment.content.query) return attachment;
1617
- return {
1618
- ...attachment,
1619
- content: {
1620
- ...attachment.content,
1621
- headers: attachment.content.headers ? redactor.redact(attachment.content.headers) : void 0,
1622
- query: attachment.content.query ? redactor.redact(attachment.content.query) : void 0
1623
- }
1624
- };
1625
- });
1626
- };
1627
-
1628
1654
  // src/axios/index.ts
1629
1655
  var import_axios3 = __toESM(require("axios"), 1);
1630
1656
 
package/dist/index.d.cts CHANGED
@@ -42,15 +42,15 @@ declare let createTextAttachment: (content: string, mimeType?: string, attachmen
42
42
  */
43
43
  declare let AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX: string;
44
44
  /**
45
- * Walks an auth config object and replaces any string value that exactly matches one of its
46
- * leaf values with a `$$MT$secret$authConfig$<path>` placeholder. Literal, whole-value
47
- * substitution only -- not a substring scan, and not a heuristic key-name/value-shape guess
48
- * like the http trace redactor in '../axios/trace'.
45
+ * Maps auth-config string leaves to `$$MT$secret$authConfig$<path>` placeholders.
46
+ * `redact` preserves whole-value substitution for HTTP traces; `redactEmbedded` also
47
+ * replaces embedded values in attachment credentials, terminating those placeholders with `$$`.
49
48
  */
50
49
  declare class AuthConfigSecretRedactor {
51
50
  private readonly secretToPlaceholder;
52
51
  constructor(authConfig: Record<string, unknown> | undefined);
53
52
  redact<T>(value: T): T;
53
+ redactEmbedded<T>(value: T): T;
54
54
  private buildSecretMap;
55
55
  private transform;
56
56
  private isPlainObject;
package/dist/index.d.ts CHANGED
@@ -42,15 +42,15 @@ declare let createTextAttachment: (content: string, mimeType?: string, attachmen
42
42
  */
43
43
  declare let AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX: string;
44
44
  /**
45
- * Walks an auth config object and replaces any string value that exactly matches one of its
46
- * leaf values with a `$$MT$secret$authConfig$<path>` placeholder. Literal, whole-value
47
- * substitution only -- not a substring scan, and not a heuristic key-name/value-shape guess
48
- * like the http trace redactor in '../axios/trace'.
45
+ * Maps auth-config string leaves to `$$MT$secret$authConfig$<path>` placeholders.
46
+ * `redact` preserves whole-value substitution for HTTP traces; `redactEmbedded` also
47
+ * replaces embedded values in attachment credentials, terminating those placeholders with `$$`.
49
48
  */
50
49
  declare class AuthConfigSecretRedactor {
51
50
  private readonly secretToPlaceholder;
52
51
  constructor(authConfig: Record<string, unknown> | undefined);
53
52
  redact<T>(value: T): T;
53
+ redactEmbedded<T>(value: T): T;
54
54
  private buildSecretMap;
55
55
  private transform;
56
56
  private isPlainObject;
@@ -942,6 +942,91 @@ var SlateActionBuilder = class {
942
942
  // src/action/getFileUrlTool.ts
943
943
  import { z } from "zod";
944
944
 
945
+ // src/auth/redact.ts
946
+ var AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = "$$MT$secret$authConfig$";
947
+ var AuthConfigSecretRedactor = class {
948
+ secretToPlaceholder = /* @__PURE__ */ new Map();
949
+ constructor(authConfig) {
950
+ if (authConfig) this.buildSecretMap(authConfig);
951
+ }
952
+ redact(value) {
953
+ return this.transform(value, (stringValue) => {
954
+ return this.secretToPlaceholder.get(stringValue) ?? stringValue;
955
+ });
956
+ }
957
+ redactEmbedded(value) {
958
+ if (this.secretToPlaceholder.size === 0) return value;
959
+ let pattern = new RegExp(
960
+ [...this.secretToPlaceholder.keys()].sort((a, b) => b.length - a.length).map((secret) => secret.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"),
961
+ "g"
962
+ );
963
+ return this.transform(value, (stringValue) => {
964
+ let exactPlaceholder = this.secretToPlaceholder.get(stringValue);
965
+ if (exactPlaceholder) return exactPlaceholder;
966
+ return stringValue.replace(
967
+ pattern,
968
+ (secret) => `${this.secretToPlaceholder.get(secret)}$$`
969
+ );
970
+ });
971
+ }
972
+ buildSecretMap(value, path = []) {
973
+ if (typeof value === "string") {
974
+ if (value.length === 0) return;
975
+ this.secretToPlaceholder.set(
976
+ value,
977
+ `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path.join(".")}`
978
+ );
979
+ return;
980
+ }
981
+ if (Array.isArray(value)) {
982
+ value.forEach((item, index) => {
983
+ this.buildSecretMap(item, [...path, String(index)]);
984
+ });
985
+ return;
986
+ }
987
+ if (this.isPlainObject(value)) {
988
+ Object.entries(value).forEach(([key, nestedValue]) => {
989
+ this.buildSecretMap(nestedValue, [...path, key]);
990
+ });
991
+ }
992
+ }
993
+ transform(value, transformString) {
994
+ if (typeof value === "string") {
995
+ return transformString(value);
996
+ }
997
+ if (Array.isArray(value)) {
998
+ return value.map((item) => this.transform(item, transformString));
999
+ }
1000
+ if (this.isPlainObject(value)) {
1001
+ let result = {};
1002
+ Object.entries(value).forEach(([key, nestedValue]) => {
1003
+ result[key] = this.transform(nestedValue, transformString);
1004
+ });
1005
+ return result;
1006
+ }
1007
+ return value;
1008
+ }
1009
+ isPlainObject(value) {
1010
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1011
+ }
1012
+ };
1013
+ var redactUrlAttachmentSecrets = (attachments, authConfig) => {
1014
+ if (!authConfig || !attachments) return attachments;
1015
+ let redactor = new AuthConfigSecretRedactor(authConfig);
1016
+ return attachments.map((attachment) => {
1017
+ if (attachment.content.type !== "url") return attachment;
1018
+ if (!attachment.content.headers && !attachment.content.query) return attachment;
1019
+ return {
1020
+ ...attachment,
1021
+ content: {
1022
+ ...attachment.content,
1023
+ headers: attachment.content.headers ? redactor.redactEmbedded(attachment.content.headers) : void 0,
1024
+ query: attachment.content.query ? redactor.redactEmbedded(attachment.content.query) : void 0
1025
+ }
1026
+ };
1027
+ });
1028
+ };
1029
+
945
1030
  // src/action/tool.ts
946
1031
  var SlateTool = class _SlateTool extends SlateAction {
947
1032
  #handleInvocation;
@@ -980,10 +1065,18 @@ var getFileUrlTool = (spec2, handleGetFileUrl) => tool(spec2, {
980
1065
  key: GET_FILE_URL_TOOL_ID,
981
1066
  name: "Get File URL",
982
1067
  description: "Reserved: refreshes an expiring attachment download URL. Hub-internal only."
983
- }).input(getFileUrlToolInputSchema).output(getFileUrlToolOutputSchema).handleInvocation(async (ctx) => ({
984
- output: await handleGetFileUrl(ctx),
985
- message: "ok"
986
- })).build();
1068
+ }).input(getFileUrlToolInputSchema).output(getFileUrlToolOutputSchema).handleInvocation(async (ctx) => {
1069
+ let output = await handleGetFileUrl(ctx);
1070
+ let redactor = new AuthConfigSecretRedactor(ctx._getAuthConfigForRedaction());
1071
+ return {
1072
+ output: {
1073
+ ...output,
1074
+ ...output.headers ? { headers: redactor.redactEmbedded(output.headers) } : {},
1075
+ ...output.query ? { query: redactor.redactEmbedded(output.query) } : {}
1076
+ },
1077
+ message: "ok"
1078
+ };
1079
+ }).build();
987
1080
 
988
1081
  // src/action/publicTool.ts
989
1082
  var SlatePublicTool = class {
@@ -1106,7 +1199,9 @@ var bufferAttachmentBody = async (body, maxSizeBytes) => {
1106
1199
  if (body.byteLength > maxSizeBytes) {
1107
1200
  throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
1108
1201
  }
1109
- return body;
1202
+ let buffered2 = new ArrayBuffer(body.byteLength);
1203
+ new Uint8Array(buffered2).set(body);
1204
+ return buffered2;
1110
1205
  }
1111
1206
  let reader = body.getReader();
1112
1207
  let chunks = [];
@@ -1122,10 +1217,11 @@ var bufferAttachmentBody = async (body, maxSizeBytes) => {
1122
1217
  }
1123
1218
  chunks.push(value);
1124
1219
  }
1125
- let buffered = new Uint8Array(sizeBytes);
1220
+ let buffered = new ArrayBuffer(sizeBytes);
1221
+ let bufferedView = new Uint8Array(buffered);
1126
1222
  let offset = 0;
1127
1223
  for (let chunk of chunks) {
1128
- buffered.set(chunk, offset);
1224
+ bufferedView.set(chunk, offset);
1129
1225
  offset += chunk.byteLength;
1130
1226
  }
1131
1227
  return buffered;
@@ -1446,76 +1542,6 @@ var normalizeOAuthTokenResponse = (data, options = {}) => {
1446
1542
  };
1447
1543
  };
1448
1544
 
1449
- // src/auth/redact.ts
1450
- var AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = "$$MT$secret$authConfig$";
1451
- var AuthConfigSecretRedactor = class {
1452
- secretToPlaceholder = /* @__PURE__ */ new Map();
1453
- constructor(authConfig) {
1454
- if (authConfig) this.buildSecretMap(authConfig);
1455
- }
1456
- redact(value) {
1457
- return this.transform(value, (stringValue) => {
1458
- return this.secretToPlaceholder.get(stringValue) ?? stringValue;
1459
- });
1460
- }
1461
- buildSecretMap(value, path = []) {
1462
- if (typeof value === "string") {
1463
- if (value.length === 0) return;
1464
- this.secretToPlaceholder.set(
1465
- value,
1466
- `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path.join(".")}`
1467
- );
1468
- return;
1469
- }
1470
- if (Array.isArray(value)) {
1471
- value.forEach((item, index) => {
1472
- this.buildSecretMap(item, [...path, String(index)]);
1473
- });
1474
- return;
1475
- }
1476
- if (this.isPlainObject(value)) {
1477
- Object.entries(value).forEach(([key, nestedValue]) => {
1478
- this.buildSecretMap(nestedValue, [...path, key]);
1479
- });
1480
- }
1481
- }
1482
- transform(value, transformString) {
1483
- if (typeof value === "string") {
1484
- return transformString(value);
1485
- }
1486
- if (Array.isArray(value)) {
1487
- return value.map((item) => this.transform(item, transformString));
1488
- }
1489
- if (this.isPlainObject(value)) {
1490
- let result = {};
1491
- Object.entries(value).forEach(([key, nestedValue]) => {
1492
- result[key] = this.transform(nestedValue, transformString);
1493
- });
1494
- return result;
1495
- }
1496
- return value;
1497
- }
1498
- isPlainObject(value) {
1499
- return typeof value === "object" && value !== null && !Array.isArray(value);
1500
- }
1501
- };
1502
- var redactUrlAttachmentSecrets = (attachments, authConfig) => {
1503
- if (!authConfig || !attachments) return attachments;
1504
- let redactor = new AuthConfigSecretRedactor(authConfig);
1505
- return attachments.map((attachment) => {
1506
- if (attachment.content.type !== "url") return attachment;
1507
- if (!attachment.content.headers && !attachment.content.query) return attachment;
1508
- return {
1509
- ...attachment,
1510
- content: {
1511
- ...attachment.content,
1512
- headers: attachment.content.headers ? redactor.redact(attachment.content.headers) : void 0,
1513
- query: attachment.content.query ? redactor.redact(attachment.content.query) : void 0
1514
- }
1515
- };
1516
- });
1517
- };
1518
-
1519
1545
  // src/axios/index.ts
1520
1546
  import baseAxios, { getAdapter, isAxiosError as isAxiosError2 } from "axios";
1521
1547
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/provider",
3
- "version": "1.0.0-rc.21",
3
+ "version": "1.0.0-rc.22",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -43,4 +43,4 @@
43
43
  "typescript": "5.8.2",
44
44
  "vitest": "^3.1.2"
45
45
  }
46
- }
46
+ }
@@ -50,7 +50,9 @@ describe('uploadAttachmentDirect', () => {
50
50
  'content-length': '5'
51
51
  }
52
52
  });
53
- expect(upload[1]!.body).toEqual(new Uint8Array([1, 2, 3, 4, 5]));
53
+ expect(new Uint8Array(upload[1]!.body as ArrayBuffer)).toEqual(
54
+ new Uint8Array([1, 2, 3, 4, 5])
55
+ );
54
56
  expect(upload[1]).not.toHaveProperty('duplex');
55
57
  });
56
58
 
@@ -16,12 +16,15 @@ export interface SlateDirectUploadInput {
16
16
  let bufferAttachmentBody = async (
17
17
  body: SlateDirectUploadInput['body'],
18
18
  maxSizeBytes: number
19
- ) => {
19
+ ): Promise<ArrayBuffer> => {
20
20
  if (body instanceof Uint8Array) {
21
21
  if (body.byteLength > maxSizeBytes) {
22
22
  throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
23
23
  }
24
- return body;
24
+
25
+ let buffered = new ArrayBuffer(body.byteLength);
26
+ new Uint8Array(buffered).set(body);
27
+ return buffered;
25
28
  }
26
29
 
27
30
  let reader = body.getReader();
@@ -41,10 +44,11 @@ let bufferAttachmentBody = async (
41
44
  chunks.push(value);
42
45
  }
43
46
 
44
- let buffered = new Uint8Array(sizeBytes);
47
+ let buffered = new ArrayBuffer(sizeBytes);
48
+ let bufferedView = new Uint8Array(buffered);
45
49
  let offset = 0;
46
50
  for (let chunk of chunks) {
47
- buffered.set(chunk, offset);
51
+ bufferedView.set(chunk, offset);
48
52
  offset += chunk.byteLength;
49
53
  }
50
54
  return buffered;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { AuthConfigSecretRedactor } from '../auth/redact';
2
3
  import type { SlateContext } from '../context/context';
3
4
  import type { SlateSpecification } from '../specification/specification';
4
5
  import { tool } from './tool';
@@ -34,8 +35,17 @@ export let getFileUrlTool = <ConfigType extends {}, AuthType extends {}>(
34
35
  })
35
36
  .input(getFileUrlToolInputSchema)
36
37
  .output(getFileUrlToolOutputSchema)
37
- .handleInvocation(async ctx => ({
38
- output: await handleGetFileUrl(ctx),
39
- message: 'ok'
40
- }))
38
+ .handleInvocation(async ctx => {
39
+ let output = await handleGetFileUrl(ctx);
40
+ let redactor = new AuthConfigSecretRedactor(ctx._getAuthConfigForRedaction());
41
+
42
+ return {
43
+ output: {
44
+ ...output,
45
+ ...(output.headers ? { headers: redactor.redactEmbedded(output.headers) } : {}),
46
+ ...(output.query ? { query: redactor.redactEmbedded(output.query) } : {})
47
+ },
48
+ message: 'ok'
49
+ };
50
+ })
41
51
  .build();
@@ -49,6 +49,98 @@ describe('AuthConfigSecretRedactor', () => {
49
49
  });
50
50
  });
51
51
 
52
+ describe('AuthConfigSecretRedactor.redactEmbedded', () => {
53
+ it('preserves the legacy placeholder format for whole-value matches', () => {
54
+ let redactor = new AuthConfigSecretRedactor({
55
+ token: 'ABC',
56
+ nested: { tokens: ['XYZ'] }
57
+ });
58
+
59
+ expect(redactor.redactEmbedded({ values: ['ABC', 'XYZ', 'plain', 42, null] })).toEqual({
60
+ values: [
61
+ '$$MT$secret$authConfig$token',
62
+ '$$MT$secret$authConfig$nested.tokens.0',
63
+ 'plain',
64
+ 42,
65
+ null
66
+ ]
67
+ });
68
+ });
69
+
70
+ it('terminates embedded placeholders before prefixes, suffixes, and adjacent literals', () => {
71
+ let redactor = new AuthConfigSecretRedactor({ token: 'ABC', token2: 'XYZ' });
72
+
73
+ expect(
74
+ redactor.redactEmbedded({
75
+ bearer: 'Bearer ABC',
76
+ suffix: 'ABC2',
77
+ multiple: 'ABC:XYZ:ABC',
78
+ adjacent: 'ABCXYZ'
79
+ })
80
+ ).toEqual({
81
+ bearer: 'Bearer $$MT$secret$authConfig$token$$',
82
+ suffix: '$$MT$secret$authConfig$token$$2',
83
+ multiple:
84
+ '$$MT$secret$authConfig$token$$:$$MT$secret$authConfig$token2$$:$$MT$secret$authConfig$token$$',
85
+ adjacent: '$$MT$secret$authConfig$token$$$$MT$secret$authConfig$token2$$'
86
+ });
87
+ });
88
+
89
+ it('matches overlapping secrets longest-first', () => {
90
+ let redactor = new AuthConfigSecretRedactor({ token: 'ABC', longer: 'ABC2' });
91
+
92
+ expect(redactor.redactEmbedded('ABC2 ABC')).toBe(
93
+ '$$MT$secret$authConfig$longer$$ $$MT$secret$authConfig$token$$'
94
+ );
95
+ expect(redactor.redactEmbedded('ABC2')).toBe('$$MT$secret$authConfig$longer');
96
+ });
97
+
98
+ it('does not scan emitted placeholders again', () => {
99
+ let redactor = new AuthConfigSecretRedactor({ token: 'ABC', other: 'token' });
100
+
101
+ expect(redactor.redactEmbedded('Bearer ABC token')).toBe(
102
+ 'Bearer $$MT$secret$authConfig$token$$ $$MT$secret$authConfig$other$$'
103
+ );
104
+ });
105
+
106
+ it.each([
107
+ '.',
108
+ '*',
109
+ '+',
110
+ '?',
111
+ '^',
112
+ '$',
113
+ '{',
114
+ '}',
115
+ '(',
116
+ ')',
117
+ '|',
118
+ '[',
119
+ ']',
120
+ '\\'
121
+ ])('treats %s literally in secret values', character => {
122
+ let secret = `${character}secret`;
123
+ let redactor = new AuthConfigSecretRedactor({ token: secret });
124
+
125
+ expect(redactor.redactEmbedded(`prefix ${secret} suffix`)).toBe(
126
+ 'prefix $$MT$secret$authConfig$token$$ suffix'
127
+ );
128
+ expect(redactor.redactEmbedded('prefix secret suffix')).toBe('prefix secret suffix');
129
+ });
130
+
131
+ it('ignores empty secrets and leaves unrelated values unchanged', () => {
132
+ let redactor = new AuthConfigSecretRedactor({ empty: '', token: 'ABC' });
133
+
134
+ expect(redactor.redactEmbedded({ empty: '', plain: 'plain', token: 'ABC' })).toEqual({
135
+ empty: '',
136
+ plain: 'plain',
137
+ token: '$$MT$secret$authConfig$token'
138
+ });
139
+ expect(new AuthConfigSecretRedactor({ empty: '' }).redactEmbedded('plain')).toBe('plain');
140
+ expect(new AuthConfigSecretRedactor(undefined).redactEmbedded('plain')).toBe('plain');
141
+ });
142
+ });
143
+
52
144
  describe('redactUrlAttachmentSecrets', () => {
53
145
  let authConfig = { token: '123SECRET', nested: { token2: 'also_secret' } };
54
146
 
@@ -69,12 +161,40 @@ describe('redactUrlAttachmentSecrets', () => {
69
161
 
70
162
  expect(redactUrlAttachmentSecrets(attachments, authConfig)).toEqual([
71
163
  urlAttachment({
72
- headers: { Authorization: 'Bearer 123SECRET' },
164
+ headers: { Authorization: 'Bearer $$MT$secret$authConfig$token$$' },
73
165
  query: { key: '$$MT$secret$authConfig$nested.token2' }
74
166
  })
75
167
  ]);
76
168
  });
77
169
 
170
+ it('only changes header and query values, preserving metadata and the original attachment', () => {
171
+ let attachment: SlateAttachment = {
172
+ mimeType: 'application/pdf',
173
+ attachmentHash: 'file-hash',
174
+ content: {
175
+ type: 'url',
176
+ url: 'https://example.com/123SECRET/file.pdf',
177
+ headers: { Authorization: 'Bearer 123SECRET' },
178
+ query: { key: 'also_secret-suffix' },
179
+ refreshReference: { id: 'file-1' },
180
+ refreshAt: '2026-09-10T00:00:00Z'
181
+ }
182
+ };
183
+ let original = structuredClone(attachment);
184
+
185
+ expect(redactUrlAttachmentSecrets([attachment], authConfig)).toEqual([
186
+ {
187
+ ...original,
188
+ content: {
189
+ ...original.content,
190
+ headers: { Authorization: 'Bearer $$MT$secret$authConfig$token$$' },
191
+ query: { key: '$$MT$secret$authConfig$nested.token2$$-suffix' }
192
+ }
193
+ }
194
+ ]);
195
+ expect(attachment).toEqual(original);
196
+ });
197
+
78
198
  it('leaves url attachments with no headers/query untouched', () => {
79
199
  let attachments = [urlAttachment()];
80
200
  expect(redactUrlAttachmentSecrets(attachments, authConfig)).toEqual(attachments);
@@ -8,10 +8,9 @@ import type { SlateAttachment } from '../action/attachment';
8
8
  export let AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = '$$MT$secret$authConfig$';
9
9
 
10
10
  /**
11
- * Walks an auth config object and replaces any string value that exactly matches one of its
12
- * leaf values with a `$$MT$secret$authConfig$<path>` placeholder. Literal, whole-value
13
- * substitution only -- not a substring scan, and not a heuristic key-name/value-shape guess
14
- * like the http trace redactor in '../axios/trace'.
11
+ * Maps auth-config string leaves to `$$MT$secret$authConfig$<path>` placeholders.
12
+ * `redact` preserves whole-value substitution for HTTP traces; `redactEmbedded` also
13
+ * replaces embedded values in attachment credentials, terminating those placeholders with `$$`.
15
14
  */
16
15
  export class AuthConfigSecretRedactor {
17
16
  private readonly secretToPlaceholder = new Map<string, string>();
@@ -26,6 +25,29 @@ export class AuthConfigSecretRedactor {
26
25
  });
27
26
  }
28
27
 
28
+ public redactEmbedded<T>(value: T): T {
29
+ if (this.secretToPlaceholder.size === 0) return value;
30
+
31
+ let pattern = new RegExp(
32
+ [...this.secretToPlaceholder.keys()]
33
+ .sort((a, b) => b.length - a.length)
34
+ // Escape regex metacharacters so values match literally; $& inserts the matched character.
35
+ .map(secret => secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
36
+ .join('|'),
37
+ 'g'
38
+ );
39
+
40
+ return this.transform(value, stringValue => {
41
+ let exactPlaceholder = this.secretToPlaceholder.get(stringValue);
42
+ if (exactPlaceholder) return exactPlaceholder;
43
+
44
+ return stringValue.replace(
45
+ pattern,
46
+ secret => `${this.secretToPlaceholder.get(secret)}$$`
47
+ );
48
+ });
49
+ }
50
+
29
51
  private buildSecretMap(value: unknown, path: string[] = []): void {
30
52
  if (typeof value === 'string') {
31
53
  // Never register "" -- it would replace every empty header/query value with a placeholder.
@@ -102,9 +124,11 @@ export let redactUrlAttachmentSecrets = (
102
124
  content: {
103
125
  ...attachment.content,
104
126
  headers: attachment.content.headers
105
- ? redactor.redact(attachment.content.headers)
127
+ ? redactor.redactEmbedded(attachment.content.headers)
106
128
  : undefined,
107
- query: attachment.content.query ? redactor.redact(attachment.content.query) : undefined
129
+ query: attachment.content.query
130
+ ? redactor.redactEmbedded(attachment.content.query)
131
+ : undefined
108
132
  }
109
133
  };
110
134
  });