@slates/provider 1.0.0-rc.2 → 1.0.0-rc.21
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 +2885 -2
- package/dist/index.d.cts +1019 -0
- package/dist/index.d.ts +1019 -9
- package/dist/index.module.js +2775 -2
- package/package.json +9 -6
- package/src/action/action.ts +92 -4
- package/src/action/attachment.ts +87 -0
- package/src/action/builder.test.ts +136 -0
- package/src/action/builder.ts +113 -15
- package/src/action/directUpload.test.ts +70 -0
- package/src/action/directUpload.ts +105 -0
- package/src/action/getFileUrlTool.ts +41 -0
- package/src/action/index.ts +4 -2
- package/src/action/publicTool.ts +36 -0
- package/src/action/scopes.test.ts +76 -0
- package/src/action/scopes.ts +103 -0
- package/src/action/tool.ts +22 -6
- package/src/action/trigger.ts +19 -11
- package/src/adapter/adapter.ts +38 -0
- package/src/adapter/index.test.ts +322 -0
- package/src/adapter/index.ts +2 -0
- package/src/adapter/spec.ts +154 -0
- package/src/auth/auth.ts +3 -3
- package/src/auth/index.ts +2 -0
- package/src/auth/oauth.test.ts +83 -0
- package/src/auth/oauth.ts +150 -0
- package/src/auth/redact.test.ts +103 -0
- package/src/auth/redact.ts +111 -0
- package/src/auth/types.ts +72 -13
- package/src/axios/http.test.ts +83 -0
- package/src/axios/index.ts +116 -10
- package/src/axios/trace.test.ts +377 -0
- package/src/axios/trace.ts +803 -0
- package/src/config/config.ts +14 -3
- package/src/context/context.ts +264 -17
- package/src/context/hook.test.ts +46 -0
- package/src/context/hook.ts +15 -6
- package/src/context/index.ts +1 -0
- package/src/context/pQueue.ts +4 -0
- package/src/crypto/hmac.test.ts +40 -0
- package/src/crypto/hmac.ts +32 -0
- package/src/crypto/index.ts +1 -0
- package/src/docs.ts +22 -0
- package/src/error/api.test.ts +114 -0
- package/src/error/api.ts +236 -0
- package/src/error/axios.ts +220 -0
- package/src/error/base.test.ts +178 -0
- package/src/error/base.ts +108 -2
- package/src/error/declaration.ts +7 -2
- package/src/error/defaults.ts +50 -0
- package/src/error/index.ts +4 -0
- package/src/error/service.ts +170 -0
- package/src/error/types.ts +75 -0
- package/src/error/utils.ts +157 -0
- package/src/file/index.test.ts +36 -0
- package/src/file/index.ts +27 -0
- package/src/index.ts +8 -0
- package/src/logger/logger.test.ts +57 -0
- package/src/logger/logger.ts +72 -11
- package/src/module/resolveDefaultExport.test.ts +32 -0
- package/src/module/resolveDefaultExport.ts +14 -0
- package/src/object/index.test.ts +38 -0
- package/src/object/index.ts +12 -0
- package/src/specification/slate.ts +40 -4
- package/src/specification/specification.ts +10 -6
- package/src/webhook/graph.test.ts +37 -0
- package/src/webhook/graph.ts +22 -0
- package/src/webhook/index.ts +2 -0
- package/src/webhook/meta.test.ts +52 -0
- package/src/webhook/meta.ts +43 -0
- package/dist/action/action.d.ts +0 -98
- package/dist/action/action.d.ts.map +0 -1
- package/dist/action/builder.d.ts +0 -27
- package/dist/action/builder.d.ts.map +0 -1
- package/dist/action/index.d.ts +0 -5
- package/dist/action/index.d.ts.map +0 -1
- package/dist/action/tool.d.ts +0 -14
- package/dist/action/tool.d.ts.map +0 -1
- package/dist/action/trigger.d.ts +0 -32
- package/dist/action/trigger.d.ts.map +0 -1
- package/dist/auth/auth.d.ts +0 -30
- package/dist/auth/auth.d.ts.map +0 -1
- package/dist/auth/index.d.ts +0 -3
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/types.d.ts +0 -148
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/axios/index.d.ts +0 -4
- package/dist/axios/index.d.ts.map +0 -1
- package/dist/config/config.d.ts +0 -26
- package/dist/config/config.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -2
- package/dist/config/index.d.ts.map +0 -1
- package/dist/context/context.d.ts +0 -20
- package/dist/context/context.d.ts.map +0 -1
- package/dist/context/hook.d.ts +0 -4
- package/dist/context/hook.d.ts.map +0 -1
- package/dist/context/index.d.ts +0 -2
- package/dist/context/index.d.ts.map +0 -1
- package/dist/error/base.d.ts +0 -5
- package/dist/error/base.d.ts.map +0 -1
- package/dist/error/declaration.d.ts +0 -6
- package/dist/error/declaration.d.ts.map +0 -1
- package/dist/error/index.d.ts +0 -3
- package/dist/error/index.d.ts.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.modern.js +0 -2
- package/dist/index.modern.js.map +0 -1
- package/dist/index.module.js.map +0 -1
- package/dist/index.umd.js +0 -2
- package/dist/index.umd.js.map +0 -1
- package/dist/logger/index.d.ts +0 -2
- package/dist/logger/index.d.ts.map +0 -1
- package/dist/logger/logger.d.ts +0 -20
- package/dist/logger/logger.d.ts.map +0 -1
- package/dist/specification/index.d.ts +0 -3
- package/dist/specification/index.d.ts.map +0 -1
- package/dist/specification/slate.d.ts +0 -15
- package/dist/specification/slate.d.ts.map +0 -1
- package/dist/specification/specification.d.ts +0 -30
- package/dist/specification/specification.d.ts.map +0 -1
- package/dist/specification/zero.d.ts +0 -13
- package/dist/specification/zero.d.ts.map +0 -1
- package/dist/tokens.d.ts +0 -34
- package/dist/tokens.d.ts.map +0 -1
package/src/config/config.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import z from 'zod';
|
|
1
|
+
import type z from 'zod';
|
|
2
|
+
import type { SlateConfigDocsReference } from '../docs';
|
|
2
3
|
|
|
3
4
|
export class SlateConfig<ConfigType extends {}> {
|
|
4
5
|
#configSchema: z.ZodType<ConfigType>;
|
|
6
|
+
#docs: SlateConfigDocsReference[] | undefined;
|
|
5
7
|
#configChanged:
|
|
6
8
|
| ((params: {
|
|
7
9
|
previousConfig: ConfigType | null;
|
|
8
10
|
newConfig: ConfigType;
|
|
9
|
-
}) => { config: ConfigType | undefined } |
|
|
11
|
+
}) => { config: ConfigType | undefined } | undefined)
|
|
10
12
|
| null = null;
|
|
11
13
|
#getDefaultConfig: (() => ConfigType) | null = null;
|
|
12
14
|
|
|
@@ -25,11 +27,16 @@ export class SlateConfig<ConfigType extends {}> {
|
|
|
25
27
|
return this as any as SlateConfig<NewConfigType>;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
docs(docs: SlateConfigDocsReference[]): SlateConfig<ConfigType> {
|
|
31
|
+
this.#docs = docs;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
onConfigChanged(
|
|
29
36
|
handler: (params: {
|
|
30
37
|
previousConfig: ConfigType | null;
|
|
31
38
|
newConfig: ConfigType;
|
|
32
|
-
}) => { config: ConfigType | undefined } |
|
|
39
|
+
}) => { config: ConfigType | undefined } | undefined
|
|
33
40
|
): SlateConfig<ConfigType> {
|
|
34
41
|
this.#configChanged = handler;
|
|
35
42
|
return this;
|
|
@@ -44,6 +51,10 @@ export class SlateConfig<ConfigType extends {}> {
|
|
|
44
51
|
return this.#configSchema;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
get docsReferences() {
|
|
55
|
+
return this.#docs;
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
get handlers() {
|
|
48
59
|
return {
|
|
49
60
|
configChanged: this.#configChanged,
|
package/src/context/context.ts
CHANGED
|
@@ -1,31 +1,111 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import {
|
|
3
|
+
createBase64Attachment,
|
|
4
|
+
createUrlAttachment,
|
|
5
|
+
type SlateAttachment
|
|
6
|
+
} from '../action/attachment';
|
|
7
|
+
import { type SlateLiveInvocationInfo, uploadAttachmentDirect } from '../action/directUpload';
|
|
8
|
+
import type { SlateHttpTrace } from '../axios/trace';
|
|
9
|
+
import type { SlateLogger, SlateLogMessageInput } from '../logger';
|
|
10
|
+
import type { SlateSpecification } from '../specification/specification';
|
|
11
|
+
import { PQueue } from './pQueue';
|
|
3
12
|
|
|
4
|
-
export
|
|
5
|
-
|
|
13
|
+
export type { SlateLiveInvocationInfo };
|
|
14
|
+
|
|
15
|
+
export type SlateAddAttachmentContent =
|
|
16
|
+
| Buffer
|
|
17
|
+
| Uint8Array
|
|
18
|
+
| ArrayBuffer
|
|
19
|
+
| ReadableStream<Uint8Array>
|
|
20
|
+
| NodeJS.ReadableStream
|
|
21
|
+
| Response;
|
|
22
|
+
|
|
23
|
+
export type SlateAddAttachmentInput =
|
|
24
|
+
| {
|
|
25
|
+
type: 'url';
|
|
26
|
+
url: string | URL;
|
|
27
|
+
mimeType?: string;
|
|
28
|
+
filename?: string;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
query?: Record<string, string>;
|
|
31
|
+
refreshReference?: unknown;
|
|
32
|
+
refreshAt?: string;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
type: 'content';
|
|
36
|
+
content: SlateAddAttachmentContent;
|
|
37
|
+
mimeType?: string;
|
|
38
|
+
filename?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let isBufferLike = (value: unknown): value is Buffer | Uint8Array | ArrayBuffer =>
|
|
42
|
+
value instanceof Uint8Array || value instanceof ArrayBuffer;
|
|
43
|
+
|
|
44
|
+
let toUint8Array = (value: Buffer | Uint8Array | ArrayBuffer) =>
|
|
45
|
+
value instanceof ArrayBuffer ? new Uint8Array(value) : value;
|
|
46
|
+
|
|
47
|
+
let isReadableStream = (value: unknown): value is ReadableStream<Uint8Array> =>
|
|
48
|
+
typeof ReadableStream !== 'undefined' && value instanceof ReadableStream;
|
|
49
|
+
|
|
50
|
+
let isNodeReadable = (value: unknown): value is NodeJS.ReadableStream =>
|
|
51
|
+
!!value &&
|
|
52
|
+
typeof value === 'object' &&
|
|
53
|
+
typeof (value as any).pipe === 'function' &&
|
|
54
|
+
typeof (value as any).on === 'function';
|
|
55
|
+
|
|
56
|
+
let isResponseLike = (value: unknown): value is Response =>
|
|
57
|
+
typeof Response !== 'undefined' && value instanceof Response;
|
|
58
|
+
|
|
59
|
+
let readStreamToBuffer = async (stream: ReadableStream<Uint8Array>): Promise<Buffer> => {
|
|
60
|
+
let reader = stream.getReader();
|
|
61
|
+
let chunks: Uint8Array[] = [];
|
|
62
|
+
|
|
63
|
+
while (true) {
|
|
64
|
+
let { done, value } = await reader.read();
|
|
65
|
+
if (done) break;
|
|
66
|
+
if (value) chunks.push(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Buffer.concat(chunks);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
interface NormalizedStreamInput {
|
|
73
|
+
stream: ReadableStream<Uint8Array>;
|
|
74
|
+
mimeType?: string;
|
|
75
|
+
filename?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class SlatePublicContext<InputType extends {}> {
|
|
6
79
|
#input: InputType;
|
|
7
|
-
#
|
|
80
|
+
#httpTraces: SlateHttpTrace[] = [];
|
|
81
|
+
|
|
82
|
+
#liveInvocation: SlateLiveInvocationInfo | null;
|
|
83
|
+
#attachmentsDisabled = false;
|
|
84
|
+
#attachments: SlateAttachment[] = [];
|
|
85
|
+
#pendingUploads: Promise<void>[] = [];
|
|
86
|
+
#uploadQueue?: InstanceType<typeof PQueue>;
|
|
87
|
+
|
|
88
|
+
#getUploadQueue() {
|
|
89
|
+
if (!this.#uploadQueue) {
|
|
90
|
+
this.#uploadQueue = new PQueue({ concurrency: 10 });
|
|
91
|
+
}
|
|
92
|
+
return this.#uploadQueue;
|
|
93
|
+
}
|
|
8
94
|
|
|
9
95
|
constructor(
|
|
10
|
-
config: ConfigType,
|
|
11
96
|
input: InputType,
|
|
12
|
-
|
|
13
|
-
private readonly
|
|
14
|
-
|
|
97
|
+
private readonly spec: SlateSpecification<any, any>,
|
|
98
|
+
private readonly logger: SlateLogger,
|
|
99
|
+
liveInvocation: SlateLiveInvocationInfo | null = null
|
|
15
100
|
) {
|
|
16
|
-
this.#config = config;
|
|
17
101
|
this.#input = input;
|
|
18
|
-
this.#
|
|
102
|
+
this.#liveInvocation = liveInvocation;
|
|
19
103
|
}
|
|
20
104
|
|
|
21
105
|
get specification() {
|
|
22
106
|
return this.spec;
|
|
23
107
|
}
|
|
24
108
|
|
|
25
|
-
get config() {
|
|
26
|
-
return Object.freeze(this.#config);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
109
|
get input() {
|
|
30
110
|
return Object.freeze(this.#input);
|
|
31
111
|
}
|
|
@@ -46,8 +126,37 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
|
|
|
46
126
|
: never;
|
|
47
127
|
}
|
|
48
128
|
|
|
49
|
-
get
|
|
50
|
-
return Object.freeze(
|
|
129
|
+
get registrationDetails() {
|
|
130
|
+
return Object.freeze(
|
|
131
|
+
(this.#input as any).registrationDetails
|
|
132
|
+
) as 'registrationDetails' extends keyof InputType
|
|
133
|
+
? InputType['registrationDetails']
|
|
134
|
+
: never;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
recordHttpTrace(trace: SlateHttpTrace) {
|
|
138
|
+
this.#httpTraces.push(trace);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getHttpTraces() {
|
|
142
|
+
return this.#httpTraces.map(trace => ({
|
|
143
|
+
...trace,
|
|
144
|
+
request: {
|
|
145
|
+
...trace.request,
|
|
146
|
+
...(trace.request.headers ? { headers: { ...trace.request.headers } } : {}),
|
|
147
|
+
...(trace.request.body ? { body: { ...trace.request.body } } : {})
|
|
148
|
+
},
|
|
149
|
+
...(trace.response
|
|
150
|
+
? {
|
|
151
|
+
response: {
|
|
152
|
+
...trace.response,
|
|
153
|
+
...(trace.response.headers ? { headers: { ...trace.response.headers } } : {}),
|
|
154
|
+
...(trace.response.body ? { body: { ...trace.response.body } } : {})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
: {}),
|
|
158
|
+
...(trace.error ? { error: { ...trace.error } } : {})
|
|
159
|
+
}));
|
|
51
160
|
}
|
|
52
161
|
|
|
53
162
|
info(message: SlateLogMessageInput) {
|
|
@@ -65,4 +174,142 @@ export class SlateContext<ConfigType extends {}, AuthType extends {}, InputType
|
|
|
65
174
|
progress(message: SlateLogMessageInput) {
|
|
66
175
|
this.logger.progress(message);
|
|
67
176
|
}
|
|
177
|
+
|
|
178
|
+
async addAttachment(input: SlateAddAttachmentInput): Promise<void> {
|
|
179
|
+
if (input.type === 'url') {
|
|
180
|
+
this.#attachments.push(
|
|
181
|
+
createUrlAttachment(input.url.toString(), {
|
|
182
|
+
mimeType: input.mimeType,
|
|
183
|
+
headers: input.headers,
|
|
184
|
+
query: input.query,
|
|
185
|
+
refreshReference: input.refreshReference,
|
|
186
|
+
refreshAt: input.refreshAt
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let content = input.content;
|
|
193
|
+
|
|
194
|
+
if (isBufferLike(content)) {
|
|
195
|
+
let bytes = toUint8Array(content);
|
|
196
|
+
this.#attachments.push(
|
|
197
|
+
createBase64Attachment(Buffer.from(bytes).toString('base64'), input.mimeType)
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let normalized = await this.#normalizeStreamInput(content, {
|
|
203
|
+
mimeType: input.mimeType,
|
|
204
|
+
filename: input.filename
|
|
205
|
+
});
|
|
206
|
+
if (!normalized) return;
|
|
207
|
+
|
|
208
|
+
if (!this.#liveInvocation || this.#attachmentsDisabled) {
|
|
209
|
+
let buffer = await readStreamToBuffer(normalized.stream);
|
|
210
|
+
this.#attachments.push(
|
|
211
|
+
createBase64Attachment(buffer.toString('base64'), normalized.mimeType)
|
|
212
|
+
);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let live = this.#liveInvocation;
|
|
217
|
+
let task = this.#getUploadQueue()
|
|
218
|
+
.add(() =>
|
|
219
|
+
uploadAttachmentDirect({
|
|
220
|
+
live,
|
|
221
|
+
mimeType: normalized.mimeType,
|
|
222
|
+
filename: normalized.filename,
|
|
223
|
+
body: normalized.stream
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
.then(attachment => {
|
|
227
|
+
this.#attachments.push(attachment);
|
|
228
|
+
})
|
|
229
|
+
.catch(err => {
|
|
230
|
+
this.#attachmentsDisabled = true;
|
|
231
|
+
this.warn({
|
|
232
|
+
message: `Attachment dropped: direct upload failed (${
|
|
233
|
+
err instanceof Error ? err.message : String(err)
|
|
234
|
+
}).`
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
this.#pendingUploads.push(task);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async #normalizeStreamInput(
|
|
242
|
+
input: ReadableStream<Uint8Array> | NodeJS.ReadableStream | Response,
|
|
243
|
+
opts: { mimeType?: string; filename?: string }
|
|
244
|
+
): Promise<NormalizedStreamInput | null> {
|
|
245
|
+
if (isResponseLike(input)) {
|
|
246
|
+
if (!input.body) {
|
|
247
|
+
this.warn({ message: 'Attachment dropped: response has no body.' });
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
stream: input.body,
|
|
252
|
+
mimeType: opts.mimeType ?? input.headers.get('content-type') ?? undefined,
|
|
253
|
+
filename: opts.filename
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (isReadableStream(input)) {
|
|
258
|
+
return { stream: input, mimeType: opts.mimeType, filename: opts.filename };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (isNodeReadable(input)) {
|
|
262
|
+
return {
|
|
263
|
+
stream: Readable.toWeb(input as any) as unknown as ReadableStream<Uint8Array>,
|
|
264
|
+
mimeType: opts.mimeType,
|
|
265
|
+
filename: opts.filename
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.warn({ message: 'Attachment dropped: unsupported attachment input type.' });
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async _finalizeAttachments(): Promise<SlateAttachment[]> {
|
|
274
|
+
await Promise.allSettled(this.#pendingUploads);
|
|
275
|
+
return this.#attachments;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_getAuthConfigForRedaction(): Record<string, unknown> | undefined {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export class SlateContext<
|
|
284
|
+
ConfigType extends {},
|
|
285
|
+
AuthType extends {},
|
|
286
|
+
InputType extends {}
|
|
287
|
+
> extends SlatePublicContext<InputType> {
|
|
288
|
+
#config: ConfigType;
|
|
289
|
+
#auth: AuthType;
|
|
290
|
+
|
|
291
|
+
constructor(
|
|
292
|
+
config: ConfigType,
|
|
293
|
+
input: InputType,
|
|
294
|
+
auth: AuthType,
|
|
295
|
+
spec: SlateSpecification<ConfigType, AuthType>,
|
|
296
|
+
logger: SlateLogger,
|
|
297
|
+
liveInvocation: SlateLiveInvocationInfo | null = null
|
|
298
|
+
) {
|
|
299
|
+
super(input, spec, logger, liveInvocation);
|
|
300
|
+
this.#config = config;
|
|
301
|
+
this.#auth = auth;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
get config() {
|
|
305
|
+
return Object.freeze(this.#config);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
get auth() {
|
|
309
|
+
return Object.freeze(this.#auth);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
override _getAuthConfigForRedaction(): Record<string, unknown> | undefined {
|
|
313
|
+
return this.#auth as Record<string, unknown>;
|
|
314
|
+
}
|
|
68
315
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { SlateContext } from './context';
|
|
3
|
+
import { runWithContext } from './hook';
|
|
4
|
+
|
|
5
|
+
describe('slate context hook', () => {
|
|
6
|
+
it('constructs a context without requiring p-queue at construction time', () => {
|
|
7
|
+
expect(
|
|
8
|
+
() =>
|
|
9
|
+
new SlateContext(
|
|
10
|
+
{},
|
|
11
|
+
{},
|
|
12
|
+
{},
|
|
13
|
+
{} as any,
|
|
14
|
+
{
|
|
15
|
+
debug: () => {},
|
|
16
|
+
error: () => {},
|
|
17
|
+
info: () => {},
|
|
18
|
+
progress: () => {},
|
|
19
|
+
warn: () => {}
|
|
20
|
+
} as any
|
|
21
|
+
)
|
|
22
|
+
).not.toThrow();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('shares context storage across duplicate module instances', async () => {
|
|
26
|
+
// @ts-expect-error Vite treats the query as a distinct module instance.
|
|
27
|
+
let duplicateHook: typeof import('./hook') = await import('./hook?duplicate-instance');
|
|
28
|
+
let context = new SlateContext(
|
|
29
|
+
{},
|
|
30
|
+
{},
|
|
31
|
+
{},
|
|
32
|
+
{} as any,
|
|
33
|
+
{
|
|
34
|
+
debug: () => {},
|
|
35
|
+
error: () => {},
|
|
36
|
+
info: () => {},
|
|
37
|
+
progress: () => {},
|
|
38
|
+
warn: () => {}
|
|
39
|
+
} as any
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
let result = await runWithContext(context, async () => duplicateHook.getCurrentContext());
|
|
43
|
+
|
|
44
|
+
expect(result).toBe(context);
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/context/hook.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
-
import { SlateContext } from './context';
|
|
2
|
+
import type { SlateContext, SlatePublicContext } from './context';
|
|
3
3
|
|
|
4
|
-
let
|
|
4
|
+
let slateContextStorageKey = Symbol.for('slates.provider.context.asyncLocalStorage');
|
|
5
|
+
let globalWithSlateContext = globalThis as typeof globalThis & {
|
|
6
|
+
[key: symbol]: AsyncLocalStorage<SlatePublicContext<any>> | undefined;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let asyncLocalStorage =
|
|
10
|
+
globalWithSlateContext[slateContextStorageKey] ??
|
|
11
|
+
new AsyncLocalStorage<SlatePublicContext<any>>();
|
|
12
|
+
|
|
13
|
+
globalWithSlateContext[slateContextStorageKey] = asyncLocalStorage;
|
|
5
14
|
|
|
6
|
-
export let runWithContext = <
|
|
7
|
-
context:
|
|
8
|
-
fn: () => Promise<
|
|
9
|
-
) => {
|
|
15
|
+
export let runWithContext = <InputType extends {}, RV>(
|
|
16
|
+
context: SlatePublicContext<InputType>,
|
|
17
|
+
fn: () => Promise<RV>
|
|
18
|
+
): Promise<RV> => {
|
|
10
19
|
return asyncLocalStorage.run(context, fn);
|
|
11
20
|
};
|
|
12
21
|
|
package/src/context/index.ts
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createHmacSignature, verifyHmacSignature } from './hmac';
|
|
3
|
+
|
|
4
|
+
describe('createHmacSignature', () => {
|
|
5
|
+
it('creates prefixed hexadecimal HMAC signatures', () => {
|
|
6
|
+
expect(
|
|
7
|
+
createHmacSignature({
|
|
8
|
+
secret: 'secret',
|
|
9
|
+
payload: 'payload',
|
|
10
|
+
digest: 'hex',
|
|
11
|
+
prefix: 'v0='
|
|
12
|
+
})
|
|
13
|
+
).toBe('v0=b82fcb791acec57859b989b430a826488ce2e479fdf92326bd0a2e8375a42ba4');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('verifyHmacSignature', () => {
|
|
18
|
+
it('verifies prefixed hexadecimal HMAC signatures', () => {
|
|
19
|
+
expect(
|
|
20
|
+
verifyHmacSignature({
|
|
21
|
+
secret: 'secret',
|
|
22
|
+
payload: 'payload',
|
|
23
|
+
signature: 'v0=b82fcb791acec57859b989b430a826488ce2e479fdf92326bd0a2e8375a42ba4',
|
|
24
|
+
digest: 'hex',
|
|
25
|
+
prefix: 'v0='
|
|
26
|
+
})
|
|
27
|
+
).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects incorrect signatures, including signatures of a different length', () => {
|
|
31
|
+
expect(
|
|
32
|
+
verifyHmacSignature({
|
|
33
|
+
secret: 'secret',
|
|
34
|
+
payload: 'payload',
|
|
35
|
+
signature: 'invalid',
|
|
36
|
+
digest: 'base64'
|
|
37
|
+
})
|
|
38
|
+
).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
|
|
3
|
+
export interface CreateHmacSignatureOptions {
|
|
4
|
+
secret: string | Uint8Array;
|
|
5
|
+
payload: string | Uint8Array;
|
|
6
|
+
algorithm?: string;
|
|
7
|
+
digest: 'hex' | 'base64';
|
|
8
|
+
prefix?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface VerifyHmacSignatureOptions extends CreateHmacSignatureOptions {
|
|
12
|
+
signature: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export let createHmacSignature = ({
|
|
16
|
+
secret,
|
|
17
|
+
payload,
|
|
18
|
+
algorithm = 'sha256',
|
|
19
|
+
digest,
|
|
20
|
+
prefix = ''
|
|
21
|
+
}: CreateHmacSignatureOptions) =>
|
|
22
|
+
`${prefix}${createHmac(algorithm, secret).update(payload).digest(digest)}`;
|
|
23
|
+
|
|
24
|
+
export let verifyHmacSignature = (options: VerifyHmacSignatureOptions) => {
|
|
25
|
+
let expected = createHmacSignature(options);
|
|
26
|
+
let expectedBytes = Buffer.from(expected);
|
|
27
|
+
let signatureBytes = Buffer.from(options.signature);
|
|
28
|
+
let lengthsMatch = signatureBytes.length === expectedBytes.length;
|
|
29
|
+
let comparableSignature = lengthsMatch ? signatureBytes : Buffer.alloc(expectedBytes.length);
|
|
30
|
+
|
|
31
|
+
return timingSafeEqual(comparableSignature, expectedBytes) && lengthsMatch;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './hmac';
|
package/src/docs.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SlateDocsReference {
|
|
2
|
+
name: string;
|
|
3
|
+
url: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SlateTypedDocsReference<Type extends string> extends SlateDocsReference {
|
|
7
|
+
type?: Type;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type SlateProviderDocsReference = SlateDocsReference;
|
|
11
|
+
|
|
12
|
+
export type SlateConfigDocsReference = SlateTypedDocsReference<'docs.config.general'>;
|
|
13
|
+
|
|
14
|
+
export type SlateActionDocsReference = SlateTypedDocsReference<'docs.action.general'>;
|
|
15
|
+
|
|
16
|
+
export type SlateAuthDocsReference = SlateTypedDocsReference<
|
|
17
|
+
| 'docs.auth.oauth'
|
|
18
|
+
| 'docs.auth.oauth_scopes'
|
|
19
|
+
| 'docs.auth.token'
|
|
20
|
+
| 'docs.auth.service_account'
|
|
21
|
+
| 'docs.auth.custom'
|
|
22
|
+
>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ServiceError } from '@lowerdeck/error';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { buildApiServiceError, collectApiErrorDetails, createApiServiceError } from './api';
|
|
4
|
+
import { SlateError } from './base';
|
|
5
|
+
|
|
6
|
+
describe('API service error helpers', () => {
|
|
7
|
+
it('collects unique nested API error details', () => {
|
|
8
|
+
let details: string[] = [];
|
|
9
|
+
|
|
10
|
+
collectApiErrorDetails(
|
|
11
|
+
{
|
|
12
|
+
message: 'Invalid request',
|
|
13
|
+
code: 'bad_request',
|
|
14
|
+
errors: [
|
|
15
|
+
{
|
|
16
|
+
detail: 'Missing name'
|
|
17
|
+
},
|
|
18
|
+
'Missing name'
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
details
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(details).toEqual(['Invalid request', 'bad_request', 'Missing name']);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('builds provider API ServiceErrors with upstream metadata', () => {
|
|
28
|
+
let parent = new Error('Request failed');
|
|
29
|
+
(parent as any).response = {
|
|
30
|
+
status: 429,
|
|
31
|
+
statusText: 'Too Many Requests',
|
|
32
|
+
data: {
|
|
33
|
+
title: 'Rate limit',
|
|
34
|
+
details: [{ message: 'Retry later' }]
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let error = buildApiServiceError(parent, {
|
|
39
|
+
providerLabel: 'Example',
|
|
40
|
+
operation: 'list widgets',
|
|
41
|
+
reason: 'example_api_error',
|
|
42
|
+
detailKeys: ['title', 'message'],
|
|
43
|
+
nestedKeys: ['details'],
|
|
44
|
+
extractUpstreamCode: () => 'RATE_LIMITED'
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(error).toBeInstanceOf(ServiceError);
|
|
48
|
+
expect(error.data.message).toBe(
|
|
49
|
+
'Example API list widgets failed: HTTP 429 Too Many Requests: Rate limit - Retry later'
|
|
50
|
+
);
|
|
51
|
+
expect(error.data.reason).toBe('example_api_error');
|
|
52
|
+
expect(error.data.upstreamStatus).toBe(429);
|
|
53
|
+
expect(error.data.upstreamCode).toBe('RATE_LIMITED');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('recovers the upstream status from interceptor-normalized SlateErrors', () => {
|
|
57
|
+
// Runtime axios instances reject with SlateError, not AxiosError: the
|
|
58
|
+
// response interceptor consumes error.response, so the status only
|
|
59
|
+
// survives at data.upstream.status / data.status.
|
|
60
|
+
let parent = new SlateError({
|
|
61
|
+
code: 'not_found',
|
|
62
|
+
message: 'Method not found.',
|
|
63
|
+
upstream: { status: 404 }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let error = buildApiServiceError(parent, {
|
|
67
|
+
providerLabel: 'Example',
|
|
68
|
+
operation: 'search messages',
|
|
69
|
+
reason: 'example_api_error'
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(error.data.upstreamStatus).toBe(404);
|
|
73
|
+
expect(error.data.message).toBe(
|
|
74
|
+
'Example API search messages failed: HTTP 404: Method not found.'
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('allows provider-specific status and message extraction', () => {
|
|
79
|
+
let error = buildApiServiceError(
|
|
80
|
+
{
|
|
81
|
+
status: 503,
|
|
82
|
+
data: {
|
|
83
|
+
message: 'Unavailable'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
providerLabel: 'Example',
|
|
88
|
+
reason: 'example_api_error',
|
|
89
|
+
extractStatus: (input, _response, helpers) =>
|
|
90
|
+
helpers.isRecord(input) && typeof input.status === 'number'
|
|
91
|
+
? input.status
|
|
92
|
+
: undefined,
|
|
93
|
+
extractMessage: (input, helpers) => {
|
|
94
|
+
if (!helpers.isRecord(input) || !helpers.isRecord(input.data)) return undefined;
|
|
95
|
+
return typeof input.data.message === 'string' ? input.data.message : undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(error.data.message).toBe('Example API request failed: HTTP 503: Unavailable');
|
|
101
|
+
expect(error.data.upstreamStatus).toBe(503);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('passes through existing ServiceErrors', () => {
|
|
105
|
+
let existing = createApiServiceError('Already normalized');
|
|
106
|
+
|
|
107
|
+
expect(
|
|
108
|
+
buildApiServiceError(existing, {
|
|
109
|
+
providerLabel: 'Example',
|
|
110
|
+
reason: 'example_api_error'
|
|
111
|
+
})
|
|
112
|
+
).toBe(existing);
|
|
113
|
+
});
|
|
114
|
+
});
|