vintasend-api 0.0.0-stage → 1.0.0-alpha5
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 +273 -2
- package/dist/app.d.ts +33 -0
- package/dist/app.js +33 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/contract/types.d.ts +196 -0
- package/dist/contract/types.js +12 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/domain/capabilities.d.ts +13 -0
- package/dist/domain/capabilities.js +19 -0
- package/dist/domain/capabilities.js.map +1 -0
- package/dist/domain/filters.d.ts +16 -0
- package/dist/domain/filters.js +82 -0
- package/dist/domain/filters.js.map +1 -0
- package/dist/domain/pagination.d.ts +20 -0
- package/dist/domain/pagination.js +24 -0
- package/dist/domain/pagination.js.map +1 -0
- package/dist/domain/schemas.d.ts +90 -0
- package/dist/domain/schemas.js +52 -0
- package/dist/domain/schemas.js.map +1 -0
- package/dist/domain/serialize.d.ts +19 -0
- package/dist/domain/serialize.js +101 -0
- package/dist/domain/serialize.js.map +1 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +94 -0
- package/dist/errors.js.map +1 -0
- package/dist/exports.d.ts +14 -0
- package/dist/exports.js +14 -0
- package/dist/exports.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/api-key-auth.d.ts +9 -0
- package/dist/middleware/api-key-auth.js +34 -0
- package/dist/middleware/api-key-auth.js.map +1 -0
- package/dist/middleware/authenticate.d.ts +34 -0
- package/dist/middleware/authenticate.js +68 -0
- package/dist/middleware/authenticate.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +35 -0
- package/dist/middleware/error-handler.js +89 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/routes/notifications.d.ts +14 -0
- package/dist/routes/notifications.js +119 -0
- package/dist/routes/notifications.js.map +1 -0
- package/dist/routes/validation.d.ts +14 -0
- package/dist/routes/validation.js +44 -0
- package/dist/routes/validation.js.map +1 -0
- package/dist/services/github-template-client.d.ts +26 -0
- package/dist/services/github-template-client.js +148 -0
- package/dist/services/github-template-client.js.map +1 -0
- package/dist/services/github-template-preview-config.d.ts +8 -0
- package/dist/services/github-template-preview-config.js +51 -0
- package/dist/services/github-template-preview-config.js.map +1 -0
- package/dist/services/notification-preview.d.ts +22 -0
- package/dist/services/notification-preview.js +66 -0
- package/dist/services/notification-preview.js.map +1 -0
- package/dist/services/notification-service-port.d.ts +40 -0
- package/dist/services/notification-service-port.js +19 -0
- package/dist/services/notification-service-port.js.map +1 -0
- package/dist/services/paged-notification-reader.d.ts +22 -0
- package/dist/services/paged-notification-reader.js +36 -0
- package/dist/services/paged-notification-reader.js.map +1 -0
- package/dist/services/service-loader.d.ts +15 -0
- package/dist/services/service-loader.js +56 -0
- package/dist/services/service-loader.js.map +1 -0
- package/dist/services/template-path-resolver.d.ts +4 -0
- package/dist/services/template-path-resolver.js +17 -0
- package/dist/services/template-path-resolver.js.map +1 -0
- package/openapi.yaml +666 -0
- package/package.json +66 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Vinta Serviços e Soluções Tecnológicas Ltda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,274 @@
|
|
|
1
|
-
#
|
|
1
|
+
# VintaSend API
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
REST API that exposes a [VintaSend](https://github.com/vintasoftware/vintasend-ts)
|
|
4
|
+
notification service over HTTP.
|
|
5
|
+
|
|
6
|
+
It exists so the [VintaSend dashboard](https://github.com/vintasoftware/vintasend-ts-dashboard)
|
|
7
|
+
no longer has to embed a notification service: the dashboard is now a pure API
|
|
8
|
+
client, and any implementation of this contract can serve it — including a
|
|
9
|
+
future one built on the Python `vintasend` package.
|
|
10
|
+
|
|
11
|
+
**[`openapi.yaml`](./openapi.yaml) is the contract.** This repository is the
|
|
12
|
+
TypeScript reference implementation of it, published to npm so a project can
|
|
13
|
+
mount it rather than copy it.
|
|
14
|
+
|
|
15
|
+
## Two ways to run it
|
|
16
|
+
|
|
17
|
+
**Mounted in your own server** — the usual case for an app that already has
|
|
18
|
+
one. `createApp` returns a [Hono](https://hono.dev) app, which takes a standard
|
|
19
|
+
`Request` and returns a `Response`, so it mounts in a Next.js route handler, in
|
|
20
|
+
TanStack Start, behind Express, or anywhere else that speaks `fetch`. You hand it
|
|
21
|
+
the service you already built to send notifications, and your own check of who
|
|
22
|
+
is calling. See [Mounting it](#mounting-it).
|
|
23
|
+
|
|
24
|
+
**On its own** — the `vintasend-api` command runs a server configured from
|
|
25
|
+
environment variables, behind one shared API key. See
|
|
26
|
+
[Running it on its own](#running-it-on-its-own).
|
|
27
|
+
|
|
28
|
+
Either way, the API ships no notification backend: database, adapters and
|
|
29
|
+
template renderer are yours.
|
|
30
|
+
|
|
31
|
+
## Installing
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install vintasend-api vintasend
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`vintasend` is a peer dependency, so your service and the API share one copy.
|
|
38
|
+
Install the same release line for both: the API is released together with
|
|
39
|
+
`vintasend`, and its version matches.
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
┌─────────────────────┐ HTTPS + API key ┌──────────────────┐
|
|
45
|
+
│ Dashboard (Next) │ ───────────────────▶ │ vintasend-api │
|
|
46
|
+
│ server-side only │ ◀─────────────────── │ (this repo) │
|
|
47
|
+
└─────────────────────┘ JSON contract └────────┬─────────┘
|
|
48
|
+
│
|
|
49
|
+
┌──────────────┴──────────────┐
|
|
50
|
+
│ Your VintaSend service │
|
|
51
|
+
│ backend + adapters + │
|
|
52
|
+
│ template renderer │
|
|
53
|
+
└─────────────────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The API owns everything that needs backend credentials — database access,
|
|
57
|
+
template rendering, GitHub template lookups. The UI owns presentation and user
|
|
58
|
+
authentication.
|
|
59
|
+
|
|
60
|
+
## Endpoints
|
|
61
|
+
|
|
62
|
+
| Method | Path | Purpose |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| GET | `/health` | Liveness probe (unauthenticated) |
|
|
65
|
+
| GET | `/api/v1/capabilities` | Filter/order capabilities of the configured backend |
|
|
66
|
+
| GET | `/api/v1/notifications` | List notifications with filters, ordering and pagination |
|
|
67
|
+
| GET | `/api/v1/notifications/pending` | Notifications awaiting send |
|
|
68
|
+
| GET | `/api/v1/notifications/future` | Notifications scheduled for the future |
|
|
69
|
+
| GET | `/api/v1/notifications/one-off` | One-off notifications |
|
|
70
|
+
| GET | `/api/v1/notifications/{id}` | One notification, including context payloads |
|
|
71
|
+
| GET | `/api/v1/notifications/{id}/preview` | Templates rendered at the notification's commit |
|
|
72
|
+
| POST | `/api/v1/notifications/{id}/resend` | Resend a notification |
|
|
73
|
+
| POST | `/api/v1/notifications/{id}/cancel` | Cancel a pending notification |
|
|
74
|
+
|
|
75
|
+
Conventions worth knowing when implementing this contract elsewhere:
|
|
76
|
+
|
|
77
|
+
- `page` is **1-indexed** in the API, in every implementation, and clients never
|
|
78
|
+
convert. What the backend wants is a separate question: the TypeScript
|
|
79
|
+
VintaSend backends are 0-indexed, the Python ones are 1-indexed. The offset
|
|
80
|
+
comes from the backend's `pagination.oneIndexed` capability — porting this
|
|
81
|
+
server's `page - 1` literally into a 1-indexed language is an off-by-one. The
|
|
82
|
+
capability is backend-facing and is not published by `/api/v1/capabilities`.
|
|
83
|
+
- `hasMore` is `true` when the next page has at least one row, so a list that
|
|
84
|
+
exactly fills its last page never offers an empty one. Backends are not
|
|
85
|
+
required to produce a total count: after a full page, the server reads the one
|
|
86
|
+
row that would follow it.
|
|
87
|
+
- List rows carry a `kind` field (`user` or `one-off`) so clients can
|
|
88
|
+
discriminate without sniffing for the presence of fields.
|
|
89
|
+
- Timestamps are ISO-8601 UTC strings, `null` when unset — never `undefined`.
|
|
90
|
+
- Errors always use the envelope `{ "error": { "code", "message", "details"? } }`.
|
|
91
|
+
Failures that come from the template source are `UPSTREAM_ERROR` (502), not a
|
|
92
|
+
generic 500.
|
|
93
|
+
- Every 400 carries `details.issues: [{ path, message }]`, whatever the mistake
|
|
94
|
+
was. `path` names the field, and is empty for the body as a whole.
|
|
95
|
+
- `FORBIDDEN` (403) is for a caller who is authenticated and not allowed — what a
|
|
96
|
+
host's own authentication answers. The API key alone never produces it.
|
|
97
|
+
- Request bodies are JSON. A request declaring `application/json` (or any
|
|
98
|
+
`application/*+json`) must carry valid JSON. A request declaring no media type,
|
|
99
|
+
or another one, counts as an omitted body when it is empty and is a 400
|
|
100
|
+
otherwise — `curl -d` sends form encoding unless told otherwise, and reading
|
|
101
|
+
its body as `{}` would resend with a regenerated context instead of the stored
|
|
102
|
+
one.
|
|
103
|
+
|
|
104
|
+
## Mounting it
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// app/api/v1/[...path]/route.ts — a Next.js app router route handler
|
|
108
|
+
import {
|
|
109
|
+
ApiError,
|
|
110
|
+
asNotificationServicePort,
|
|
111
|
+
createApp,
|
|
112
|
+
createGitHubTemplateClientFromEnv,
|
|
113
|
+
} from 'vintasend-api';
|
|
114
|
+
import { notificationService } from '@/lib/notifications';
|
|
115
|
+
import { getSession } from '@/lib/auth';
|
|
116
|
+
|
|
117
|
+
const app = createApp({
|
|
118
|
+
// The service your app already sends with. VintaSend services are generic over your
|
|
119
|
+
// notification config, so the port takes it through a cast confined to this one call.
|
|
120
|
+
getService: async () => asNotificationServicePort(notificationService),
|
|
121
|
+
// Runs before every /api/v1 route. Throw to refuse.
|
|
122
|
+
authenticate: async (c) => {
|
|
123
|
+
const user = await getSession(c.req.raw);
|
|
124
|
+
if (!user) throw ApiError.unauthorized('Sign in first.');
|
|
125
|
+
if (!user.canManageNotifications) throw ApiError.forbidden('Not allowed.');
|
|
126
|
+
return {};
|
|
127
|
+
},
|
|
128
|
+
// Where previews read templates from, at the commit a notification was sent with.
|
|
129
|
+
getTemplateClient: () => createGitHubTemplateClientFromEnv(),
|
|
130
|
+
// Every error not mapped to a contract error. Defaults to one redacted log line.
|
|
131
|
+
onUnhandledError: (error, _c, { requestId }) => errorTracker.capture(error, { requestId }),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const handler = (request: Request) => app.fetch(request);
|
|
135
|
+
export { handler as GET, handler as POST };
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Behind Express, hand `getRequestListener(app.fetch)` from `@hono/node-server` to a
|
|
139
|
+
route that keeps the path whole — `server.all(...)`, not `server.use('/api/v1', ...)`,
|
|
140
|
+
which strips the prefix the API's routes include.
|
|
141
|
+
|
|
142
|
+
`authenticate` has the same shape in
|
|
143
|
+
[`vintasend-templates-management-api`](https://github.com/vintasoftware/vintasend-ts-templates-management-api),
|
|
144
|
+
so an app mounting both passes them one function. It may throw the `ApiError` of
|
|
145
|
+
either package: both recognise an error by its name and code, not by its class.
|
|
146
|
+
Throw `ApiError.unauthorized` for a caller with no valid credential and
|
|
147
|
+
`ApiError.forbidden` for one you know and refuse: a 401 would tell a signed-in
|
|
148
|
+
user to sign in again. For one shared secret, pass
|
|
149
|
+
`authenticate: apiKeyAuthenticator(key)`, which compares in constant time.
|
|
150
|
+
|
|
151
|
+
The app uses Web APIs only — no Node built-ins — so it runs wherever `fetch`
|
|
152
|
+
does. The standalone server and the module-path service loader are the Node-only
|
|
153
|
+
parts, and `createApp` loads neither.
|
|
154
|
+
|
|
155
|
+
An unexpected error is reported to the client as a generic 500 with an
|
|
156
|
+
`X-Request-Id` header. By default it is logged as one line — the error's name,
|
|
157
|
+
the request id and the route pattern — and never with its message, its stack or
|
|
158
|
+
the request: errors from a notification backend or provider can quote
|
|
159
|
+
notification content and context values, which in the applications this API
|
|
160
|
+
serves can be health data. `onUnhandledError` hands the error to your own
|
|
161
|
+
tracker instead; keeping health data out of it is then your call. If it throws,
|
|
162
|
+
the default line is logged in its place.
|
|
163
|
+
|
|
164
|
+
## Running it on its own
|
|
165
|
+
|
|
166
|
+
Every `/api/v1` request must carry the shared secret:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
Authorization: Bearer $VINTASEND_API_KEY
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The dashboard calls this API only from its own server side, so the key never
|
|
173
|
+
reaches a browser. If you do need to call the API from a browser, set
|
|
174
|
+
`VINTASEND_API_CORS_ORIGINS` to the allowed origins — and put a per-user auth
|
|
175
|
+
layer in front of it first.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
VINTASEND_API_KEY=… VINTASEND_SERVICE_MODULE=./vintasend.config.js npx vintasend-api
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
To work on this repository instead:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npm install
|
|
185
|
+
cp .env.example .env
|
|
186
|
+
npm run dev
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Configuring your VintaSend service
|
|
190
|
+
|
|
191
|
+
The standalone server ships no backend of its own: which database, adapters and
|
|
192
|
+
template renderer to use is a deployment decision. Point
|
|
193
|
+
`VINTASEND_SERVICE_MODULE` at a module that default-exports a factory returning a
|
|
194
|
+
configured VintaSend service:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// src/vintasend.config.ts
|
|
198
|
+
import { VintaSendFactory } from 'vintasend';
|
|
199
|
+
|
|
200
|
+
export default async function createVintaSendService() {
|
|
201
|
+
const backend = /* your backend */;
|
|
202
|
+
const renderer = /* your template renderer */;
|
|
203
|
+
const adapter = /* your notification adapter */;
|
|
204
|
+
|
|
205
|
+
return new VintaSendFactory<Config>().create(backend, [adapter], contextGenerators);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Start from [`src/vintasend.config.example.ts`](./src/vintasend.config.example.ts),
|
|
210
|
+
copying it to `src/vintasend.config.ts` (gitignored) so it is compiled along with
|
|
211
|
+
the rest of `src`. The factory is called once at startup, and a failure there
|
|
212
|
+
stops the server rather than surfacing on the first request.
|
|
213
|
+
|
|
214
|
+
`VINTASEND_SERVICE_MODULE` accepts a path relative to the working directory or a
|
|
215
|
+
bare package specifier. Use `./dist/vintasend.config.js` with `npm start`, and
|
|
216
|
+
`./src/vintasend.config.ts` with `npm run dev`, which runs TypeScript directly.
|
|
217
|
+
|
|
218
|
+
## Environment variables
|
|
219
|
+
|
|
220
|
+
| Variable | Required | Description |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `VINTASEND_API_KEY` | yes | Shared secret clients must send as a bearer token. |
|
|
223
|
+
| `VINTASEND_SERVICE_MODULE` | no | Module building your VintaSend service. Defaults to `./dist/vintasend.config.js`. |
|
|
224
|
+
| `VINTASEND_BACKEND_IDENTIFIER` | no | Read from a non-primary backend registered in your service. |
|
|
225
|
+
| `VINTASEND_API_CORS_ORIGINS` | no | Comma-separated browser origins allowed to call the API. |
|
|
226
|
+
| `PORT` / `HOST` | no | Listen address. Defaults to `3333` / `0.0.0.0`. |
|
|
227
|
+
| `GITHUB_REPO` | preview only | Repository holding the templates, as `owner/repo` or a full URL. |
|
|
228
|
+
| `GITHUB_API_KEY` | preview only | Token with read access to that repository. |
|
|
229
|
+
| `GITHUB_API_BASE_URL` | no | Defaults to `https://api.github.com`. |
|
|
230
|
+
| `GITHUB_TEMPLATES_BASE_PATH` | no | Prefix added to template paths before the GitHub lookup. |
|
|
231
|
+
|
|
232
|
+
The `GITHUB_*` variables are only read when `/preview` is called, so the API
|
|
233
|
+
runs fine without them if you do not use template previews.
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npm run dev # watch mode
|
|
239
|
+
npm test # vitest
|
|
240
|
+
npm run typecheck # tsc --noEmit
|
|
241
|
+
npm run lint # biome
|
|
242
|
+
npm run build # compile to dist/
|
|
243
|
+
npm start # run the compiled server
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Tests drive the real Hono app through `app.request()` with an injected fake
|
|
247
|
+
service, so they cover routing, auth, validation, filter negotiation and
|
|
248
|
+
serialization without needing a database.
|
|
249
|
+
|
|
250
|
+
## Implementing this contract in another language
|
|
251
|
+
|
|
252
|
+
1. Read `openapi.yaml` — it is normative, including status codes and error codes.
|
|
253
|
+
2. Mirror the `hasMore` rule (another page has a row) and the `kind`
|
|
254
|
+
discriminator exactly; the dashboard depends on both.
|
|
255
|
+
3. Take the page offset from your backend's `pagination.oneIndexed` capability,
|
|
256
|
+
not from this implementation. The wire stays 1-indexed either way, so a
|
|
257
|
+
1-indexed backend passes the page straight through — no `- 1`. Apply it to
|
|
258
|
+
every paginated read, and keep the capability out of the `/capabilities`
|
|
259
|
+
response so no client converts on top of you.
|
|
260
|
+
4. Treat `stringLookups.caseSensitive` and `stringLookups.caseInsensitive` as
|
|
261
|
+
independent: a backend can be incapable of either one, and deriving one from
|
|
262
|
+
the other declines the single lookup such a backend actually supports.
|
|
263
|
+
5. Negotiate string lookups and ordering against your backend's capabilities,
|
|
264
|
+
and report what you support from `/api/v1/capabilities`. Dropping an
|
|
265
|
+
unsupported ordering is correct; failing the request is not.
|
|
266
|
+
6. Report template-source failures as `UPSTREAM_ERROR` (502) rather than a
|
|
267
|
+
generic 500: a rate-limited or unreachable template host is not a fault of
|
|
268
|
+
the API, and the dashboard shows the message to the operator.
|
|
269
|
+
7. Keep the error envelope identical — the dashboard branches on `error.code` —
|
|
270
|
+
including `details.issues` on every 400 and the request-body rule.
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the HTTP application.
|
|
3
|
+
*
|
|
4
|
+
* Everything the API needs from the outside world — who the caller is, the VintaSend service and
|
|
5
|
+
* the template source — is injected, so the app can be exercised in tests without a database, a
|
|
6
|
+
* mail provider, or GitHub, and mounted inside a host's own server behind its own authentication.
|
|
7
|
+
*/
|
|
8
|
+
import { Hono } from 'hono';
|
|
9
|
+
import { type Authenticator } from './middleware/authenticate.js';
|
|
10
|
+
import { type UnhandledErrorHandler } from './middleware/error-handler.js';
|
|
11
|
+
import type { TemplateSourceClient } from './services/notification-preview.js';
|
|
12
|
+
import type { NotificationServicePort } from './services/notification-service-port.js';
|
|
13
|
+
export type AppDependencies = {
|
|
14
|
+
/**
|
|
15
|
+
* Runs before every `/api/v1` route. It refuses a caller by throwing `ApiError.unauthorized` or
|
|
16
|
+
* `ApiError.forbidden`. `apiKeyAuthenticator(key)` is the shared-secret case.
|
|
17
|
+
*/
|
|
18
|
+
authenticate: Authenticator;
|
|
19
|
+
getService: () => Promise<NotificationServicePort>;
|
|
20
|
+
/** Where previews read a notification's templates from, at the commit it was sent with. */
|
|
21
|
+
getTemplateClient: () => TemplateSourceClient;
|
|
22
|
+
backendIdentifier?: string | undefined;
|
|
23
|
+
corsOrigins?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Receives every error the API does not map to a contract error. Defaults to a single log line
|
|
26
|
+
* with the error's name, a request id and the route — never the error object or the request.
|
|
27
|
+
* If the handler throws, that default line is logged instead. The client gets the generic 500
|
|
28
|
+
* either way.
|
|
29
|
+
*/
|
|
30
|
+
onUnhandledError?: UnhandledErrorHandler;
|
|
31
|
+
};
|
|
32
|
+
export declare const API_BASE_PATH = "/api/v1";
|
|
33
|
+
export declare function createApp(deps: AppDependencies): Hono;
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the HTTP application.
|
|
3
|
+
*
|
|
4
|
+
* Everything the API needs from the outside world — who the caller is, the VintaSend service and
|
|
5
|
+
* the template source — is injected, so the app can be exercised in tests without a database, a
|
|
6
|
+
* mail provider, or GitHub, and mounted inside a host's own server behind its own authentication.
|
|
7
|
+
*/
|
|
8
|
+
import { Hono } from 'hono';
|
|
9
|
+
import { cors } from 'hono/cors';
|
|
10
|
+
import { API_VERSION } from './contract/types.js';
|
|
11
|
+
import { authenticateWith } from './middleware/authenticate.js';
|
|
12
|
+
import { createErrorHandler, handleNotFound, } from './middleware/error-handler.js';
|
|
13
|
+
import { createNotificationRoutes } from './routes/notifications.js';
|
|
14
|
+
export const API_BASE_PATH = `/api/${API_VERSION}`;
|
|
15
|
+
export function createApp(deps) {
|
|
16
|
+
const app = new Hono();
|
|
17
|
+
app.onError(createErrorHandler(deps.onUnhandledError));
|
|
18
|
+
app.notFound(handleNotFound);
|
|
19
|
+
if (deps.corsOrigins && deps.corsOrigins.length > 0) {
|
|
20
|
+
const allowedOrigins = deps.corsOrigins;
|
|
21
|
+
app.use(`${API_BASE_PATH}/*`, cors({
|
|
22
|
+
origin: (origin) => (allowedOrigins.includes(origin) ? origin : null),
|
|
23
|
+
allowHeaders: ['Authorization', 'Content-Type'],
|
|
24
|
+
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
// Unauthenticated: used by load balancers and container health checks.
|
|
28
|
+
app.get('/health', (c) => c.json({ status: 'ok', apiVersion: API_VERSION }));
|
|
29
|
+
app.use(`${API_BASE_PATH}/*`, authenticateWith(deps.authenticate));
|
|
30
|
+
app.route(API_BASE_PATH, createNotificationRoutes(deps));
|
|
31
|
+
return app;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAuB,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAsB,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EACL,kBAAkB,EAClB,cAAc,GAEf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAwBrE,MAAM,CAAC,MAAM,aAAa,GAAG,QAAQ,WAAW,EAAE,CAAC;AAEnD,MAAM,UAAU,SAAS,CAAC,IAAqB;IAC7C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,GAAG,CAAC,OAAO,CAAC,kBAAkB,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACvD,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAE7B,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,CAAC;QACxC,GAAG,CAAC,GAAG,CACL,GAAG,aAAa,IAAI,EACpB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YACrE,YAAY,EAAE,CAAC,eAAe,EAAE,cAAc,CAAC;YAC/C,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC;SACzC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,uEAAuE;IACvE,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAiB,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;IAE7F,GAAG,CAAC,GAAG,CAAC,GAAG,aAAa,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IACnE,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,wBAAwB,CAAC,IAAI,CAAC,CAAC,CAAC;IAEzD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server configuration read from the environment. Validated once at startup so
|
|
3
|
+
* a misconfigured deployment fails immediately instead of on the first request.
|
|
4
|
+
*/
|
|
5
|
+
export type ServerConfig = {
|
|
6
|
+
port: number;
|
|
7
|
+
host: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
corsOrigins: string[];
|
|
10
|
+
serviceModule: string;
|
|
11
|
+
backendIdentifier: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
export declare function loadServerConfig(env?: NodeJS.ProcessEnv): ServerConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server configuration read from the environment. Validated once at startup so
|
|
3
|
+
* a misconfigured deployment fails immediately instead of on the first request.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_PORT = 3333;
|
|
6
|
+
const DEFAULT_HOST = '0.0.0.0';
|
|
7
|
+
const DEFAULT_SERVICE_MODULE = './dist/vintasend.config.js';
|
|
8
|
+
function splitList(value) {
|
|
9
|
+
if (!value) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return value
|
|
13
|
+
.split(',')
|
|
14
|
+
.map((entry) => entry.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
export function loadServerConfig(env = process.env) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const apiKey = env.VINTASEND_API_KEY?.trim();
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
errors.push('VINTASEND_API_KEY is required');
|
|
22
|
+
}
|
|
23
|
+
const port = Number(env.PORT ?? DEFAULT_PORT);
|
|
24
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
25
|
+
errors.push('PORT must be a positive integer');
|
|
26
|
+
}
|
|
27
|
+
if (errors.length > 0) {
|
|
28
|
+
throw new Error(`Invalid API configuration:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
port,
|
|
32
|
+
host: env.HOST?.trim() || DEFAULT_HOST,
|
|
33
|
+
apiKey: apiKey,
|
|
34
|
+
corsOrigins: splitList(env.VINTASEND_API_CORS_ORIGINS),
|
|
35
|
+
serviceModule: env.VINTASEND_SERVICE_MODULE?.trim() || DEFAULT_SERVICE_MODULE,
|
|
36
|
+
backendIdentifier: env.VINTASEND_BACKEND_IDENTIFIER?.trim() || undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,YAAY,GAAG,IAAI,CAAC;AAC1B,MAAM,YAAY,GAAG,SAAS,CAAC;AAC/B,MAAM,sBAAsB,GAAG,4BAA4B,CAAC;AAE5D,SAAS,SAAS,CAAC,KAAyB;IAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACnE,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,MAAM,MAAM,GAAG,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,YAAY,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,+BAA+B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7F,CAAC;IAED,OAAO;QACL,IAAI;QACJ,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,YAAY;QACtC,MAAM,EAAE,MAAgB;QACxB,WAAW,EAAE,SAAS,CAAC,GAAG,CAAC,0BAA0B,CAAC;QACtD,aAAa,EAAE,GAAG,CAAC,wBAAwB,EAAE,IAAI,EAAE,IAAI,sBAAsB;QAC7E,iBAAiB,EAAE,GAAG,CAAC,4BAA4B,EAAE,IAAI,EAAE,IAAI,SAAS;KACzE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire contract for the VintaSend dashboard API.
|
|
3
|
+
*
|
|
4
|
+
* These types describe the JSON payloads exchanged over HTTP. They intentionally
|
|
5
|
+
* avoid importing anything from `vintasend`: any implementation of this contract
|
|
6
|
+
* (the TypeScript one in this repo, a future Python one) must produce exactly
|
|
7
|
+
* these shapes, and any UI consuming the API only needs these definitions.
|
|
8
|
+
*
|
|
9
|
+
* All timestamps are ISO-8601 strings in UTC.
|
|
10
|
+
*/
|
|
11
|
+
export declare const API_VERSION = "v1";
|
|
12
|
+
export type NotificationStatus = 'PENDING_SEND' | 'SENT' | 'FAILED' | 'READ' | 'CANCELLED';
|
|
13
|
+
export type NotificationType = 'EMAIL' | 'SMS' | 'PUSH' | 'IN_APP';
|
|
14
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
15
|
+
[key: string]: JsonValue;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Attachment metadata exposed on notification details.
|
|
19
|
+
*/
|
|
20
|
+
export type NotificationAttachment = {
|
|
21
|
+
id: string;
|
|
22
|
+
filename: string;
|
|
23
|
+
contentType: string;
|
|
24
|
+
size: number;
|
|
25
|
+
description?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Fields shared by regular and one-off notifications in list responses.
|
|
29
|
+
*/
|
|
30
|
+
type NotificationBase = {
|
|
31
|
+
id: string;
|
|
32
|
+
notificationType: NotificationType;
|
|
33
|
+
title: string | null;
|
|
34
|
+
contextName: string;
|
|
35
|
+
status: NotificationStatus;
|
|
36
|
+
sendAfter: string | null;
|
|
37
|
+
sentAt: string | null;
|
|
38
|
+
readAt: string | null;
|
|
39
|
+
createdAt: string | null;
|
|
40
|
+
updatedAt: string | null;
|
|
41
|
+
adapterUsed: string | null;
|
|
42
|
+
bodyTemplate: string;
|
|
43
|
+
subjectTemplate: string | null;
|
|
44
|
+
gitCommitSha: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* The template version this notification asked for, pinned when it was created or updated.
|
|
47
|
+
* `null` means it was left unpinned and renders whatever version is current at send time.
|
|
48
|
+
* Always `null` when the service's template renderer has no versions.
|
|
49
|
+
*/
|
|
50
|
+
requestedTemplateVersion: number | null;
|
|
51
|
+
/**
|
|
52
|
+
* The template version the renderer reported after the notification was sent. `null` until it
|
|
53
|
+
* has been sent, and always `null` when the template renderer has no versions. On an unpinned
|
|
54
|
+
* notification this is the only record of what went out.
|
|
55
|
+
*/
|
|
56
|
+
usedTemplateVersion: number | null;
|
|
57
|
+
tenant: string | null;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A notification addressed to a known user.
|
|
61
|
+
*/
|
|
62
|
+
export type UserNotification = NotificationBase & {
|
|
63
|
+
kind: 'user';
|
|
64
|
+
userId: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* A notification addressed to a raw email/phone, without a user record.
|
|
68
|
+
*/
|
|
69
|
+
export type OneOffNotification = NotificationBase & {
|
|
70
|
+
kind: 'one-off';
|
|
71
|
+
emailOrPhone: string;
|
|
72
|
+
firstName: string | null;
|
|
73
|
+
lastName: string | null;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* List-view notification. `kind` discriminates the two variants.
|
|
77
|
+
*/
|
|
78
|
+
export type Notification = UserNotification | OneOffNotification;
|
|
79
|
+
type NotificationDetailFields = {
|
|
80
|
+
contextUsed: JsonValue | null;
|
|
81
|
+
contextParameters: JsonValue | null;
|
|
82
|
+
extraParams: JsonValue | null;
|
|
83
|
+
attachments: NotificationAttachment[];
|
|
84
|
+
};
|
|
85
|
+
export type UserNotificationDetail = UserNotification & NotificationDetailFields;
|
|
86
|
+
export type OneOffNotificationDetail = OneOffNotification & NotificationDetailFields;
|
|
87
|
+
/**
|
|
88
|
+
* Detail-view notification, including the (potentially large) context payloads.
|
|
89
|
+
*/
|
|
90
|
+
export type NotificationDetail = UserNotificationDetail | OneOffNotificationDetail;
|
|
91
|
+
export type NotificationOrderByField = 'sendAfter' | 'sentAt' | 'readAt' | 'createdAt' | 'updatedAt';
|
|
92
|
+
export type NotificationOrderDirection = 'asc' | 'desc';
|
|
93
|
+
/**
|
|
94
|
+
* Query parameters accepted by `GET /api/v1/notifications`.
|
|
95
|
+
* Every field is optional; date fields are ISO-8601 strings.
|
|
96
|
+
*/
|
|
97
|
+
export type NotificationListQuery = {
|
|
98
|
+
page?: number;
|
|
99
|
+
pageSize?: number;
|
|
100
|
+
status?: NotificationStatus;
|
|
101
|
+
notificationType?: NotificationType;
|
|
102
|
+
adapterUsed?: string;
|
|
103
|
+
userId?: string;
|
|
104
|
+
bodyTemplate?: string;
|
|
105
|
+
subjectTemplate?: string;
|
|
106
|
+
contextName?: string;
|
|
107
|
+
tenant?: string;
|
|
108
|
+
/** Notifications pinned to this template version. Unpinned notifications never match. */
|
|
109
|
+
requestedTemplateVersion?: number;
|
|
110
|
+
/** Notifications that rendered this template version. Unsent notifications never match. */
|
|
111
|
+
usedTemplateVersion?: number;
|
|
112
|
+
createdAtFrom?: string;
|
|
113
|
+
createdAtTo?: string;
|
|
114
|
+
sentAtFrom?: string;
|
|
115
|
+
sentAtTo?: string;
|
|
116
|
+
orderByField?: NotificationOrderByField;
|
|
117
|
+
orderByDirection?: NotificationOrderDirection;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Envelope returned by every paginated endpoint. `page` is 1-indexed, whatever
|
|
121
|
+
* numbering the backend behind the API uses.
|
|
122
|
+
*/
|
|
123
|
+
export type PaginatedResponse<T> = {
|
|
124
|
+
data: T[];
|
|
125
|
+
page: number;
|
|
126
|
+
pageSize: number;
|
|
127
|
+
/**
|
|
128
|
+
* True when the next page has at least one row, so a list that exactly fills its last page
|
|
129
|
+
* never offers an empty one. There is no total: backends are not required to count.
|
|
130
|
+
*/
|
|
131
|
+
hasMore: boolean;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Envelope returned by every single-resource endpoint.
|
|
135
|
+
*/
|
|
136
|
+
export type DataResponse<T> = {
|
|
137
|
+
data: T;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Filter capabilities advertised by the configured backend. Consumers use it to
|
|
141
|
+
* hide sorting/filtering affordances the backend cannot honour. Keys mirror
|
|
142
|
+
* VintaSend's capability keys (e.g. `orderBy.sentAt`, `stringLookups.includes`).
|
|
143
|
+
*
|
|
144
|
+
* They describe the backend, not the API: `pagination.oneIndexed` reports how
|
|
145
|
+
* the backend numbers pages, while the API's own `page` is always 1-indexed.
|
|
146
|
+
*/
|
|
147
|
+
export type FilterCapabilities = Record<string, boolean>;
|
|
148
|
+
/**
|
|
149
|
+
* Payload of `GET /api/v1/notifications/{id}/preview` on success.
|
|
150
|
+
*/
|
|
151
|
+
export type NotificationPreview = {
|
|
152
|
+
gitCommitSha: string;
|
|
153
|
+
bodyTemplatePath: string;
|
|
154
|
+
subjectTemplatePath: string | null;
|
|
155
|
+
renderedBodyHtml: string;
|
|
156
|
+
renderedSubjectHtml: string;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Payload of `POST /api/v1/notifications/{id}/cancel` on success.
|
|
160
|
+
*/
|
|
161
|
+
export type CancelledNotification = {
|
|
162
|
+
id: string;
|
|
163
|
+
status: NotificationStatus;
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Machine-readable error codes. Clients should branch on these, not on messages.
|
|
167
|
+
*/
|
|
168
|
+
export type ApiErrorCode = 'BAD_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'CONFLICT' | 'PREVIEW_UNAVAILABLE' | 'UPSTREAM_ERROR' | 'INTERNAL_ERROR';
|
|
169
|
+
/**
|
|
170
|
+
* One thing wrong with a request, in a 400's `details.issues`.
|
|
171
|
+
*
|
|
172
|
+
* `path` is the dotted field, and empty for the body as a whole or for a refusal that names no
|
|
173
|
+
* field.
|
|
174
|
+
*/
|
|
175
|
+
export type ApiErrorIssue = {
|
|
176
|
+
path: string;
|
|
177
|
+
message: string;
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Error envelope returned with every non-2xx response.
|
|
181
|
+
*
|
|
182
|
+
* Every 400 carries `details.issues: ApiErrorIssue[]`, whatever the mistake was, and may carry
|
|
183
|
+
* other keys beside it.
|
|
184
|
+
*/
|
|
185
|
+
export type ApiErrorResponse = {
|
|
186
|
+
error: {
|
|
187
|
+
code: ApiErrorCode;
|
|
188
|
+
message: string;
|
|
189
|
+
details?: JsonValue;
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
export type HealthResponse = {
|
|
193
|
+
status: 'ok';
|
|
194
|
+
apiVersion: string;
|
|
195
|
+
};
|
|
196
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire contract for the VintaSend dashboard API.
|
|
3
|
+
*
|
|
4
|
+
* These types describe the JSON payloads exchanged over HTTP. They intentionally
|
|
5
|
+
* avoid importing anything from `vintasend`: any implementation of this contract
|
|
6
|
+
* (the TypeScript one in this repo, a future Python one) must produce exactly
|
|
7
|
+
* these shapes, and any UI consuming the API only needs these definitions.
|
|
8
|
+
*
|
|
9
|
+
* All timestamps are ISO-8601 strings in UTC.
|
|
10
|
+
*/
|
|
11
|
+
export const API_VERSION = 'v1';
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/contract/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability map is a backend report, and only part of it is any of a
|
|
3
|
+
* client's business.
|
|
4
|
+
*
|
|
5
|
+
* `/api/v1/capabilities` exists so consumers can hide sorting and filtering
|
|
6
|
+
* affordances the backend cannot honour. Pagination conventions are not that:
|
|
7
|
+
* the wire contract is unconditionally 1-indexed and this server does the
|
|
8
|
+
* conversion, so publishing `pagination.oneIndexed` would only invite a client
|
|
9
|
+
* to convert a second time.
|
|
10
|
+
*/
|
|
11
|
+
import type { NotificationFilterCapabilities } from 'vintasend';
|
|
12
|
+
import type { FilterCapabilities } from '../contract/types.js';
|
|
13
|
+
export declare function toWireCapabilities(capabilities: NotificationFilterCapabilities): FilterCapabilities;
|