@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +730 -0
  3. package/dist/add.js +89 -0
  4. package/dist/cli.js +12 -0
  5. package/dist/init.js +20 -0
  6. package/dist/utils/patch.js +8 -0
  7. package/package.json +52 -0
  8. package/templates/init/.env.template +2 -0
  9. package/templates/init/GUIDE.md +543 -0
  10. package/templates/init/README.md +59 -0
  11. package/templates/init/jest.config.ts +19 -0
  12. package/templates/init/package.json +22 -0
  13. package/templates/init/src/lib/auth.test.ts +48 -0
  14. package/templates/init/src/lib/basic-auth.ts +6 -0
  15. package/templates/init/src/lib/bearer-token.ts +5 -0
  16. package/templates/init/src/lib/http.test.ts +197 -0
  17. package/templates/init/src/lib/http.ts +81 -0
  18. package/templates/init/src/lib/oauth.test.ts +61 -0
  19. package/templates/init/src/lib/oauth.ts +15 -0
  20. package/templates/init/src/lib/token.ts +5 -0
  21. package/templates/init/src/load-env.ts +4 -0
  22. package/templates/init/src/runbooks/example.ts +33 -0
  23. package/templates/init/src/suite.test.ts +94 -0
  24. package/templates/init/src/suite.ts +70 -0
  25. package/templates/init/src/types/http.ts +12 -0
  26. package/templates/init/src/types/oauth.ts +13 -0
  27. package/templates/init/src/types/suite.ts +37 -0
  28. package/templates/init/tsconfig.json +14 -0
  29. package/templates/init/tsconfig.test.json +8 -0
  30. package/templates/plugins/aws-dynamo/env.txt +2 -0
  31. package/templates/plugins/aws-dynamo/example.ts +75 -0
  32. package/templates/plugins/aws-dynamo/plugin.json +38 -0
  33. package/templates/plugins/aws-dynamo/service.test.ts +180 -0
  34. package/templates/plugins/aws-dynamo/service.ts +73 -0
  35. package/templates/plugins/aws-dynamo/types.ts +29 -0
  36. package/templates/plugins/aws-s3/env.txt +2 -0
  37. package/templates/plugins/aws-s3/example.ts +41 -0
  38. package/templates/plugins/aws-s3/plugin.json +38 -0
  39. package/templates/plugins/aws-s3/service.test.ts +150 -0
  40. package/templates/plugins/aws-s3/service.ts +43 -0
  41. package/templates/plugins/aws-s3/types.ts +28 -0
  42. package/templates/plugins/aws-sns/env.txt +2 -0
  43. package/templates/plugins/aws-sns/example.ts +18 -0
  44. package/templates/plugins/aws-sns/plugin.json +37 -0
  45. package/templates/plugins/aws-sns/service.test.ts +79 -0
  46. package/templates/plugins/aws-sns/service.ts +28 -0
  47. package/templates/plugins/aws-sns/types.ts +8 -0
  48. package/templates/plugins/aws-sqs/env.txt +2 -0
  49. package/templates/plugins/aws-sqs/example.ts +16 -0
  50. package/templates/plugins/aws-sqs/plugin.json +37 -0
  51. package/templates/plugins/aws-sqs/service.test.ts +63 -0
  52. package/templates/plugins/aws-sqs/service.ts +27 -0
  53. package/templates/plugins/aws-sqs/types.ts +8 -0
  54. package/templates/plugins/open-ai/env.txt +1 -0
  55. package/templates/plugins/open-ai/example.ts +27 -0
  56. package/templates/plugins/open-ai/plugin.json +36 -0
  57. package/templates/plugins/open-ai/service.test.ts +55 -0
  58. package/templates/plugins/open-ai/service.ts +26 -0
  59. package/templates/plugins/open-ai/types.ts +61 -0
  60. 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
+ });
@@ -0,0 +1,6 @@
1
+ import type { BasicAuthConfig } from "../types/suite.js";
2
+
3
+ export async function basicAuth(config: BasicAuthConfig): Promise<string> {
4
+ const token = Buffer.from(`${config!.username}:${config!.password}`).toString("base64");
5
+ return `Basic ${token}`;
6
+ }
@@ -0,0 +1,5 @@
1
+ import type { BearerTokenConfig } from "../types/suite.js";
2
+
3
+ export async function bearerToken(config: BearerTokenConfig): Promise<string> {
4
+ return `${config.prefix} ${config.token}`;
5
+ }