@slates/provider 1.0.0-rc.20 → 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 {
@@ -1210,8 +1303,41 @@ var createTextAttachment = (content, mimeType, attachmentHash) => ({
1210
1303
  });
1211
1304
 
1212
1305
  // src/action/directUpload.ts
1306
+ var bufferAttachmentBody = async (body, maxSizeBytes) => {
1307
+ if (body instanceof Uint8Array) {
1308
+ if (body.byteLength > maxSizeBytes) {
1309
+ throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
1310
+ }
1311
+ let buffered2 = new ArrayBuffer(body.byteLength);
1312
+ new Uint8Array(buffered2).set(body);
1313
+ return buffered2;
1314
+ }
1315
+ let reader = body.getReader();
1316
+ let chunks = [];
1317
+ let sizeBytes = 0;
1318
+ while (true) {
1319
+ let { done, value } = await reader.read();
1320
+ if (done) break;
1321
+ if (!value) continue;
1322
+ sizeBytes += value.byteLength;
1323
+ if (sizeBytes > maxSizeBytes) {
1324
+ await reader.cancel();
1325
+ throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
1326
+ }
1327
+ chunks.push(value);
1328
+ }
1329
+ let buffered = new ArrayBuffer(sizeBytes);
1330
+ let bufferedView = new Uint8Array(buffered);
1331
+ let offset = 0;
1332
+ for (let chunk of chunks) {
1333
+ bufferedView.set(chunk, offset);
1334
+ offset += chunk.byteLength;
1335
+ }
1336
+ return buffered;
1337
+ };
1213
1338
  var uploadAttachmentDirect = async (d) => {
1214
1339
  let baseUrl = d.live.baseUrl.replace(/\/$/, "");
1340
+ let body = await bufferAttachmentBody(d.body, d.live.maxAttachmentSizeBytes);
1215
1341
  let registerRes = await fetch(`${baseUrl}/slates-hub/live-invocation/attachments`, {
1216
1342
  method: "POST",
1217
1343
  headers: {
@@ -1219,7 +1345,13 @@ var uploadAttachmentDirect = async (d) => {
1219
1345
  authorization: `Bearer ${d.live.token}`
1220
1346
  },
1221
1347
  body: JSON.stringify({
1222
- attachments: [{ mimeType: d.mimeType, filename: d.filename }]
1348
+ attachments: [
1349
+ {
1350
+ mimeType: d.mimeType,
1351
+ filename: d.filename,
1352
+ sizeBytes: body.byteLength || void 0
1353
+ }
1354
+ ]
1223
1355
  })
1224
1356
  });
1225
1357
  if (!registerRes.ok) {
@@ -1234,9 +1366,11 @@ var uploadAttachmentDirect = async (d) => {
1234
1366
  }
1235
1367
  let putRes = await fetch(registration.uploadUrl, {
1236
1368
  method: "PUT",
1237
- ...d.body instanceof Uint8Array ? {} : { duplex: "half" },
1238
- headers: d.mimeType ? { "content-type": d.mimeType } : void 0,
1239
- body: d.body
1369
+ headers: {
1370
+ ...d.mimeType ? { "content-type": d.mimeType } : {},
1371
+ "content-length": String(body.byteLength)
1372
+ },
1373
+ body
1240
1374
  });
1241
1375
  if (!putRes.ok) {
1242
1376
  throw new Error(`Failed to upload attachment content (status ${putRes.status})`);
@@ -1517,76 +1651,6 @@ var normalizeOAuthTokenResponse = (data, options = {}) => {
1517
1651
  };
1518
1652
  };
1519
1653
 
1520
- // src/auth/redact.ts
1521
- var AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = "$$MT$secret$authConfig$";
1522
- var AuthConfigSecretRedactor = class {
1523
- secretToPlaceholder = /* @__PURE__ */ new Map();
1524
- constructor(authConfig) {
1525
- if (authConfig) this.buildSecretMap(authConfig);
1526
- }
1527
- redact(value) {
1528
- return this.transform(value, (stringValue) => {
1529
- return this.secretToPlaceholder.get(stringValue) ?? stringValue;
1530
- });
1531
- }
1532
- buildSecretMap(value, path = []) {
1533
- if (typeof value === "string") {
1534
- if (value.length === 0) return;
1535
- this.secretToPlaceholder.set(
1536
- value,
1537
- `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path.join(".")}`
1538
- );
1539
- return;
1540
- }
1541
- if (Array.isArray(value)) {
1542
- value.forEach((item, index) => {
1543
- this.buildSecretMap(item, [...path, String(index)]);
1544
- });
1545
- return;
1546
- }
1547
- if (this.isPlainObject(value)) {
1548
- Object.entries(value).forEach(([key, nestedValue]) => {
1549
- this.buildSecretMap(nestedValue, [...path, key]);
1550
- });
1551
- }
1552
- }
1553
- transform(value, transformString) {
1554
- if (typeof value === "string") {
1555
- return transformString(value);
1556
- }
1557
- if (Array.isArray(value)) {
1558
- return value.map((item) => this.transform(item, transformString));
1559
- }
1560
- if (this.isPlainObject(value)) {
1561
- let result = {};
1562
- Object.entries(value).forEach(([key, nestedValue]) => {
1563
- result[key] = this.transform(nestedValue, transformString);
1564
- });
1565
- return result;
1566
- }
1567
- return value;
1568
- }
1569
- isPlainObject(value) {
1570
- return typeof value === "object" && value !== null && !Array.isArray(value);
1571
- }
1572
- };
1573
- var redactUrlAttachmentSecrets = (attachments, authConfig) => {
1574
- if (!authConfig || !attachments) return attachments;
1575
- let redactor = new AuthConfigSecretRedactor(authConfig);
1576
- return attachments.map((attachment) => {
1577
- if (attachment.content.type !== "url") return attachment;
1578
- if (!attachment.content.headers && !attachment.content.query) return attachment;
1579
- return {
1580
- ...attachment,
1581
- content: {
1582
- ...attachment.content,
1583
- headers: attachment.content.headers ? redactor.redact(attachment.content.headers) : void 0,
1584
- query: attachment.content.query ? redactor.redact(attachment.content.query) : void 0
1585
- }
1586
- };
1587
- });
1588
- };
1589
-
1590
1654
  // src/axios/index.ts
1591
1655
  var import_axios3 = __toESM(require("axios"), 1);
1592
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 {
@@ -1101,8 +1194,41 @@ var createTextAttachment = (content, mimeType, attachmentHash) => ({
1101
1194
  });
1102
1195
 
1103
1196
  // src/action/directUpload.ts
1197
+ var bufferAttachmentBody = async (body, maxSizeBytes) => {
1198
+ if (body instanceof Uint8Array) {
1199
+ if (body.byteLength > maxSizeBytes) {
1200
+ throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
1201
+ }
1202
+ let buffered2 = new ArrayBuffer(body.byteLength);
1203
+ new Uint8Array(buffered2).set(body);
1204
+ return buffered2;
1205
+ }
1206
+ let reader = body.getReader();
1207
+ let chunks = [];
1208
+ let sizeBytes = 0;
1209
+ while (true) {
1210
+ let { done, value } = await reader.read();
1211
+ if (done) break;
1212
+ if (!value) continue;
1213
+ sizeBytes += value.byteLength;
1214
+ if (sizeBytes > maxSizeBytes) {
1215
+ await reader.cancel();
1216
+ throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
1217
+ }
1218
+ chunks.push(value);
1219
+ }
1220
+ let buffered = new ArrayBuffer(sizeBytes);
1221
+ let bufferedView = new Uint8Array(buffered);
1222
+ let offset = 0;
1223
+ for (let chunk of chunks) {
1224
+ bufferedView.set(chunk, offset);
1225
+ offset += chunk.byteLength;
1226
+ }
1227
+ return buffered;
1228
+ };
1104
1229
  var uploadAttachmentDirect = async (d) => {
1105
1230
  let baseUrl = d.live.baseUrl.replace(/\/$/, "");
1231
+ let body = await bufferAttachmentBody(d.body, d.live.maxAttachmentSizeBytes);
1106
1232
  let registerRes = await fetch(`${baseUrl}/slates-hub/live-invocation/attachments`, {
1107
1233
  method: "POST",
1108
1234
  headers: {
@@ -1110,7 +1236,13 @@ var uploadAttachmentDirect = async (d) => {
1110
1236
  authorization: `Bearer ${d.live.token}`
1111
1237
  },
1112
1238
  body: JSON.stringify({
1113
- attachments: [{ mimeType: d.mimeType, filename: d.filename }]
1239
+ attachments: [
1240
+ {
1241
+ mimeType: d.mimeType,
1242
+ filename: d.filename,
1243
+ sizeBytes: body.byteLength || void 0
1244
+ }
1245
+ ]
1114
1246
  })
1115
1247
  });
1116
1248
  if (!registerRes.ok) {
@@ -1125,9 +1257,11 @@ var uploadAttachmentDirect = async (d) => {
1125
1257
  }
1126
1258
  let putRes = await fetch(registration.uploadUrl, {
1127
1259
  method: "PUT",
1128
- ...d.body instanceof Uint8Array ? {} : { duplex: "half" },
1129
- headers: d.mimeType ? { "content-type": d.mimeType } : void 0,
1130
- body: d.body
1260
+ headers: {
1261
+ ...d.mimeType ? { "content-type": d.mimeType } : {},
1262
+ "content-length": String(body.byteLength)
1263
+ },
1264
+ body
1131
1265
  });
1132
1266
  if (!putRes.ok) {
1133
1267
  throw new Error(`Failed to upload attachment content (status ${putRes.status})`);
@@ -1408,76 +1542,6 @@ var normalizeOAuthTokenResponse = (data, options = {}) => {
1408
1542
  };
1409
1543
  };
1410
1544
 
1411
- // src/auth/redact.ts
1412
- var AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = "$$MT$secret$authConfig$";
1413
- var AuthConfigSecretRedactor = class {
1414
- secretToPlaceholder = /* @__PURE__ */ new Map();
1415
- constructor(authConfig) {
1416
- if (authConfig) this.buildSecretMap(authConfig);
1417
- }
1418
- redact(value) {
1419
- return this.transform(value, (stringValue) => {
1420
- return this.secretToPlaceholder.get(stringValue) ?? stringValue;
1421
- });
1422
- }
1423
- buildSecretMap(value, path = []) {
1424
- if (typeof value === "string") {
1425
- if (value.length === 0) return;
1426
- this.secretToPlaceholder.set(
1427
- value,
1428
- `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path.join(".")}`
1429
- );
1430
- return;
1431
- }
1432
- if (Array.isArray(value)) {
1433
- value.forEach((item, index) => {
1434
- this.buildSecretMap(item, [...path, String(index)]);
1435
- });
1436
- return;
1437
- }
1438
- if (this.isPlainObject(value)) {
1439
- Object.entries(value).forEach(([key, nestedValue]) => {
1440
- this.buildSecretMap(nestedValue, [...path, key]);
1441
- });
1442
- }
1443
- }
1444
- transform(value, transformString) {
1445
- if (typeof value === "string") {
1446
- return transformString(value);
1447
- }
1448
- if (Array.isArray(value)) {
1449
- return value.map((item) => this.transform(item, transformString));
1450
- }
1451
- if (this.isPlainObject(value)) {
1452
- let result = {};
1453
- Object.entries(value).forEach(([key, nestedValue]) => {
1454
- result[key] = this.transform(nestedValue, transformString);
1455
- });
1456
- return result;
1457
- }
1458
- return value;
1459
- }
1460
- isPlainObject(value) {
1461
- return typeof value === "object" && value !== null && !Array.isArray(value);
1462
- }
1463
- };
1464
- var redactUrlAttachmentSecrets = (attachments, authConfig) => {
1465
- if (!authConfig || !attachments) return attachments;
1466
- let redactor = new AuthConfigSecretRedactor(authConfig);
1467
- return attachments.map((attachment) => {
1468
- if (attachment.content.type !== "url") return attachment;
1469
- if (!attachment.content.headers && !attachment.content.query) return attachment;
1470
- return {
1471
- ...attachment,
1472
- content: {
1473
- ...attachment.content,
1474
- headers: attachment.content.headers ? redactor.redact(attachment.content.headers) : void 0,
1475
- query: attachment.content.query ? redactor.redact(attachment.content.query) : void 0
1476
- }
1477
- };
1478
- });
1479
- };
1480
-
1481
1545
  // src/axios/index.ts
1482
1546
  import baseAxios, { getAdapter, isAxiosError as isAxiosError2 } from "axios";
1483
1547
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/provider",
3
- "version": "1.0.0-rc.20",
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
+ }
@@ -0,0 +1,72 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { uploadAttachmentDirect } from './directUpload';
3
+
4
+ let live = {
5
+ token: 'live-token',
6
+ baseUrl: 'https://hub.example/',
7
+ maxAttachmentSizeBytes: 10
8
+ };
9
+
10
+ describe('uploadAttachmentDirect', () => {
11
+ afterEach(() => {
12
+ vi.restoreAllMocks();
13
+ });
14
+
15
+ it('buffers streams and uploads them with their exact content length', async () => {
16
+ let fetchSpy = vi
17
+ .spyOn(globalThis, 'fetch')
18
+ .mockResolvedValueOnce(
19
+ Response.json({
20
+ attachments: [
21
+ { referenceId: 'attachment-reference', uploadUrl: 'https://s3.example/upload' }
22
+ ]
23
+ })
24
+ )
25
+ .mockResolvedValueOnce(new Response(null, { status: 200 }));
26
+ let body = new ReadableStream<Uint8Array>({
27
+ start(controller) {
28
+ controller.enqueue(new Uint8Array([1, 2]));
29
+ controller.enqueue(new Uint8Array([3, 4, 5]));
30
+ controller.close();
31
+ }
32
+ });
33
+
34
+ await expect(
35
+ uploadAttachmentDirect({ live, body, mimeType: 'application/octet-stream' })
36
+ ).resolves.toEqual({
37
+ mimeType: 'application/octet-stream',
38
+ content: { type: 'upload_reference', referenceId: 'attachment-reference' }
39
+ });
40
+
41
+ expect(JSON.parse(fetchSpy.mock.calls[0]![1]!.body as string)).toEqual({
42
+ attachments: [{ mimeType: 'application/octet-stream', sizeBytes: 5 }]
43
+ });
44
+ let upload = fetchSpy.mock.calls[1]!;
45
+ expect(upload[0]).toBe('https://s3.example/upload');
46
+ expect(upload[1]).toMatchObject({
47
+ method: 'PUT',
48
+ headers: {
49
+ 'content-type': 'application/octet-stream',
50
+ 'content-length': '5'
51
+ }
52
+ });
53
+ expect(new Uint8Array(upload[1]!.body as ArrayBuffer)).toEqual(
54
+ new Uint8Array([1, 2, 3, 4, 5])
55
+ );
56
+ expect(upload[1]).not.toHaveProperty('duplex');
57
+ });
58
+
59
+ it('rejects an oversized stream before registering an upload', async () => {
60
+ let fetchSpy = vi.spyOn(globalThis, 'fetch');
61
+ let body = new ReadableStream<Uint8Array>({
62
+ start(controller) {
63
+ controller.enqueue(new Uint8Array(11));
64
+ }
65
+ });
66
+
67
+ await expect(uploadAttachmentDirect({ live, body })).rejects.toThrow(
68
+ 'Attachment exceeds the maximum size of 10 bytes'
69
+ );
70
+ expect(fetchSpy).not.toHaveBeenCalled();
71
+ });
72
+ });
@@ -13,10 +13,52 @@ export interface SlateDirectUploadInput {
13
13
  body: ReadableStream<Uint8Array> | Uint8Array;
14
14
  }
15
15
 
16
+ let bufferAttachmentBody = async (
17
+ body: SlateDirectUploadInput['body'],
18
+ maxSizeBytes: number
19
+ ): Promise<ArrayBuffer> => {
20
+ if (body instanceof Uint8Array) {
21
+ if (body.byteLength > maxSizeBytes) {
22
+ throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
23
+ }
24
+
25
+ let buffered = new ArrayBuffer(body.byteLength);
26
+ new Uint8Array(buffered).set(body);
27
+ return buffered;
28
+ }
29
+
30
+ let reader = body.getReader();
31
+ let chunks: Uint8Array[] = [];
32
+ let sizeBytes = 0;
33
+
34
+ while (true) {
35
+ let { done, value } = await reader.read();
36
+ if (done) break;
37
+ if (!value) continue;
38
+
39
+ sizeBytes += value.byteLength;
40
+ if (sizeBytes > maxSizeBytes) {
41
+ await reader.cancel();
42
+ throw new Error(`Attachment exceeds the maximum size of ${maxSizeBytes} bytes`);
43
+ }
44
+ chunks.push(value);
45
+ }
46
+
47
+ let buffered = new ArrayBuffer(sizeBytes);
48
+ let bufferedView = new Uint8Array(buffered);
49
+ let offset = 0;
50
+ for (let chunk of chunks) {
51
+ bufferedView.set(chunk, offset);
52
+ offset += chunk.byteLength;
53
+ }
54
+ return buffered;
55
+ };
56
+
16
57
  export let uploadAttachmentDirect = async (
17
58
  d: SlateDirectUploadInput
18
59
  ): Promise<SlateAttachment> => {
19
60
  let baseUrl = d.live.baseUrl.replace(/\/$/, '');
61
+ let body = await bufferAttachmentBody(d.body, d.live.maxAttachmentSizeBytes);
20
62
 
21
63
  let registerRes = await fetch(`${baseUrl}/slates-hub/live-invocation/attachments`, {
22
64
  method: 'POST',
@@ -25,7 +67,13 @@ export let uploadAttachmentDirect = async (
25
67
  authorization: `Bearer ${d.live.token}`
26
68
  },
27
69
  body: JSON.stringify({
28
- attachments: [{ mimeType: d.mimeType, filename: d.filename }]
70
+ attachments: [
71
+ {
72
+ mimeType: d.mimeType,
73
+ filename: d.filename,
74
+ sizeBytes: body.byteLength || undefined
75
+ }
76
+ ]
29
77
  })
30
78
  });
31
79
  if (!registerRes.ok) {
@@ -44,9 +92,11 @@ export let uploadAttachmentDirect = async (
44
92
 
45
93
  let putRes = await fetch(registration.uploadUrl, {
46
94
  method: 'PUT',
47
- ...(d.body instanceof Uint8Array ? {} : ({ duplex: 'half' } as any)),
48
- headers: d.mimeType ? { 'content-type': d.mimeType } : undefined,
49
- body: d.body
95
+ headers: {
96
+ ...(d.mimeType ? { 'content-type': d.mimeType } : {}),
97
+ 'content-length': String(body.byteLength)
98
+ },
99
+ body
50
100
  });
51
101
  if (!putRes.ok) {
52
102
  throw new Error(`Failed to upload attachment content (status ${putRes.status})`);
@@ -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
  });