@slates/provider 1.0.0-rc.27 → 1.0.0-rc.29
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 +2305 -1876
- package/dist/index.d.cts +82 -8
- package/dist/index.d.ts +82 -8
- package/dist/index.module.js +2295 -1876
- package/package.json +1 -1
- package/src/action/getFileUrlTool.ts +2 -1
- package/src/action/triggerGroup.ts +65 -3
- package/src/auth/redact.test.ts +60 -2
- package/src/auth/redact.ts +59 -23
- package/src/axios/trace.test.ts +326 -1
- package/src/axios/trace.ts +334 -14
- package/src/error/api.test.ts +123 -1
- package/src/error/api.ts +148 -0
- package/src/error/axios.ts +48 -13
- package/src/error/utils.test.ts +654 -0
- package/src/error/utils.ts +268 -13
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { AuthConfigSecretRedactor } from '../auth/redact';
|
|
2
|
+
import { AuthConfigSecretRedactor, isUrlEmbeddedCredential } from '../auth/redact';
|
|
3
3
|
import type { SlateContext } from '../context/context';
|
|
4
4
|
import type { SlateSpecification } from '../specification/specification';
|
|
5
5
|
import { tool } from './tool';
|
|
@@ -42,6 +42,7 @@ export let getFileUrlTool = <ConfigType extends {}, AuthType extends {}>(
|
|
|
42
42
|
return {
|
|
43
43
|
output: {
|
|
44
44
|
...output,
|
|
45
|
+
url: redactor.redactEmbedded(output.url, isUrlEmbeddedCredential),
|
|
45
46
|
...(output.headers ? { headers: redactor.redactEmbedded(output.headers) } : {}),
|
|
46
47
|
...(output.query ? { query: redactor.redactEmbedded(output.query) } : {})
|
|
47
48
|
},
|
|
@@ -31,6 +31,39 @@ export type SlateTriggerGroupPollHandler<ConfigType extends {}, AuthType extends
|
|
|
31
31
|
updatedState?: any;
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
|
+
export interface SlateGatewayEvent {
|
|
35
|
+
payload: any;
|
|
36
|
+
idempotencyKey?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SlateTriggerGroupGatewayConnectHandler<
|
|
40
|
+
ConfigType extends {},
|
|
41
|
+
AuthType extends {}
|
|
42
|
+
> = (
|
|
43
|
+
context: SlateContext<ConfigType, AuthType, { state: any | null }>
|
|
44
|
+
) => Promise<{ url: string; state?: any }>;
|
|
45
|
+
|
|
46
|
+
/** `closed` is set, with no frames, when the socket closed unexpectedly. */
|
|
47
|
+
export type SlateTriggerGroupGatewayReceiveHandler<
|
|
48
|
+
ConfigType extends {},
|
|
49
|
+
AuthType extends {}
|
|
50
|
+
> = (
|
|
51
|
+
context: SlateContext<
|
|
52
|
+
ConfigType,
|
|
53
|
+
AuthType,
|
|
54
|
+
{ state: any | null; frames: string[]; closed?: { code: number; reason: string } | null }
|
|
55
|
+
>
|
|
56
|
+
) => Promise<{
|
|
57
|
+
state?: any;
|
|
58
|
+
send?: string[];
|
|
59
|
+
events?: SlateGatewayEvent[];
|
|
60
|
+
/** With `expectsAck`, the hub reconnects when no `heartbeatAcked` arrives before the next beat. */
|
|
61
|
+
heartbeat?: { intervalMs: number; frame: string; expectsAck?: boolean } | null;
|
|
62
|
+
/** Set when `frames` included the provider's heartbeat acknowledgement. */
|
|
63
|
+
heartbeatAcked?: boolean;
|
|
64
|
+
close?: { reconnect: boolean; reason?: string } | null;
|
|
65
|
+
}>;
|
|
66
|
+
|
|
34
67
|
export type SlateWebhookTargetListHandler<ConfigType extends {}, AuthType extends {}> = (
|
|
35
68
|
context: SlateContext<ConfigType, AuthType, { pageToken: any | null }>
|
|
36
69
|
) => Promise<{
|
|
@@ -121,6 +154,14 @@ export interface SlateTriggerGroupPollingParameters<
|
|
|
121
154
|
pollEvents: SlateTriggerGroupPollHandler<ConfigType, AuthType>;
|
|
122
155
|
}
|
|
123
156
|
|
|
157
|
+
export interface SlateTriggerGroupGatewayParameters<
|
|
158
|
+
ConfigType extends {},
|
|
159
|
+
AuthType extends {}
|
|
160
|
+
> {
|
|
161
|
+
connect: SlateTriggerGroupGatewayConnectHandler<ConfigType, AuthType>;
|
|
162
|
+
receive: SlateTriggerGroupGatewayReceiveHandler<ConfigType, AuthType>;
|
|
163
|
+
}
|
|
164
|
+
|
|
124
165
|
export interface SlateTriggerGroupWebhookAutoRegistrationParameters<
|
|
125
166
|
ConfigType extends {},
|
|
126
167
|
AuthType extends {}
|
|
@@ -164,9 +205,10 @@ export interface SlateTriggerGroupParameters<
|
|
|
164
205
|
AuthType extends {},
|
|
165
206
|
InputType extends {} = Record<string, unknown>
|
|
166
207
|
> extends SlateTriggerGroupCreateParameters<InputType> {
|
|
167
|
-
source: 'polling' | 'webhook';
|
|
208
|
+
source: 'polling' | 'webhook' | 'gateway';
|
|
168
209
|
polling?: SlateTriggerGroupPollingParameters<ConfigType, AuthType>;
|
|
169
210
|
webhook?: SlateTriggerGroupWebhookParameters<ConfigType, AuthType>;
|
|
211
|
+
gateway?: SlateTriggerGroupGatewayParameters<ConfigType, AuthType>;
|
|
170
212
|
routingMatchers: SlateTriggerGroupRoutingMatchersHandler<ConfigType, AuthType>;
|
|
171
213
|
}
|
|
172
214
|
|
|
@@ -227,6 +269,10 @@ export class SlateTriggerGroup<
|
|
|
227
269
|
return this._params.webhook;
|
|
228
270
|
}
|
|
229
271
|
|
|
272
|
+
get gateway() {
|
|
273
|
+
return this._params.gateway;
|
|
274
|
+
}
|
|
275
|
+
|
|
230
276
|
get routingMatchers() {
|
|
231
277
|
return this._params.routingMatchers;
|
|
232
278
|
}
|
|
@@ -237,9 +283,10 @@ export class SlateTriggerGroupBuilder<
|
|
|
237
283
|
AuthType extends {},
|
|
238
284
|
InputType extends {} = Record<string, unknown>
|
|
239
285
|
> {
|
|
240
|
-
#source: 'polling' | 'webhook' | null = null;
|
|
286
|
+
#source: 'polling' | 'webhook' | 'gateway' | null = null;
|
|
241
287
|
#polling: SlateTriggerGroupPollingParameters<ConfigType, AuthType> | null = null;
|
|
242
288
|
#webhook: SlateTriggerGroupWebhookParameters<ConfigType, AuthType> | null = null;
|
|
289
|
+
#gateway: SlateTriggerGroupGatewayParameters<ConfigType, AuthType> | null = null;
|
|
243
290
|
#routingMatchers: SlateTriggerGroupRoutingMatchersHandler<ConfigType, AuthType> | null =
|
|
244
291
|
null;
|
|
245
292
|
|
|
@@ -302,6 +349,20 @@ export class SlateTriggerGroupBuilder<
|
|
|
302
349
|
return this;
|
|
303
350
|
}
|
|
304
351
|
|
|
352
|
+
/** Receive events over a platform-managed persistent connection (e.g. a WebSocket gateway). */
|
|
353
|
+
gateway(
|
|
354
|
+
props: SlateTriggerGroupGatewayParameters<ConfigType, AuthType>
|
|
355
|
+
): SlateTriggerGroupBuilder<ConfigType, AuthType, InputType> {
|
|
356
|
+
if (this.#source) {
|
|
357
|
+
throw new SlateDeclarationError('Trigger group invocation is already defined');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.#source = 'gateway';
|
|
361
|
+
this.#gateway = { connect: props.connect, receive: props.receive };
|
|
362
|
+
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
|
|
305
366
|
routingMatchers(
|
|
306
367
|
handler: SlateTriggerGroupRoutingMatchersHandler<ConfigType, AuthType>
|
|
307
368
|
): SlateTriggerGroupBuilder<ConfigType, AuthType, InputType> {
|
|
@@ -312,7 +373,7 @@ export class SlateTriggerGroupBuilder<
|
|
|
312
373
|
build(): SlateTriggerGroup<ConfigType, AuthType, InputType> {
|
|
313
374
|
if (!this.#source) {
|
|
314
375
|
throw new SlateDeclarationError(
|
|
315
|
-
'Trigger group invocation (polling or
|
|
376
|
+
'Trigger group invocation (polling, webhook, or gateway) is not defined'
|
|
316
377
|
);
|
|
317
378
|
}
|
|
318
379
|
if (!this.#routingMatchers) {
|
|
@@ -324,6 +385,7 @@ export class SlateTriggerGroupBuilder<
|
|
|
324
385
|
source: this.#source,
|
|
325
386
|
polling: this.#polling ?? undefined,
|
|
326
387
|
webhook: this.#webhook ?? undefined,
|
|
388
|
+
gateway: this.#gateway ?? undefined,
|
|
327
389
|
routingMatchers: this.#routingMatchers
|
|
328
390
|
});
|
|
329
391
|
}
|
package/src/auth/redact.test.ts
CHANGED
|
@@ -141,6 +141,35 @@ describe('AuthConfigSecretRedactor.redactEmbedded', () => {
|
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
describe('AuthConfigSecretRedactor filters', () => {
|
|
145
|
+
let authConfig = { token: 'secret-token', accountId: '123456', username: 'secret-token' };
|
|
146
|
+
let onlyToken = (_secret: string, path: string) => path === 'token';
|
|
147
|
+
|
|
148
|
+
it('limits whole-value replacement to fields that pass the filter', () => {
|
|
149
|
+
let redactor = new AuthConfigSecretRedactor(authConfig);
|
|
150
|
+
|
|
151
|
+
expect(redactor.redact(['123456', 'secret-token'], onlyToken)).toEqual([
|
|
152
|
+
'123456',
|
|
153
|
+
'$$MT$secret$authConfig$token'
|
|
154
|
+
]);
|
|
155
|
+
expect(
|
|
156
|
+
redactor.redactEmbedded(['123456', 'secret-token'], () => false, onlyToken)
|
|
157
|
+
).toEqual(['123456', '$$MT$secret$authConfig$token']);
|
|
158
|
+
expect(redactor.redactEmbedded('id 123456 / secret-token', onlyToken, onlyToken)).toBe(
|
|
159
|
+
'id 123456 / $$MT$secret$authConfig$token$$'
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('keeps replacing every field, last path first, without a filter', () => {
|
|
164
|
+
let redactor = new AuthConfigSecretRedactor(authConfig);
|
|
165
|
+
|
|
166
|
+
expect(redactor.redact(['123456', 'secret-token'])).toEqual([
|
|
167
|
+
'$$MT$secret$authConfig$accountId',
|
|
168
|
+
'$$MT$secret$authConfig$username'
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
144
173
|
describe('redactUrlAttachmentSecrets', () => {
|
|
145
174
|
let authConfig = { token: '123SECRET', nested: { token2: 'also_secret' } };
|
|
146
175
|
|
|
@@ -167,7 +196,7 @@ describe('redactUrlAttachmentSecrets', () => {
|
|
|
167
196
|
]);
|
|
168
197
|
});
|
|
169
198
|
|
|
170
|
-
it('only changes
|
|
199
|
+
it('only changes credential values, preserving metadata and the original attachment', () => {
|
|
171
200
|
let attachment: SlateAttachment = {
|
|
172
201
|
mimeType: 'application/pdf',
|
|
173
202
|
attachmentHash: 'file-hash',
|
|
@@ -187,6 +216,7 @@ describe('redactUrlAttachmentSecrets', () => {
|
|
|
187
216
|
...original,
|
|
188
217
|
content: {
|
|
189
218
|
...original.content,
|
|
219
|
+
url: 'https://example.com/$$MT$secret$authConfig$token$$/file.pdf',
|
|
190
220
|
headers: { Authorization: 'Bearer $$MT$secret$authConfig$token$$' },
|
|
191
221
|
query: { key: '$$MT$secret$authConfig$nested.token2$$-suffix' }
|
|
192
222
|
}
|
|
@@ -195,7 +225,35 @@ describe('redactUrlAttachmentSecrets', () => {
|
|
|
195
225
|
expect(attachment).toEqual(original);
|
|
196
226
|
});
|
|
197
227
|
|
|
198
|
-
it('
|
|
228
|
+
it('redacts a bot token embedded in the url path', () => {
|
|
229
|
+
let attachments = [
|
|
230
|
+
urlAttachment({ url: 'https://api.telegram.org/file/bot123:ABCDEFGH/photos/a.jpg' })
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
expect(
|
|
234
|
+
redactUrlAttachmentSecrets(attachments, { token: '123:ABCDEFGH', botId: '12345678' })
|
|
235
|
+
).toEqual([
|
|
236
|
+
urlAttachment({
|
|
237
|
+
url: 'https://api.telegram.org/file/bot$$MT$secret$authConfig$token$$/photos/a.jpg'
|
|
238
|
+
})
|
|
239
|
+
]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('keeps non-credential and short auth values in the url', () => {
|
|
243
|
+
let attachments = [
|
|
244
|
+
urlAttachment({ url: 'https://example.com/AC12345678/12345678/abc/file.pdf' })
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
expect(
|
|
248
|
+
redactUrlAttachmentSecrets(attachments, {
|
|
249
|
+
account_sid: 'AC12345678',
|
|
250
|
+
botId: '12345678',
|
|
251
|
+
token: 'abc'
|
|
252
|
+
})
|
|
253
|
+
).toEqual(attachments);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('leaves url attachments with no credentials untouched', () => {
|
|
199
257
|
let attachments = [urlAttachment()];
|
|
200
258
|
expect(redactUrlAttachmentSecrets(attachments, authConfig)).toEqual(attachments);
|
|
201
259
|
});
|
package/src/auth/redact.ts
CHANGED
|
@@ -7,29 +7,52 @@ import type { SlateAttachment } from '../action/attachment';
|
|
|
7
7
|
*/
|
|
8
8
|
export let AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = '$$MT$secret$authConfig$';
|
|
9
9
|
|
|
10
|
+
/** Decides whether an auth-config value (with its dotted path) may be replaced. */
|
|
11
|
+
export type AuthConfigSecretFilter = (secret: string, path: string) => boolean;
|
|
12
|
+
|
|
13
|
+
let ALWAYS: AuthConfigSecretFilter = () => true;
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* Maps auth-config string leaves to `$$MT$secret$authConfig$<path>` placeholders.
|
|
12
17
|
* `redact` preserves whole-value substitution for HTTP traces; `redactEmbedded` also
|
|
13
18
|
* replaces embedded values in attachment credentials, terminating those placeholders with `$$`.
|
|
19
|
+
* Both accept a `wholeValue` filter so diagnostics (traces, error text) can skip
|
|
20
|
+
* non-secret fields such as account IDs, emails, or subdomains; attachment callers keep
|
|
21
|
+
* the default, because the hub resolves every placeholder back to its value.
|
|
14
22
|
*/
|
|
15
23
|
export class AuthConfigSecretRedactor {
|
|
16
|
-
|
|
24
|
+
// A value shared by several fields keeps every path, so a filter that rejects one
|
|
25
|
+
// field (e.g. `username`) can still match another (e.g. `token`) with the same value.
|
|
26
|
+
private readonly secretToPaths = new Map<string, string[]>();
|
|
17
27
|
|
|
18
28
|
public constructor(authConfig: Record<string, unknown> | undefined) {
|
|
19
29
|
if (authConfig) this.buildSecretMap(authConfig);
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
public redact<T>(value: T): T {
|
|
32
|
+
public redact<T>(value: T, wholeValue: AuthConfigSecretFilter = ALWAYS): T {
|
|
23
33
|
return this.transform(value, stringValue => {
|
|
24
|
-
return this.
|
|
34
|
+
return this.placeholderFor(stringValue, wholeValue) ?? stringValue;
|
|
25
35
|
});
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
/**
|
|
39
|
+
* `embeddable` limits which secrets are also replaced inside longer strings;
|
|
40
|
+
* `wholeValue` limits which secrets replace a string that equals them exactly.
|
|
41
|
+
*/
|
|
42
|
+
public redactEmbedded<T>(
|
|
43
|
+
value: T,
|
|
44
|
+
embeddable: AuthConfigSecretFilter = ALWAYS,
|
|
45
|
+
wholeValue: AuthConfigSecretFilter = ALWAYS
|
|
46
|
+
): T {
|
|
47
|
+
let embeddedPlaceholders = new Map<string, string>();
|
|
48
|
+
for (let secret of this.secretToPaths.keys()) {
|
|
49
|
+
let placeholder = this.placeholderFor(secret, embeddable);
|
|
50
|
+
if (placeholder) embeddedPlaceholders.set(secret, placeholder);
|
|
51
|
+
}
|
|
52
|
+
if (embeddedPlaceholders.size === 0) return this.redact(value, wholeValue);
|
|
30
53
|
|
|
31
54
|
let pattern = new RegExp(
|
|
32
|
-
[...
|
|
55
|
+
[...embeddedPlaceholders.keys()]
|
|
33
56
|
.sort((a, b) => b.length - a.length)
|
|
34
57
|
// Escape regex metacharacters so values match literally; $& inserts the matched character.
|
|
35
58
|
.map(secret => secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
@@ -38,25 +61,34 @@ export class AuthConfigSecretRedactor {
|
|
|
38
61
|
);
|
|
39
62
|
|
|
40
63
|
return this.transform(value, stringValue => {
|
|
41
|
-
let exactPlaceholder = this.
|
|
64
|
+
let exactPlaceholder = this.placeholderFor(stringValue, wholeValue);
|
|
42
65
|
if (exactPlaceholder) return exactPlaceholder;
|
|
43
66
|
|
|
44
|
-
return stringValue.replace(
|
|
45
|
-
pattern,
|
|
46
|
-
secret => `${this.secretToPlaceholder.get(secret)}$$`
|
|
47
|
-
);
|
|
67
|
+
return stringValue.replace(pattern, secret => `${embeddedPlaceholders.get(secret)}$$`);
|
|
48
68
|
});
|
|
49
69
|
}
|
|
50
70
|
|
|
71
|
+
// The last registered path that passes the filter wins, matching the historical
|
|
72
|
+
// last-write-wins behavior when no filter is given.
|
|
73
|
+
private placeholderFor(value: string, filter: AuthConfigSecretFilter) {
|
|
74
|
+
let paths = this.secretToPaths.get(value);
|
|
75
|
+
if (!paths) return undefined;
|
|
76
|
+
for (let index = paths.length - 1; index >= 0; index--) {
|
|
77
|
+
let path = paths[index];
|
|
78
|
+
if (path !== undefined && filter(value, path))
|
|
79
|
+
return `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path}`;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
private buildSecretMap(value: unknown, path: string[] = []): void {
|
|
52
85
|
if (typeof value === 'string') {
|
|
53
86
|
// Never register "" -- it would replace every empty header/query value with a placeholder.
|
|
54
87
|
if (value.length === 0) return;
|
|
55
88
|
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
89
|
+
let paths = this.secretToPaths.get(value);
|
|
90
|
+
if (paths) paths.push(path.join('.'));
|
|
91
|
+
else this.secretToPaths.set(value, [path.join('.')]);
|
|
60
92
|
return;
|
|
61
93
|
}
|
|
62
94
|
|
|
@@ -102,10 +134,17 @@ export class AuthConfigSecretRedactor {
|
|
|
102
134
|
}
|
|
103
135
|
}
|
|
104
136
|
|
|
137
|
+
// Credentials replaced inside URLs (e.g. Telegram's `/file/bot<token>/` path). Kept narrow:
|
|
138
|
+
// a placeholder in a URL makes the hub proxy the download instead of handing out the link.
|
|
139
|
+
let URL_CREDENTIAL_KEY = /^(?:.*[-_])?(?:token|secret|password|api[-_]?key)$/i;
|
|
140
|
+
|
|
141
|
+
export let isUrlEmbeddedCredential = (secret: string, path: string) =>
|
|
142
|
+
secret.length >= 8 && URL_CREDENTIAL_KEY.test(path.split('.').pop() ?? '');
|
|
143
|
+
|
|
105
144
|
/**
|
|
106
145
|
* Applied to every finalized attachment list right before it's sent over
|
|
107
146
|
* `slates/action.tool.invoke` (see @slates/provider-handler). Only `type: 'url'` attachments
|
|
108
|
-
* carry
|
|
147
|
+
* carry credentials -- content/upload_reference attachments pass through untouched.
|
|
109
148
|
*/
|
|
110
149
|
export let redactUrlAttachmentSecrets = (
|
|
111
150
|
attachments: SlateAttachment[] | undefined,
|
|
@@ -117,18 +156,15 @@ export let redactUrlAttachmentSecrets = (
|
|
|
117
156
|
|
|
118
157
|
return attachments.map(attachment => {
|
|
119
158
|
if (attachment.content.type !== 'url') return attachment;
|
|
120
|
-
if (!attachment.content.headers && !attachment.content.query) return attachment;
|
|
121
159
|
|
|
160
|
+
let { url, headers, query } = attachment.content;
|
|
122
161
|
return {
|
|
123
162
|
...attachment,
|
|
124
163
|
content: {
|
|
125
164
|
...attachment.content,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
query: attachment.content.query
|
|
130
|
-
? redactor.redactEmbedded(attachment.content.query)
|
|
131
|
-
: undefined
|
|
165
|
+
url: redactor.redactEmbedded(url, isUrlEmbeddedCredential),
|
|
166
|
+
...(headers ? { headers: redactor.redactEmbedded(headers) } : {}),
|
|
167
|
+
...(query ? { query: redactor.redactEmbedded(query) } : {})
|
|
132
168
|
}
|
|
133
169
|
};
|
|
134
170
|
});
|