vintasend-api 0.0.0-stage → 1.0.0-alpha6

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -2
  3. package/dist/app.d.ts +33 -0
  4. package/dist/app.js +33 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/config.d.ts +13 -0
  7. package/dist/config.js +39 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/contract/types.d.ts +196 -0
  10. package/dist/contract/types.js +12 -0
  11. package/dist/contract/types.js.map +1 -0
  12. package/dist/domain/capabilities.d.ts +13 -0
  13. package/dist/domain/capabilities.js +19 -0
  14. package/dist/domain/capabilities.js.map +1 -0
  15. package/dist/domain/filters.d.ts +16 -0
  16. package/dist/domain/filters.js +82 -0
  17. package/dist/domain/filters.js.map +1 -0
  18. package/dist/domain/pagination.d.ts +20 -0
  19. package/dist/domain/pagination.js +24 -0
  20. package/dist/domain/pagination.js.map +1 -0
  21. package/dist/domain/schemas.d.ts +90 -0
  22. package/dist/domain/schemas.js +52 -0
  23. package/dist/domain/schemas.js.map +1 -0
  24. package/dist/domain/serialize.d.ts +19 -0
  25. package/dist/domain/serialize.js +101 -0
  26. package/dist/domain/serialize.js.map +1 -0
  27. package/dist/errors.d.ts +45 -0
  28. package/dist/errors.js +94 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/exports.d.ts +14 -0
  31. package/dist/exports.js +14 -0
  32. package/dist/exports.js.map +1 -0
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.js +34 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/middleware/authenticate.d.ts +34 -0
  37. package/dist/middleware/authenticate.js +68 -0
  38. package/dist/middleware/authenticate.js.map +1 -0
  39. package/dist/middleware/error-handler.d.ts +35 -0
  40. package/dist/middleware/error-handler.js +89 -0
  41. package/dist/middleware/error-handler.js.map +1 -0
  42. package/dist/routes/notifications.d.ts +14 -0
  43. package/dist/routes/notifications.js +119 -0
  44. package/dist/routes/notifications.js.map +1 -0
  45. package/dist/routes/validation.d.ts +14 -0
  46. package/dist/routes/validation.js +44 -0
  47. package/dist/routes/validation.js.map +1 -0
  48. package/dist/services/github-template-client.d.ts +26 -0
  49. package/dist/services/github-template-client.js +148 -0
  50. package/dist/services/github-template-client.js.map +1 -0
  51. package/dist/services/github-template-preview-config.d.ts +8 -0
  52. package/dist/services/github-template-preview-config.js +51 -0
  53. package/dist/services/github-template-preview-config.js.map +1 -0
  54. package/dist/services/notification-preview.d.ts +22 -0
  55. package/dist/services/notification-preview.js +66 -0
  56. package/dist/services/notification-preview.js.map +1 -0
  57. package/dist/services/notification-service-port.d.ts +40 -0
  58. package/dist/services/notification-service-port.js +19 -0
  59. package/dist/services/notification-service-port.js.map +1 -0
  60. package/dist/services/paged-notification-reader.d.ts +22 -0
  61. package/dist/services/paged-notification-reader.js +36 -0
  62. package/dist/services/paged-notification-reader.js.map +1 -0
  63. package/dist/services/service-loader.d.ts +15 -0
  64. package/dist/services/service-loader.js +56 -0
  65. package/dist/services/service-loader.js.map +1 -0
  66. package/dist/services/template-path-resolver.d.ts +4 -0
  67. package/dist/services/template-path-resolver.js +17 -0
  68. package/dist/services/template-path-resolver.js.map +1 -0
  69. package/openapi.yaml +666 -0
  70. package/package.json +66 -4
package/openapi.yaml ADDED
@@ -0,0 +1,666 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: VintaSend Dashboard API
4
+ version: 1.0.0
5
+ description: |
6
+ HTTP contract between a VintaSend notification service and the VintaSend
7
+ dashboard UI.
8
+
9
+ This document is the source of truth for the contract. The TypeScript server
10
+ in this repository implements it; any other implementation (for example one
11
+ backed by the Python `vintasend` package) must match it exactly so the same
12
+ dashboard UI can consume either.
13
+
14
+ Conventions:
15
+ * All timestamps are ISO-8601 strings in UTC, and are `null` when unset.
16
+ * `page` is 1-indexed in the API, in every implementation of this contract,
17
+ and clients never convert. Backends are not consistent about this — the
18
+ TypeScript VintaSend backends are 0-indexed while the Python ones are
19
+ 1-indexed — so a server must read its backend's `pagination.oneIndexed`
20
+ capability rather than copy another implementation's offset. That
21
+ capability is backend-facing and is not published on the wire.
22
+ * Every non-2xx response uses the `ErrorResponse` envelope, and every 400
23
+ carries `details.issues`, a list of `{ path, message }`: `path` names
24
+ the offending field, and is empty for the body as a whole.
25
+ * Request bodies are JSON. A request that declares `application/json` (or
26
+ any `application/*+json`) must carry valid JSON, so an empty body there
27
+ is a 400. A request that declares no media type, or another one, counts
28
+ as an omitted body when its body is empty, and is a 400 otherwise.
29
+ * `hasMore` is true when the next page has at least one row, so a list
30
+ that exactly fills its last page never offers an empty one.
31
+ license:
32
+ name: MIT
33
+ servers:
34
+ - url: http://localhost:3333
35
+ description: Local development
36
+ security:
37
+ - apiKey: []
38
+ tags:
39
+ - name: notifications
40
+ - name: system
41
+
42
+ paths:
43
+ /health:
44
+ get:
45
+ tags: [system]
46
+ summary: Liveness probe
47
+ security: []
48
+ responses:
49
+ '200':
50
+ description: The service is up.
51
+ content:
52
+ application/json:
53
+ schema:
54
+ type: object
55
+ required: [status, apiVersion]
56
+ properties:
57
+ status:
58
+ type: string
59
+ enum: [ok]
60
+ apiVersion:
61
+ type: string
62
+ example: v1
63
+
64
+ /api/v1/capabilities:
65
+ get:
66
+ tags: [system]
67
+ summary: Filter capabilities of the configured backend
68
+ description: |
69
+ Flat dotted capability keys (for example `orderBy.sentAt`,
70
+ `stringLookups.includes`). Consumers use this to hide sorting and
71
+ filtering affordances the backend cannot honour. Missing keys mean
72
+ "supported".
73
+
74
+ Only capabilities a client can act on appear here. Capabilities that
75
+ describe how the server talks to its backend — the `pagination.*`
76
+ namespace, which reports how the backend numbers its pages — are filtered
77
+ out, because the API's own `page` parameter is 1-indexed either way and a
78
+ client that saw them might convert a second time.
79
+ responses:
80
+ '200':
81
+ description: Capability map.
82
+ content:
83
+ application/json:
84
+ schema:
85
+ type: object
86
+ required: [data]
87
+ properties:
88
+ data:
89
+ type: object
90
+ additionalProperties:
91
+ type: boolean
92
+ '401':
93
+ $ref: '#/components/responses/Unauthorized'
94
+ '403':
95
+ $ref: '#/components/responses/Forbidden'
96
+
97
+ /api/v1/notifications:
98
+ get:
99
+ tags: [notifications]
100
+ summary: List notifications
101
+ description: |
102
+ Filters are combined with AND. String filters (`bodyTemplate`,
103
+ `subjectTemplate`, `contextName`) are matched with the most precise
104
+ lookup the backend supports: case-insensitive `includes` when available,
105
+ otherwise a case-insensitive exact match, otherwise a plain equality
106
+ match. The template-version filters match exactly and never match a
107
+ notification whose version is null. Ordering defaults to `createdAt desc`,
108
+ and is dropped when the backend reports it cannot order by the requested
109
+ field.
110
+ parameters:
111
+ - $ref: '#/components/parameters/page'
112
+ - $ref: '#/components/parameters/pageSize'
113
+ - name: status
114
+ in: query
115
+ schema:
116
+ $ref: '#/components/schemas/NotificationStatus'
117
+ - name: notificationType
118
+ in: query
119
+ schema:
120
+ $ref: '#/components/schemas/NotificationType'
121
+ - name: adapterUsed
122
+ in: query
123
+ schema: { type: string, minLength: 1 }
124
+ - name: userId
125
+ in: query
126
+ schema: { type: string, minLength: 1 }
127
+ - name: bodyTemplate
128
+ in: query
129
+ schema: { type: string, minLength: 1 }
130
+ - name: subjectTemplate
131
+ in: query
132
+ schema: { type: string, minLength: 1 }
133
+ - name: contextName
134
+ in: query
135
+ schema: { type: string, minLength: 1 }
136
+ - name: tenant
137
+ in: query
138
+ schema: { type: string, minLength: 1 }
139
+ - name: requestedTemplateVersion
140
+ in: query
141
+ description: >-
142
+ Notifications pinned to this template version. Unpinned notifications never
143
+ match, since a null version is not version zero.
144
+ schema: { type: integer, minimum: 0 }
145
+ - name: usedTemplateVersion
146
+ in: query
147
+ description: >-
148
+ Notifications that rendered this template version. Notifications that have not
149
+ been sent, and those whose renderer has no versions, never match.
150
+ schema: { type: integer, minimum: 0 }
151
+ - name: createdAtFrom
152
+ in: query
153
+ schema: { type: string, format: date-time }
154
+ - name: createdAtTo
155
+ in: query
156
+ schema: { type: string, format: date-time }
157
+ - name: sentAtFrom
158
+ in: query
159
+ schema: { type: string, format: date-time }
160
+ - name: sentAtTo
161
+ in: query
162
+ schema: { type: string, format: date-time }
163
+ - name: orderByField
164
+ in: query
165
+ schema:
166
+ type: string
167
+ enum: [sendAfter, sentAt, readAt, createdAt, updatedAt]
168
+ - name: orderByDirection
169
+ in: query
170
+ schema:
171
+ type: string
172
+ enum: [asc, desc]
173
+ responses:
174
+ '200':
175
+ $ref: '#/components/responses/NotificationPage'
176
+ '400':
177
+ $ref: '#/components/responses/BadRequest'
178
+ '401':
179
+ $ref: '#/components/responses/Unauthorized'
180
+ '403':
181
+ $ref: '#/components/responses/Forbidden'
182
+
183
+ /api/v1/notifications/pending:
184
+ get:
185
+ tags: [notifications]
186
+ summary: List notifications awaiting send
187
+ parameters:
188
+ - $ref: '#/components/parameters/page'
189
+ - $ref: '#/components/parameters/pageSize'
190
+ responses:
191
+ '200':
192
+ $ref: '#/components/responses/NotificationPage'
193
+ '400':
194
+ $ref: '#/components/responses/BadRequest'
195
+ '401':
196
+ $ref: '#/components/responses/Unauthorized'
197
+ '403':
198
+ $ref: '#/components/responses/Forbidden'
199
+
200
+ /api/v1/notifications/future:
201
+ get:
202
+ tags: [notifications]
203
+ summary: List notifications scheduled for the future
204
+ parameters:
205
+ - $ref: '#/components/parameters/page'
206
+ - $ref: '#/components/parameters/pageSize'
207
+ responses:
208
+ '200':
209
+ $ref: '#/components/responses/NotificationPage'
210
+ '400':
211
+ $ref: '#/components/responses/BadRequest'
212
+ '401':
213
+ $ref: '#/components/responses/Unauthorized'
214
+ '403':
215
+ $ref: '#/components/responses/Forbidden'
216
+
217
+ /api/v1/notifications/one-off:
218
+ get:
219
+ tags: [notifications]
220
+ summary: List one-off notifications
221
+ parameters:
222
+ - $ref: '#/components/parameters/page'
223
+ - $ref: '#/components/parameters/pageSize'
224
+ responses:
225
+ '200':
226
+ $ref: '#/components/responses/NotificationPage'
227
+ '400':
228
+ $ref: '#/components/responses/BadRequest'
229
+ '401':
230
+ $ref: '#/components/responses/Unauthorized'
231
+ '403':
232
+ $ref: '#/components/responses/Forbidden'
233
+
234
+ /api/v1/notifications/{id}:
235
+ get:
236
+ tags: [notifications]
237
+ summary: Get one notification with its context payloads
238
+ description: |
239
+ Looks the id up as a user notification first, then as a one-off one.
240
+ parameters:
241
+ - $ref: '#/components/parameters/notificationId'
242
+ responses:
243
+ '200':
244
+ description: The notification.
245
+ content:
246
+ application/json:
247
+ schema:
248
+ type: object
249
+ required: [data]
250
+ properties:
251
+ data:
252
+ $ref: '#/components/schemas/NotificationDetail'
253
+ '401':
254
+ $ref: '#/components/responses/Unauthorized'
255
+ '403':
256
+ $ref: '#/components/responses/Forbidden'
257
+ '404':
258
+ $ref: '#/components/responses/NotFound'
259
+
260
+ /api/v1/notifications/{id}/preview:
261
+ get:
262
+ tags: [notifications]
263
+ summary: Render the notification's templates
264
+ description: |
265
+ Fetches the body and subject templates from the template source at the
266
+ commit SHA persisted with the notification, then renders them with the
267
+ stored context (or regenerates the context when none was stored).
268
+ Notifications that are still `PENDING_SEND` and have no stored SHA fall
269
+ back to the current `main` commit.
270
+ parameters:
271
+ - $ref: '#/components/parameters/notificationId'
272
+ responses:
273
+ '200':
274
+ description: Rendered templates.
275
+ content:
276
+ application/json:
277
+ schema:
278
+ type: object
279
+ required: [data]
280
+ properties:
281
+ data:
282
+ $ref: '#/components/schemas/NotificationPreview'
283
+ '401':
284
+ $ref: '#/components/responses/Unauthorized'
285
+ '403':
286
+ $ref: '#/components/responses/Forbidden'
287
+ '404':
288
+ $ref: '#/components/responses/NotFound'
289
+ '409':
290
+ description: |
291
+ No commit SHA is available for this notification, so its templates
292
+ cannot be resolved (`PREVIEW_UNAVAILABLE`).
293
+ content:
294
+ application/json:
295
+ schema:
296
+ $ref: '#/components/schemas/ErrorResponse'
297
+ '502':
298
+ description: |
299
+ The template source could not be reached, or refused the request —
300
+ for example a rate limit, a revoked token, or a template file that
301
+ does not exist at that commit (`UPSTREAM_ERROR`).
302
+ content:
303
+ application/json:
304
+ schema:
305
+ $ref: '#/components/schemas/ErrorResponse'
306
+
307
+ /api/v1/notifications/{id}/resend:
308
+ post:
309
+ tags: [notifications]
310
+ summary: Resend a notification
311
+ description: |
312
+ Creates a new notification based on an existing one. Only user
313
+ notifications that have already been sent or failed can be resent.
314
+ parameters:
315
+ - $ref: '#/components/parameters/notificationId'
316
+ requestBody:
317
+ required: false
318
+ content:
319
+ application/json:
320
+ schema:
321
+ type: object
322
+ properties:
323
+ useStoredContext:
324
+ type: boolean
325
+ default: false
326
+ description: |
327
+ Reuse the context stored with the original notification
328
+ instead of regenerating it from current data.
329
+ responses:
330
+ '201':
331
+ description: The newly created notification.
332
+ content:
333
+ application/json:
334
+ schema:
335
+ type: object
336
+ required: [data]
337
+ properties:
338
+ data:
339
+ $ref: '#/components/schemas/UserNotification'
340
+ '400':
341
+ $ref: '#/components/responses/BadRequest'
342
+ '401':
343
+ $ref: '#/components/responses/Unauthorized'
344
+ '403':
345
+ $ref: '#/components/responses/Forbidden'
346
+ '409':
347
+ description: |
348
+ The notification cannot be resent — it does not exist, is a one-off
349
+ notification, or is scheduled for the future (`CONFLICT`).
350
+ content:
351
+ application/json:
352
+ schema:
353
+ $ref: '#/components/schemas/ErrorResponse'
354
+
355
+ /api/v1/notifications/{id}/cancel:
356
+ post:
357
+ tags: [notifications]
358
+ summary: Cancel a pending notification
359
+ parameters:
360
+ - $ref: '#/components/parameters/notificationId'
361
+ responses:
362
+ '200':
363
+ description: The notification was cancelled.
364
+ content:
365
+ application/json:
366
+ schema:
367
+ type: object
368
+ required: [data]
369
+ properties:
370
+ data:
371
+ type: object
372
+ required: [id, status]
373
+ properties:
374
+ id: { type: string }
375
+ status:
376
+ $ref: '#/components/schemas/NotificationStatus'
377
+ '401':
378
+ $ref: '#/components/responses/Unauthorized'
379
+ '403':
380
+ $ref: '#/components/responses/Forbidden'
381
+ '404':
382
+ $ref: '#/components/responses/NotFound'
383
+ '409':
384
+ description: The notification is not in `PENDING_SEND` status (`CONFLICT`).
385
+ content:
386
+ application/json:
387
+ schema:
388
+ $ref: '#/components/schemas/ErrorResponse'
389
+
390
+ components:
391
+ securitySchemes:
392
+ apiKey:
393
+ type: http
394
+ scheme: bearer
395
+ description: |
396
+ Shared secret configured as `VINTASEND_API_KEY` for the standalone
397
+ server. The dashboard calls the API from its server side only, so the
398
+ key is never exposed to a browser. A host that mounts the API inside its
399
+ own server authenticates callers its own way instead.
400
+
401
+ parameters:
402
+ page:
403
+ name: page
404
+ in: query
405
+ description: |
406
+ 1-indexed page number: `page=1` is the first page. Translating this to
407
+ the backend's own numbering is the server's job.
408
+ schema: { type: integer, minimum: 1, default: 1 }
409
+ pageSize:
410
+ name: pageSize
411
+ in: query
412
+ schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
413
+ notificationId:
414
+ name: id
415
+ in: path
416
+ required: true
417
+ schema: { type: string }
418
+
419
+ responses:
420
+ NotificationPage:
421
+ description: A page of notifications.
422
+ content:
423
+ application/json:
424
+ schema:
425
+ $ref: '#/components/schemas/PaginatedNotifications'
426
+ BadRequest:
427
+ description: |
428
+ The request failed validation (`BAD_REQUEST`). `details.issues` lists
429
+ what was wrong.
430
+ content:
431
+ application/json:
432
+ schema:
433
+ $ref: '#/components/schemas/ErrorResponse'
434
+ Unauthorized:
435
+ description: |
436
+ No valid credential was presented — a missing or wrong API key, or
437
+ whatever the host's authentication refuses (`UNAUTHORIZED`).
438
+ content:
439
+ application/json:
440
+ schema:
441
+ $ref: '#/components/schemas/ErrorResponse'
442
+ Forbidden:
443
+ description: |
444
+ The caller is authenticated and not allowed to do this (`FORBIDDEN`).
445
+ Answered by a host's own authentication; the API key never produces
446
+ it. Unlike a 401, it is not a reason to sign in again.
447
+ content:
448
+ application/json:
449
+ schema:
450
+ $ref: '#/components/schemas/ErrorResponse'
451
+ NotFound:
452
+ description: No such notification (`NOT_FOUND`).
453
+ content:
454
+ application/json:
455
+ schema:
456
+ $ref: '#/components/schemas/ErrorResponse'
457
+
458
+ schemas:
459
+ NotificationStatus:
460
+ type: string
461
+ enum: [PENDING_SEND, SENT, FAILED, READ, CANCELLED]
462
+
463
+ NotificationType:
464
+ type: string
465
+ enum: [EMAIL, SMS, PUSH, IN_APP]
466
+
467
+ NotificationAttachment:
468
+ type: object
469
+ required: [id, filename, contentType, size]
470
+ properties:
471
+ id: { type: string }
472
+ filename: { type: string }
473
+ contentType: { type: string }
474
+ size: { type: integer }
475
+ description: { type: string }
476
+
477
+ NotificationBase:
478
+ type: object
479
+ required:
480
+ - id
481
+ - kind
482
+ - notificationType
483
+ - title
484
+ - contextName
485
+ - status
486
+ - sendAfter
487
+ - sentAt
488
+ - readAt
489
+ - createdAt
490
+ - updatedAt
491
+ - adapterUsed
492
+ - bodyTemplate
493
+ - subjectTemplate
494
+ - gitCommitSha
495
+ - requestedTemplateVersion
496
+ - usedTemplateVersion
497
+ - tenant
498
+ properties:
499
+ id: { type: string }
500
+ kind:
501
+ type: string
502
+ enum: [user, one-off]
503
+ description: Discriminates the two notification variants.
504
+ notificationType:
505
+ $ref: '#/components/schemas/NotificationType'
506
+ title:
507
+ type: [string, 'null']
508
+ contextName: { type: string }
509
+ status:
510
+ $ref: '#/components/schemas/NotificationStatus'
511
+ sendAfter:
512
+ type: [string, 'null']
513
+ format: date-time
514
+ sentAt:
515
+ type: [string, 'null']
516
+ format: date-time
517
+ readAt:
518
+ type: [string, 'null']
519
+ format: date-time
520
+ createdAt:
521
+ type: [string, 'null']
522
+ format: date-time
523
+ updatedAt:
524
+ type: [string, 'null']
525
+ format: date-time
526
+ adapterUsed:
527
+ type: [string, 'null']
528
+ bodyTemplate: { type: string }
529
+ subjectTemplate:
530
+ type: [string, 'null']
531
+ gitCommitSha:
532
+ type: [string, 'null']
533
+ requestedTemplateVersion:
534
+ type: [integer, 'null']
535
+ description: >-
536
+ The template version this notification asked for, pinned when it was created or
537
+ updated. Null means it was left unpinned and renders whatever version is current
538
+ at send time. Always null when the service's template renderer has no versions.
539
+ usedTemplateVersion:
540
+ type: [integer, 'null']
541
+ description: >-
542
+ The template version the renderer reported after the notification was sent. Null
543
+ until it has been sent, and always null when the template renderer has no
544
+ versions. On an unpinned notification this is the only record of what went out.
545
+ tenant:
546
+ type: [string, 'null']
547
+
548
+ UserNotification:
549
+ allOf:
550
+ - $ref: '#/components/schemas/NotificationBase'
551
+ - type: object
552
+ required: [userId]
553
+ properties:
554
+ kind:
555
+ type: string
556
+ enum: [user]
557
+ userId: { type: string }
558
+
559
+ OneOffNotification:
560
+ allOf:
561
+ - $ref: '#/components/schemas/NotificationBase'
562
+ - type: object
563
+ required: [emailOrPhone, firstName, lastName]
564
+ properties:
565
+ kind:
566
+ type: string
567
+ enum: [one-off]
568
+ emailOrPhone: { type: string }
569
+ firstName:
570
+ type: [string, 'null']
571
+ lastName:
572
+ type: [string, 'null']
573
+
574
+ Notification:
575
+ oneOf:
576
+ - $ref: '#/components/schemas/UserNotification'
577
+ - $ref: '#/components/schemas/OneOffNotification'
578
+ discriminator:
579
+ propertyName: kind
580
+ mapping:
581
+ user: '#/components/schemas/UserNotification'
582
+ one-off: '#/components/schemas/OneOffNotification'
583
+
584
+ NotificationDetailFields:
585
+ type: object
586
+ required: [contextUsed, contextParameters, extraParams, attachments]
587
+ properties:
588
+ contextUsed:
589
+ description: Context the notification was rendered with, when stored.
590
+ nullable: true
591
+ contextParameters:
592
+ description: Parameters used to generate the context.
593
+ nullable: true
594
+ extraParams:
595
+ nullable: true
596
+ attachments:
597
+ type: array
598
+ items:
599
+ $ref: '#/components/schemas/NotificationAttachment'
600
+
601
+ NotificationDetail:
602
+ oneOf:
603
+ - allOf:
604
+ - $ref: '#/components/schemas/UserNotification'
605
+ - $ref: '#/components/schemas/NotificationDetailFields'
606
+ - allOf:
607
+ - $ref: '#/components/schemas/OneOffNotification'
608
+ - $ref: '#/components/schemas/NotificationDetailFields'
609
+
610
+ PaginatedNotifications:
611
+ type: object
612
+ required: [data, page, pageSize, hasMore]
613
+ properties:
614
+ data:
615
+ type: array
616
+ items:
617
+ $ref: '#/components/schemas/Notification'
618
+ page: { type: integer }
619
+ pageSize: { type: integer }
620
+ hasMore:
621
+ type: boolean
622
+ description: |
623
+ True when the next page has at least one row. Backends are not
624
+ required to report a total count, so after a full page the server
625
+ reads the one row that would come next.
626
+
627
+ NotificationPreview:
628
+ type: object
629
+ required:
630
+ - gitCommitSha
631
+ - bodyTemplatePath
632
+ - subjectTemplatePath
633
+ - renderedBodyHtml
634
+ - renderedSubjectHtml
635
+ properties:
636
+ gitCommitSha: { type: string }
637
+ bodyTemplatePath: { type: string }
638
+ subjectTemplatePath:
639
+ type: [string, 'null']
640
+ renderedBodyHtml: { type: string }
641
+ renderedSubjectHtml: { type: string }
642
+
643
+ ErrorResponse:
644
+ type: object
645
+ required: [error]
646
+ properties:
647
+ error:
648
+ type: object
649
+ required: [code, message]
650
+ properties:
651
+ code:
652
+ type: string
653
+ enum:
654
+ - BAD_REQUEST
655
+ - UNAUTHORIZED
656
+ - FORBIDDEN
657
+ - NOT_FOUND
658
+ - CONFLICT
659
+ - PREVIEW_UNAVAILABLE
660
+ - UPSTREAM_ERROR
661
+ - INTERNAL_ERROR
662
+ message: { type: string }
663
+ details:
664
+ description: |
665
+ Optional machine-readable context. Every 400 carries `issues`, a
666
+ list of `{ path, message }`, and may carry other keys beside it.