@jordanalec/dtk 1.0.0
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/LICENSE +21 -0
- package/README.md +730 -0
- package/dist/add.js +89 -0
- package/dist/cli.js +12 -0
- package/dist/init.js +20 -0
- package/dist/utils/patch.js +8 -0
- package/package.json +52 -0
- package/templates/init/.env.template +2 -0
- package/templates/init/GUIDE.md +543 -0
- package/templates/init/README.md +59 -0
- package/templates/init/jest.config.ts +19 -0
- package/templates/init/package.json +22 -0
- package/templates/init/src/lib/auth.test.ts +48 -0
- package/templates/init/src/lib/basic-auth.ts +6 -0
- package/templates/init/src/lib/bearer-token.ts +5 -0
- package/templates/init/src/lib/http.test.ts +197 -0
- package/templates/init/src/lib/http.ts +81 -0
- package/templates/init/src/lib/oauth.test.ts +61 -0
- package/templates/init/src/lib/oauth.ts +15 -0
- package/templates/init/src/lib/token.ts +5 -0
- package/templates/init/src/load-env.ts +4 -0
- package/templates/init/src/runbooks/example.ts +33 -0
- package/templates/init/src/suite.test.ts +94 -0
- package/templates/init/src/suite.ts +70 -0
- package/templates/init/src/types/http.ts +12 -0
- package/templates/init/src/types/oauth.ts +13 -0
- package/templates/init/src/types/suite.ts +37 -0
- package/templates/init/tsconfig.json +14 -0
- package/templates/init/tsconfig.test.json +8 -0
- package/templates/plugins/aws-dynamo/env.txt +2 -0
- package/templates/plugins/aws-dynamo/example.ts +75 -0
- package/templates/plugins/aws-dynamo/plugin.json +38 -0
- package/templates/plugins/aws-dynamo/service.test.ts +180 -0
- package/templates/plugins/aws-dynamo/service.ts +73 -0
- package/templates/plugins/aws-dynamo/types.ts +29 -0
- package/templates/plugins/aws-s3/env.txt +2 -0
- package/templates/plugins/aws-s3/example.ts +41 -0
- package/templates/plugins/aws-s3/plugin.json +38 -0
- package/templates/plugins/aws-s3/service.test.ts +150 -0
- package/templates/plugins/aws-s3/service.ts +43 -0
- package/templates/plugins/aws-s3/types.ts +28 -0
- package/templates/plugins/aws-sns/env.txt +2 -0
- package/templates/plugins/aws-sns/example.ts +18 -0
- package/templates/plugins/aws-sns/plugin.json +37 -0
- package/templates/plugins/aws-sns/service.test.ts +79 -0
- package/templates/plugins/aws-sns/service.ts +28 -0
- package/templates/plugins/aws-sns/types.ts +8 -0
- package/templates/plugins/aws-sqs/env.txt +2 -0
- package/templates/plugins/aws-sqs/example.ts +16 -0
- package/templates/plugins/aws-sqs/plugin.json +37 -0
- package/templates/plugins/aws-sqs/service.test.ts +63 -0
- package/templates/plugins/aws-sqs/service.ts +27 -0
- package/templates/plugins/aws-sqs/types.ts +8 -0
- package/templates/plugins/open-ai/env.txt +1 -0
- package/templates/plugins/open-ai/example.ts +27 -0
- package/templates/plugins/open-ai/plugin.json +36 -0
- package/templates/plugins/open-ai/service.test.ts +55 -0
- package/templates/plugins/open-ai/service.ts +26 -0
- package/templates/plugins/open-ai/types.ts +61 -0
- package/templates/plugins/tsconfig.json +11 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# Project Guide
|
|
2
|
+
|
|
3
|
+
Everything you need to know about working with this project, extending it, and adding new services.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [How it works](#how-it-works)
|
|
10
|
+
2. [Environment variables](#environment-variables)
|
|
11
|
+
3. [Writing a runbook](#writing-a-runbook)
|
|
12
|
+
4. [Auth patterns](#auth-patterns)
|
|
13
|
+
5. [Passing data between steps](#passing-data-between-steps)
|
|
14
|
+
6. [Adding a plugin](#adding-a-plugin)
|
|
15
|
+
7. [Available plugins](#available-plugins)
|
|
16
|
+
8. [Writing a custom service](#writing-a-custom-service)
|
|
17
|
+
9. [File reference](#file-reference)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
Each runbook creates a suite, configures auth and services via builder methods, chains steps, then calls `.run()`.
|
|
24
|
+
|
|
25
|
+
Steps execute in sequence. Each step receives a shared context (`ctx`) containing auth helpers, an HTTP client, and any wired service instances. The return value of each step is stored in `ctx.outputs` and available to all subsequent steps.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import "../load-env.js";
|
|
29
|
+
import { suite } from "../suite.js";
|
|
30
|
+
|
|
31
|
+
await suite()
|
|
32
|
+
.oauth({ clientId, clientSecret, tokenUrl })
|
|
33
|
+
.step("get-token", async (ctx) => {
|
|
34
|
+
return ctx.auth.clientCredentials();
|
|
35
|
+
})
|
|
36
|
+
.step("use-token", async (ctx) => {
|
|
37
|
+
const token = ctx.outputs["get-token"] as { access_token: string };
|
|
38
|
+
return ctx.http.get("https://api.example.com/data", {
|
|
39
|
+
headers: { Authorization: `Bearer ${token.access_token}` },
|
|
40
|
+
});
|
|
41
|
+
})
|
|
42
|
+
.run("throwOnError");
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Run options
|
|
46
|
+
|
|
47
|
+
| Option | Behaviour |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `"throwOnError"` | Stops on first failure and throws |
|
|
50
|
+
| `"stopOnError"` | Logs the failure and stops without throwing |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Environment variables
|
|
55
|
+
|
|
56
|
+
Copy `.env.template` to `.env` and fill in values. `.env.local` overrides `.env` if present. Neither file should be committed -- both are in `.gitignore`.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cp .env.template .env
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`load-env.ts` handles loading. Import it as the first line of every runbook:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import "../load-env.js";
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Writing a runbook
|
|
71
|
+
|
|
72
|
+
Create a `.ts` file in `src/runbooks/`. Import `load-env.js` first, then import `suite` from `suite.js`.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import "../load-env.js";
|
|
76
|
+
import { suite } from "../suite.js";
|
|
77
|
+
|
|
78
|
+
await suite()
|
|
79
|
+
.step("fetch", async (ctx) => {
|
|
80
|
+
const data = await ctx.http.get<{ name: string }>("https://api.example.com/thing/1");
|
|
81
|
+
console.log(data.name);
|
|
82
|
+
return data;
|
|
83
|
+
})
|
|
84
|
+
.run("throwOnError");
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Add a script to `package.json` so you can run it with `npm run`:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
"runbook:my-workflow": "tsx src/runbooks/my-workflow.ts"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or run it directly:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx tsx src/runbooks/my-workflow.ts
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Auth patterns
|
|
102
|
+
|
|
103
|
+
### OAuth client credentials
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
await suite()
|
|
107
|
+
.oauth({
|
|
108
|
+
clientId: process.env.CLIENT_ID!,
|
|
109
|
+
clientSecret: process.env.CLIENT_SECRET!,
|
|
110
|
+
tokenUrl: process.env.TOKEN_URL!,
|
|
111
|
+
scope: "openid", // optional
|
|
112
|
+
})
|
|
113
|
+
.step("fetch", async (ctx) => {
|
|
114
|
+
const token = await ctx.auth.clientCredentials();
|
|
115
|
+
return ctx.http.get("https://api.example.com/data", {
|
|
116
|
+
headers: { Authorization: `Bearer ${token.access_token}` },
|
|
117
|
+
});
|
|
118
|
+
})
|
|
119
|
+
.run("throwOnError");
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Basic auth
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
await suite()
|
|
126
|
+
.basicAuth({
|
|
127
|
+
username: process.env.API_USER!,
|
|
128
|
+
password: process.env.API_PASS!,
|
|
129
|
+
})
|
|
130
|
+
.step("fetch", async (ctx) => {
|
|
131
|
+
const header = await ctx.auth.basicAuth();
|
|
132
|
+
return ctx.http.get("https://api.example.com/data", {
|
|
133
|
+
headers: { Authorization: header },
|
|
134
|
+
});
|
|
135
|
+
})
|
|
136
|
+
.run("throwOnError");
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Bearer token
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
await suite()
|
|
143
|
+
.bearerToken({
|
|
144
|
+
token: process.env.API_TOKEN!,
|
|
145
|
+
prefix: "Bearer",
|
|
146
|
+
})
|
|
147
|
+
.step("fetch", async (ctx) => {
|
|
148
|
+
const header = await ctx.auth.bearerToken();
|
|
149
|
+
return ctx.http.get("https://api.example.com/data", {
|
|
150
|
+
headers: { Authorization: header },
|
|
151
|
+
});
|
|
152
|
+
})
|
|
153
|
+
.run("throwOnError");
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### JWT claim extraction
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const claims = ctx.auth.getClaimValues(token.access_token);
|
|
160
|
+
console.log(claims.sub);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Passing data between steps
|
|
166
|
+
|
|
167
|
+
Every step's return value is stored in `ctx.outputs` under the step name. Cast to the type you expect:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
await suite()
|
|
171
|
+
.step("get-user", async (ctx) => {
|
|
172
|
+
return ctx.http.get<{ id: number; name: string }>("https://api.example.com/user/1");
|
|
173
|
+
})
|
|
174
|
+
.step("get-orders", async (ctx) => {
|
|
175
|
+
const user = ctx.outputs["get-user"] as { id: number; name: string };
|
|
176
|
+
return ctx.http.get(`https://api.example.com/orders?userId=${user.id}`);
|
|
177
|
+
})
|
|
178
|
+
.run("throwOnError");
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Adding a plugin
|
|
184
|
+
|
|
185
|
+
Requires the dtk CLI to be installed globally. From this project directory:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
dtk add <plugin>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This will automatically:
|
|
192
|
+
- Copy the service and types files into `src/`
|
|
193
|
+
- Patch `src/suite.ts` to wire the service in
|
|
194
|
+
- Patch `src/types/suite.ts` to add the service types to the step context
|
|
195
|
+
- Append the required env vars to `.env.template`
|
|
196
|
+
- Create an example runbook in `src/runbooks/`
|
|
197
|
+
- Add a `runbook:<plugin>` script to `package.json`
|
|
198
|
+
- Run `npm install` for any required dependencies
|
|
199
|
+
|
|
200
|
+
Running `dtk add` on an already-added plugin is safe -- nothing is duplicated.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Available plugins
|
|
205
|
+
|
|
206
|
+
### aws-sqs
|
|
207
|
+
|
|
208
|
+
Sends messages to an AWS SQS queue.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
dtk add aws-sqs
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Required env vars:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
SQS_QUEUE_URL=
|
|
218
|
+
AWS_REGION=
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Usage:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
await suite()
|
|
225
|
+
.sqs({
|
|
226
|
+
queueUrl: process.env.SQS_QUEUE_URL!,
|
|
227
|
+
region: process.env.AWS_REGION!,
|
|
228
|
+
})
|
|
229
|
+
.step("send", async (ctx) => {
|
|
230
|
+
const result = await ctx.services.sqs.sendMessage("hello world");
|
|
231
|
+
console.log("messageId:", result.messageId);
|
|
232
|
+
return result;
|
|
233
|
+
})
|
|
234
|
+
.run("throwOnError");
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
AWS credentials are picked up automatically from `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in your environment.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### aws-sns
|
|
242
|
+
|
|
243
|
+
Publishes messages to an AWS SNS topic.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
dtk add aws-sns
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Required env vars:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
SNS_TOPIC_ARN=
|
|
253
|
+
AWS_REGION=
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Usage:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
await suite()
|
|
260
|
+
.sns({
|
|
261
|
+
topicArn: process.env.SNS_TOPIC_ARN!,
|
|
262
|
+
region: process.env.AWS_REGION!,
|
|
263
|
+
})
|
|
264
|
+
.step("publish", async (ctx) => {
|
|
265
|
+
const result = await ctx.services.sns.publish("hello world", "optional subject");
|
|
266
|
+
console.log("messageId:", result.messageId);
|
|
267
|
+
return result;
|
|
268
|
+
})
|
|
269
|
+
.run("throwOnError");
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### aws-dynamo
|
|
275
|
+
|
|
276
|
+
Reads and writes items in AWS DynamoDB tables.
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
dtk add aws-dynamo
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Required env vars:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
AWS_REGION=
|
|
286
|
+
DYNAMO_TABLE_NAME=
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Usage:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
await suite()
|
|
293
|
+
.dynamo({ region: process.env.AWS_REGION! })
|
|
294
|
+
.step("put", async (ctx) => {
|
|
295
|
+
return ctx.services.dynamo.putItem(process.env.DYNAMO_TABLE_NAME!, {
|
|
296
|
+
id: "user-123",
|
|
297
|
+
name: "John Doe",
|
|
298
|
+
});
|
|
299
|
+
})
|
|
300
|
+
.step("get", async (ctx) => {
|
|
301
|
+
return ctx.services.dynamo.getItem(process.env.DYNAMO_TABLE_NAME!, { id: "user-123" });
|
|
302
|
+
})
|
|
303
|
+
.step("update", async (ctx) => {
|
|
304
|
+
return ctx.services.dynamo.updateItem(
|
|
305
|
+
process.env.DYNAMO_TABLE_NAME!,
|
|
306
|
+
{ id: "user-123" },
|
|
307
|
+
{
|
|
308
|
+
UpdateExpression: "SET #n = :n",
|
|
309
|
+
ExpressionAttributeNames: { "#n": "name" },
|
|
310
|
+
ExpressionAttributeValues: { ":n": { S: "Jane Doe" } },
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
})
|
|
314
|
+
.step("query", async (ctx) => {
|
|
315
|
+
return ctx.services.dynamo.queryItems(process.env.DYNAMO_TABLE_NAME!, {
|
|
316
|
+
KeyConditionExpression: "#pk = :pk",
|
|
317
|
+
ExpressionAttributeNames: { "#pk": "id" },
|
|
318
|
+
ExpressionAttributeValues: { ":pk": { S: "user-123" } },
|
|
319
|
+
});
|
|
320
|
+
})
|
|
321
|
+
.step("scan", async (ctx) => {
|
|
322
|
+
return ctx.services.dynamo.scanItems(process.env.DYNAMO_TABLE_NAME!, { Limit: 10 });
|
|
323
|
+
})
|
|
324
|
+
.step("delete", async (ctx) => {
|
|
325
|
+
return ctx.services.dynamo.deleteItem(process.env.DYNAMO_TABLE_NAME!, { id: "user-123" });
|
|
326
|
+
})
|
|
327
|
+
.run("throwOnError");
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
AWS credentials are picked up automatically from `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in your environment.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### aws-s3
|
|
335
|
+
|
|
336
|
+
Uploads files, downloads files, and generates presigned URLs for AWS S3.
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
dtk add aws-s3
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Required env vars:
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
AWS_REGION=
|
|
346
|
+
S3_BUCKET_NAME=
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Usage:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
await suite()
|
|
353
|
+
.s3({ region: process.env.AWS_REGION! })
|
|
354
|
+
.step("upload", async (ctx) => {
|
|
355
|
+
return ctx.services.s3.uploadFile(
|
|
356
|
+
process.env.S3_BUCKET_NAME!,
|
|
357
|
+
"uploads/example.txt",
|
|
358
|
+
"./example.txt",
|
|
359
|
+
{ contentType: "text/plain", metadata: { source: "my-runbook" } }
|
|
360
|
+
);
|
|
361
|
+
})
|
|
362
|
+
.step("presign", async (ctx) => {
|
|
363
|
+
const result = await ctx.services.s3.getPresignedUrl(
|
|
364
|
+
process.env.S3_BUCKET_NAME!,
|
|
365
|
+
"uploads/example.txt",
|
|
366
|
+
300
|
|
367
|
+
);
|
|
368
|
+
console.log("url:", result.url);
|
|
369
|
+
return result;
|
|
370
|
+
})
|
|
371
|
+
.step("download", async (ctx) => {
|
|
372
|
+
return ctx.services.s3.downloadFile(
|
|
373
|
+
process.env.S3_BUCKET_NAME!,
|
|
374
|
+
"uploads/example.txt",
|
|
375
|
+
"./downloaded.txt"
|
|
376
|
+
);
|
|
377
|
+
})
|
|
378
|
+
.run("throwOnError");
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
AWS credentials are picked up automatically from `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in your environment.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
### open-ai
|
|
386
|
+
|
|
387
|
+
Lists models and sends responses via the OpenAI API.
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
dtk add open-ai
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Required env vars:
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
OPENAI_API_KEY=
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Usage:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
await suite()
|
|
403
|
+
.openAi({ baseUrl: "https://api.openai.com" })
|
|
404
|
+
.step("respond", async (ctx) => {
|
|
405
|
+
const token = `Bearer ${process.env.OPENAI_API_KEY!}`;
|
|
406
|
+
return ctx.services.openAi.response(token, "gpt-4o-mini", "text", "Say hello.");
|
|
407
|
+
})
|
|
408
|
+
.run("throwOnError");
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Writing a custom service
|
|
414
|
+
|
|
415
|
+
If there is no plugin for the service you need, wire one in manually. Four files are involved.
|
|
416
|
+
|
|
417
|
+
### 1. Create the service factory
|
|
418
|
+
|
|
419
|
+
`src/services/my-service.ts`:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import { httpGet, httpPost, httpPut, httpDelete } from "../lib/http.js";
|
|
423
|
+
|
|
424
|
+
export interface MyServiceConfig {
|
|
425
|
+
baseUrl: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function createMyService(config?: MyServiceConfig) {
|
|
429
|
+
return {
|
|
430
|
+
getItem: async (id: number): Promise<{ id: number; name: string }> => {
|
|
431
|
+
return httpGet(`${config!.baseUrl}/items/${id}`);
|
|
432
|
+
},
|
|
433
|
+
createItem: async (name: string): Promise<{ id: number }> => {
|
|
434
|
+
return httpPost(`${config!.baseUrl}/items`, { name });
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### 2. Add the type shape to `src/types/suite.ts`
|
|
441
|
+
|
|
442
|
+
Add your config type and service shape. Place them above the `// dtk:service-types` sentinel:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
export interface MyServiceConfig { baseUrl: string; }
|
|
446
|
+
|
|
447
|
+
// inside StepContext.services:
|
|
448
|
+
services: {
|
|
449
|
+
myService: {
|
|
450
|
+
getItem(id: number): Promise<{ id: number; name: string }>;
|
|
451
|
+
createItem(name: string): Promise<{ id: number }>;
|
|
452
|
+
};
|
|
453
|
+
// dtk:service-types <-- do not remove this line
|
|
454
|
+
};
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 3. Wire the service into `src/suite.ts`
|
|
458
|
+
|
|
459
|
+
Add the import before `// dtk:imports`:
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
import { createMyService } from "./services/my-service.js";
|
|
463
|
+
import type { MyServiceConfig } from "./services/my-service.js";
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Add a private field before `// dtk:configs`:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
private myServiceConfig?: MyServiceConfig;
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Add a builder method before `// dtk:methods`:
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
myService(config: MyServiceConfig): this { this.myServiceConfig = config; return this; }
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Add the service instance before `// dtk:services`:
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
myService: createMyService(this.myServiceConfig),
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### 4. Use it in a runbook
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
await suite()
|
|
488
|
+
.myService({ baseUrl: process.env.MY_SERVICE_BASE_URL! })
|
|
489
|
+
.step("get-item", async (ctx) => {
|
|
490
|
+
const item = await ctx.services.myService.getItem(1);
|
|
491
|
+
console.log(item.name);
|
|
492
|
+
return item;
|
|
493
|
+
})
|
|
494
|
+
.run("throwOnError");
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## File reference
|
|
500
|
+
|
|
501
|
+
### `src/suite.ts`
|
|
502
|
+
|
|
503
|
+
The core of the project. The `TestSuite` class holds service configs, collects steps, builds the step context, and runs steps in sequence. The sentinel comments (`// dtk:imports`, `// dtk:configs`, `// dtk:methods`, `// dtk:services`) are injection points used by `dtk add` -- do not remove them.
|
|
504
|
+
|
|
505
|
+
### `src/types/suite.ts`
|
|
506
|
+
|
|
507
|
+
All shared type definitions: `StepContext`, `StepFn`, `Step`, the `SuiteRunOption` string union, and auth config types. The sentinels (`// dtk:type-imports`, `// dtk:service-types`) are also injection points -- do not remove them.
|
|
508
|
+
|
|
509
|
+
### `src/types/oauth.ts`
|
|
510
|
+
|
|
511
|
+
`OAuthConfig` and `TokenResponse` -- the input and output types for the OAuth client credentials flow.
|
|
512
|
+
|
|
513
|
+
### `src/lib/http.ts`
|
|
514
|
+
|
|
515
|
+
Axios wrapper. Provides `httpGet`, `httpPost`, `httpPut`, and `httpDelete`. Normalises errors into plain `Error` objects with readable messages. Use this inside service factories instead of calling axios directly.
|
|
516
|
+
|
|
517
|
+
### `src/lib/oauth.ts`
|
|
518
|
+
|
|
519
|
+
Implements the OAuth 2.0 client credentials flow. Posts to the token URL with `application/x-www-form-urlencoded` and returns a `TokenResponse`.
|
|
520
|
+
|
|
521
|
+
### `src/lib/basic-auth.ts`
|
|
522
|
+
|
|
523
|
+
Base64-encodes `username:password` and returns a `Basic <token>` string ready to use as an `Authorization` header.
|
|
524
|
+
|
|
525
|
+
### `src/lib/bearer-token.ts`
|
|
526
|
+
|
|
527
|
+
Returns `<prefix> <token>` (e.g. `Bearer sk-abc123`) ready to use as an `Authorization` header.
|
|
528
|
+
|
|
529
|
+
### `src/lib/token.ts`
|
|
530
|
+
|
|
531
|
+
`getClaimValues(token)` decodes the payload of a JWT and returns the claims as a plain object. Does not verify the signature.
|
|
532
|
+
|
|
533
|
+
### `src/load-env.ts`
|
|
534
|
+
|
|
535
|
+
Loads `.env` then `.env.local` (local overrides). Import this as the very first line of every runbook.
|
|
536
|
+
|
|
537
|
+
### `.env.template`
|
|
538
|
+
|
|
539
|
+
Lists all environment variables the project needs. Copy to `.env` and fill in real values. Never commit `.env`.
|
|
540
|
+
|
|
541
|
+
### `tsconfig.json`
|
|
542
|
+
|
|
543
|
+
TypeScript config. `module: NodeNext` means all imports must use `.js` extensions even though the source files are `.ts`. `strict: true` is enabled.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# My dtk Project
|
|
2
|
+
|
|
3
|
+
A TypeScript runbook project for orchestrating multi-step API workflows. Uses a fluent builder API to chain authentication, HTTP calls, and service interactions into readable, repeatable scripts.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
cp .env.template .env
|
|
12
|
+
# fill in .env with your credentials
|
|
13
|
+
npm run runbook:example
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Running Runbooks
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run runbook:example # unauthenticated HTTP call (GitHub API)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Add your own runbook scripts to `package.json` as you create them:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
"runbook:my-workflow": "tsx src/runbooks/my-workflow.ts"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Adding a Plugin
|
|
33
|
+
|
|
34
|
+
Install the dtk CLI if you haven't already, then run from this directory:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dtk add <plugin>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Available plugins: `aws-sqs`, `aws-sns`, `aws-dynamo`, `aws-s3`, `open-ai`
|
|
41
|
+
|
|
42
|
+
Each plugin adds a service, wires it into the suite, appends env vars to `.env.template`, creates an example runbook, and installs dependencies automatically.
|
|
43
|
+
|
|
44
|
+
See `GUIDE.md` for full details.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Project Structure
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
src/
|
|
52
|
+
suite.ts # core runner -- extend this with custom services
|
|
53
|
+
load-env.ts # dotenv bootstrap -- import first in every runbook
|
|
54
|
+
lib/ # HTTP client, OAuth, auth helpers
|
|
55
|
+
types/ # type definitions
|
|
56
|
+
services/ # service factories added by plugins or custom code
|
|
57
|
+
runbooks/ # your runbook scripts
|
|
58
|
+
.env.template # list of required env vars -- copy to .env
|
|
59
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Config } from 'jest';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
preset: 'ts-jest',
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
moduleNameMapper: {
|
|
7
|
+
// source files use .js extensions in imports; map them to .ts for jest
|
|
8
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
9
|
+
},
|
|
10
|
+
transform: {
|
|
11
|
+
'^.+\\.ts$': ['ts-jest', {
|
|
12
|
+
tsconfig: 'tsconfig.test.json',
|
|
13
|
+
diagnostics: { ignoreCodes: [151002] },
|
|
14
|
+
}],
|
|
15
|
+
},
|
|
16
|
+
testMatch: ['**/*.test.ts'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default config;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-dtk-project",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"runbook:example": "tsx src/runbooks/example.ts",
|
|
7
|
+
"test": "node node_modules/jest/bin/jest.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"axios": "^1.6.0",
|
|
11
|
+
"dotenv": "^16.4.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/jest": "^29.0.0",
|
|
15
|
+
"@types/node": "^20.0.0",
|
|
16
|
+
"jest": "^29.0.0",
|
|
17
|
+
"ts-jest": "^29.0.0",
|
|
18
|
+
"ts-node": "^10.0.0",
|
|
19
|
+
"tsx": "^4.0.0",
|
|
20
|
+
"typescript": "^5.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { basicAuth } from './basic-auth.js';
|
|
2
|
+
import { bearerToken } from './bearer-token.js';
|
|
3
|
+
import { getClaimValues } from './token.js';
|
|
4
|
+
|
|
5
|
+
describe('basicAuth', () => {
|
|
6
|
+
it('returns a Base64-encoded Basic auth header', async () => {
|
|
7
|
+
const result = await basicAuth({ username: 'user', password: 'pass' });
|
|
8
|
+
const expected = `Basic ${Buffer.from('user:pass').toString('base64')}`;
|
|
9
|
+
expect(result).toBe(expected);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('correctly encodes special characters in credentials', async () => {
|
|
13
|
+
const result = await basicAuth({ username: 'user@domain.com', password: 'p@ss:word!' });
|
|
14
|
+
const decoded = Buffer.from(result.replace('Basic ', ''), 'base64').toString('utf-8');
|
|
15
|
+
expect(decoded).toBe('user@domain.com:p@ss:word!');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('bearerToken', () => {
|
|
20
|
+
it('combines prefix and token with a space', async () => {
|
|
21
|
+
const result = await bearerToken({ token: 'abc123', prefix: 'Bearer' });
|
|
22
|
+
expect(result).toBe('Bearer abc123');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('uses whatever prefix is supplied', async () => {
|
|
26
|
+
const result = await bearerToken({ token: 'tok', prefix: 'Token' });
|
|
27
|
+
expect(result).toBe('Token tok');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('getClaimValues', () => {
|
|
32
|
+
function makeJwt(payload: object): string {
|
|
33
|
+
const encoded = Buffer.from(JSON.stringify(payload)).toString('base64');
|
|
34
|
+
return `header.${encoded}.signature`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it('decodes the payload section of a JWT', () => {
|
|
38
|
+
const claims = getClaimValues(makeJwt({ sub: 'user-123', name: 'Test User' }));
|
|
39
|
+
expect(claims.sub).toBe('user-123');
|
|
40
|
+
expect(claims.name).toBe('Test User');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns all claims present in the token', () => {
|
|
44
|
+
const payload = { sub: '1', role: 'admin', org: 'acme', exp: '9999999999' };
|
|
45
|
+
const claims = getClaimValues(makeJwt(payload));
|
|
46
|
+
expect(claims).toMatchObject(payload);
|
|
47
|
+
});
|
|
48
|
+
});
|