@slates/proto 1.0.0-rc.26 → 1.0.0-rc.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/proto",
3
- "version": "1.0.0-rc.26",
3
+ "version": "1.0.0-rc.28",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,11 +1,43 @@
1
1
  import { badRequestError, ServiceError } from '@lowerdeck/error';
2
- import { describe, expect, it } from 'vitest';
2
+ import { describe, expect, expectTypeOf, it } from 'vitest';
3
+ import { createApiServiceError } from '../../../provider/src/error/api';
3
4
  import { SlateError } from '../../../provider/src/error/base';
5
+ import type { SlateErrorResponse } from '../../../provider/src/error/types';
6
+ import { type SlatesErrorResponse, slatesErrorResponse } from '../types';
4
7
  import {
5
8
  createSlatesProviderProtoHandler,
6
9
  SlatesProviderProtoHandlerManager
7
10
  } from './provider';
8
11
 
12
+ // Zod looseObject adds index signatures that interfaces lack; compare declared fields only.
13
+ type Declared<T> = T extends readonly (infer Item)[]
14
+ ? Declared<Item>[]
15
+ : T extends object
16
+ ? {
17
+ [Key in keyof T as string extends Key
18
+ ? never
19
+ : number extends Key
20
+ ? never
21
+ : Key]: Declared<T[Key]>;
22
+ }
23
+ : T;
24
+
25
+ describe('slate error response shape', () => {
26
+ it('keeps the provider SlateErrorResponse and the proto schema in sync', () => {
27
+ type Provider = Declared<SlateErrorResponse>;
28
+ type Proto = Declared<SlatesErrorResponse>;
29
+
30
+ expectTypeOf<Provider>().toExtend<Proto>();
31
+ expectTypeOf<keyof Provider>().toEqualTypeOf<keyof Proto>();
32
+ expectTypeOf<keyof NonNullable<Provider['upstream']>>().toEqualTypeOf<
33
+ keyof NonNullable<Proto['upstream']>
34
+ >();
35
+ expectTypeOf<keyof NonNullable<Provider['provider']>>().toEqualTypeOf<
36
+ keyof NonNullable<Proto['provider']>
37
+ >();
38
+ });
39
+ });
40
+
9
41
  describe('SlatesProviderProtoHandlerManager', () => {
10
42
  it('serializes SlateError instances directly into JSON-RPC errors', async () => {
11
43
  let manager = await createSlatesProviderProtoHandler(async manager => {
@@ -46,6 +78,7 @@ describe('SlatesProviderProtoHandlerManager', () => {
46
78
  status: 429,
47
79
  code: 'rate_limited'
48
80
  },
81
+ upstreamCode: 'rate_limited',
49
82
  baggage: {
50
83
  scope: 'issues'
51
84
  }
@@ -118,4 +151,140 @@ describe('SlatesProviderProtoHandlerManager', () => {
118
151
  }
119
152
  });
120
153
  });
154
+
155
+ let throwFromIdentify = async (error: unknown) => {
156
+ let manager = await createSlatesProviderProtoHandler(async manager => {
157
+ manager.onRequest('slates/provider.identify', async () => {
158
+ throw error;
159
+ });
160
+ }).run();
161
+
162
+ let response = await SlatesProviderProtoHandlerManager.handleInput(manager, {
163
+ jsonrpc: '2.0',
164
+ id: 'req_upstream',
165
+ method: 'slates/provider.identify',
166
+ params: {}
167
+ });
168
+
169
+ return (response as { error: Record<string, unknown> }).error;
170
+ };
171
+
172
+ it('surfaces the upstream code of API service errors as a top-level field', async () => {
173
+ let error = await throwFromIdentify(
174
+ createApiServiceError('Stripe API request failed: 402 - Card declined', {
175
+ upstreamStatus: 402,
176
+ upstreamCode: 'card_declined'
177
+ })
178
+ );
179
+
180
+ expect(error).toEqual({
181
+ code: 'request.bad',
182
+ kind: 'request',
183
+ message: 'Stripe API request failed: 402 - Card declined',
184
+ retryable: false,
185
+ status: 400,
186
+ upstreamCode: 'card_declined',
187
+ baggage: {
188
+ serviceError: {
189
+ code: 'bad_request',
190
+ status: 400
191
+ },
192
+ serviceErrorData: {
193
+ upstreamStatus: 402,
194
+ upstreamCode: 'card_declined'
195
+ }
196
+ }
197
+ });
198
+ expect(slatesErrorResponse.parse(error).upstreamCode).toBe('card_declined');
199
+ });
200
+
201
+ it('omits upstreamCode when the service error has no string upstream code', async () => {
202
+ for (let upstreamCode of [undefined, '']) {
203
+ let error = await throwFromIdentify(
204
+ createApiServiceError('Upstream failed', { upstreamStatus: 500, upstreamCode })
205
+ );
206
+ expect(error).not.toHaveProperty('upstreamCode');
207
+ }
208
+
209
+ let numeric = new ServiceError(badRequestError({ message: 'Upstream failed' }));
210
+ numeric.data.upstreamCode = 42;
211
+ expect(await throwFromIdentify(numeric)).not.toHaveProperty('upstreamCode');
212
+ });
213
+
214
+ it('drops upstreamCode values that are not code-shaped', async () => {
215
+ for (let upstreamCode of ['x'.repeat(129), 'has space', '`x`', 'a\nb', '<script>']) {
216
+ expect(
217
+ await throwFromIdentify(
218
+ createApiServiceError('Upstream failed', { upstreamStatus: 400, upstreamCode })
219
+ )
220
+ ).not.toHaveProperty('upstreamCode');
221
+
222
+ let payload = await throwFromIdentify({
223
+ code: 'upstream.error',
224
+ kind: 'upstream',
225
+ message: 'Upstream failed',
226
+ upstreamCode,
227
+ upstream: { code: upstreamCode }
228
+ });
229
+ expect(payload).not.toHaveProperty('upstreamCode');
230
+ }
231
+
232
+ expect(
233
+ await throwFromIdentify({
234
+ code: 'upstream.error',
235
+ kind: 'upstream',
236
+ message: 'Upstream failed',
237
+ upstreamCode: 'not a code',
238
+ upstream: { code: 'AADSTS700016' }
239
+ })
240
+ ).toMatchObject({ upstreamCode: 'AADSTS700016' });
241
+ });
242
+
243
+ it('keeps upstreamCode in service error baggage for older hubs', async () => {
244
+ let error = await throwFromIdentify(
245
+ createApiServiceError('Upstream failed', { upstreamCode: 'invalid_grant' })
246
+ );
247
+
248
+ expect(error).toMatchObject({
249
+ upstreamCode: 'invalid_grant',
250
+ baggage: { serviceErrorData: { upstreamCode: 'invalid_grant' } }
251
+ });
252
+ });
253
+
254
+ it('derives upstreamCode from upstream.code for slate error payloads that predate it', async () => {
255
+ let error = await throwFromIdentify({
256
+ code: 'upstream.error',
257
+ kind: 'upstream',
258
+ message: 'Upstream failed',
259
+ upstream: { status: 502, code: 'BAD_GATEWAY' }
260
+ });
261
+
262
+ expect(error).toMatchObject({ code: 'upstream.error', upstreamCode: 'BAD_GATEWAY' });
263
+ });
264
+
265
+ it('keeps an explicit upstreamCode on slate errors', async () => {
266
+ let error = await throwFromIdentify(
267
+ new SlateError({
268
+ code: 'upstream.error',
269
+ message: 'Upstream failed',
270
+ upstream: { code: 'nested' },
271
+ upstreamCode: 'explicit'
272
+ })
273
+ );
274
+
275
+ expect(error).toMatchObject({ upstream: { code: 'nested' }, upstreamCode: 'explicit' });
276
+ });
277
+
278
+ it('carries the upstream code when a service error is converted to a SlateError', async () => {
279
+ let error = await throwFromIdentify(
280
+ SlateError.fromServiceError(
281
+ createApiServiceError('Rate limited', {
282
+ upstreamStatus: 429,
283
+ upstreamCode: 'RATE_LIMITED'
284
+ })
285
+ )
286
+ );
287
+
288
+ expect(error).toMatchObject({ code: 'request.bad', upstreamCode: 'RATE_LIMITED' });
289
+ });
121
290
  });
@@ -12,6 +12,7 @@ import {
12
12
  slatesNotificationsByMethod,
13
13
  slatesRequestsByMethod
14
14
  } from '../messages';
15
+ import type { SlatesErrorResponse } from '../types';
15
16
 
16
17
  export class SlatesProviderProtoHandlerManager {
17
18
  #implMap = new Map<
@@ -163,9 +164,9 @@ export let createSlatesProviderProtoHandler = (
163
164
  let toSlateErrorResponse = (
164
165
  error: unknown,
165
166
  defaults: Partial<{ code: string; message: string }> = {}
166
- ) => {
167
+ ): SlatesErrorResponse => {
167
168
  let slateError = getSlateErrorResponse(error);
168
- if (slateError) return slateError;
169
+ if (slateError) return withUpstreamCode(slateError);
169
170
 
170
171
  let serviceError = getServiceErrorData(error);
171
172
  if (serviceError) {
@@ -199,9 +200,7 @@ let isRecord = (value: unknown): value is Record<string, unknown> =>
199
200
  let isStructuredValue = (value: unknown): value is Record<string, unknown> | Function =>
200
201
  (typeof value === 'object' || typeof value === 'function') && value !== null;
201
202
 
202
- let isSlateErrorPayload = (
203
- value: unknown
204
- ): value is { code: string; message: string; kind?: string; [key: string]: unknown } =>
203
+ let isSlateErrorPayload = (value: unknown): value is SlatesErrorResponse =>
205
204
  isRecord(value) &&
206
205
  typeof value.code === 'string' &&
207
206
  typeof value.message === 'string' &&
@@ -273,6 +272,20 @@ let getServiceErrorData = (error: unknown) => {
273
272
  return null;
274
273
  };
275
274
 
275
+ let UPSTREAM_CODE_PATTERN = /^[\w.:-]{1,128}$/;
276
+
277
+ // upstreamCode is copied from untrusted upstream responses to the top level of the
278
+ // error, so anything not code-shaped is dropped.
279
+ let normalizeUpstreamCode = (value: unknown): string | undefined =>
280
+ typeof value === 'string' && UPSTREAM_CODE_PATTERN.test(value) ? value : undefined;
281
+
282
+ let withUpstreamCode = (response: SlatesErrorResponse): SlatesErrorResponse => {
283
+ let { upstreamCode: rawUpstreamCode, ...rest } = response;
284
+ let upstreamCode =
285
+ normalizeUpstreamCode(rawUpstreamCode) ?? normalizeUpstreamCode(response.upstream?.code);
286
+ return upstreamCode ? { ...rest, upstreamCode } : rest;
287
+ };
288
+
276
289
  let mapKind = (code: string) => {
277
290
  if (code.startsWith('declaration.')) return 'declaration';
278
291
  if (code.startsWith('input.')) return 'validation';
@@ -345,6 +358,7 @@ let mapServiceErrorToSlateErrorResponse = (
345
358
  defaults: Partial<{ code: string; message: string }> = {}
346
359
  ) => {
347
360
  let code = defaults.code ?? mapServiceErrorCode(error.code, error.status);
361
+ let upstreamCode = normalizeUpstreamCode(error.upstreamCode);
348
362
  let extraEntries = Object.entries(error).filter(
349
363
  ([key]) =>
350
364
  ![
@@ -367,6 +381,7 @@ let mapServiceErrorToSlateErrorResponse = (
367
381
  retryable: mapRetryable(code),
368
382
  status: error.status,
369
383
  ...(mapIssues(error.errors) ? { issues: mapIssues(error.errors) } : {}),
384
+ ...(upstreamCode ? { upstreamCode } : {}),
370
385
  baggage: {
371
386
  serviceError: {
372
387
  code: error.code,
@@ -36,13 +36,39 @@ export type SlatesMessageHubLiveInvocationSetNotification = z.infer<
36
36
  typeof slatesMessageHubLiveInvocationSetNotification
37
37
  >;
38
38
 
39
+ export let slatesRuntimeInfo = z.object({
40
+ metorial: z
41
+ .object({
42
+ apiUrl: z.string().url().max(2048),
43
+ region: z.string().max(64).nullish(),
44
+ environment: z.string().max(64).nullish()
45
+ })
46
+ .optional()
47
+ });
48
+
49
+ export type SlatesRuntimeInfo = z.infer<typeof slatesRuntimeInfo>;
50
+
51
+ export let slatesMessageHubRuntimeInfoSetNotification = z.object({
52
+ jsonrpc: z.literal('2.0'),
53
+ method: z.literal('slates/hub.runtime_info.set'),
54
+ params: z.object({
55
+ runtimeInfo: slatesRuntimeInfo
56
+ })
57
+ });
58
+
59
+ export type SlatesMessageHubRuntimeInfoSetNotification = z.infer<
60
+ typeof slatesMessageHubRuntimeInfoSetNotification
61
+ >;
62
+
39
63
  export type SlatesHubNotifications =
40
64
  | SlatesMessageHubCapabilitiesSetNotification
41
- | SlatesMessageHubLiveInvocationSetNotification;
65
+ | SlatesMessageHubLiveInvocationSetNotification
66
+ | SlatesMessageHubRuntimeInfoSetNotification;
42
67
 
43
68
  export let slatesHubNotificationsByMethod = {
44
69
  'slates/hub.capabilities.set': slatesMessageHubCapabilitiesSetNotification,
45
- 'slates/hub.live_invocation.set': slatesMessageHubLiveInvocationSetNotification
70
+ 'slates/hub.live_invocation.set': slatesMessageHubLiveInvocationSetNotification,
71
+ 'slates/hub.runtime_info.set': slatesMessageHubRuntimeInfoSetNotification
46
72
  };
47
73
 
48
74
  export let slatesMessageProviderCapabilitiesGetRequest = z.object({
@@ -64,7 +90,8 @@ export let slatesMessageProviderCapabilitiesGetResponse = z.object({
64
90
  hub: z
65
91
  .object({
66
92
  capabilitiesNotification: z.boolean().optional(),
67
- liveInvocation: z.boolean().optional()
93
+ liveInvocation: z.boolean().optional(),
94
+ runtimeInfo: z.boolean().optional()
68
95
  })
69
96
  .optional(),
70
97
  provider: z
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { slatesAuthenticationMethod } from './authMethod';
3
+
4
+ let authMethod = {
5
+ id: 'oauth',
6
+ name: 'OAuth',
7
+ type: 'auth.oauth',
8
+ inputSchema: {},
9
+ outputSchema: {},
10
+ docs: []
11
+ };
12
+
13
+ describe('slatesAuthenticationMethod', () => {
14
+ it('parses the adminSetup capability with and without the required flag', () => {
15
+ for (let adminSetup of [
16
+ { enabled: true },
17
+ { enabled: true, required: false },
18
+ { enabled: true, required: true }
19
+ ]) {
20
+ let parsed = slatesAuthenticationMethod.parse({
21
+ ...authMethod,
22
+ capabilities: { adminSetup }
23
+ });
24
+ expect(parsed.capabilities.adminSetup).toEqual(adminSetup);
25
+ }
26
+ });
27
+
28
+ it('rejects a non-boolean required flag', () => {
29
+ expect(
30
+ slatesAuthenticationMethod.safeParse({
31
+ ...authMethod,
32
+ capabilities: { adminSetup: { enabled: true, required: 'yes' } }
33
+ }).success
34
+ ).toBe(false);
35
+ });
36
+ });
@@ -33,7 +33,7 @@ export let slatesAuthenticationMethod = z.object({
33
33
  handleChangedInput: z.object({ enabled: z.boolean() }).optional(),
34
34
  handleTokenRefresh: z.object({ enabled: z.boolean() }).optional(),
35
35
  getProfile: z.object({ enabled: z.boolean() }).optional(),
36
- adminSetup: z.object({ enabled: z.boolean() }).optional()
36
+ adminSetup: z.object({ enabled: z.boolean(), required: z.boolean().optional() }).optional()
37
37
  }),
38
38
 
39
39
  docs: z.array(
@@ -0,0 +1,47 @@
1
+ import z from 'zod';
2
+ import { slatesRequestTrace } from '../messages/tracing';
3
+
4
+ export let slatesErrorResponse = z.looseObject({
5
+ code: z.string(),
6
+ kind: z.string(),
7
+ message: z.string(),
8
+ retryable: z.boolean().optional(),
9
+ status: z.number().optional(),
10
+
11
+ issues: z
12
+ .array(
13
+ z.looseObject({
14
+ path: z.array(z.string()).optional(),
15
+ code: z.string().optional(),
16
+ message: z.string()
17
+ })
18
+ )
19
+ .optional(),
20
+
21
+ provider: z
22
+ .looseObject({
23
+ key: z.string().optional(),
24
+ service: z.string().optional(),
25
+ operation: z.string().optional()
26
+ })
27
+ .optional(),
28
+
29
+ upstream: z
30
+ .looseObject({
31
+ status: z.number().optional(),
32
+ code: z.string().optional(),
33
+ type: z.string().optional(),
34
+ requestId: z.string().optional(),
35
+ method: z.string().optional(),
36
+ url: z.string().optional()
37
+ })
38
+ .optional(),
39
+
40
+ // The upstream API's own error code, next to the normalized protocol `code`.
41
+ upstreamCode: z.string().optional(),
42
+
43
+ baggage: z.record(z.string(), z.unknown()).optional(),
44
+ requestTraces: z.array(slatesRequestTrace).optional()
45
+ });
46
+
47
+ export type SlatesErrorResponse = z.infer<typeof slatesErrorResponse>;
@@ -1,6 +1,7 @@
1
1
  export * from './action';
2
2
  export * from './adapter';
3
3
  export * from './authMethod';
4
+ export * from './error';
4
5
  export * from './participant';
5
6
  export * from './toolCategory';
6
7
  export * from './triggerGroup';