@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,365 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EVENTS
|
|
3
|
+
} from "./chunk-MNDVYPEP.mjs";
|
|
4
|
+
import {
|
|
5
|
+
TapError,
|
|
6
|
+
TapServerError
|
|
7
|
+
} from "./chunk-CSTYBKZC.mjs";
|
|
8
|
+
|
|
9
|
+
// src/server/codes.ts
|
|
10
|
+
var SERVER_ERROR_CODES = {
|
|
11
|
+
ROUTE_UNKNOWN: "TAP_ROUTE_UNKNOWN",
|
|
12
|
+
HANDLER_MISSING: "TAP_HANDLER_MISSING",
|
|
13
|
+
INTERNAL_ERROR: "TAP_INTERNAL_ERROR",
|
|
14
|
+
TOKEN_MISSING: "TAP_TOKEN_MISSING",
|
|
15
|
+
TOKEN_INVALID: "TAP_TOKEN_INVALID",
|
|
16
|
+
TOKEN_AUDIENCE: "TAP_TOKEN_AUDIENCE",
|
|
17
|
+
EVENT_ID_MISSING: "TAP_EVENT_ID_MISSING",
|
|
18
|
+
BODY_INVALID: "TAP_BODY_INVALID",
|
|
19
|
+
BODY_TOO_LARGE: "TAP_BODY_TOO_LARGE",
|
|
20
|
+
WEBHOOK_REJECTED: "TAP_WEBHOOK_REJECTED"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/server/token.ts
|
|
24
|
+
import {
|
|
25
|
+
createLocalJWKSet,
|
|
26
|
+
createRemoteJWKSet,
|
|
27
|
+
jwtVerify
|
|
28
|
+
} from "jose";
|
|
29
|
+
var DEFAULT_JWKS_URL = "https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json";
|
|
30
|
+
var CLOCK_TOLERANCE_SECONDS = 60;
|
|
31
|
+
var remoteSets = /* @__PURE__ */ new Map();
|
|
32
|
+
function keySet(options) {
|
|
33
|
+
if (options.jwks) return createLocalJWKSet(options.jwks);
|
|
34
|
+
const url = options.jwksUrl ?? DEFAULT_JWKS_URL;
|
|
35
|
+
const cached = remoteSets.get(url);
|
|
36
|
+
if (cached) return cached;
|
|
37
|
+
const created = createRemoteJWKSet(new URL(url));
|
|
38
|
+
remoteSets.set(url, created);
|
|
39
|
+
return created;
|
|
40
|
+
}
|
|
41
|
+
function readString(payload, key) {
|
|
42
|
+
const value = payload[key];
|
|
43
|
+
return typeof value === "string" ? value : void 0;
|
|
44
|
+
}
|
|
45
|
+
function toClaims(payload, audience) {
|
|
46
|
+
const aud = payload.aud;
|
|
47
|
+
const matches = aud === audience || Array.isArray(aud) && aud.includes(audience);
|
|
48
|
+
if (!matches) {
|
|
49
|
+
throw new TapError(
|
|
50
|
+
"TAP_TOKEN_AUDIENCE",
|
|
51
|
+
`This install token was issued for a different extension. Create the handler with the same extensionId as the "id" in tappify.extension.json, so its audience is "${audience}".`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const installId = readString(payload, "installId");
|
|
55
|
+
const extensionId = readString(payload, "extensionId");
|
|
56
|
+
const organizationId = readString(payload, "organizationId");
|
|
57
|
+
if (installId === void 0 || extensionId === void 0 || organizationId === void 0) {
|
|
58
|
+
throw new TapError(
|
|
59
|
+
"TAP_TOKEN_INVALID",
|
|
60
|
+
`The install token is missing the installId, extensionId or organizationId claim. Verify against ${DEFAULT_JWKS_URL} and do not reuse tokens across environments.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const scopes = payload.scopes;
|
|
64
|
+
const userId = readString(payload, "userId");
|
|
65
|
+
const claims = {
|
|
66
|
+
iss: readString(payload, "iss") ?? "",
|
|
67
|
+
aud: audience,
|
|
68
|
+
exp: typeof payload.exp === "number" ? payload.exp : 0,
|
|
69
|
+
iat: typeof payload.iat === "number" ? payload.iat : 0,
|
|
70
|
+
installId,
|
|
71
|
+
extensionId,
|
|
72
|
+
projectId: readString(payload, "projectId") ?? null,
|
|
73
|
+
organizationId,
|
|
74
|
+
scopes: Array.isArray(scopes) ? scopes.filter((scope) => typeof scope === "string") : []
|
|
75
|
+
};
|
|
76
|
+
if (userId !== void 0) claims.userId = userId;
|
|
77
|
+
return claims;
|
|
78
|
+
}
|
|
79
|
+
async function verifyTappifyToken(token, options) {
|
|
80
|
+
const audience = `ext:${options.extensionId}`;
|
|
81
|
+
let payload;
|
|
82
|
+
try {
|
|
83
|
+
const verified = await jwtVerify(token, keySet(options), {
|
|
84
|
+
algorithms: ["RS256"],
|
|
85
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS
|
|
86
|
+
});
|
|
87
|
+
payload = verified.payload;
|
|
88
|
+
} catch {
|
|
89
|
+
throw new TapError(
|
|
90
|
+
"TAP_TOKEN_INVALID",
|
|
91
|
+
"The install token did not verify. Check that your server reads the Authorization header unchanged, that its clock is correct, and that jwksUrl points at the Tappify environment that called you."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return toClaims(payload, audience);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/server/wire.ts
|
|
98
|
+
var INTERNAL_MESSAGE = "The extension server failed to handle this call. Check your own logs; Tappify records the failure against your extension.";
|
|
99
|
+
function json(data, status = 200) {
|
|
100
|
+
return new Response(JSON.stringify(data), {
|
|
101
|
+
status,
|
|
102
|
+
headers: { "content-type": "application/json" }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function wireError(code, message, status) {
|
|
106
|
+
return json({ error: { code, message } }, status);
|
|
107
|
+
}
|
|
108
|
+
function failure(code, message, status) {
|
|
109
|
+
return wireError(code, message, status);
|
|
110
|
+
}
|
|
111
|
+
function readUnknown(value, key) {
|
|
112
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
113
|
+
return value[key];
|
|
114
|
+
}
|
|
115
|
+
function readRecord(value, key) {
|
|
116
|
+
const nested = readUnknown(value, key);
|
|
117
|
+
return typeof nested === "object" && nested !== null ? nested : {};
|
|
118
|
+
}
|
|
119
|
+
function readText(value, key) {
|
|
120
|
+
const nested = readUnknown(value, key);
|
|
121
|
+
return typeof nested === "string" ? nested : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/server/handler.ts
|
|
125
|
+
var MAX_BODY_BYTES = 1048576;
|
|
126
|
+
var WORK_OPERATIONS = [
|
|
127
|
+
"list_containers",
|
|
128
|
+
"create",
|
|
129
|
+
"attach",
|
|
130
|
+
"comment",
|
|
131
|
+
"status",
|
|
132
|
+
"resolve"
|
|
133
|
+
];
|
|
134
|
+
var TOKEN_FAILURES = [
|
|
135
|
+
SERVER_ERROR_CODES.TOKEN_INVALID,
|
|
136
|
+
SERVER_ERROR_CODES.TOKEN_AUDIENCE
|
|
137
|
+
];
|
|
138
|
+
function toInstall(payload, claims) {
|
|
139
|
+
const raw = readRecord(payload, "install");
|
|
140
|
+
return {
|
|
141
|
+
id: claims.installId,
|
|
142
|
+
extensionId: claims.extensionId,
|
|
143
|
+
projectId: claims.projectId ?? readText(raw, "projectId"),
|
|
144
|
+
organizationId: claims.organizationId
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function toPlatform(value) {
|
|
148
|
+
return value === "ios" || value === "android" ? value : "all";
|
|
149
|
+
}
|
|
150
|
+
function toPreset(value) {
|
|
151
|
+
return value === "7d" || value === "30d" || value === "90d" ? value : void 0;
|
|
152
|
+
}
|
|
153
|
+
function toContext(payload) {
|
|
154
|
+
const raw = readRecord(payload, "context");
|
|
155
|
+
const filters = readRecord(raw, "filters");
|
|
156
|
+
const rawRange = readRecord(filters, "range");
|
|
157
|
+
const from = readText(raw, "from") ?? readText(rawRange, "from") ?? "";
|
|
158
|
+
const to = readText(raw, "to") ?? readText(rawRange, "to") ?? "";
|
|
159
|
+
const range = {
|
|
160
|
+
from: readText(rawRange, "from") ?? from,
|
|
161
|
+
to: readText(rawRange, "to") ?? to
|
|
162
|
+
};
|
|
163
|
+
const preset = toPreset(readText(rawRange, "preset"));
|
|
164
|
+
if (preset !== void 0) range.preset = preset;
|
|
165
|
+
return {
|
|
166
|
+
projectId: readText(raw, "projectId"),
|
|
167
|
+
platform: toPlatform(readText(raw, "platform")),
|
|
168
|
+
from,
|
|
169
|
+
to,
|
|
170
|
+
filters: {
|
|
171
|
+
range,
|
|
172
|
+
platform: toPlatform(readText(filters, "platform")),
|
|
173
|
+
country: readText(filters, "country") ?? "all"
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function toCredentials(payload) {
|
|
178
|
+
const raw = readRecord(payload, "credentials");
|
|
179
|
+
const credentials = {};
|
|
180
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
181
|
+
if (typeof value === "string") credentials[name] = value;
|
|
182
|
+
}
|
|
183
|
+
return credentials;
|
|
184
|
+
}
|
|
185
|
+
function tooLarge() {
|
|
186
|
+
return new TapServerError(
|
|
187
|
+
SERVER_ERROR_CODES.BODY_TOO_LARGE,
|
|
188
|
+
"A Tappify request body is at most 1 MB. Return a cursor or an id the host can follow instead of inlining the payload.",
|
|
189
|
+
413
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
async function readBody(request) {
|
|
193
|
+
if (request.method === "GET" || request.method === "HEAD") return {};
|
|
194
|
+
const length = request.headers.get("content-length");
|
|
195
|
+
if (length !== null && Number(length) > MAX_BODY_BYTES) throw tooLarge();
|
|
196
|
+
const bytes = await request.arrayBuffer();
|
|
197
|
+
if (bytes.byteLength > MAX_BODY_BYTES) throw tooLarge();
|
|
198
|
+
if (bytes.byteLength === 0) return {};
|
|
199
|
+
const text = new TextDecoder().decode(bytes);
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(text);
|
|
202
|
+
} catch {
|
|
203
|
+
throw new TapServerError(
|
|
204
|
+
SERVER_ERROR_CODES.BODY_INVALID,
|
|
205
|
+
"The request body was not JSON. Do not parse or re-encode the body before handing the Request to createTappifyHandler().",
|
|
206
|
+
400
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function route(handler, input, event, noContent = false) {
|
|
211
|
+
return { handler, input, event, noContent };
|
|
212
|
+
}
|
|
213
|
+
function handlerFor(map, name) {
|
|
214
|
+
return map?.[name];
|
|
215
|
+
}
|
|
216
|
+
function resolveRoute(options, method, segments, url, payload) {
|
|
217
|
+
const [head, name] = segments;
|
|
218
|
+
const bodyInput = readUnknown(payload, "input") ?? {};
|
|
219
|
+
if (head === "metrics" && method === "POST") {
|
|
220
|
+
return route(options.metrics, bodyInput);
|
|
221
|
+
}
|
|
222
|
+
if (head === "tools" && method === "POST" && name !== void 0) {
|
|
223
|
+
return route(handlerFor(options.tools, name), bodyInput);
|
|
224
|
+
}
|
|
225
|
+
if (head === "mentions" && method === "GET" && name !== void 0) {
|
|
226
|
+
return route(handlerFor(options.mentions, name), {
|
|
227
|
+
q: url.searchParams.get("q") ?? ""
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (head === "actions" && method === "POST" && name !== void 0) {
|
|
231
|
+
return route(handlerFor(options.actions, name), bodyInput);
|
|
232
|
+
}
|
|
233
|
+
if (head === "procedures" && method === "POST" && name !== void 0) {
|
|
234
|
+
return route(handlerFor(options.procedures, name), bodyInput);
|
|
235
|
+
}
|
|
236
|
+
if (head === "context" && method === "GET" && name !== void 0) {
|
|
237
|
+
return route(handlerFor(options.context, name), void 0);
|
|
238
|
+
}
|
|
239
|
+
if (head === "work" && method === "POST" && name !== void 0) {
|
|
240
|
+
const operation = WORK_OPERATIONS.find((candidate) => candidate === name);
|
|
241
|
+
return route(
|
|
242
|
+
operation === void 0 ? void 0 : options.work?.[operation],
|
|
243
|
+
bodyInput
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (head === "events" && method === "POST") {
|
|
247
|
+
const envelope = readRecord(payload, "event");
|
|
248
|
+
const eventName = readText(envelope, "name");
|
|
249
|
+
if (eventName === null) {
|
|
250
|
+
throw new TapServerError(
|
|
251
|
+
SERVER_ERROR_CODES.BODY_INVALID,
|
|
252
|
+
'The events body has no event.name. Post the envelope Tappify sends unchanged, with the event name under "event".',
|
|
253
|
+
400
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const declared = EVENTS.find((candidate) => candidate === eventName);
|
|
257
|
+
return route(
|
|
258
|
+
declared === void 0 ? void 0 : options.events?.[declared],
|
|
259
|
+
void 0,
|
|
260
|
+
readUnknown(envelope, "payload") ?? {},
|
|
261
|
+
true
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
function createTappifyHandler(options) {
|
|
267
|
+
const basePath = options.basePath ?? "";
|
|
268
|
+
return async function handle(request) {
|
|
269
|
+
const url = new URL(request.url);
|
|
270
|
+
const path = url.pathname.startsWith(basePath) ? url.pathname.slice(basePath.length) : url.pathname;
|
|
271
|
+
const segments = path.split("/").filter((segment) => segment !== "");
|
|
272
|
+
if (segments[0] !== "tappify") {
|
|
273
|
+
return failure(
|
|
274
|
+
SERVER_ERROR_CODES.ROUTE_UNKNOWN,
|
|
275
|
+
`Nothing is mounted at ${path}. Tappify calls paths under /tappify; set basePath if your framework mounts the handler under a prefix.`,
|
|
276
|
+
404
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const rest = segments.slice(1);
|
|
280
|
+
if (rest[0] === "health" && request.method === "GET") {
|
|
281
|
+
const health = options.health ? await options.health() : { ok: true };
|
|
282
|
+
return json(health);
|
|
283
|
+
}
|
|
284
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
285
|
+
const token = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length) : "";
|
|
286
|
+
if (token === "") {
|
|
287
|
+
return failure(
|
|
288
|
+
SERVER_ERROR_CODES.TOKEN_MISSING,
|
|
289
|
+
"This route needs an install token. Tappify sends it as Authorization: Bearer <token>; forward the header unchanged from your framework.",
|
|
290
|
+
401
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const eventId = request.headers.get("x-tappify-event-id");
|
|
294
|
+
if (eventId === null || eventId === "") {
|
|
295
|
+
return failure(
|
|
296
|
+
SERVER_ERROR_CODES.EVENT_ID_MISSING,
|
|
297
|
+
"Every Tappify call carries X-Tappify-Event-Id for idempotency. Forward the header unchanged, and use it as the key when you deduplicate.",
|
|
298
|
+
400
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const claims = await verifyTappifyToken(token, {
|
|
303
|
+
extensionId: options.extensionId,
|
|
304
|
+
jwksUrl: options.jwksUrl,
|
|
305
|
+
jwks: options.jwks
|
|
306
|
+
});
|
|
307
|
+
const payload = await readBody(request);
|
|
308
|
+
const matched = resolveRoute(options, request.method, rest, url, payload);
|
|
309
|
+
if (matched === null) {
|
|
310
|
+
return failure(
|
|
311
|
+
SERVER_ERROR_CODES.ROUTE_UNKNOWN,
|
|
312
|
+
`Tappify has no ${request.method} ${path} route. The paths it calls are listed under "Vendor server endpoints" in the reference; check the method and the spelling.`,
|
|
313
|
+
404
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (matched.handler === void 0) {
|
|
317
|
+
return failure(
|
|
318
|
+
SERVER_ERROR_CODES.HANDLER_MISSING,
|
|
319
|
+
`No handler is registered for ${request.method} ${path}. Add it to createTappifyHandler(), or remove the contribution from tappify.extension.json.`,
|
|
320
|
+
404
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const documents = readRecord(payload, "documents");
|
|
324
|
+
const typed = {
|
|
325
|
+
install: toInstall(payload, claims),
|
|
326
|
+
context: toContext(payload),
|
|
327
|
+
input: matched.input,
|
|
328
|
+
credentials: toCredentials(payload),
|
|
329
|
+
documents,
|
|
330
|
+
event: matched.event,
|
|
331
|
+
eventId,
|
|
332
|
+
claims,
|
|
333
|
+
request
|
|
334
|
+
};
|
|
335
|
+
const result = await matched.handler(typed);
|
|
336
|
+
return matched.noContent ? new Response(null, { status: 204 }) : json(result);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (TapServerError.is(error)) {
|
|
339
|
+
return wireError(error.code, error.message, error.status ?? 400);
|
|
340
|
+
}
|
|
341
|
+
if (TapError.is(error)) {
|
|
342
|
+
const tokenFailure = TOKEN_FAILURES.find(
|
|
343
|
+
(candidate) => candidate === error.code
|
|
344
|
+
);
|
|
345
|
+
if (tokenFailure !== void 0) {
|
|
346
|
+
return failure(tokenFailure, error.message, 401);
|
|
347
|
+
}
|
|
348
|
+
return wireError(error.code, error.message, 400);
|
|
349
|
+
}
|
|
350
|
+
return failure(SERVER_ERROR_CODES.INTERNAL_ERROR, INTERNAL_MESSAGE, 500);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export {
|
|
356
|
+
SERVER_ERROR_CODES,
|
|
357
|
+
INTERNAL_MESSAGE,
|
|
358
|
+
wireError,
|
|
359
|
+
readRecord,
|
|
360
|
+
readText,
|
|
361
|
+
DEFAULT_JWKS_URL,
|
|
362
|
+
verifyTappifyToken,
|
|
363
|
+
createTappifyHandler
|
|
364
|
+
};
|
|
365
|
+
//# sourceMappingURL=chunk-ZT35X7KL.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/codes.ts","../src/server/token.ts","../src/server/wire.ts","../src/server/handler.ts"],"sourcesContent":["import type { TapErrorCode } from '../client/errors';\n\n/**\n * The codes `createTappifyHandler` answers with itself, keyed by a short name.\n *\n * @remarks\n * Every one of these comes from the SDK, not from your own handlers: a route or\n * handler that is not there, a token that is missing or does not verify, a missing\n * event id, a body that is not JSON or is over 1 MB, a webhook Tappify rejected,\n * and the catch-all for an error a handler let escape. Throw a `TapServerError`\n * with a code of your own for anything your server decides.\n */\nexport const SERVER_ERROR_CODES = {\n ROUTE_UNKNOWN: 'TAP_ROUTE_UNKNOWN',\n HANDLER_MISSING: 'TAP_HANDLER_MISSING',\n INTERNAL_ERROR: 'TAP_INTERNAL_ERROR',\n TOKEN_MISSING: 'TAP_TOKEN_MISSING',\n TOKEN_INVALID: 'TAP_TOKEN_INVALID',\n TOKEN_AUDIENCE: 'TAP_TOKEN_AUDIENCE',\n EVENT_ID_MISSING: 'TAP_EVENT_ID_MISSING',\n BODY_INVALID: 'TAP_BODY_INVALID',\n BODY_TOO_LARGE: 'TAP_BODY_TOO_LARGE',\n WEBHOOK_REJECTED: 'TAP_WEBHOOK_REJECTED',\n} as const satisfies Record<string, TapErrorCode>;\n\n/** Every code the handler answers with itself, as a union of its own values. */\nexport type TapServerErrorCode =\n (typeof SERVER_ERROR_CODES)[keyof typeof SERVER_ERROR_CODES];\n","import {\n createLocalJWKSet,\n createRemoteJWKSet,\n jwtVerify,\n type JWTPayload,\n type JWTVerifyGetKey,\n} from 'jose';\nimport { TapError } from '../client/errors';\nimport type { TappifyClaims, VerifyTokenOptions } from './types';\n\n/**\n * The key set `verifyTappifyToken` fetches when no `jwksUrl` or `jwks` is given.\n *\n * @remarks\n * This is production. Point `jwksUrl` at the environment that is calling you when\n * you run against another one, and pass `jwks` in tests.\n */\nexport const DEFAULT_JWKS_URL =\n 'https://api.tappify.ai/api/v1/extensions/.well-known/jwks.json';\n\nconst CLOCK_TOLERANCE_SECONDS = 60;\n\nconst remoteSets = new Map<string, JWTVerifyGetKey>();\n\nfunction keySet(options: VerifyTokenOptions): JWTVerifyGetKey {\n if (options.jwks) return createLocalJWKSet(options.jwks);\n\n const url = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const cached = remoteSets.get(url);\n if (cached) return cached;\n\n const created = createRemoteJWKSet(new URL(url));\n remoteSets.set(url, created);\n return created;\n}\n\nfunction readString(payload: JWTPayload, key: string): string | undefined {\n const value = payload[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction toClaims(payload: JWTPayload, audience: string): TappifyClaims {\n const aud = payload.aud;\n const matches =\n aud === audience || (Array.isArray(aud) && aud.includes(audience));\n\n if (!matches) {\n throw new TapError(\n 'TAP_TOKEN_AUDIENCE',\n `This install token was issued for a different extension. Create the handler with the same extensionId as the \"id\" in tappify.extension.json, so its audience is \"${audience}\".`,\n );\n }\n\n const installId = readString(payload, 'installId');\n const extensionId = readString(payload, 'extensionId');\n const organizationId = readString(payload, 'organizationId');\n\n if (\n installId === undefined ||\n extensionId === undefined ||\n organizationId === undefined\n ) {\n throw new TapError(\n 'TAP_TOKEN_INVALID',\n `The install token is missing the installId, extensionId or organizationId claim. Verify against ${DEFAULT_JWKS_URL} and do not reuse tokens across environments.`,\n );\n }\n\n const scopes = payload.scopes;\n const userId = readString(payload, 'userId');\n\n const claims: TappifyClaims = {\n iss: readString(payload, 'iss') ?? '',\n aud: audience,\n exp: typeof payload.exp === 'number' ? payload.exp : 0,\n iat: typeof payload.iat === 'number' ? payload.iat : 0,\n installId,\n extensionId,\n projectId: readString(payload, 'projectId') ?? null,\n organizationId,\n scopes: Array.isArray(scopes)\n ? scopes.filter((scope): scope is string => typeof scope === 'string')\n : [],\n };\n\n if (userId !== undefined) claims.userId = userId;\n return claims;\n}\n\n/**\n * Verifies an install token against Tappify's signing keys and returns its\n * claims.\n *\n * @remarks\n * Checks the RS256 signature, the expiry with 60 seconds of clock tolerance, and\n * that the audience is `ext:<extensionId>`. Rejects with a `TapError` carrying\n * `TAP_TOKEN_INVALID` when the signature, the expiry or a required claim is wrong,\n * and `TAP_TOKEN_AUDIENCE` when the token was issued for another extension. A\n * remote key set is fetched once per url and cached for the process, so calling\n * this per request costs nothing after the first. `createTappifyHandler` already\n * verifies every route but health, so call this only outside the handler, or\n * where you accept a Tappify token on a route of your own.\n *\n * @example\n * ```ts\n * import { verifyTappifyToken } from '@tappify/extension-sdk/server';\n *\n * async function whoCalled(request: Request): Promise<string> {\n * const token = (request.headers.get('authorization') ?? '').replace('Bearer ', '');\n * const claims = await verifyTappifyToken(token, { extensionId: 'starter' });\n * return claims.installId;\n * }\n * ```\n */\nexport async function verifyTappifyToken(\n token: string,\n options: VerifyTokenOptions,\n): Promise<TappifyClaims> {\n const audience = `ext:${options.extensionId}`;\n\n let payload: JWTPayload;\n try {\n const verified = await jwtVerify(token, keySet(options), {\n algorithms: ['RS256'],\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n });\n payload = verified.payload;\n } catch {\n throw new TapError(\n 'TAP_TOKEN_INVALID',\n 'The install token did not verify. Check that your server reads the Authorization header unchanged, that its clock is correct, and that jwksUrl points at the Tappify environment that called you.',\n );\n }\n\n return toClaims(payload, audience);\n}\n","import type { TapServerErrorCode } from './codes';\n\nexport const INTERNAL_MESSAGE =\n 'The extension server failed to handle this call. Check your own logs; Tappify records the failure against your extension.';\n\nexport function json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n}\n\nexport function wireError(\n code: string,\n message: string,\n status: number,\n): Response {\n return json({ error: { code, message } }, status);\n}\n\nexport function failure(\n code: TapServerErrorCode,\n message: string,\n status: number,\n): Response {\n return wireError(code, message, status);\n}\n\nexport function readUnknown(value: unknown, key: string): unknown {\n if (typeof value !== 'object' || value === null) return undefined;\n return (value as Record<string, unknown>)[key];\n}\n\nexport function readRecord(\n value: unknown,\n key: string,\n): Record<string, unknown> {\n const nested = readUnknown(value, key);\n return typeof nested === 'object' && nested !== null\n ? (nested as Record<string, unknown>)\n : {};\n}\n\nexport function readText(value: unknown, key: string): string | null {\n const nested = readUnknown(value, key);\n return typeof nested === 'string' ? nested : null;\n}\n","import { TapError, TapServerError } from '../client/errors';\nimport type { TapCredentialValues, TapDateRange } from '../client/types';\nimport { EVENTS } from '../manifest/constants';\nimport { SERVER_ERROR_CODES, type TapServerErrorCode } from './codes';\nimport { verifyTappifyToken } from './token';\nimport type {\n ContextBlock,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyCallContext,\n TappifyClaims,\n TappifyDocuments,\n TappifyHandlerOptions,\n TappifyHealth,\n TappifyInstall,\n TappifyRequest,\n WorkOperation,\n} from './types';\nimport {\n INTERNAL_MESSAGE,\n failure,\n json,\n readRecord,\n readText,\n readUnknown,\n wireError,\n} from './wire';\n\n/**\n * What `createTappifyHandler` returns: a `fetch` handler, which is what Cloudflare\n * Workers, Vercel and the two adapters all take.\n */\nexport type TappifyFetchHandler = (request: Request) => Promise<Response>;\n\nconst MAX_BODY_BYTES = 1_048_576;\n\nconst WORK_OPERATIONS: readonly WorkOperation[] = [\n 'list_containers',\n 'create',\n 'attach',\n 'comment',\n 'status',\n 'resolve',\n];\n\nconst TOKEN_FAILURES: readonly TapServerErrorCode[] = [\n SERVER_ERROR_CODES.TOKEN_INVALID,\n SERVER_ERROR_CODES.TOKEN_AUDIENCE,\n];\n\n/**\n * The token is signed by Tappify and the body is not, so the claims win wherever\n * they carry a value and the body only fills what the token leaves out.\n */\nfunction toInstall(payload: unknown, claims: TappifyClaims): TappifyInstall {\n const raw = readRecord(payload, 'install');\n return {\n id: claims.installId,\n extensionId: claims.extensionId,\n projectId: claims.projectId ?? readText(raw, 'projectId'),\n organizationId: claims.organizationId,\n };\n}\n\nfunction toPlatform(value: string | null): TappifyCallContext['platform'] {\n return value === 'ios' || value === 'android' ? value : 'all';\n}\n\nfunction toPreset(value: string | null): TapDateRange['preset'] {\n return value === '7d' || value === '30d' || value === '90d'\n ? value\n : undefined;\n}\n\nfunction toContext(payload: unknown): TappifyCallContext {\n const raw = readRecord(payload, 'context');\n const filters = readRecord(raw, 'filters');\n const rawRange = readRecord(filters, 'range');\n const from = readText(raw, 'from') ?? readText(rawRange, 'from') ?? '';\n const to = readText(raw, 'to') ?? readText(rawRange, 'to') ?? '';\n\n const range: TapDateRange = {\n from: readText(rawRange, 'from') ?? from,\n to: readText(rawRange, 'to') ?? to,\n };\n\n const preset = toPreset(readText(rawRange, 'preset'));\n if (preset !== undefined) range.preset = preset;\n\n return {\n projectId: readText(raw, 'projectId'),\n platform: toPlatform(readText(raw, 'platform')),\n from,\n to,\n filters: {\n range,\n platform: toPlatform(readText(filters, 'platform')),\n country: readText(filters, 'country') ?? 'all',\n },\n };\n}\n\nfunction toCredentials(payload: unknown): TapCredentialValues {\n const raw = readRecord(payload, 'credentials');\n const credentials: Record<string, string> = {};\n for (const [name, value] of Object.entries(raw)) {\n if (typeof value === 'string') credentials[name] = value;\n }\n return credentials;\n}\n\nfunction tooLarge(): TapServerError {\n return new TapServerError(\n SERVER_ERROR_CODES.BODY_TOO_LARGE,\n 'A Tappify request body is at most 1 MB. Return a cursor or an id the host can follow instead of inlining the payload.',\n 413,\n );\n}\n\nasync function readBody(request: Request): Promise<unknown> {\n if (request.method === 'GET' || request.method === 'HEAD') return {};\n\n const length = request.headers.get('content-length');\n if (length !== null && Number(length) > MAX_BODY_BYTES) throw tooLarge();\n\n const bytes = await request.arrayBuffer();\n if (bytes.byteLength > MAX_BODY_BYTES) throw tooLarge();\n if (bytes.byteLength === 0) return {};\n\n const text = new TextDecoder().decode(bytes);\n try {\n return JSON.parse(text);\n } catch {\n throw new TapServerError(\n SERVER_ERROR_CODES.BODY_INVALID,\n 'The request body was not JSON. Do not parse or re-encode the body before handing the Request to createTappifyHandler().',\n 400,\n );\n }\n}\n\n/**\n * The route table holds handlers whose declared input and event types the\n * manifest augmentations narrow. Method syntax keeps the parameter bivariant,\n * so a narrowed handler still fits a table the wire types as `unknown`.\n */\ntype AnyRouteHandler = {\n route(request: TappifyRequest<unknown, unknown>): unknown;\n}['route'];\n\ninterface Route {\n handler: AnyRouteHandler | undefined;\n input: unknown;\n event: unknown;\n noContent: boolean;\n}\n\nfunction route(\n handler: AnyRouteHandler | undefined,\n input: unknown,\n event?: unknown,\n noContent = false,\n): Route {\n return { handler, input, event, noContent };\n}\n\nfunction handlerFor(\n map: Partial<Record<string, AnyRouteHandler>> | undefined,\n name: string,\n): AnyRouteHandler | undefined {\n return map?.[name];\n}\n\nfunction resolveRoute(\n options: TappifyHandlerOptions,\n method: string,\n segments: string[],\n url: URL,\n payload: unknown,\n): Route | null {\n const [head, name] = segments;\n const bodyInput = readUnknown(payload, 'input') ?? {};\n\n if (head === 'metrics' && method === 'POST') {\n return route(options.metrics, bodyInput);\n }\n if (head === 'tools' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.tools, name), bodyInput);\n }\n if (head === 'mentions' && method === 'GET' && name !== undefined) {\n return route(handlerFor(options.mentions, name), {\n q: url.searchParams.get('q') ?? '',\n });\n }\n if (head === 'actions' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.actions, name), bodyInput);\n }\n if (head === 'procedures' && method === 'POST' && name !== undefined) {\n return route(handlerFor(options.procedures, name), bodyInput);\n }\n if (head === 'context' && method === 'GET' && name !== undefined) {\n return route(handlerFor(options.context, name), undefined);\n }\n if (head === 'work' && method === 'POST' && name !== undefined) {\n const operation = WORK_OPERATIONS.find(candidate => candidate === name);\n return route(\n operation === undefined ? undefined : options.work?.[operation],\n bodyInput,\n );\n }\n if (head === 'events' && method === 'POST') {\n const envelope = readRecord(payload, 'event');\n const eventName = readText(envelope, 'name');\n if (eventName === null) {\n throw new TapServerError(\n SERVER_ERROR_CODES.BODY_INVALID,\n 'The events body has no event.name. Post the envelope Tappify sends unchanged, with the event name under \"event\".',\n 400,\n );\n }\n const declared = EVENTS.find(candidate => candidate === eventName);\n return route(\n declared === undefined ? undefined : options.events?.[declared],\n undefined,\n readUnknown(envelope, 'payload') ?? {},\n true,\n );\n }\n\n return null;\n}\n\n/**\n * Builds the one `fetch` handler that answers every route Tappify calls on your\n * server.\n *\n * @remarks\n * It routes the paths under `/tappify`, minus `basePath` when your framework mounts\n * it under a prefix, and hands each matched handler a `TappifyRequest`. Every\n * route but `GET /tappify/health` needs a bearer install token, which it verifies\n * with `verifyTappifyToken`, and an `X-Tappify-Event-Id` header; a missing token is\n * 401 `TAP_TOKEN_MISSING`, a missing id 400 `TAP_EVENT_ID_MISSING`, an unknown path\n * 404 `TAP_ROUTE_UNKNOWN` and a declared name with no handler 404\n * `TAP_HANDLER_MISSING`. A body that is not JSON is 400 `TAP_BODY_INVALID` and one\n * over 1 MB is 413 `TAP_BODY_TOO_LARGE`. A `TapServerError` a handler throws is\n * answered with its own code and status, and a `TapError` other than the two token\n * failures with its own code and 400; anything else becomes 500\n * `TAP_INTERNAL_ERROR` with a fixed message, so an internal failure never reaches\n * the owner. An events route answers 204 with no body, and every other route\n * answers the handler's return value as JSON. Export the options object as well as\n * the handler: `createTestClient` takes the options.\n *\n * @example\n * ```ts\n * import {\n * createTappifyHandler,\n * type TappifyHandlerOptions,\n * } from '@tappify/extension-sdk/server';\n *\n * const handlerOptions: TappifyHandlerOptions = {\n * extensionId: 'starter',\n * health: () => ({ ok: true, version: '0.1.0' }),\n * procedures: {\n * getSummary: () => ({ installs: 1260, delta: 0.087, topKeyword: 'photo editor' }),\n * },\n * };\n *\n * const handler = createTappifyHandler(handlerOptions);\n * ```\n */\nexport function createTappifyHandler(\n options: TappifyHandlerOptions,\n): TappifyFetchHandler {\n const basePath = options.basePath ?? '';\n\n return async function handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = url.pathname.startsWith(basePath)\n ? url.pathname.slice(basePath.length)\n : url.pathname;\n\n const segments = path.split('/').filter(segment => segment !== '');\n if (segments[0] !== 'tappify') {\n return failure(\n SERVER_ERROR_CODES.ROUTE_UNKNOWN,\n `Nothing is mounted at ${path}. Tappify calls paths under /tappify; set basePath if your framework mounts the handler under a prefix.`,\n 404,\n );\n }\n\n const rest = segments.slice(1);\n\n if (rest[0] === 'health' && request.method === 'GET') {\n const health: TappifyHealth = options.health\n ? await options.health()\n : { ok: true };\n return json(health);\n }\n\n const authorization = request.headers.get('authorization') ?? '';\n const token = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : '';\n\n if (token === '') {\n return failure(\n SERVER_ERROR_CODES.TOKEN_MISSING,\n 'This route needs an install token. Tappify sends it as Authorization: Bearer <token>; forward the header unchanged from your framework.',\n 401,\n );\n }\n\n const eventId = request.headers.get('x-tappify-event-id');\n if (eventId === null || eventId === '') {\n return failure(\n SERVER_ERROR_CODES.EVENT_ID_MISSING,\n 'Every Tappify call carries X-Tappify-Event-Id for idempotency. Forward the header unchanged, and use it as the key when you deduplicate.',\n 400,\n );\n }\n\n try {\n const claims = await verifyTappifyToken(token, {\n extensionId: options.extensionId,\n jwksUrl: options.jwksUrl,\n jwks: options.jwks,\n });\n\n const payload = await readBody(request);\n const matched = resolveRoute(options, request.method, rest, url, payload);\n\n if (matched === null) {\n return failure(\n SERVER_ERROR_CODES.ROUTE_UNKNOWN,\n `Tappify has no ${request.method} ${path} route. The paths it calls are listed under \"Vendor server endpoints\" in the reference; check the method and the spelling.`,\n 404,\n );\n }\n\n if (matched.handler === undefined) {\n return failure(\n SERVER_ERROR_CODES.HANDLER_MISSING,\n `No handler is registered for ${request.method} ${path}. Add it to createTappifyHandler(), or remove the contribution from tappify.extension.json.`,\n 404,\n );\n }\n\n const documents = readRecord(payload, 'documents');\n\n const typed: TappifyRequest<unknown, unknown> = {\n install: toInstall(payload, claims),\n context: toContext(payload),\n input: matched.input,\n credentials: toCredentials(payload),\n documents: documents as TappifyDocuments,\n event: matched.event,\n eventId,\n claims,\n request,\n };\n\n const result = await matched.handler(typed);\n return matched.noContent\n ? new Response(null, { status: 204 })\n : json(result);\n } catch (error) {\n if (TapServerError.is(error)) {\n return wireError(error.code, error.message, error.status ?? 400);\n }\n if (TapError.is(error)) {\n const tokenFailure = TOKEN_FAILURES.find(\n candidate => candidate === error.code,\n );\n if (tokenFailure !== undefined) {\n return failure(tokenFailure, error.message, 401);\n }\n return wireError(error.code, error.message, 400);\n }\n return failure(SERVER_ERROR_CODES.INTERNAL_ERROR, INTERNAL_MESSAGE, 500);\n }\n };\n}\n\nexport type {\n ContextBlock,\n MentionItems,\n MetricsInput,\n MetricsResponse,\n TappifyClaims,\n};\n"],"mappings":"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","names":[]}
|