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

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.
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { z } from 'zod';
3
3
  import { SlateTool } from '../action/tool';
4
+ import { SlateTriggerGroup } from '../action/triggerGroup';
4
5
  import { SlateAuth } from '../auth';
5
6
  import { SlateConfig } from '../config';
6
7
  import { Slate } from '../specification/slate';
@@ -41,6 +42,16 @@ describe('SlateAdapterSpec', () => {
41
42
  }))
42
43
  .build();
43
44
 
45
+ let emailTriggerGroup = SlateTriggerGroup.create(spec, {
46
+ key: 'emails',
47
+ name: 'Emails'
48
+ })
49
+ .polling({
50
+ pollEvents: async () => ({ events: [] })
51
+ })
52
+ .routingMatchers(async () => [])
53
+ .build();
54
+
44
55
  let emailReceivedTrigger = gmailAdapterSpec
45
56
  .trigger(spec, {
46
57
  key: 'email_received',
@@ -48,13 +59,13 @@ describe('SlateAdapterSpec', () => {
48
59
  })
49
60
  .input(z.object({ messageId: z.string() }))
50
61
  .output(z.object({ type: z.literal('email.received') }))
51
- .polling({
52
- handleEvent: async ctx => ({
53
- type: 'email.received',
54
- id: ctx.input.messageId,
55
- output: { type: 'email.received' as const }
56
- })
57
- })
62
+ .triggerGroup(emailTriggerGroup)
63
+ .matches(() => true)
64
+ .map(async ctx => ({
65
+ type: 'email.received',
66
+ id: ctx.input.messageId,
67
+ output: { type: 'email.received' as const }
68
+ }))
58
69
  .build();
59
70
 
60
71
  expect(sendEmailTool.adapter).toBe('gmail');
@@ -218,6 +229,16 @@ describe('SlateAdapterSpec', () => {
218
229
  }))
219
230
  .build();
220
231
 
232
+ let sendEmailTriggerGroup = SlateTriggerGroup.create(spec, {
233
+ key: 'sent_emails',
234
+ name: 'Sent Emails'
235
+ })
236
+ .polling({
237
+ pollEvents: async () => ({ events: [] })
238
+ })
239
+ .routingMatchers(async () => [])
240
+ .build();
241
+
221
242
  let sendEmailTrigger = gmailAdapterSpec
222
243
  .trigger(spec, {
223
244
  key: 'send_email',
@@ -225,13 +246,13 @@ describe('SlateAdapterSpec', () => {
225
246
  })
226
247
  .input(z.object({}))
227
248
  .output(z.object({ type: z.string() }))
228
- .polling({
229
- handleEvent: async () => ({
230
- type: 'email.sent',
231
- id: '1',
232
- output: { type: 'email.sent' }
233
- })
234
- })
249
+ .triggerGroup(sendEmailTriggerGroup)
250
+ .matches(() => true)
251
+ .map(async () => ({
252
+ type: 'email.sent',
253
+ id: '1',
254
+ output: { type: 'email.sent' }
255
+ }))
235
256
  .build();
236
257
 
237
258
  expect(() =>
@@ -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
  });
@@ -19,9 +19,9 @@ describe('resolveDefaultExport', () => {
19
19
  });
20
20
 
21
21
  it('unwraps nested default exports from double-bundling', () => {
22
- expect(
23
- resolveDefaultExport<typeof Example>({ default: { default: Example } })
24
- ).toBe(Example);
22
+ expect(resolveDefaultExport<typeof Example>({ default: { default: Example } })).toBe(
23
+ Example
24
+ );
25
25
  });
26
26
 
27
27
  it('throws when the export is not a constructor', () => {
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { SlateTrigger, SlateTriggerGroup } from '../action';
4
+ import { SlateAuth } from '../auth';
5
+ import { SlateConfig } from '../config';
6
+ import { Slate } from './slate';
7
+ import { SlateSpecification } from './specification';
8
+
9
+ let createTestSpec = () => {
10
+ let config = SlateConfig.create(z.object({}));
11
+ let auth = SlateAuth.create<{}>().output(z.object({}));
12
+
13
+ return SlateSpecification.create({
14
+ key: 'demo',
15
+ name: 'Demo',
16
+ config,
17
+ auth
18
+ });
19
+ };
20
+
21
+ let createManualWebhookTriggerGroup = (spec: ReturnType<typeof createTestSpec>, key: string) =>
22
+ SlateTriggerGroup.create(spec, {
23
+ key,
24
+ name: key
25
+ })
26
+ .webhook({
27
+ manualRegistration: {
28
+ userConfigSchema: z.object({}),
29
+ fullConfigSchema: z.object({}),
30
+ setup: async () => ({
31
+ webhookSetupDocument: '',
32
+ partialWebhookRegistrationPayload: {}
33
+ })
34
+ },
35
+ process: async () => ({ events: [] })
36
+ })
37
+ .routingMatchers(async () => [])
38
+ .build();
39
+
40
+ let createAutoWebhookTriggerGroup = (spec: ReturnType<typeof createTestSpec>, key: string) =>
41
+ SlateTriggerGroup.create(spec, {
42
+ key,
43
+ name: key
44
+ })
45
+ .webhook({
46
+ autoRegistration: {
47
+ webhookTargetList: async () => ({ targets: [], nextPageToken: null }),
48
+ webhookRegister: async () => ({
49
+ webhookRegistrationIdentifier: 'reg',
50
+ webhookRegistrationPayload: {}
51
+ }),
52
+ webhookUnregister: async () => {}
53
+ },
54
+ process: async () => ({ events: [] })
55
+ })
56
+ .routingMatchers(async () => [])
57
+ .build();
58
+
59
+ describe('Slate.create', () => {
60
+ it('rejects more than one manual webhook trigger group', () => {
61
+ let spec = createTestSpec();
62
+ let first = createManualWebhookTriggerGroup(spec, 'manual_one');
63
+ let second = createManualWebhookTriggerGroup(spec, 'manual_two');
64
+
65
+ expect(() =>
66
+ Slate.create({
67
+ spec,
68
+ tools: [],
69
+ triggers: [],
70
+ triggerGroups: [first, second]
71
+ })
72
+ ).toThrow(
73
+ 'Only one trigger group may use manual webhook registration, but found: manual_one, manual_two'
74
+ );
75
+ });
76
+
77
+ it('allows a single manual webhook trigger group alongside other trigger groups', () => {
78
+ let spec = createTestSpec();
79
+ let manual = createManualWebhookTriggerGroup(spec, 'manual_group');
80
+ let auto = createAutoWebhookTriggerGroup(spec, 'auto_group');
81
+
82
+ let manualTrigger = SlateTrigger.create(spec, {
83
+ key: 'manual_trigger',
84
+ name: 'Manual Trigger'
85
+ })
86
+ .input(z.object({}))
87
+ .output(z.object({ type: z.literal('demo') }))
88
+ .triggerGroup(manual)
89
+ .matches(() => true)
90
+ .map(async () => ({ type: 'demo', id: '1', output: { type: 'demo' as const } }))
91
+ .build();
92
+
93
+ let slate = Slate.create({
94
+ spec,
95
+ tools: [],
96
+ triggers: [manualTrigger],
97
+ triggerGroups: [manual, auto]
98
+ });
99
+
100
+ expect(slate.triggerGroups.map(group => group.key)).toEqual(['manual_group', 'auto_group']);
101
+ });
102
+ });
@@ -1,4 +1,4 @@
1
- import type { SlateTool, SlateTrigger } from '../action';
1
+ import type { SlateTool, SlateTrigger, SlateTriggerGroup } from '../action';
2
2
  import type { SlateAdapter } from '../adapter';
3
3
  import { SlateDeclarationError } from '../error';
4
4
  import type { SlateSpecification } from './specification';
@@ -10,7 +10,8 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
10
10
  | SlateTrigger<ConfigType, AuthType, any, any>
11
11
  | SlateTool<ConfigType, AuthType, any, any>
12
12
  )[],
13
- private readonly _adapters: SlateAdapter<ConfigType, AuthType>[]
13
+ private readonly _adapters: SlateAdapter<ConfigType, AuthType>[],
14
+ private readonly _triggerGroups: SlateTriggerGroup<ConfigType, AuthType>[]
14
15
  ) {}
15
16
 
16
17
  static create<ConfigType extends {}, AuthType extends {}>(params: {
@@ -18,6 +19,7 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
18
19
  triggers: SlateTrigger<ConfigType, AuthType, any, any>[];
19
20
  tools: SlateTool<ConfigType, AuthType, any, any>[];
20
21
  adapters?: SlateAdapter<ConfigType, AuthType>[];
22
+ triggerGroups?: SlateTriggerGroup<ConfigType, AuthType>[];
21
23
  }) {
22
24
  let adapters = params.adapters ?? [];
23
25
  let actions = [...params.triggers, ...params.tools];
@@ -47,7 +49,29 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
47
49
  }
48
50
  }
49
51
 
50
- return new Slate(params.spec, actions, adapters);
52
+ let triggerGroups = params.triggerGroups ?? [];
53
+ let seenTriggerGroupKeys = new Set<string>();
54
+ for (let group of triggerGroups) {
55
+ if (seenTriggerGroupKeys.has(group.key)) {
56
+ throw new SlateDeclarationError(
57
+ `Trigger group "${group.key}" is registered more than once`
58
+ );
59
+ }
60
+ seenTriggerGroupKeys.add(group.key);
61
+ }
62
+
63
+ let manualWebhookTriggerGroups = triggerGroups.filter(
64
+ group => group.source === 'webhook' && group.webhook?.manualRegistration
65
+ );
66
+ if (manualWebhookTriggerGroups.length > 1) {
67
+ throw new SlateDeclarationError(
68
+ `Only one trigger group may use manual webhook registration, but found: ${manualWebhookTriggerGroups
69
+ .map(group => group.key)
70
+ .join(', ')}`
71
+ );
72
+ }
73
+
74
+ return new Slate(params.spec, actions, adapters, triggerGroups);
51
75
  }
52
76
 
53
77
  get spec() {
@@ -61,4 +85,8 @@ export class Slate<ConfigType extends {}, AuthType extends {}> {
61
85
  get adapters() {
62
86
  return this._adapters;
63
87
  }
88
+
89
+ get triggerGroups() {
90
+ return this._triggerGroups;
91
+ }
64
92
  }
@@ -1,4 +1,4 @@
1
- import type { SlateWebhookHttpOptions, SlateWebhookHttpResponseInit } from '../action';
1
+ import type { SlateWebhookHttpResponseInit } from '../action';
2
2
 
3
3
  export const graphWebhookHttp = {
4
4
  methods: ['POST'],
@@ -6,7 +6,7 @@ export const graphWebhookHttp = {
6
6
  mode: 'match',
7
7
  match: [{ hasQueryParam: 'validationToken' }]
8
8
  }
9
- } satisfies SlateWebhookHttpOptions;
9
+ };
10
10
 
11
11
  export let getGraphWebhookValidationResponse = (
12
12
  request: Request
@@ -1,4 +1,4 @@
1
- import type { SlateWebhookHttpOptions, SlateWebhookHttpResponseInit } from '../action';
1
+ import type { SlateWebhookHttpResponseInit } from '../action';
2
2
 
3
3
  export const metaWebhookHttp = {
4
4
  methods: ['GET', 'POST'],
@@ -6,7 +6,7 @@ export const metaWebhookHttp = {
6
6
  mode: 'match',
7
7
  match: [{ method: 'GET', hasQueryParam: 'hub.mode' }]
8
8
  }
9
- } satisfies SlateWebhookHttpOptions;
9
+ };
10
10
 
11
11
  export let getMetaWebhookVerificationResponse = (
12
12
  request: Request,