@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 +142 -78
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.module.js +142 -78
- package/package.json +2 -2
- package/src/action/directUpload.test.ts +72 -0
- package/src/action/directUpload.ts +54 -4
- package/src/action/getFileUrlTool.ts +14 -4
- package/src/auth/redact.test.ts +121 -1
- package/src/auth/redact.ts +30 -6
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
|
|
1094
|
-
|
|
1095
|
-
|
|
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: [
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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.module.js
CHANGED
|
@@ -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
|
|
985
|
-
|
|
986
|
-
|
|
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: [
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
@@ -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: [
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
39
|
-
|
|
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();
|
package/src/auth/redact.test.ts
CHANGED
|
@@ -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
|
|
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);
|
package/src/auth/redact.ts
CHANGED
|
@@ -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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
127
|
+
? redactor.redactEmbedded(attachment.content.headers)
|
|
106
128
|
: undefined,
|
|
107
|
-
query: attachment.content.query
|
|
129
|
+
query: attachment.content.query
|
|
130
|
+
? redactor.redactEmbedded(attachment.content.query)
|
|
131
|
+
: undefined
|
|
108
132
|
}
|
|
109
133
|
};
|
|
110
134
|
});
|