@tappify/extension-sdk 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/chunk-2DBSVKAU.cjs +65 -0
- package/dist/chunk-2DBSVKAU.cjs.map +1 -0
- package/dist/chunk-3YWVYA42.cjs +142 -0
- package/dist/chunk-3YWVYA42.cjs.map +1 -0
- package/dist/chunk-4C5Z3CIK.mjs +26 -0
- package/dist/chunk-4C5Z3CIK.mjs.map +1 -0
- package/dist/chunk-5AAPD5WV.cjs +250 -0
- package/dist/chunk-5AAPD5WV.cjs.map +1 -0
- package/dist/chunk-7KIKOH5H.cjs +26 -0
- package/dist/chunk-7KIKOH5H.cjs.map +1 -0
- package/dist/chunk-BDCX6LRQ.mjs +250 -0
- package/dist/chunk-BDCX6LRQ.mjs.map +1 -0
- package/dist/chunk-CSTYBKZC.mjs +65 -0
- package/dist/chunk-CSTYBKZC.mjs.map +1 -0
- package/dist/chunk-IGK2AP6J.mjs +142 -0
- package/dist/chunk-IGK2AP6J.mjs.map +1 -0
- package/dist/chunk-MNDVYPEP.mjs +302 -0
- package/dist/chunk-MNDVYPEP.mjs.map +1 -0
- package/dist/chunk-OPPYEA7F.cjs +33 -0
- package/dist/chunk-OPPYEA7F.cjs.map +1 -0
- package/dist/chunk-PYJMJF44.cjs +302 -0
- package/dist/chunk-PYJMJF44.cjs.map +1 -0
- package/dist/chunk-QHM7XNRK.mjs +33 -0
- package/dist/chunk-QHM7XNRK.mjs.map +1 -0
- package/dist/chunk-RMUWRESK.cjs +365 -0
- package/dist/chunk-RMUWRESK.cjs.map +1 -0
- package/dist/chunk-ZT35X7KL.mjs +365 -0
- package/dist/chunk-ZT35X7KL.mjs.map +1 -0
- package/dist/codegen.cjs +300 -0
- package/dist/codegen.cjs.map +1 -0
- package/dist/codegen.d.cts +432 -0
- package/dist/codegen.d.ts +432 -0
- package/dist/codegen.mjs +272 -0
- package/dist/codegen.mjs.map +1 -0
- package/dist/constants-DJ_KgZpN.d.cts +449 -0
- package/dist/constants-DJ_KgZpN.d.ts +449 -0
- package/dist/context-Dt6xh_5t.d.cts +30 -0
- package/dist/context-Ml7k_V14.d.ts +30 -0
- package/dist/errors-COsBh7kd.d.cts +88 -0
- package/dist/errors-COsBh7kd.d.ts +88 -0
- package/dist/handler-CZIlZ5vu.d.ts +227 -0
- package/dist/handler-kU9Jf36K.d.cts +227 -0
- package/dist/host.cjs +24 -0
- package/dist/host.cjs.map +1 -0
- package/dist/host.d.cts +24 -0
- package/dist/host.d.ts +24 -0
- package/dist/host.mjs +24 -0
- package/dist/host.mjs.map +1 -0
- package/dist/index.cjs +698 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1016 -0
- package/dist/index.d.ts +1016 -0
- package/dist/index.mjs +698 -0
- package/dist/index.mjs.map +1 -0
- package/dist/manifest.cjs +872 -0
- package/dist/manifest.cjs.map +1 -0
- package/dist/manifest.d.cts +432 -0
- package/dist/manifest.d.ts +432 -0
- package/dist/manifest.mjs +872 -0
- package/dist/manifest.mjs.map +1 -0
- package/dist/matchers-Cz93AK1n.d.cts +74 -0
- package/dist/matchers-Cz93AK1n.d.ts +74 -0
- package/dist/server.cjs +210 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +243 -0
- package/dist/server.d.ts +243 -0
- package/dist/server.mjs +210 -0
- package/dist/server.mjs.map +1 -0
- package/dist/styles.css +367 -0
- package/dist/testing/index.cjs +887 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +512 -0
- package/dist/testing/index.d.ts +512 -0
- package/dist/testing/index.mjs +887 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/testing/jest.cjs +17 -0
- package/dist/testing/jest.cjs.map +1 -0
- package/dist/testing/jest.d.cts +12 -0
- package/dist/testing/jest.d.ts +12 -0
- package/dist/testing/jest.mjs +17 -0
- package/dist/testing/jest.mjs.map +1 -0
- package/dist/testing/vitest.cjs +15 -0
- package/dist/testing/vitest.cjs.map +1 -0
- package/dist/testing/vitest.d.cts +10 -0
- package/dist/testing/vitest.d.ts +10 -0
- package/dist/testing/vitest.mjs +15 -0
- package/dist/testing/vitest.mjs.map +1 -0
- package/dist/types-DDAGLpov.d.ts +715 -0
- package/dist/types-DrpQSv_2.d.cts +715 -0
- package/dist/vite.cjs +1344 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +434 -0
- package/dist/vite.d.ts +434 -0
- package/dist/vite.mjs +1314 -0
- package/dist/vite.mjs.map +1 -0
- package/package.json +268 -0
- package/schema/extension-v2.json +1500 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
export { c as TapServerError } from './errors-COsBh7kd.cjs';
|
|
2
|
+
import { ad as WebhookName, ae as WebhookPayload } from './types-DrpQSv_2.cjs';
|
|
3
|
+
export { A as ActionId, k as ActionInput, w as TapCredentialValues, a as TapFilters, c as TapHostEvents, $ as TapSettingsValues, ab as ToolId, ac as ToolInput } from './types-DrpQSv_2.cjs';
|
|
4
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import { T as TappifyFetchHandler, V as VerifyTokenOptions, a as TappifyClaims } from './handler-kU9Jf36K.cjs';
|
|
6
|
+
export { C as ContextBlock, M as MentionItems, b as MetricsInput, c as MetricsResponse, d as MetricsSeries, e as TappifyCallContext, f as TappifyDocuments, g as TappifyHandlerOptions, h as TappifyHealth, i as TappifyInstall, j as TappifyRequest, k as TappifyRouteHandler, W as WorkOperation, l as createTappifyHandler } from './handler-kU9Jf36K.cjs';
|
|
7
|
+
import './constants-DJ_KgZpN.cjs';
|
|
8
|
+
import 'jose';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The part of a Node readable stream the adapters use, so neither adapter has to
|
|
12
|
+
* name a framework's own request type.
|
|
13
|
+
*/
|
|
14
|
+
interface BodyStream {
|
|
15
|
+
/** Each chunk of the body, in order. */
|
|
16
|
+
on(event: 'data', listener: (chunk: Uint8Array) => void): unknown;
|
|
17
|
+
/** The body is complete. */
|
|
18
|
+
on(event: 'end', listener: () => void): unknown;
|
|
19
|
+
/** The stream failed; the adapter rejects with the error. */
|
|
20
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
21
|
+
}
|
|
22
|
+
/** The part of an Express request `toExpress` reads. */
|
|
23
|
+
interface ExpressLikeRequest {
|
|
24
|
+
method: string;
|
|
25
|
+
/** Preferred over `url`, so a router-mounted path keeps its prefix. */
|
|
26
|
+
originalUrl?: string;
|
|
27
|
+
url?: string;
|
|
28
|
+
headers: Record<string, string | string[] | undefined>;
|
|
29
|
+
/** A parsed body from `express.json()`, or a raw string. */
|
|
30
|
+
body?: unknown;
|
|
31
|
+
/** Used as the scheme of the url handed to the handler. Defaults to `https`. */
|
|
32
|
+
protocol?: string;
|
|
33
|
+
/** Present when no parser has consumed the stream, which is then read as-is. */
|
|
34
|
+
on?: BodyStream['on'];
|
|
35
|
+
}
|
|
36
|
+
/** The part of an Express response `toExpress` writes. */
|
|
37
|
+
interface ExpressLikeResponse {
|
|
38
|
+
/** Sets the status and returns the response, so the call chains into `send`. */
|
|
39
|
+
status(code: number): ExpressLikeResponse;
|
|
40
|
+
/** Called once per header the handler's response carries. */
|
|
41
|
+
setHeader(name: string, value: string): void;
|
|
42
|
+
/** Called once, with the handler's response body as text. */
|
|
43
|
+
send(body: string): void;
|
|
44
|
+
}
|
|
45
|
+
/** What `toExpress` returns: an Express middleware over the three arguments. */
|
|
46
|
+
type ExpressMiddleware = (request: ExpressLikeRequest, response: ExpressLikeResponse, next: (error?: unknown) => void) => void;
|
|
47
|
+
/** What `toNode` returns: a listener for `http.createServer`. */
|
|
48
|
+
type NodeRequestListener = (request: IncomingMessage, response: ServerResponse) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Wraps the handler as Express middleware, for a server that speaks Express
|
|
51
|
+
* rather than `fetch`.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* It builds the url from `originalUrl` and the `host` header, forwards every
|
|
55
|
+
* header, and takes the body from `request.body` when a parser has already read
|
|
56
|
+
* it or from the stream when none has. A request with neither — a parser that
|
|
57
|
+
* consumed the stream without leaving a body — fails with a `TapServerError`
|
|
58
|
+
* carrying `TAP_BODY_INVALID` rather than calling your handler against an empty
|
|
59
|
+
* body, so mount `express.json()` before this middleware or mount this one first.
|
|
60
|
+
* A `TapServerError` is written as the handler's own JSON error; anything else
|
|
61
|
+
* goes to `next`, so your own error middleware sees it.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { createTappifyHandler, toExpress } from '@tappify/extension-sdk/server';
|
|
66
|
+
*
|
|
67
|
+
* const middleware = toExpress(
|
|
68
|
+
* createTappifyHandler({ extensionId: 'starter', health: () => ({ ok: true }) }),
|
|
69
|
+
* );
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function toExpress(handler: TappifyFetchHandler): ExpressMiddleware;
|
|
73
|
+
/**
|
|
74
|
+
* Wraps the handler as a listener for Node's own `http.createServer`, with no
|
|
75
|
+
* framework in between.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* It builds the url from `request.url` and the `host` header over `http`, forwards
|
|
79
|
+
* every header, and reads the body straight off the stream, so nothing may have
|
|
80
|
+
* consumed it first. It answers the whole response itself: a `TapServerError`
|
|
81
|
+
* becomes its own code and status, and anything else becomes 500
|
|
82
|
+
* `TAP_INTERNAL_ERROR`. Put it behind a TLS-terminating proxy in production; the
|
|
83
|
+
* scheme it builds is only what the handler sees in `request.url`.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { createTappifyHandler, toNode } from '@tappify/extension-sdk/server';
|
|
88
|
+
* import { createServer } from 'node:http';
|
|
89
|
+
*
|
|
90
|
+
* const listener = toNode(
|
|
91
|
+
* createTappifyHandler({ extensionId: 'starter', health: () => ({ ok: true }) }),
|
|
92
|
+
* );
|
|
93
|
+
* const server = createServer(listener);
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function toNode(handler: TappifyFetchHandler): NodeRequestListener;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The codes `createTappifyHandler` answers with itself, keyed by a short name.
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* Every one of these comes from the SDK, not from your own handlers: a route or
|
|
103
|
+
* handler that is not there, a token that is missing or does not verify, a missing
|
|
104
|
+
* event id, a body that is not JSON or is over 1 MB, a webhook Tappify rejected,
|
|
105
|
+
* and the catch-all for an error a handler let escape. Throw a `TapServerError`
|
|
106
|
+
* with a code of your own for anything your server decides.
|
|
107
|
+
*/
|
|
108
|
+
declare const SERVER_ERROR_CODES: {
|
|
109
|
+
readonly ROUTE_UNKNOWN: "TAP_ROUTE_UNKNOWN";
|
|
110
|
+
readonly HANDLER_MISSING: "TAP_HANDLER_MISSING";
|
|
111
|
+
readonly INTERNAL_ERROR: "TAP_INTERNAL_ERROR";
|
|
112
|
+
readonly TOKEN_MISSING: "TAP_TOKEN_MISSING";
|
|
113
|
+
readonly TOKEN_INVALID: "TAP_TOKEN_INVALID";
|
|
114
|
+
readonly TOKEN_AUDIENCE: "TAP_TOKEN_AUDIENCE";
|
|
115
|
+
readonly EVENT_ID_MISSING: "TAP_EVENT_ID_MISSING";
|
|
116
|
+
readonly BODY_INVALID: "TAP_BODY_INVALID";
|
|
117
|
+
readonly BODY_TOO_LARGE: "TAP_BODY_TOO_LARGE";
|
|
118
|
+
readonly WEBHOOK_REJECTED: "TAP_WEBHOOK_REJECTED";
|
|
119
|
+
};
|
|
120
|
+
/** Every code the handler answers with itself, as a union of its own values. */
|
|
121
|
+
type TapServerErrorCode = (typeof SERVER_ERROR_CODES)[keyof typeof SERVER_ERROR_CODES];
|
|
122
|
+
|
|
123
|
+
/** The body `sendEvent` posts to Tappify, and the body the signature covers. */
|
|
124
|
+
interface WebhookEnvelope {
|
|
125
|
+
/** The declared webhook name. */
|
|
126
|
+
event: string;
|
|
127
|
+
/** The extension id the event came from. */
|
|
128
|
+
source: string;
|
|
129
|
+
/** When the event happened, as an ISO timestamp. */
|
|
130
|
+
occurredAt: string;
|
|
131
|
+
/** The build the event belongs to, when it belongs to one. */
|
|
132
|
+
build?: string;
|
|
133
|
+
/** The key Tappify deduplicates on, so a retry is not counted twice. */
|
|
134
|
+
dedupeKey: string;
|
|
135
|
+
payload: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
/** The third argument of `sendEvent`. */
|
|
138
|
+
interface SendEventOptions {
|
|
139
|
+
/** Which install the event belongs to. */
|
|
140
|
+
installId: string;
|
|
141
|
+
extensionId: string;
|
|
142
|
+
/** The inbound signing secret from the extension's Server page. */
|
|
143
|
+
secret: string;
|
|
144
|
+
/** A full url to post to, which replaces `baseUrl` and the built path. */
|
|
145
|
+
endpoint?: string;
|
|
146
|
+
/** The Tappify origin to build the path under. Defaults to production. */
|
|
147
|
+
baseUrl?: string;
|
|
148
|
+
/** Defaults to a fresh UUID, so give your own to make a retry idempotent. */
|
|
149
|
+
dedupeKey?: string;
|
|
150
|
+
build?: string;
|
|
151
|
+
/** Defaults to now, in ISO form. */
|
|
152
|
+
occurredAt?: string;
|
|
153
|
+
/** A `fetch` of your own, for a test or a proxy. Defaults to the global one. */
|
|
154
|
+
fetch?: typeof fetch;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Returns the hex HMAC-SHA-256 of a raw body under a shared secret.
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* This is the value `sendEvent` sends as `X-Tappify-Signature`, computed over the
|
|
161
|
+
* exact bytes of the body it posts. Call it directly only when you build the
|
|
162
|
+
* request yourself, and sign the serialised string you send rather than
|
|
163
|
+
* re-serialising the object, because a different key order is a different
|
|
164
|
+
* signature. Uses Web Crypto, so it runs on Node, Cloudflare Workers and Vercel
|
|
165
|
+
* alike.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* import { signWebhook } from '@tappify/extension-sdk/server';
|
|
170
|
+
*
|
|
171
|
+
* async function signed(body: string): Promise<Record<string, string>> {
|
|
172
|
+
* return { 'x-tappify-signature': await signWebhook('whsec_test', body) };
|
|
173
|
+
* }
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare function signWebhook(secret: string, body: string): Promise<string>;
|
|
177
|
+
/**
|
|
178
|
+
* Posts one of your declared webhook events to Tappify, signed with the install's
|
|
179
|
+
* inbound secret.
|
|
180
|
+
*
|
|
181
|
+
* @remarks
|
|
182
|
+
* The name and the payload are narrowed by the `TapWebhookMap` augmentation
|
|
183
|
+
* `tappify extension types` writes. It builds the envelope, signs the serialised
|
|
184
|
+
* body and posts it to the install's hook path under `baseUrl`, or to `endpoint`
|
|
185
|
+
* when you give one. A response Tappify did not accept rejects with a
|
|
186
|
+
* `TapServerError` whose `code` is always `TAP_WEBHOOK_REJECTED`, carrying the HTTP
|
|
187
|
+
* status, and with Tappify's own code in the message text rather than in `code`;
|
|
188
|
+
* the two usual causes are the wrong signing
|
|
189
|
+
* secret and an event the manifest does not declare. Pass your own `dedupeKey` to
|
|
190
|
+
* make a retry idempotent, because the default is a fresh UUID.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { sendEvent } from '@tappify/extension-sdk/server';
|
|
195
|
+
*
|
|
196
|
+
* async function reportAnomaly(installId: string, why: string): Promise<void> {
|
|
197
|
+
* await sendEvent('anomaly.detected', { why }, {
|
|
198
|
+
* installId,
|
|
199
|
+
* extensionId: 'starter',
|
|
200
|
+
* secret: process.env.TAPPIFY_INBOUND_SECRET ?? '',
|
|
201
|
+
* dedupeKey: `anomaly-${installId}-${why}`,
|
|
202
|
+
* });
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
declare function sendEvent<E extends WebhookName>(name: E, payload: WebhookPayload<E>, options: SendEventOptions): Promise<void>;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The key set `verifyTappifyToken` fetches when no `jwksUrl` or `jwks` is given.
|
|
210
|
+
*
|
|
211
|
+
* @remarks
|
|
212
|
+
* This is production. Point `jwksUrl` at the environment that is calling you when
|
|
213
|
+
* you run against another one, and pass `jwks` in tests.
|
|
214
|
+
*/
|
|
215
|
+
declare const DEFAULT_JWKS_URL = "https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json";
|
|
216
|
+
/**
|
|
217
|
+
* Verifies an install token against Tappify's signing keys and returns its
|
|
218
|
+
* claims.
|
|
219
|
+
*
|
|
220
|
+
* @remarks
|
|
221
|
+
* Checks the RS256 signature, the expiry with 60 seconds of clock tolerance, and
|
|
222
|
+
* that the audience is `ext:<extensionId>`. Rejects with a `TapError` carrying
|
|
223
|
+
* `TAP_TOKEN_INVALID` when the signature, the expiry or a required claim is wrong,
|
|
224
|
+
* and `TAP_TOKEN_AUDIENCE` when the token was issued for another extension. A
|
|
225
|
+
* remote key set is fetched once per url and cached for the process, so calling
|
|
226
|
+
* this per request costs nothing after the first. `createTappifyHandler` already
|
|
227
|
+
* verifies every route but health, so call this only outside the handler, or
|
|
228
|
+
* where you accept a Tappify token on a route of your own.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* import { verifyTappifyToken } from '@tappify/extension-sdk/server';
|
|
233
|
+
*
|
|
234
|
+
* async function whoCalled(request: Request): Promise<string> {
|
|
235
|
+
* const token = (request.headers.get('authorization') ?? '').replace('Bearer ', '');
|
|
236
|
+
* const claims = await verifyTappifyToken(token, { extensionId: 'starter' });
|
|
237
|
+
* return claims.installId;
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
declare function verifyTappifyToken(token: string, options: VerifyTokenOptions): Promise<TappifyClaims>;
|
|
242
|
+
|
|
243
|
+
export { type BodyStream, DEFAULT_JWKS_URL, type ExpressLikeRequest, type ExpressLikeResponse, type ExpressMiddleware, type NodeRequestListener, SERVER_ERROR_CODES, type SendEventOptions, type TapServerErrorCode, TappifyClaims, TappifyFetchHandler, VerifyTokenOptions, type WebhookEnvelope, WebhookName, WebhookPayload, sendEvent, signWebhook, toExpress, toNode, verifyTappifyToken };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
export { c as TapServerError } from './errors-COsBh7kd.js';
|
|
2
|
+
import { ad as WebhookName, ae as WebhookPayload } from './types-DDAGLpov.js';
|
|
3
|
+
export { A as ActionId, k as ActionInput, w as TapCredentialValues, a as TapFilters, c as TapHostEvents, $ as TapSettingsValues, ab as ToolId, ac as ToolInput } from './types-DDAGLpov.js';
|
|
4
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import { T as TappifyFetchHandler, V as VerifyTokenOptions, a as TappifyClaims } from './handler-CZIlZ5vu.js';
|
|
6
|
+
export { C as ContextBlock, M as MentionItems, b as MetricsInput, c as MetricsResponse, d as MetricsSeries, e as TappifyCallContext, f as TappifyDocuments, g as TappifyHandlerOptions, h as TappifyHealth, i as TappifyInstall, j as TappifyRequest, k as TappifyRouteHandler, W as WorkOperation, l as createTappifyHandler } from './handler-CZIlZ5vu.js';
|
|
7
|
+
import './constants-DJ_KgZpN.js';
|
|
8
|
+
import 'jose';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The part of a Node readable stream the adapters use, so neither adapter has to
|
|
12
|
+
* name a framework's own request type.
|
|
13
|
+
*/
|
|
14
|
+
interface BodyStream {
|
|
15
|
+
/** Each chunk of the body, in order. */
|
|
16
|
+
on(event: 'data', listener: (chunk: Uint8Array) => void): unknown;
|
|
17
|
+
/** The body is complete. */
|
|
18
|
+
on(event: 'end', listener: () => void): unknown;
|
|
19
|
+
/** The stream failed; the adapter rejects with the error. */
|
|
20
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
21
|
+
}
|
|
22
|
+
/** The part of an Express request `toExpress` reads. */
|
|
23
|
+
interface ExpressLikeRequest {
|
|
24
|
+
method: string;
|
|
25
|
+
/** Preferred over `url`, so a router-mounted path keeps its prefix. */
|
|
26
|
+
originalUrl?: string;
|
|
27
|
+
url?: string;
|
|
28
|
+
headers: Record<string, string | string[] | undefined>;
|
|
29
|
+
/** A parsed body from `express.json()`, or a raw string. */
|
|
30
|
+
body?: unknown;
|
|
31
|
+
/** Used as the scheme of the url handed to the handler. Defaults to `https`. */
|
|
32
|
+
protocol?: string;
|
|
33
|
+
/** Present when no parser has consumed the stream, which is then read as-is. */
|
|
34
|
+
on?: BodyStream['on'];
|
|
35
|
+
}
|
|
36
|
+
/** The part of an Express response `toExpress` writes. */
|
|
37
|
+
interface ExpressLikeResponse {
|
|
38
|
+
/** Sets the status and returns the response, so the call chains into `send`. */
|
|
39
|
+
status(code: number): ExpressLikeResponse;
|
|
40
|
+
/** Called once per header the handler's response carries. */
|
|
41
|
+
setHeader(name: string, value: string): void;
|
|
42
|
+
/** Called once, with the handler's response body as text. */
|
|
43
|
+
send(body: string): void;
|
|
44
|
+
}
|
|
45
|
+
/** What `toExpress` returns: an Express middleware over the three arguments. */
|
|
46
|
+
type ExpressMiddleware = (request: ExpressLikeRequest, response: ExpressLikeResponse, next: (error?: unknown) => void) => void;
|
|
47
|
+
/** What `toNode` returns: a listener for `http.createServer`. */
|
|
48
|
+
type NodeRequestListener = (request: IncomingMessage, response: ServerResponse) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Wraps the handler as Express middleware, for a server that speaks Express
|
|
51
|
+
* rather than `fetch`.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* It builds the url from `originalUrl` and the `host` header, forwards every
|
|
55
|
+
* header, and takes the body from `request.body` when a parser has already read
|
|
56
|
+
* it or from the stream when none has. A request with neither — a parser that
|
|
57
|
+
* consumed the stream without leaving a body — fails with a `TapServerError`
|
|
58
|
+
* carrying `TAP_BODY_INVALID` rather than calling your handler against an empty
|
|
59
|
+
* body, so mount `express.json()` before this middleware or mount this one first.
|
|
60
|
+
* A `TapServerError` is written as the handler's own JSON error; anything else
|
|
61
|
+
* goes to `next`, so your own error middleware sees it.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { createTappifyHandler, toExpress } from '@tappify/extension-sdk/server';
|
|
66
|
+
*
|
|
67
|
+
* const middleware = toExpress(
|
|
68
|
+
* createTappifyHandler({ extensionId: 'starter', health: () => ({ ok: true }) }),
|
|
69
|
+
* );
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function toExpress(handler: TappifyFetchHandler): ExpressMiddleware;
|
|
73
|
+
/**
|
|
74
|
+
* Wraps the handler as a listener for Node's own `http.createServer`, with no
|
|
75
|
+
* framework in between.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* It builds the url from `request.url` and the `host` header over `http`, forwards
|
|
79
|
+
* every header, and reads the body straight off the stream, so nothing may have
|
|
80
|
+
* consumed it first. It answers the whole response itself: a `TapServerError`
|
|
81
|
+
* becomes its own code and status, and anything else becomes 500
|
|
82
|
+
* `TAP_INTERNAL_ERROR`. Put it behind a TLS-terminating proxy in production; the
|
|
83
|
+
* scheme it builds is only what the handler sees in `request.url`.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { createTappifyHandler, toNode } from '@tappify/extension-sdk/server';
|
|
88
|
+
* import { createServer } from 'node:http';
|
|
89
|
+
*
|
|
90
|
+
* const listener = toNode(
|
|
91
|
+
* createTappifyHandler({ extensionId: 'starter', health: () => ({ ok: true }) }),
|
|
92
|
+
* );
|
|
93
|
+
* const server = createServer(listener);
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function toNode(handler: TappifyFetchHandler): NodeRequestListener;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The codes `createTappifyHandler` answers with itself, keyed by a short name.
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* Every one of these comes from the SDK, not from your own handlers: a route or
|
|
103
|
+
* handler that is not there, a token that is missing or does not verify, a missing
|
|
104
|
+
* event id, a body that is not JSON or is over 1 MB, a webhook Tappify rejected,
|
|
105
|
+
* and the catch-all for an error a handler let escape. Throw a `TapServerError`
|
|
106
|
+
* with a code of your own for anything your server decides.
|
|
107
|
+
*/
|
|
108
|
+
declare const SERVER_ERROR_CODES: {
|
|
109
|
+
readonly ROUTE_UNKNOWN: "TAP_ROUTE_UNKNOWN";
|
|
110
|
+
readonly HANDLER_MISSING: "TAP_HANDLER_MISSING";
|
|
111
|
+
readonly INTERNAL_ERROR: "TAP_INTERNAL_ERROR";
|
|
112
|
+
readonly TOKEN_MISSING: "TAP_TOKEN_MISSING";
|
|
113
|
+
readonly TOKEN_INVALID: "TAP_TOKEN_INVALID";
|
|
114
|
+
readonly TOKEN_AUDIENCE: "TAP_TOKEN_AUDIENCE";
|
|
115
|
+
readonly EVENT_ID_MISSING: "TAP_EVENT_ID_MISSING";
|
|
116
|
+
readonly BODY_INVALID: "TAP_BODY_INVALID";
|
|
117
|
+
readonly BODY_TOO_LARGE: "TAP_BODY_TOO_LARGE";
|
|
118
|
+
readonly WEBHOOK_REJECTED: "TAP_WEBHOOK_REJECTED";
|
|
119
|
+
};
|
|
120
|
+
/** Every code the handler answers with itself, as a union of its own values. */
|
|
121
|
+
type TapServerErrorCode = (typeof SERVER_ERROR_CODES)[keyof typeof SERVER_ERROR_CODES];
|
|
122
|
+
|
|
123
|
+
/** The body `sendEvent` posts to Tappify, and the body the signature covers. */
|
|
124
|
+
interface WebhookEnvelope {
|
|
125
|
+
/** The declared webhook name. */
|
|
126
|
+
event: string;
|
|
127
|
+
/** The extension id the event came from. */
|
|
128
|
+
source: string;
|
|
129
|
+
/** When the event happened, as an ISO timestamp. */
|
|
130
|
+
occurredAt: string;
|
|
131
|
+
/** The build the event belongs to, when it belongs to one. */
|
|
132
|
+
build?: string;
|
|
133
|
+
/** The key Tappify deduplicates on, so a retry is not counted twice. */
|
|
134
|
+
dedupeKey: string;
|
|
135
|
+
payload: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
/** The third argument of `sendEvent`. */
|
|
138
|
+
interface SendEventOptions {
|
|
139
|
+
/** Which install the event belongs to. */
|
|
140
|
+
installId: string;
|
|
141
|
+
extensionId: string;
|
|
142
|
+
/** The inbound signing secret from the extension's Server page. */
|
|
143
|
+
secret: string;
|
|
144
|
+
/** A full url to post to, which replaces `baseUrl` and the built path. */
|
|
145
|
+
endpoint?: string;
|
|
146
|
+
/** The Tappify origin to build the path under. Defaults to production. */
|
|
147
|
+
baseUrl?: string;
|
|
148
|
+
/** Defaults to a fresh UUID, so give your own to make a retry idempotent. */
|
|
149
|
+
dedupeKey?: string;
|
|
150
|
+
build?: string;
|
|
151
|
+
/** Defaults to now, in ISO form. */
|
|
152
|
+
occurredAt?: string;
|
|
153
|
+
/** A `fetch` of your own, for a test or a proxy. Defaults to the global one. */
|
|
154
|
+
fetch?: typeof fetch;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Returns the hex HMAC-SHA-256 of a raw body under a shared secret.
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* This is the value `sendEvent` sends as `X-Tappify-Signature`, computed over the
|
|
161
|
+
* exact bytes of the body it posts. Call it directly only when you build the
|
|
162
|
+
* request yourself, and sign the serialised string you send rather than
|
|
163
|
+
* re-serialising the object, because a different key order is a different
|
|
164
|
+
* signature. Uses Web Crypto, so it runs on Node, Cloudflare Workers and Vercel
|
|
165
|
+
* alike.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* import { signWebhook } from '@tappify/extension-sdk/server';
|
|
170
|
+
*
|
|
171
|
+
* async function signed(body: string): Promise<Record<string, string>> {
|
|
172
|
+
* return { 'x-tappify-signature': await signWebhook('whsec_test', body) };
|
|
173
|
+
* }
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare function signWebhook(secret: string, body: string): Promise<string>;
|
|
177
|
+
/**
|
|
178
|
+
* Posts one of your declared webhook events to Tappify, signed with the install's
|
|
179
|
+
* inbound secret.
|
|
180
|
+
*
|
|
181
|
+
* @remarks
|
|
182
|
+
* The name and the payload are narrowed by the `TapWebhookMap` augmentation
|
|
183
|
+
* `tappify extension types` writes. It builds the envelope, signs the serialised
|
|
184
|
+
* body and posts it to the install's hook path under `baseUrl`, or to `endpoint`
|
|
185
|
+
* when you give one. A response Tappify did not accept rejects with a
|
|
186
|
+
* `TapServerError` whose `code` is always `TAP_WEBHOOK_REJECTED`, carrying the HTTP
|
|
187
|
+
* status, and with Tappify's own code in the message text rather than in `code`;
|
|
188
|
+
* the two usual causes are the wrong signing
|
|
189
|
+
* secret and an event the manifest does not declare. Pass your own `dedupeKey` to
|
|
190
|
+
* make a retry idempotent, because the default is a fresh UUID.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { sendEvent } from '@tappify/extension-sdk/server';
|
|
195
|
+
*
|
|
196
|
+
* async function reportAnomaly(installId: string, why: string): Promise<void> {
|
|
197
|
+
* await sendEvent('anomaly.detected', { why }, {
|
|
198
|
+
* installId,
|
|
199
|
+
* extensionId: 'starter',
|
|
200
|
+
* secret: process.env.TAPPIFY_INBOUND_SECRET ?? '',
|
|
201
|
+
* dedupeKey: `anomaly-${installId}-${why}`,
|
|
202
|
+
* });
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
declare function sendEvent<E extends WebhookName>(name: E, payload: WebhookPayload<E>, options: SendEventOptions): Promise<void>;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The key set `verifyTappifyToken` fetches when no `jwksUrl` or `jwks` is given.
|
|
210
|
+
*
|
|
211
|
+
* @remarks
|
|
212
|
+
* This is production. Point `jwksUrl` at the environment that is calling you when
|
|
213
|
+
* you run against another one, and pass `jwks` in tests.
|
|
214
|
+
*/
|
|
215
|
+
declare const DEFAULT_JWKS_URL = "https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json";
|
|
216
|
+
/**
|
|
217
|
+
* Verifies an install token against Tappify's signing keys and returns its
|
|
218
|
+
* claims.
|
|
219
|
+
*
|
|
220
|
+
* @remarks
|
|
221
|
+
* Checks the RS256 signature, the expiry with 60 seconds of clock tolerance, and
|
|
222
|
+
* that the audience is `ext:<extensionId>`. Rejects with a `TapError` carrying
|
|
223
|
+
* `TAP_TOKEN_INVALID` when the signature, the expiry or a required claim is wrong,
|
|
224
|
+
* and `TAP_TOKEN_AUDIENCE` when the token was issued for another extension. A
|
|
225
|
+
* remote key set is fetched once per url and cached for the process, so calling
|
|
226
|
+
* this per request costs nothing after the first. `createTappifyHandler` already
|
|
227
|
+
* verifies every route but health, so call this only outside the handler, or
|
|
228
|
+
* where you accept a Tappify token on a route of your own.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* import { verifyTappifyToken } from '@tappify/extension-sdk/server';
|
|
233
|
+
*
|
|
234
|
+
* async function whoCalled(request: Request): Promise<string> {
|
|
235
|
+
* const token = (request.headers.get('authorization') ?? '').replace('Bearer ', '');
|
|
236
|
+
* const claims = await verifyTappifyToken(token, { extensionId: 'starter' });
|
|
237
|
+
* return claims.installId;
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
declare function verifyTappifyToken(token: string, options: VerifyTokenOptions): Promise<TappifyClaims>;
|
|
242
|
+
|
|
243
|
+
export { type BodyStream, DEFAULT_JWKS_URL, type ExpressLikeRequest, type ExpressLikeResponse, type ExpressMiddleware, type NodeRequestListener, SERVER_ERROR_CODES, type SendEventOptions, type TapServerErrorCode, TappifyClaims, TappifyFetchHandler, VerifyTokenOptions, type WebhookEnvelope, WebhookName, WebhookPayload, sendEvent, signWebhook, toExpress, toNode, verifyTappifyToken };
|