@felixgeelhaar/jira-sdk 0.2.0 → 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 (46) hide show
  1. package/README.md +214 -374
  2. package/dist/auth/index.cjs +400 -0
  3. package/dist/auth/index.cjs.map +1 -0
  4. package/dist/auth/index.d.cts +192 -0
  5. package/dist/auth/index.d.ts +192 -0
  6. package/dist/auth/index.js +386 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/errors/index.cjs +272 -0
  9. package/dist/errors/index.cjs.map +1 -0
  10. package/dist/errors/index.d.cts +203 -0
  11. package/dist/errors/index.d.ts +203 -0
  12. package/dist/errors/index.js +254 -0
  13. package/dist/errors/index.js.map +1 -0
  14. package/dist/http-client-BSzRYQZa.d.cts +317 -0
  15. package/dist/http-client-erRvYNs-.d.ts +317 -0
  16. package/dist/index.cjs +9582 -13466
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +1242 -76
  19. package/dist/index.d.ts +1242 -76
  20. package/dist/index.js +9166 -13318
  21. package/dist/index.js.map +1 -1
  22. package/dist/schemas/index.cjs +2481 -13092
  23. package/dist/schemas/index.cjs.map +1 -1
  24. package/dist/schemas/index.d.cts +335 -207
  25. package/dist/schemas/index.d.ts +335 -207
  26. package/dist/schemas/index.js +2138 -13047
  27. package/dist/schemas/index.js.map +1 -1
  28. package/dist/services/index.cjs +6375 -13323
  29. package/dist/services/index.cjs.map +1 -1
  30. package/dist/services/index.d.cts +3852 -299
  31. package/dist/services/index.d.ts +3852 -299
  32. package/dist/services/index.js +6350 -13321
  33. package/dist/services/index.js.map +1 -1
  34. package/dist/transport/index.cjs +1016 -0
  35. package/dist/transport/index.cjs.map +1 -0
  36. package/dist/transport/index.d.cts +372 -0
  37. package/dist/transport/index.d.ts +372 -0
  38. package/dist/transport/index.js +997 -0
  39. package/dist/transport/index.js.map +1 -0
  40. package/dist/types-E6djPHpW.d.cts +95 -0
  41. package/dist/types-E6djPHpW.d.ts +95 -0
  42. package/dist/webhook-Bn8gme6Y.d.cts +6959 -0
  43. package/dist/webhook-Bn8gme6Y.d.ts +6959 -0
  44. package/package.json +73 -27
  45. package/dist/project-BtUx-eSv.d.cts +0 -1480
  46. package/dist/project-BtUx-eSv.d.ts +0 -1480
package/README.md CHANGED
@@ -1,17 +1,21 @@
1
- # @felixgeelhaar/jira-sdk
1
+ # Jira SDK TypeScript
2
2
 
3
- Type-safe TypeScript SDK for the Atlassian Jira REST API with Zod validation and built-in resilience.
3
+ [![security](https://raw.githubusercontent.com/felixgeelhaar/jirasdk-ts/main/.github/badges/security.svg)](https://github.com/felixgeelhaar/jirasdk-ts)
4
+
5
+ A type-safe, production-ready SDK for the Jira REST API with built-in resilience patterns.
4
6
 
5
7
  ## Features
6
8
 
7
- - **Type-Safe**: Full TypeScript support with strict mode
9
+ - **Type-Safe**: Full TypeScript support with strict mode enabled
8
10
  - **Runtime Validation**: Zod schemas for request/response validation
9
- - **Resilience Built-In**: Circuit breaker, retry, rate limiting
10
- - **Multiple Auth Methods**: API Token, PAT, Basic Auth, OAuth 2.0
11
- - **Middleware Architecture**: Extensible request/response pipeline
11
+ - **Resilience Built-In**: Circuit breaker, retry with exponential backoff, rate limiting
12
+ - **Multiple Auth Methods**: API Token, Personal Access Token, Basic Auth, OAuth 2.0
13
+ - **Middleware Architecture**: Extensible request/response processing pipeline
12
14
  - **Tree-Shakeable**: ESM and CJS builds with proper exports
13
15
 
14
- ## Installation
16
+ ## Quick Start
17
+
18
+ ### Installation
15
19
 
16
20
  ```bash
17
21
  npm install @felixgeelhaar/jira-sdk
@@ -21,7 +25,7 @@ pnpm add @felixgeelhaar/jira-sdk
21
25
  yarn add @felixgeelhaar/jira-sdk
22
26
  ```
23
27
 
24
- ## Quick Start
28
+ ### Basic Usage
25
29
 
26
30
  ```typescript
27
31
  import { createJiraClient, createApiTokenAuth } from '@felixgeelhaar/jira-sdk';
@@ -29,7 +33,7 @@ import { createJiraClient, createApiTokenAuth } from '@felixgeelhaar/jira-sdk';
29
33
  // Create auth provider
30
34
  const auth = createApiTokenAuth({
31
35
  email: 'your-email@example.com',
32
- apiToken: 'your-api-token', // From https://id.atlassian.com/manage-profile/security/api-tokens
36
+ apiToken: 'your-api-token',
33
37
  });
34
38
 
35
39
  // Create client
@@ -47,262 +51,235 @@ const results = await client.search.jql('project = PROJ AND status = Open');
47
51
  for (const issue of results.issues) {
48
52
  console.log(issue.key, issue.fields.summary);
49
53
  }
50
- ```
51
54
 
52
- ## Authentication
55
+ // Create an issue
56
+ const newIssue = await client.issues.create({
57
+ fields: {
58
+ project: { key: 'PROJ' },
59
+ summary: 'New issue from SDK',
60
+ issuetype: { name: 'Task' },
61
+ },
62
+ });
63
+ ```
53
64
 
54
- ### API Token (Atlassian Cloud)
65
+ ### From environment variables
55
66
 
56
- Recommended for Atlassian Cloud instances:
67
+ The SDK reads the same variables as the Go SDK, so both can share a deployment's
68
+ configuration:
57
69
 
58
70
  ```typescript
59
- import { createApiTokenAuth } from '@felixgeelhaar/jira-sdk';
71
+ import { createJiraClientFromEnv } from '@felixgeelhaar/jira-sdk';
60
72
 
61
- const auth = createApiTokenAuth({
62
- email: 'your-email@example.com',
63
- apiToken: 'your-api-token',
64
- });
73
+ // JIRA_BASE_URL=https://your-domain.atlassian.net
74
+ // JIRA_EMAIL=you@example.com
75
+ // JIRA_API_TOKEN=...
76
+ const client = createJiraClientFromEnv();
65
77
  ```
66
78
 
67
- Get your API token from [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens).
68
-
69
- ### Personal Access Token (Server/Data Center)
79
+ Credentials resolve in this order: `JIRA_EMAIL` + `JIRA_API_TOKEN`, then
80
+ `JIRA_PAT`, then `JIRA_USERNAME` + `JIRA_PASSWORD`, then `JIRA_OAUTH_CLIENT_ID` +
81
+ `JIRA_OAUTH_CLIENT_SECRET`. Also read: `JIRA_TIMEOUT` (seconds), `JIRA_MAX_RETRIES`,
82
+ `JIRA_USER_AGENT`.
70
83
 
71
- For Jira Server or Data Center:
84
+ ### With Resilience
72
85
 
73
86
  ```typescript
74
- import { createPatAuth } from '@felixgeelhaar/jira-sdk';
87
+ import {
88
+ createJiraClient,
89
+ createApiTokenAuth,
90
+ createResilienceMiddleware,
91
+ } from '@felixgeelhaar/jira-sdk';
75
92
 
76
- const auth = createPatAuth({
77
- token: 'your-personal-access-token',
93
+ const auth = createApiTokenAuth({
94
+ email: 'your-email@example.com',
95
+ apiToken: 'your-api-token',
78
96
  });
79
- ```
80
-
81
- ### Basic Auth
82
97
 
83
- ```typescript
84
- import { createBasicAuth } from '@felixgeelhaar/jira-sdk';
98
+ // Create resilience middleware with circuit breaker, retry, and rate limiting
99
+ const { middleware, circuitBreaker } = createResilienceMiddleware({
100
+ retry: {
101
+ maxRetries: 3,
102
+ initialDelayMs: 1000,
103
+ maxDelayMs: 30000,
104
+ },
105
+ rateLimit: {
106
+ maxRequests: 90,
107
+ windowMs: 60000,
108
+ },
109
+ circuitBreaker: {
110
+ failureThreshold: 5,
111
+ resetTimeoutMs: 30000,
112
+ },
113
+ });
85
114
 
86
- const auth = createBasicAuth({
87
- username: 'your-username',
88
- password: 'your-password',
115
+ const client = createJiraClient({
116
+ host: 'https://your-domain.atlassian.net',
117
+ auth,
118
+ middleware: [middleware],
89
119
  });
90
- ```
91
120
 
92
- ### OAuth 2.0
121
+ // Monitor circuit breaker state
122
+ console.log(circuitBreaker?.getStats());
123
+ ```
93
124
 
94
- For OAuth 2.0 3LO (three-legged OAuth):
125
+ Or apply the whole stack as a client option, which also exposes the breaker on
126
+ the client:
95
127
 
96
128
  ```typescript
97
- import { createOAuth2Auth } from '@felixgeelhaar/jira-sdk';
129
+ import { createJiraClient, withResilience } from '@felixgeelhaar/jira-sdk';
98
130
 
99
- const auth = createOAuth2Auth({
100
- clientId: 'your-client-id',
101
- clientSecret: 'your-client-secret',
102
- accessToken: 'current-access-token',
103
- refreshToken: 'refresh-token',
104
- expiresAt: Date.now() + 3600000,
105
- });
131
+ const client = createJiraClient({ host, auth }, withResilience());
106
132
 
107
- // Automatic token refresh with persistence
108
- auth.onTokenRefresh = async (tokens) => {
109
- await saveTokensToDatabase(tokens);
110
- };
133
+ console.log(client.circuitBreaker?.getStats());
111
134
  ```
112
135
 
113
- ## Services
136
+ `withResilience()` disables the client's built-in retry, since the resilience
137
+ stack supplies its own — otherwise the two would compound into
138
+ `maxRetries * maxRetries` attempts.
114
139
 
115
- ### Issues
140
+ ## Building Content and Queries
141
+
142
+ ### Rich text (ADF)
143
+
144
+ Jira represents descriptions and comment bodies as Atlassian Document Format,
145
+ not plain strings:
116
146
 
117
147
  ```typescript
118
- // Get an issue
119
- const issue = await client.issues.get('PROJ-123');
120
- const issue = await client.issues.get('PROJ-123', {
121
- fields: ['summary', 'status', 'assignee'],
122
- expand: ['changelog', 'renderedFields'],
123
- });
148
+ import { AdfBuilder } from '@felixgeelhaar/jira-sdk';
124
149
 
125
- // Create an issue
126
- const newIssue = await client.issues.create({
127
- fields: {
128
- project: { key: 'PROJ' },
129
- summary: 'New issue from SDK',
130
- description: 'Detailed description here',
131
- issuetype: { name: 'Task' },
132
- priority: { name: 'High' },
133
- assignee: { accountId: 'user-account-id' },
134
- },
135
- });
150
+ const description = new AdfBuilder()
151
+ .addHeading('Steps to reproduce', 2)
152
+ .addOrderedList(['Open the app', 'Click Save', 'Observe the error'])
153
+ .addCodeBlock('TypeError: undefined is not a function', 'text')
154
+ .toDocument();
136
155
 
137
- // Update an issue
138
- await client.issues.update('PROJ-123', {
156
+ await client.issues.create({
139
157
  fields: {
140
- summary: 'Updated summary',
141
- description: 'New description',
158
+ project: { key: 'PROJ' },
159
+ summary: 'Crash on save',
160
+ issuetype: { name: 'Bug' },
161
+ description,
142
162
  },
143
163
  });
144
-
145
- // Delete an issue
146
- await client.issues.delete('PROJ-123');
147
-
148
- // Transition an issue
149
- await client.issues.transition('PROJ-123', {
150
- transition: { id: '31' }, // Transition ID
151
- });
152
-
153
- // Get available transitions
154
- const transitions = await client.issues.getTransitions('PROJ-123');
155
164
  ```
156
165
 
157
- ### Comments
166
+ ### JQL
167
+
168
+ The builder quotes and escapes every value, so user input cannot break out of a
169
+ string literal and inject JQL:
158
170
 
159
171
  ```typescript
160
- // Get comments
161
- const comments = await client.issues.getComments('PROJ-123');
172
+ import { JqlQueryBuilder } from '@felixgeelhaar/jira-sdk';
162
173
 
163
- // Add a comment
164
- await client.issues.addComment('PROJ-123', {
165
- body: 'This is a comment from the SDK',
166
- });
174
+ const jql = new JqlQueryBuilder()
175
+ .project('PROJ')
176
+ .status('In Progress')
177
+ .assignee(untrustedUserInput) // safely escaped
178
+ .orderBy('created', 'DESC')
179
+ .build();
167
180
 
168
- // Update a comment
169
- await client.issues.updateComment('PROJ-123', 'comment-id', {
170
- body: 'Updated comment text',
171
- });
172
-
173
- // Delete a comment
174
- await client.issues.deleteComment('PROJ-123', 'comment-id');
181
+ const results = await client.search.jql(jql);
175
182
  ```
176
183
 
177
- ### Search (JQL)
184
+ ### Custom fields
178
185
 
179
186
  ```typescript
180
- // Simple JQL search
181
- const results = await client.search.jql('project = PROJ');
182
-
183
- // Advanced search with options
184
- const results = await client.search.jql(
185
- 'project = PROJ AND status = "In Progress" ORDER BY created DESC',
186
- {
187
- startAt: 0,
188
- maxResults: 50,
189
- fields: ['summary', 'status', 'assignee'],
190
- expand: ['changelog'],
191
- }
192
- );
193
-
194
- // Iterate through all results
195
- for (const issue of results.issues) {
196
- console.log(`${issue.key}: ${issue.fields.summary}`);
197
- }
187
+ import { CustomFields } from '@felixgeelhaar/jira-sdk';
198
188
 
199
- // Pagination info
200
- console.log(`Showing ${results.startAt + 1} to ${results.startAt + results.issues.length} of ${results.total}`);
189
+ const fields = new CustomFields()
190
+ .setString('customfield_10001', 'Team Alpha')
191
+ .setNumber('customfield_10002', 42)
192
+ .setLabels('customfield_10003', ['backend', 'urgent']);
201
193
  ```
202
194
 
203
- ### Projects
195
+ ## Authentication Methods
196
+
197
+ ### API Token (Atlassian Cloud)
204
198
 
205
199
  ```typescript
206
- // List all projects
207
- const projects = await client.projects.list();
200
+ import { createApiTokenAuth } from '@felixgeelhaar/jira-sdk';
208
201
 
209
- // Get a project
210
- const project = await client.projects.get('PROJ');
211
- const project = await client.projects.get('PROJ', {
212
- expand: ['description', 'lead', 'issueTypes'],
202
+ const auth = createApiTokenAuth({
203
+ email: 'your-email@example.com',
204
+ apiToken: 'your-api-token', // From https://id.atlassian.com/manage-profile/security/api-tokens
213
205
  });
214
206
  ```
215
207
 
216
- ### Users
208
+ ### Personal Access Token (Server/Data Center)
217
209
 
218
210
  ```typescript
219
- // Get current user
220
- const myself = await client.users.getCurrentUser();
221
-
222
- // Get user by account ID
223
- const user = await client.users.get('account-id');
224
-
225
- // Search users
226
- const users = await client.users.search({
227
- query: 'john',
228
- maxResults: 10,
229
- });
211
+ import { createPatAuth } from '@felixgeelhaar/jira-sdk';
230
212
 
231
- // Find users assignable to a project
232
- const assignable = await client.users.findAssignable({
233
- project: 'PROJ',
234
- query: 'jane',
213
+ const auth = createPatAuth({
214
+ token: 'your-personal-access-token',
235
215
  });
236
216
  ```
237
217
 
238
- ## Resilience
239
-
240
- ### Built-in Resilience Middleware
241
-
242
- Add retry, rate limiting, and circuit breaker with one call:
218
+ ### OAuth 2.0
243
219
 
244
220
  ```typescript
245
- import { createJiraClient, createResilienceMiddleware } from '@felixgeelhaar/jira-sdk';
246
-
247
- const { middleware, circuitBreaker } = createResilienceMiddleware({
248
- retry: {
249
- maxRetries: 3,
250
- initialDelayMs: 1000,
251
- maxDelayMs: 30000,
252
- multiplier: 2,
253
- jitter: true,
254
- },
255
- rateLimit: {
256
- maxRequests: 90, // Jira Cloud limit is ~100/min
257
- windowMs: 60000,
258
- waitForSlot: true,
259
- },
260
- circuitBreaker: {
261
- failureThreshold: 5,
262
- resetTimeoutMs: 30000,
263
- failureWindowMs: 60000,
264
- },
265
- });
221
+ import { createOAuth2Auth } from '@felixgeelhaar/jira-sdk';
266
222
 
267
- const client = createJiraClient({
268
- host: 'https://your-domain.atlassian.net',
269
- auth,
270
- middleware: [middleware],
223
+ const auth = createOAuth2Auth({
224
+ clientId: 'your-client-id',
225
+ clientSecret: 'your-client-secret',
226
+ accessToken: 'current-access-token',
227
+ refreshToken: 'refresh-token',
228
+ expiresAt: Date.now() + 3600000, // 1 hour
271
229
  });
272
230
 
273
- // Monitor circuit breaker
274
- setInterval(() => {
275
- const stats = circuitBreaker?.getStats();
276
- console.log('Circuit state:', stats?.state);
277
- }, 10000);
231
+ // Automatic token refresh with callback
232
+ auth.onTokenRefresh = async (tokens) => {
233
+ await saveTokensToDatabase(tokens);
234
+ };
278
235
  ```
279
236
 
280
- ### Using Default Configuration
237
+ ## Available Services
238
+
239
+ All 27 services are reachable from the client and constructed lazily on first
240
+ access, so an application that only touches issues never pays for the rest.
241
+
242
+ | Area | Accessor | Covers |
243
+ | ------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
244
+ | Work items | `client.issues` | CRUD, comments, transitions, worklogs, attachments, links, votes, watchers |
245
+ | | `client.projects` | CRUD, components, versions, archive/restore |
246
+ | | `client.search` | JQL search, plus the enhanced token-paginated API |
247
+ | | `client.users` | Lookup, search, groups, user properties, default columns |
248
+ | Agile | `client.agile` | Boards, sprints, epics, backlog (`/rest/agile/1.0`) |
249
+ | Configuration | `client.fields` | Custom fields, contexts, options, project association |
250
+ | | `client.issueTypes` | Issue types and issue type schemes |
251
+ | | `client.issueLinkTypes` | Issue link types |
252
+ | | `client.priorities` / `client.resolutions` | Priorities and resolutions |
253
+ | | `client.screens` | Screens, tabs, screen fields |
254
+ | | `client.workflows` | Workflows, statuses, status categories, schemes |
255
+ | Views | `client.dashboards` | Dashboards and gadgets |
256
+ | | `client.filters` | Saved filters, favourites, share permissions |
257
+ | Access | `client.groups` | Groups and membership |
258
+ | | `client.myself` | Current user and preferences |
259
+ | | `client.permissions` | Permissions, schemes, project roles |
260
+ | | `client.securityLevels` | Issue security levels and schemes |
261
+ | Operations | `client.appProperties` | Application properties, advanced settings |
262
+ | | `client.audit` | Audit records |
263
+ | | `client.bulk` | Bulk create/delete with task progress polling |
264
+ | | `client.expressions` | Jira expression evaluation and analysis |
265
+ | | `client.labels` | Labels and suggestions |
266
+ | | `client.notifications` | Notification schemes, issue notifications |
267
+ | | `client.serverInfo` | Server info and instance configuration |
268
+ | | `client.timeTracking` | Providers, configuration, worklogs |
269
+ | | `client.webhooks` | Registration, refresh, delivery failures |
270
+
271
+ ### Pagination
272
+
273
+ Every paginated endpoint exposes an async iterator plus an `all()` collector:
281
274
 
282
275
  ```typescript
283
- import { createJiraClient, withResilience } from '@felixgeelhaar/jira-sdk';
284
-
285
- // Apply default resilience configuration
286
- const options = withResilience();
287
-
288
- const client = createJiraClient({
289
- host: 'https://your-domain.atlassian.net',
290
- auth,
291
- ...options,
292
- });
293
- ```
294
-
295
- ### Disable Specific Features
276
+ // Stream, one page fetched at a time
277
+ for await (const issue of client.search.iterate('project = PROJ')) {
278
+ console.log(issue.key);
279
+ }
296
280
 
297
- ```typescript
298
- const { middleware } = createResilienceMiddleware({
299
- retry: {
300
- maxRetries: 5,
301
- initialDelayMs: 500,
302
- },
303
- rateLimit: false, // Disable rate limiting
304
- circuitBreaker: false, // Disable circuit breaker
305
- });
281
+ // Or collect everything
282
+ const boards = await client.agile.allBoards();
306
283
  ```
307
284
 
308
285
  ## Error Handling
@@ -311,14 +288,9 @@ const { middleware } = createResilienceMiddleware({
311
288
  import {
312
289
  ApiError,
313
290
  UnauthorizedError,
314
- ForbiddenError,
315
291
  NotFoundError,
316
292
  RateLimitError,
317
- ServerError,
318
293
  NetworkError,
319
- TimeoutError,
320
- ValidationError,
321
- CircuitBreakerOpenError,
322
294
  } from '@felixgeelhaar/jira-sdk';
323
295
 
324
296
  try {
@@ -327,23 +299,11 @@ try {
327
299
  if (error instanceof NotFoundError) {
328
300
  console.log('Issue not found');
329
301
  } else if (error instanceof UnauthorizedError) {
330
- console.log('Invalid credentials - check your API token');
331
- } else if (error instanceof ForbiddenError) {
332
- console.log('You do not have permission to access this issue');
302
+ console.log('Invalid credentials');
333
303
  } else if (error instanceof RateLimitError) {
334
304
  console.log(`Rate limited. Retry after ${error.retryAfter}ms`);
335
- } else if (error instanceof ServerError) {
336
- console.log('Jira server error - try again later');
337
305
  } else if (error instanceof NetworkError) {
338
306
  console.log('Network error:', error.message);
339
- } else if (error instanceof TimeoutError) {
340
- console.log('Request timed out');
341
- } else if (error instanceof CircuitBreakerOpenError) {
342
- console.log('Circuit breaker open - Jira may be unavailable');
343
- } else if (error instanceof ValidationError) {
344
- console.log('Invalid data:', error.errors);
345
- } else if (error instanceof ApiError) {
346
- console.log(`API error ${error.statusCode}:`, error.responseBody);
347
307
  }
348
308
  }
349
309
  ```
@@ -352,183 +312,63 @@ try {
352
312
 
353
313
  ```typescript
354
314
  const client = createJiraClient({
355
- // Required
356
315
  host: 'https://your-domain.atlassian.net',
357
316
  auth,
358
-
359
- // Optional
360
- apiVersion: '3', // Jira API version (default: '3')
361
- timeout: 30000, // Request timeout in ms (default: 30000)
362
- middleware: [], // Custom middleware
363
- allowInsecureHttp: false, // Allow HTTP (not recommended)
317
+ apiVersion: '3', // Jira API version (default: '3')
318
+ timeout: 30000, // Request timeout in ms (default: 30000)
319
+ middleware: [], // Custom middleware
320
+ allowInsecureHttp: false, // Allow HTTP (not recommended)
364
321
  });
365
322
  ```
366
323
 
367
- ## Request Cancellation
324
+ ## Development
368
325
 
369
- ```typescript
370
- const controller = new AbortController();
326
+ ### Prerequisites
371
327
 
372
- // Start request
373
- const promise = client.search.jql('project = PROJ', {
374
- signal: controller.signal,
375
- });
328
+ - Node.js >= 20.0.0
329
+ - pnpm >= 9.0.0
376
330
 
377
- // Cancel after 5 seconds
378
- setTimeout(() => controller.abort(), 5000);
331
+ ### Setup
379
332
 
380
- try {
381
- const results = await promise;
382
- } catch (error) {
383
- if (error instanceof AbortError) {
384
- console.log('Request was cancelled');
385
- }
386
- }
387
- ```
388
-
389
- ## Custom Middleware
390
-
391
- ```typescript
392
- import type { Middleware } from '@felixgeelhaar/jira-sdk';
393
-
394
- // Timing middleware
395
- const timingMiddleware: Middleware = async (context, next) => {
396
- const start = Date.now();
397
- const response = await next(context);
398
- console.log(`${context.request.method} ${context.request.url} - ${Date.now() - start}ms`);
399
- return response;
400
- };
401
-
402
- // Add custom headers
403
- const headerMiddleware: Middleware = async (context, next) => {
404
- context.request.headers = {
405
- ...context.request.headers,
406
- 'X-Custom-Header': 'value',
407
- };
408
- return next(context);
409
- };
410
-
411
- const client = createJiraClient({
412
- host: 'https://your-domain.atlassian.net',
413
- auth,
414
- middleware: [timingMiddleware, headerMiddleware],
415
- });
416
- ```
417
-
418
- ## Logging
419
-
420
- ```typescript
421
- import { createJiraClient, createLoggingMiddleware, ConsoleLogger } from '@felixgeelhaar/jira-sdk';
422
-
423
- const logger = new ConsoleLogger('debug');
424
-
425
- const client = createJiraClient({
426
- host: 'https://your-domain.atlassian.net',
427
- auth,
428
- middleware: [createLoggingMiddleware(logger)],
429
- });
430
- ```
431
-
432
- ## Subpath Exports
433
-
434
- Import specific modules for better tree-shaking:
435
-
436
- ```typescript
437
- // Schemas only
438
- import { IssueSchema, ProjectSchema } from '@felixgeelhaar/jira-sdk/schemas';
439
-
440
- // Services only
441
- import { IssueService, SearchService } from '@felixgeelhaar/jira-sdk/services';
442
- ```
333
+ ```bash
334
+ # Install dependencies
335
+ pnpm install
443
336
 
444
- ## TypeScript Support
337
+ # Build
338
+ pnpm build
445
339
 
446
- Full TypeScript support with strict mode:
340
+ # Run tests
341
+ pnpm test
447
342
 
448
- ```typescript
449
- import type {
450
- Issue,
451
- Project,
452
- User,
453
- SearchResults,
454
- JiraClientConfig,
455
- ResilienceConfig,
456
- } from '@felixgeelhaar/jira-sdk';
457
-
458
- // Type-safe issue fields
459
- const issue: Issue = await client.issues.get('PROJ-123');
460
- console.log(issue.fields.summary); // Type-safe access
343
+ # Type check
344
+ pnpm typecheck
461
345
  ```
462
346
 
463
- ## Examples
464
-
465
- ### Bulk Operations
347
+ ### Project Structure
466
348
 
467
- ```typescript
468
- // Create multiple issues
469
- const issues = await Promise.all([
470
- client.issues.create({
471
- fields: {
472
- project: { key: 'PROJ' },
473
- summary: 'Task 1',
474
- issuetype: { name: 'Task' },
475
- },
476
- }),
477
- client.issues.create({
478
- fields: {
479
- project: { key: 'PROJ' },
480
- summary: 'Task 2',
481
- issuetype: { name: 'Task' },
482
- },
483
- }),
484
- ]);
485
349
  ```
486
-
487
- ### Iterate All Issues
488
-
489
- ```typescript
490
- async function* getAllIssues(client: JiraClient, jql: string) {
491
- let startAt = 0;
492
- const maxResults = 100;
493
-
494
- while (true) {
495
- const results = await client.search.jql(jql, { startAt, maxResults });
496
-
497
- for (const issue of results.issues) {
498
- yield issue;
499
- }
500
-
501
- startAt += results.issues.length;
502
- if (startAt >= results.total) break;
503
- }
504
- }
505
-
506
- // Usage
507
- for await (const issue of getAllIssues(client, 'project = PROJ')) {
508
- console.log(issue.key);
509
- }
350
+ jirasdk-ts/
351
+ ├── src/
352
+ │ ├── adf/ # Atlassian Document Format builder
353
+ │ ├── auth/ # Authentication providers
354
+ │ ├── client/ # Client, options, env configuration
355
+ │ ├── custom-fields/ # Typed custom-field accessors
356
+ │ ├── errors/ # Error hierarchy
357
+ │ ├── jql/ # JQL query builder and escaping
358
+ │ ├── logging/ # Logging abstraction
359
+ │ ├── pagination/ # Shared pagination primitives
360
+ │ ├── schemas/ # Zod schemas, one directory per domain
361
+ │ ├── services/ # 27 domain services
362
+ │ ├── transport/ # HTTP client, middleware, circuit breaker
363
+ │ └── utils/ # Utility functions
364
+ └── package.json
510
365
  ```
511
366
 
512
- ### Webhook Handler
367
+ ## Security
513
368
 
514
- ```typescript
515
- import express from 'express';
516
- import { IssueSchema } from '@felixgeelhaar/jira-sdk/schemas';
517
-
518
- const app = express();
519
-
520
- app.post('/webhook/jira', express.json(), (req, res) => {
521
- const result = IssueSchema.safeParse(req.body.issue);
522
-
523
- if (result.success) {
524
- console.log('Issue updated:', result.data.key);
525
- res.sendStatus(200);
526
- } else {
527
- console.error('Invalid webhook payload:', result.error);
528
- res.sendStatus(400);
529
- }
530
- });
531
- ```
369
+ - **HTTPS Required**: HTTP URLs throw in production (configurable)
370
+ - **Credential Safety**: Sensitive headers automatically redacted in logs
371
+ - **Token Refresh**: OAuth2 uses mutex to prevent concurrent refresh races
532
372
 
533
373
  ## License
534
374