@fragno-dev/api-fragment 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE.md +16 -0
  2. package/README.md +249 -2
  3. package/dist/browser/client/client-3ZV9sw3i.js +2580 -0
  4. package/dist/browser/client/client-3ZV9sw3i.js.map +1 -0
  5. package/dist/browser/client/client-DPjmoaJQ.d.ts +711 -0
  6. package/dist/browser/client/client-DPjmoaJQ.d.ts.map +1 -0
  7. package/dist/browser/client/client.d.ts +1645 -0
  8. package/dist/browser/client/client.d.ts.map +1 -0
  9. package/dist/browser/client/client.js +2 -0
  10. package/dist/browser/client/react.d.ts +1665 -0
  11. package/dist/browser/client/react.d.ts.map +1 -0
  12. package/dist/browser/client/react.js +194 -0
  13. package/dist/browser/client/react.js.map +1 -0
  14. package/dist/browser/client/solid.d.ts +1668 -0
  15. package/dist/browser/client/solid.d.ts.map +1 -0
  16. package/dist/browser/client/solid.js +129 -0
  17. package/dist/browser/client/solid.js.map +1 -0
  18. package/dist/browser/client/svelte.d.ts +1668 -0
  19. package/dist/browser/client/svelte.d.ts.map +1 -0
  20. package/dist/browser/client/svelte.js +131 -0
  21. package/dist/browser/client/svelte.js.map +1 -0
  22. package/dist/browser/client/vanilla.d.ts +1695 -0
  23. package/dist/browser/client/vanilla.d.ts.map +1 -0
  24. package/dist/browser/client/vanilla.js +159 -0
  25. package/dist/browser/client/vanilla.js.map +1 -0
  26. package/dist/browser/client/vue.d.ts +1668 -0
  27. package/dist/browser/client/vue.d.ts.map +1 -0
  28. package/dist/browser/client/vue.js +127 -0
  29. package/dist/browser/client/vue.js.map +1 -0
  30. package/dist/browser/client-D4NnY-zV.d.ts +713 -0
  31. package/dist/browser/client-D4NnY-zV.d.ts.map +1 -0
  32. package/dist/browser/client-DQE0c7GI.d.ts +711 -0
  33. package/dist/browser/client-DQE0c7GI.d.ts.map +1 -0
  34. package/dist/browser/index.d.ts +3130 -0
  35. package/dist/browser/index.d.ts.map +1 -0
  36. package/dist/browser/index.js +2 -0
  37. package/dist/browser/src-BrYPVbAN.js +2404 -0
  38. package/dist/browser/src-BrYPVbAN.js.map +1 -0
  39. package/dist/browser/src-t-4wp9-f.js +2400 -0
  40. package/dist/browser/src-t-4wp9-f.js.map +1 -0
  41. package/dist/node/api-types.d.ts +731 -0
  42. package/dist/node/api-types.d.ts.map +1 -0
  43. package/dist/node/api-types.js +281 -0
  44. package/dist/node/api-types.js.map +1 -0
  45. package/dist/node/crypto.d.ts +6 -0
  46. package/dist/node/crypto.d.ts.map +1 -0
  47. package/dist/node/crypto.js +103 -0
  48. package/dist/node/crypto.js.map +1 -0
  49. package/dist/node/definition.d.ts +638 -0
  50. package/dist/node/definition.d.ts.map +1 -0
  51. package/dist/node/definition.js +315 -0
  52. package/dist/node/definition.js.map +1 -0
  53. package/dist/node/oauth.js +124 -0
  54. package/dist/node/oauth.js.map +1 -0
  55. package/dist/node/routes.d.ts +1585 -0
  56. package/dist/node/routes.d.ts.map +1 -0
  57. package/dist/node/routes.js +1100 -0
  58. package/dist/node/routes.js.map +1 -0
  59. package/dist/node/schema.d.ts +99 -0
  60. package/dist/node/schema.d.ts.map +1 -0
  61. package/dist/node/schema.js +16 -0
  62. package/dist/node/schema.js.map +1 -0
  63. package/dist/node/server.d.ts +1585 -0
  64. package/dist/node/server.d.ts.map +1 -0
  65. package/dist/node/server.js +12 -0
  66. package/dist/node/server.js.map +1 -0
  67. package/dist/node/services.d.ts +10 -0
  68. package/dist/node/services.d.ts.map +1 -0
  69. package/dist/node/services.js +304 -0
  70. package/dist/node/services.js.map +1 -0
  71. package/dist/node/webhooks/auth.d.ts +133 -0
  72. package/dist/node/webhooks/auth.d.ts.map +1 -0
  73. package/dist/node/webhooks/auth.js +268 -0
  74. package/dist/node/webhooks/auth.js.map +1 -0
  75. package/dist/node/webhooks/verification.d.ts +102 -0
  76. package/dist/node/webhooks/verification.d.ts.map +1 -0
  77. package/dist/node/webhooks/verification.js +119 -0
  78. package/dist/node/webhooks/verification.js.map +1 -0
  79. package/package.json +136 -3
@@ -0,0 +1,1100 @@
1
+ import { apiSchema } from "./schema.js";
2
+ import { sha256Base64Url, utf8Bytes } from "./crypto.js";
3
+ import { getSensitiveWebhookAuthValues, getWebhookAuthSecretRefs, verifyWebhookAuth } from "./webhooks/auth.js";
4
+ import { createWebhookRequestValues, evaluateWebhookVerification } from "./webhooks/verification.js";
5
+ import { API_OAUTH_REDIRECT_URI_QUERY_PARAMETER, apiAuthStatusSchema, apiConnectionOutputSchema, apiConnectionSlugSchema, apiConnectionsPageSchema, apiOAuthPendingSchema, apiRequestInputSchema, apiRequestOutputSchema, createApiConnectionInputSchema, createWebhookEndpointInputSchema, oauthRedirectUriSchema, oauthStartInputSchema, tokenAuthInputSchema, updateWebhookEndpointInputSchema, webhookEndpointOutputSchema, webhookRejectionCodeSchema } from "./api-types.js";
6
+ import { assertAllowedBaseUrl, isApiAuthReady, pendingOAuthLink, projectApiAuthStatus } from "./services.js";
7
+ import { apiFragmentDefinition } from "./definition.js";
8
+ import { defineRoutes } from "@fragno-dev/core";
9
+ import { isUniqueConstraintError } from "@fragno-dev/db";
10
+ import { z } from "zod";
11
+ import { decodeCursor } from "@fragno-dev/db/cursor";
12
+ //#region src/routes.ts
13
+ const CONNECTIONS_PAGE_SIZE = 50;
14
+ const webhookEndpointsOutputSchema = z.object({ endpoints: z.array(webhookEndpointOutputSchema) });
15
+ const oauthStartOutputSchema = z.object({
16
+ authorizationUrl: z.string(),
17
+ state: z.string()
18
+ });
19
+ const oauthCallbackOutputSchema = z.object({
20
+ authenticated: z.boolean(),
21
+ mode: z.string()
22
+ });
23
+ const logApiRouteError = (message, err) => {
24
+ const detail = err instanceof Error ? err.message : String(err);
25
+ console.error(message, detail, err);
26
+ };
27
+ function publicConnection(connection) {
28
+ return {
29
+ slug: connection.id.toString(),
30
+ name: connection.name ?? null,
31
+ baseUrl: connection.baseUrl,
32
+ authMode: connection.authMode,
33
+ status: connection.status,
34
+ createdAt: connection.createdAt,
35
+ updatedAt: connection.updatedAt
36
+ };
37
+ }
38
+ function connectionHookSnapshot(connection) {
39
+ return {
40
+ slug: connection.id.toString(),
41
+ name: connection.name ?? null,
42
+ baseUrl: connection.baseUrl,
43
+ authMode: connection.authMode,
44
+ status: connection.status
45
+ };
46
+ }
47
+ function webhookSecretId(endpointId, ref) {
48
+ return `${endpointId}:${ref}`;
49
+ }
50
+ function webhookEndpointAuthStorage(auth) {
51
+ if (auth.type === "none") return {
52
+ authConfig: { type: "none" },
53
+ secrets: []
54
+ };
55
+ if (auth.type === "bearer") return {
56
+ authConfig: {
57
+ type: "bearer",
58
+ tokenRef: "token"
59
+ },
60
+ secrets: [{
61
+ ref: "token",
62
+ payload: auth.token
63
+ }]
64
+ };
65
+ if (auth.type === "apiKey") return {
66
+ authConfig: {
67
+ type: "apiKey",
68
+ location: auth.location,
69
+ name: auth.name,
70
+ secretRef: "secret"
71
+ },
72
+ secrets: [{
73
+ ref: "secret",
74
+ payload: auth.secret
75
+ }]
76
+ };
77
+ if (auth.type === "basic") return {
78
+ authConfig: {
79
+ type: "basic",
80
+ usernameRef: "username",
81
+ passwordRef: "password"
82
+ },
83
+ secrets: [{
84
+ ref: "username",
85
+ payload: auth.username
86
+ }, {
87
+ ref: "password",
88
+ payload: auth.password
89
+ }]
90
+ };
91
+ return {
92
+ authConfig: {
93
+ type: "hmac",
94
+ secretRef: "secret",
95
+ algorithm: auth.algorithm,
96
+ signature: auth.signature,
97
+ signedPayload: auth.signedPayload
98
+ },
99
+ secrets: [{
100
+ ref: "secret",
101
+ payload: auth.secret
102
+ }]
103
+ };
104
+ }
105
+ function parseWebhookEndpointStatus(status) {
106
+ if (status === "draft" || status === "active" || status === "disabled") return status;
107
+ throw new Error(`Unexpected webhook endpoint status: ${status}`);
108
+ }
109
+ function webhookEndpointHookSnapshot(endpoint) {
110
+ return {
111
+ name: endpoint.name,
112
+ status: parseWebhookEndpointStatus(endpoint.status),
113
+ authConfig: endpoint.authConfig,
114
+ verification: endpoint.verification,
115
+ deliveryIdentity: endpoint.deliveryIdentity,
116
+ secretRefs: [...getWebhookAuthSecretRefs(endpoint.authConfig)]
117
+ };
118
+ }
119
+ function recordFromSearchParams(query, sensitiveQueryNames) {
120
+ const result = {};
121
+ for (const [name, value] of query.entries()) result[name] = sensitiveQueryNames.has(name) ? "[redacted]" : value;
122
+ return result;
123
+ }
124
+ function recordFromHeaders(headers, sensitiveHeaderNames) {
125
+ const result = {};
126
+ for (const [name, value] of headers.entries()) result[name] = sensitiveHeaderNames.has(name.toLowerCase()) ? "[redacted]" : value;
127
+ return result;
128
+ }
129
+ function extractWebhookDeliveryId(input) {
130
+ const value = input.requestValues.read(input.identity);
131
+ if (!value.ok) return {
132
+ ok: false,
133
+ reason: value.reason === "missing" ? "missing" : "invalid_value"
134
+ };
135
+ return normalizeWebhookDeliveryId(value.value);
136
+ }
137
+ function normalizeWebhookDeliveryId(value) {
138
+ if (typeof value !== "string" && typeof value !== "number") return value == null ? {
139
+ ok: false,
140
+ reason: "missing"
141
+ } : {
142
+ ok: false,
143
+ reason: "invalid_value"
144
+ };
145
+ const deliveryId = `${value}`.trim();
146
+ if (!deliveryId) return {
147
+ ok: false,
148
+ reason: "missing"
149
+ };
150
+ return {
151
+ ok: true,
152
+ deliveryId
153
+ };
154
+ }
155
+ async function webhookHookId(endpointId, deliveryId) {
156
+ return `webhook_${await sha256Base64Url(`${endpointId}\0${deliveryId}`)}`;
157
+ }
158
+ function webhookVerificationResponse(body) {
159
+ const bytes = utf8Bytes(body);
160
+ return new Response(bytes, {
161
+ status: 200,
162
+ headers: {
163
+ "content-type": "text/plain; charset=utf-8",
164
+ "content-length": `${bytes.byteLength}`,
165
+ "x-content-type-options": "nosniff"
166
+ }
167
+ });
168
+ }
169
+ /** Configuration writes reset delivery evidence, which then covers only the new configuration. */
170
+ const noWebhookDeliveryEvidence = {
171
+ lastAcceptedAt: null,
172
+ lastRejectedAt: null,
173
+ lastRejectionCode: null
174
+ };
175
+ function publicWebhookEndpoint(endpoint) {
176
+ return {
177
+ id: endpoint.id.toString(),
178
+ name: endpoint.name,
179
+ status: parseWebhookEndpointStatus(endpoint.status),
180
+ authConfig: endpoint.authConfig,
181
+ verification: endpoint.verification,
182
+ deliveryIdentity: endpoint.deliveryIdentity,
183
+ secretRefs: [...getWebhookAuthSecretRefs(endpoint.authConfig)],
184
+ deliveries: {
185
+ lastAcceptedAt: endpoint.lastAcceptedAt?.toISOString() ?? null,
186
+ lastRejection: endpoint.lastRejectedAt === null ? null : {
187
+ receivedAt: endpoint.lastRejectedAt.toISOString(),
188
+ code: webhookRejectionCodeSchema.parse(endpoint.lastRejectionCode)
189
+ }
190
+ },
191
+ createdAt: endpoint.createdAt,
192
+ updatedAt: endpoint.updatedAt
193
+ };
194
+ }
195
+ /**
196
+ * Records the outcome of a receive request; unknown endpoints have no row to record on. Writes skip
197
+ * the version check, so concurrent deliveries never conflict and the last one wins.
198
+ */
199
+ function recordWebhookRequestOutcome(uow, endpointId, rejection) {
200
+ if (rejection === "WEBHOOK_ENDPOINT_NOT_FOUND") return;
201
+ uow.update("webhookEndpoint", endpointId, (b) => rejection === null ? b.set({ lastAcceptedAt: b.now() }) : b.set({
202
+ lastRejectedAt: b.now(),
203
+ lastRejectionCode: rejection
204
+ }));
205
+ }
206
+ const apiRoutesFactory = defineRoutes(apiFragmentDefinition).create(({ services, defineRoute, config }) => [
207
+ defineRoute({
208
+ method: "PUT",
209
+ path: "/webhooks/endpoints/:endpointId",
210
+ inputSchema: createWebhookEndpointInputSchema,
211
+ outputSchema: webhookEndpointOutputSchema,
212
+ handler: async function({ input, pathParams }, { json }) {
213
+ const body = await input.valid();
214
+ const authStorage = webhookEndpointAuthStorage(body.auth);
215
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("webhookEndpoint", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.endpointId))).find("webhookSecret", (b) => b.whereIndex("idx_webhook_secret_endpoint_ref", (eb) => eb("endpointId", "=", pathParams.endpointId)))).mutate(({ forSchema, retrieveResult: [existing, existingSecrets] }) => {
216
+ const uow = forSchema(apiSchema);
217
+ const endpoint = {
218
+ id: pathParams.endpointId,
219
+ name: body.name,
220
+ status: body.status,
221
+ authConfig: authStorage.authConfig,
222
+ verification: body.verification,
223
+ deliveryIdentity: body.deliveryIdentity,
224
+ ...noWebhookDeliveryEvidence,
225
+ createdAt: existing?.createdAt
226
+ };
227
+ if (existing) uow.update("webhookEndpoint", existing.id, (b) => b.set({
228
+ name: endpoint.name,
229
+ status: endpoint.status,
230
+ authConfig: endpoint.authConfig,
231
+ verification: endpoint.verification,
232
+ deliveryIdentity: endpoint.deliveryIdentity,
233
+ ...noWebhookDeliveryEvidence,
234
+ updatedAt: b.now()
235
+ }).check());
236
+ else uow.create("webhookEndpoint", {
237
+ id: endpoint.id,
238
+ name: endpoint.name,
239
+ status: endpoint.status,
240
+ authConfig: endpoint.authConfig,
241
+ verification: endpoint.verification,
242
+ deliveryIdentity: endpoint.deliveryIdentity
243
+ });
244
+ const nextSecretRefs = new Set(authStorage.secrets.map((secret) => secret.ref));
245
+ const existingSecretsByRef = new Map(existingSecrets.map((secret) => [secret.ref, secret]));
246
+ for (const secret of existingSecrets) if (!nextSecretRefs.has(secret.ref)) uow.delete("webhookSecret", secret.id);
247
+ for (const secret of authStorage.secrets) {
248
+ const existingSecret = existingSecretsByRef.get(secret.ref);
249
+ if (existingSecret) uow.update("webhookSecret", existingSecret.id, (b) => b.set({
250
+ payload: secret.payload,
251
+ updatedAt: b.now()
252
+ }).check());
253
+ else uow.create("webhookSecret", {
254
+ id: webhookSecretId(pathParams.endpointId, secret.ref),
255
+ endpointId: pathParams.endpointId,
256
+ ref: secret.ref,
257
+ payload: secret.payload
258
+ });
259
+ }
260
+ uow.triggerHook("onWebhookEndpointChanged", existing ? {
261
+ change: "updated",
262
+ endpointId: pathParams.endpointId,
263
+ endpoint: webhookEndpointHookSnapshot(endpoint),
264
+ previousStatus: parseWebhookEndpointStatus(existing.status)
265
+ } : {
266
+ change: "created",
267
+ endpointId: pathParams.endpointId,
268
+ endpoint: webhookEndpointHookSnapshot(endpoint)
269
+ });
270
+ return {
271
+ created: !existing,
272
+ endpoint
273
+ };
274
+ }).execute();
275
+ return json(publicWebhookEndpoint(result.endpoint), result.created ? 201 : 200);
276
+ }
277
+ }),
278
+ defineRoute({
279
+ method: "GET",
280
+ path: "/webhooks/endpoints",
281
+ outputSchema: webhookEndpointsOutputSchema,
282
+ handler: async function(_, { json }) {
283
+ const [endpoints] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).find("webhookEndpoint", (b) => b.whereIndex("primary"))).execute();
284
+ return json({ endpoints: endpoints.map(publicWebhookEndpoint) });
285
+ }
286
+ }),
287
+ defineRoute({
288
+ method: "GET",
289
+ path: "/webhooks/endpoints/:endpointId",
290
+ outputSchema: webhookEndpointOutputSchema,
291
+ errorCodes: ["WEBHOOK_ENDPOINT_NOT_FOUND"],
292
+ handler: async function({ pathParams }, { json, error }) {
293
+ const [endpoint] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("webhookEndpoint", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.endpointId)))).execute();
294
+ if (!endpoint) return error({
295
+ code: "WEBHOOK_ENDPOINT_NOT_FOUND",
296
+ message: "Webhook endpoint not found"
297
+ }, 404);
298
+ return json(publicWebhookEndpoint(endpoint));
299
+ }
300
+ }),
301
+ defineRoute({
302
+ method: "GET",
303
+ path: "/webhooks/endpoints/:endpointId/events",
304
+ errorCodes: [
305
+ "WEBHOOK_ENDPOINT_NOT_FOUND",
306
+ "WEBHOOK_ENDPOINT_DISABLED",
307
+ "WEBHOOK_ENDPOINT_DRAFT",
308
+ "WEBHOOK_AUTH_FAILED",
309
+ "WEBHOOK_VERIFICATION_NOT_MATCHED",
310
+ "WEBHOOK_VERIFICATION_RESPONSE_INVALID"
311
+ ],
312
+ handler: async function({ pathParams, headers, query, rawBody, request }, { error }) {
313
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("webhookEndpoint", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.endpointId))).find("webhookSecret", (b) => b.whereIndex("idx_webhook_secret_endpoint_ref", (eb) => eb("endpointId", "=", pathParams.endpointId)))).transformRetrieve(async ([endpoint, secrets]) => {
314
+ if (!endpoint) return {
315
+ ok: false,
316
+ code: "WEBHOOK_ENDPOINT_NOT_FOUND"
317
+ };
318
+ if (endpoint.status === "draft") return {
319
+ ok: false,
320
+ code: "WEBHOOK_ENDPOINT_DRAFT"
321
+ };
322
+ if (endpoint.status !== "active") return {
323
+ ok: false,
324
+ code: "WEBHOOK_ENDPOINT_DISABLED"
325
+ };
326
+ if (!request) throw new Error("Webhook receive route requires a web-standard Request");
327
+ const secretValues = new Map(secrets.map((secret) => [secret.ref, secret.payload]));
328
+ if (!(await verifyWebhookAuth({
329
+ config: endpoint.authConfig,
330
+ request,
331
+ secrets: { get: async (ref) => secretValues.get(ref) }
332
+ })).ok) return {
333
+ ok: false,
334
+ code: "WEBHOOK_AUTH_FAILED"
335
+ };
336
+ const verification = evaluateWebhookVerification({
337
+ config: endpoint.verification,
338
+ method: "GET",
339
+ requestValues: createWebhookRequestValues({
340
+ headers,
341
+ query,
342
+ rawBody
343
+ })
344
+ });
345
+ if (verification.type === "not_verification") return {
346
+ ok: false,
347
+ code: "WEBHOOK_VERIFICATION_NOT_MATCHED"
348
+ };
349
+ if (verification.type === "invalid_response") return {
350
+ ok: false,
351
+ code: "WEBHOOK_VERIFICATION_RESPONSE_INVALID"
352
+ };
353
+ return {
354
+ ok: true,
355
+ body: verification.body
356
+ };
357
+ }).mutate(({ forSchema, retrieveResult }) => {
358
+ recordWebhookRequestOutcome(forSchema(apiSchema), pathParams.endpointId, retrieveResult.ok ? null : retrieveResult.code);
359
+ return retrieveResult;
360
+ }).execute();
361
+ if (!result.ok) {
362
+ if (result.code === "WEBHOOK_ENDPOINT_NOT_FOUND") return error({
363
+ code: result.code,
364
+ message: "Webhook endpoint not found"
365
+ }, 404);
366
+ if (result.code === "WEBHOOK_ENDPOINT_DISABLED") return error({
367
+ code: result.code,
368
+ message: "Webhook endpoint is disabled"
369
+ }, 409);
370
+ if (result.code === "WEBHOOK_ENDPOINT_DRAFT") return error({
371
+ code: result.code,
372
+ message: "Webhook endpoint is not configured yet"
373
+ }, 409);
374
+ if (result.code === "WEBHOOK_AUTH_FAILED") return error({
375
+ code: result.code,
376
+ message: "Webhook authentication failed"
377
+ }, 401);
378
+ if (result.code === "WEBHOOK_VERIFICATION_RESPONSE_INVALID") return error({
379
+ code: result.code,
380
+ message: "Webhook challenge is invalid"
381
+ }, 400);
382
+ return error({
383
+ code: result.code,
384
+ message: "Webhook verification request not found"
385
+ }, 404);
386
+ }
387
+ return webhookVerificationResponse(result.body);
388
+ }
389
+ }),
390
+ defineRoute({
391
+ method: "POST",
392
+ path: "/webhooks/endpoints/:endpointId/events",
393
+ errorCodes: [
394
+ "WEBHOOK_ENDPOINT_NOT_FOUND",
395
+ "WEBHOOK_ENDPOINT_DISABLED",
396
+ "WEBHOOK_ENDPOINT_DRAFT",
397
+ "WEBHOOK_AUTH_FAILED",
398
+ "WEBHOOK_VERIFICATION_RESPONSE_INVALID",
399
+ "WEBHOOK_BODY_INVALID",
400
+ "WEBHOOK_DELIVERY_ID_MISSING",
401
+ "WEBHOOK_DELIVERY_ID_INVALID"
402
+ ],
403
+ handler: async function({ pathParams, headers, query, rawBody, request }, { json, error }) {
404
+ let result;
405
+ try {
406
+ result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("webhookEndpoint", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.endpointId))).find("webhookSecret", (b) => b.whereIndex("idx_webhook_secret_endpoint_ref", (eb) => eb("endpointId", "=", pathParams.endpointId)))).transformRetrieve(async ([endpoint, secrets]) => {
407
+ if (!endpoint) return {
408
+ ok: false,
409
+ code: "WEBHOOK_ENDPOINT_NOT_FOUND"
410
+ };
411
+ if (endpoint.status === "draft") return {
412
+ ok: false,
413
+ code: "WEBHOOK_ENDPOINT_DRAFT"
414
+ };
415
+ if (endpoint.status !== "active") return {
416
+ ok: false,
417
+ code: "WEBHOOK_ENDPOINT_DISABLED"
418
+ };
419
+ if (!request) throw new Error("Webhook receive route requires a web-standard Request");
420
+ const authConfig = endpoint.authConfig;
421
+ const secretValues = new Map(secrets.map((secret) => [secret.ref, secret.payload]));
422
+ if (!(await verifyWebhookAuth({
423
+ config: authConfig,
424
+ request,
425
+ secrets: { get: async (ref) => secretValues.get(ref) }
426
+ })).ok) return {
427
+ ok: false,
428
+ code: "WEBHOOK_AUTH_FAILED"
429
+ };
430
+ const requestValues = createWebhookRequestValues({
431
+ headers,
432
+ query,
433
+ rawBody
434
+ });
435
+ const verification = evaluateWebhookVerification({
436
+ config: endpoint.verification,
437
+ method: "POST",
438
+ requestValues
439
+ });
440
+ if (verification.type === "invalid_response") return {
441
+ ok: false,
442
+ code: "WEBHOOK_VERIFICATION_RESPONSE_INVALID"
443
+ };
444
+ if (verification.type === "response") return {
445
+ ok: true,
446
+ type: "verification",
447
+ body: verification.body
448
+ };
449
+ const body = requestValues.jsonBody();
450
+ if (!body.ok) return {
451
+ ok: false,
452
+ code: "WEBHOOK_BODY_INVALID"
453
+ };
454
+ const deliveryId = extractWebhookDeliveryId({
455
+ identity: endpoint.deliveryIdentity,
456
+ requestValues
457
+ });
458
+ if (!deliveryId.ok) return {
459
+ ok: false,
460
+ code: deliveryId.reason === "missing" ? "WEBHOOK_DELIVERY_ID_MISSING" : "WEBHOOK_DELIVERY_ID_INVALID"
461
+ };
462
+ return {
463
+ ok: true,
464
+ type: "delivery",
465
+ authConfig,
466
+ deliveryId: deliveryId.deliveryId,
467
+ hookId: await webhookHookId(pathParams.endpointId, deliveryId.deliveryId),
468
+ body: body.body
469
+ };
470
+ }).mutate(({ forSchema, retrieveResult }) => {
471
+ const uow = forSchema(apiSchema);
472
+ recordWebhookRequestOutcome(uow, pathParams.endpointId, retrieveResult.ok ? null : retrieveResult.code);
473
+ if (!retrieveResult.ok || retrieveResult.type === "verification") return retrieveResult;
474
+ const sensitiveValues = getSensitiveWebhookAuthValues(retrieveResult.authConfig);
475
+ const sensitiveHeaderNames = new Set(sensitiveValues.flatMap((value) => value.location === "header" ? [value.name.toLowerCase()] : []));
476
+ const sensitiveQueryNames = new Set(sensitiveValues.flatMap((value) => value.location === "query" ? [value.name] : []));
477
+ uow.triggerHook("onWebhookReceived", {
478
+ endpointId: pathParams.endpointId,
479
+ deliveryId: retrieveResult.deliveryId,
480
+ hookId: retrieveResult.hookId,
481
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
482
+ headers: recordFromHeaders(headers, sensitiveHeaderNames),
483
+ query: recordFromSearchParams(query, sensitiveQueryNames),
484
+ rawBody: rawBody ?? "",
485
+ body: retrieveResult.body,
486
+ contentType: headers.get("content-type")
487
+ }, { id: retrieveResult.hookId });
488
+ return {
489
+ ok: true,
490
+ type: "delivery"
491
+ };
492
+ }).execute();
493
+ } catch (err) {
494
+ if (isUniqueConstraintError(err)) return json({ accepted: true }, 202);
495
+ throw err;
496
+ }
497
+ if (!result.ok) {
498
+ if (result.code === "WEBHOOK_ENDPOINT_NOT_FOUND") return error({
499
+ code: result.code,
500
+ message: "Webhook endpoint not found"
501
+ }, 404);
502
+ if (result.code === "WEBHOOK_ENDPOINT_DISABLED") return error({
503
+ code: result.code,
504
+ message: "Webhook endpoint is disabled"
505
+ }, 409);
506
+ if (result.code === "WEBHOOK_ENDPOINT_DRAFT") return error({
507
+ code: result.code,
508
+ message: "Webhook endpoint is not configured yet"
509
+ }, 409);
510
+ if (result.code === "WEBHOOK_VERIFICATION_RESPONSE_INVALID") return error({
511
+ code: result.code,
512
+ message: "Webhook challenge is invalid"
513
+ }, 400);
514
+ if (result.code === "WEBHOOK_BODY_INVALID") return error({
515
+ code: result.code,
516
+ message: "Webhook body must be a JSON object"
517
+ }, 400);
518
+ if (result.code === "WEBHOOK_DELIVERY_ID_MISSING") return error({
519
+ code: result.code,
520
+ message: "Webhook delivery ID is missing"
521
+ }, 400);
522
+ if (result.code === "WEBHOOK_DELIVERY_ID_INVALID") return error({
523
+ code: result.code,
524
+ message: "Webhook delivery ID is invalid"
525
+ }, 400);
526
+ return error({
527
+ code: result.code,
528
+ message: "Webhook authentication failed"
529
+ }, 401);
530
+ }
531
+ if (result.type === "verification") return webhookVerificationResponse(result.body);
532
+ return json({ accepted: true }, 202);
533
+ }
534
+ }),
535
+ defineRoute({
536
+ method: "PATCH",
537
+ path: "/webhooks/endpoints/:endpointId",
538
+ inputSchema: updateWebhookEndpointInputSchema,
539
+ outputSchema: webhookEndpointOutputSchema,
540
+ errorCodes: ["WEBHOOK_ENDPOINT_NOT_FOUND"],
541
+ handler: async function({ input, pathParams }, { json, error }) {
542
+ const body = await input.valid();
543
+ const authStorage = body.auth ? webhookEndpointAuthStorage(body.auth) : null;
544
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("webhookEndpoint", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.endpointId))).find("webhookSecret", (b) => b.whereIndex("idx_webhook_secret_endpoint_ref", (eb) => eb("endpointId", "=", pathParams.endpointId)))).mutate(({ forSchema, retrieveResult: [endpoint, secrets] }) => {
545
+ if (!endpoint) return { found: false };
546
+ const uow = forSchema(apiSchema);
547
+ const authConfig = authStorage?.authConfig ?? endpoint.authConfig;
548
+ const verification = body.verification ?? endpoint.verification;
549
+ const deliveryIdentity = body.deliveryIdentity ?? endpoint.deliveryIdentity;
550
+ const next = {
551
+ id: endpoint.id,
552
+ name: body.name ?? endpoint.name,
553
+ status: body.status ?? endpoint.status,
554
+ authConfig,
555
+ verification,
556
+ deliveryIdentity,
557
+ ...noWebhookDeliveryEvidence,
558
+ createdAt: endpoint.createdAt
559
+ };
560
+ uow.update("webhookEndpoint", endpoint.id, (b) => b.set({
561
+ name: next.name,
562
+ status: next.status,
563
+ authConfig,
564
+ verification,
565
+ deliveryIdentity,
566
+ ...noWebhookDeliveryEvidence,
567
+ updatedAt: b.now()
568
+ }).check());
569
+ if (authStorage) {
570
+ for (const secret of secrets) uow.delete("webhookSecret", secret.id);
571
+ for (const secret of authStorage.secrets) uow.create("webhookSecret", {
572
+ id: webhookSecretId(pathParams.endpointId, secret.ref),
573
+ endpointId: pathParams.endpointId,
574
+ ref: secret.ref,
575
+ payload: secret.payload
576
+ });
577
+ }
578
+ uow.triggerHook("onWebhookEndpointChanged", {
579
+ change: "updated",
580
+ endpointId: pathParams.endpointId,
581
+ endpoint: webhookEndpointHookSnapshot(next),
582
+ previousStatus: parseWebhookEndpointStatus(endpoint.status)
583
+ });
584
+ return {
585
+ found: true,
586
+ endpoint: next
587
+ };
588
+ }).execute();
589
+ if (!result.found) return error({
590
+ code: "WEBHOOK_ENDPOINT_NOT_FOUND",
591
+ message: "Webhook endpoint not found"
592
+ }, 404);
593
+ return json(publicWebhookEndpoint(result.endpoint));
594
+ }
595
+ }),
596
+ defineRoute({
597
+ method: "DELETE",
598
+ path: "/webhooks/endpoints/:endpointId",
599
+ handler: async function({ pathParams }, { empty }) {
600
+ await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("webhookEndpoint", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.endpointId))).find("webhookSecret", (b) => b.whereIndex("idx_webhook_secret_endpoint_ref", (eb) => eb("endpointId", "=", pathParams.endpointId)))).mutate(({ forSchema, retrieveResult: [endpoint, secrets] }) => {
601
+ if (!endpoint) return { deleted: false };
602
+ const uow = forSchema(apiSchema);
603
+ for (const secret of secrets) uow.delete("webhookSecret", secret.id);
604
+ uow.delete("webhookEndpoint", endpoint.id);
605
+ uow.triggerHook("onWebhookEndpointChanged", {
606
+ change: "deleted",
607
+ endpointId: pathParams.endpointId
608
+ });
609
+ return { deleted: true };
610
+ }).execute();
611
+ return empty(204);
612
+ }
613
+ }),
614
+ defineRoute({
615
+ method: "PUT",
616
+ path: "/connections/:slug",
617
+ inputSchema: createApiConnectionInputSchema,
618
+ outputSchema: apiConnectionOutputSchema,
619
+ errorCodes: [
620
+ "CONNECTION_EXISTS",
621
+ "BASE_URL_NOT_ALLOWED",
622
+ "INVALID_SLUG"
623
+ ],
624
+ handler: async function({ input, pathParams }, { json, error }) {
625
+ const slug = apiConnectionSlugSchema.safeParse(pathParams.slug);
626
+ if (!slug.success) return error({
627
+ code: "INVALID_SLUG",
628
+ message: slug.error.issues[0].message
629
+ }, 400);
630
+ const body = await input.valid();
631
+ try {
632
+ assertAllowedBaseUrl(body.baseUrl, config);
633
+ } catch (err) {
634
+ logApiRouteError("API connection base URL rejected", err);
635
+ return error({
636
+ code: "BASE_URL_NOT_ALLOWED",
637
+ message: "API base URL is not allowed"
638
+ }, 400);
639
+ }
640
+ const payload = body.auth.type === "none" ? void 0 : JSON.stringify(body.auth);
641
+ if ((await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("api_connection", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug)))).mutate(({ forSchema, retrieveResult: [existing] }) => {
642
+ if (existing) return { exists: true };
643
+ const uow = forSchema(apiSchema);
644
+ uow.create("api_connection", {
645
+ id: pathParams.slug,
646
+ name: body.name ?? null,
647
+ baseUrl: body.baseUrl,
648
+ authMode: body.auth.type,
649
+ status: "active"
650
+ });
651
+ if (payload) uow.create("secret", {
652
+ id: `${pathParams.slug}:auth`,
653
+ connectionId: pathParams.slug,
654
+ kind: "auth",
655
+ payload,
656
+ expiresAt: null
657
+ });
658
+ uow.triggerHook("onConnectionChanged", {
659
+ connectionId: pathParams.slug,
660
+ connection: connectionHookSnapshot({
661
+ id: pathParams.slug,
662
+ name: body.name ?? null,
663
+ baseUrl: body.baseUrl,
664
+ authMode: body.auth.type,
665
+ status: "active"
666
+ })
667
+ });
668
+ uow.triggerHook("onConnectionReadinessChanged", {
669
+ connectionId: pathParams.slug,
670
+ ready: isApiAuthReady(body.auth.type, payload === void 0 ? void 0 : {
671
+ payload,
672
+ expiresAt: null
673
+ })
674
+ });
675
+ return { exists: false };
676
+ }).execute()).exists) return error({
677
+ code: "CONNECTION_EXISTS",
678
+ message: "API connection already exists"
679
+ }, 409);
680
+ return json(publicConnection({
681
+ id: pathParams.slug,
682
+ name: body.name ?? null,
683
+ baseUrl: body.baseUrl,
684
+ authMode: body.auth.type,
685
+ status: "active"
686
+ }), 201);
687
+ }
688
+ }),
689
+ defineRoute({
690
+ method: "GET",
691
+ path: "/connections",
692
+ queryParameters: ["cursor"],
693
+ outputSchema: apiConnectionsPageSchema,
694
+ errorCodes: ["INVALID_CURSOR"],
695
+ handler: async function({ query }, { json, error }) {
696
+ let cursor = null;
697
+ const rawCursor = query.get("cursor");
698
+ if (rawCursor) try {
699
+ cursor = decodeCursor(rawCursor);
700
+ if (cursor.indexName !== "_primary" || cursor.orderDirection !== "asc" || cursor.pageSize !== CONNECTIONS_PAGE_SIZE || typeof cursor.indexValues["id"] !== "string") throw new Error("Connection cursor does not match this listing");
701
+ } catch {
702
+ return error({
703
+ code: "INVALID_CURSOR",
704
+ message: "Invalid connection cursor"
705
+ }, 400);
706
+ }
707
+ const [page] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findWithCursor("api_connection", (b) => {
708
+ const ordered = b.whereIndex("primary").orderByIndex("primary", "asc").pageSize(CONNECTIONS_PAGE_SIZE);
709
+ return cursor ? ordered.after(cursor) : ordered;
710
+ })).execute();
711
+ return json({
712
+ connections: page.items.map(publicConnection),
713
+ cursor: page.cursor?.encode() ?? null
714
+ });
715
+ }
716
+ }),
717
+ defineRoute({
718
+ method: "GET",
719
+ path: "/connections/:slug",
720
+ outputSchema: apiConnectionOutputSchema,
721
+ errorCodes: ["CONNECTION_NOT_FOUND"],
722
+ handler: async function({ pathParams }, { json, error }) {
723
+ const [connection] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("api_connection", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug)))).execute();
724
+ if (!connection) return error({
725
+ code: "CONNECTION_NOT_FOUND",
726
+ message: "API connection not found"
727
+ }, 404);
728
+ return json(publicConnection(connection));
729
+ }
730
+ }),
731
+ defineRoute({
732
+ method: "PUT",
733
+ path: "/connections/:slug/configuration",
734
+ inputSchema: createApiConnectionInputSchema,
735
+ outputSchema: apiConnectionOutputSchema,
736
+ errorCodes: ["CONNECTION_NOT_FOUND", "BASE_URL_NOT_ALLOWED"],
737
+ handler: async function({ input, pathParams }, { json, error }) {
738
+ const body = await input.valid();
739
+ try {
740
+ assertAllowedBaseUrl(body.baseUrl, config);
741
+ } catch (err) {
742
+ logApiRouteError("API connection base URL rejected", err);
743
+ return error({
744
+ code: "BASE_URL_NOT_ALLOWED",
745
+ message: "API base URL is not allowed"
746
+ }, 400);
747
+ }
748
+ const payload = body.auth.type === "none" ? null : JSON.stringify(body.auth);
749
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("api_connection", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("secret", (b) => b.whereIndex("idx_secret_connection_kind", (eb) => eb("connectionId", "=", pathParams.slug))).find("oauthState", (b) => b.whereIndex("idx_oauth_state_connection", (eb) => eb("connectionId", "=", pathParams.slug)))).mutate(({ forSchema, retrieveResult: [connection, secrets, states] }) => {
750
+ if (!connection) return { connection: null };
751
+ const uow = forSchema(apiSchema);
752
+ const replaced = {
753
+ ...connection,
754
+ name: body.name ?? null,
755
+ baseUrl: body.baseUrl,
756
+ authMode: body.auth.type
757
+ };
758
+ uow.update("api_connection", connection.id, (b) => b.set({
759
+ name: replaced.name,
760
+ baseUrl: replaced.baseUrl,
761
+ authMode: replaced.authMode,
762
+ updatedAt: b.now()
763
+ }).check());
764
+ const authSecret = secrets.find((secret) => secret.kind === "auth");
765
+ for (const secret of secrets) if (secret !== authSecret || payload === null) uow.delete("secret", secret.id);
766
+ if (payload !== null) if (authSecret) uow.update("secret", authSecret.id, (b) => b.set({
767
+ payload,
768
+ expiresAt: null,
769
+ updatedAt: b.now()
770
+ }).check());
771
+ else uow.create("secret", {
772
+ id: `${pathParams.slug}:auth`,
773
+ connectionId: pathParams.slug,
774
+ kind: "auth",
775
+ payload,
776
+ expiresAt: null
777
+ });
778
+ for (const state of states) uow.delete("oauthState", state.id);
779
+ uow.triggerHook("onConnectionChanged", {
780
+ connectionId: pathParams.slug,
781
+ connection: connectionHookSnapshot(replaced)
782
+ });
783
+ const ready = isApiAuthReady(replaced.authMode, payload === null ? void 0 : {
784
+ payload,
785
+ expiresAt: null
786
+ });
787
+ if (ready !== isApiAuthReady(connection.authMode, authSecret)) uow.triggerHook("onConnectionReadinessChanged", {
788
+ connectionId: pathParams.slug,
789
+ ready
790
+ });
791
+ return { connection: replaced };
792
+ }).execute();
793
+ if (!result.connection) return error({
794
+ code: "CONNECTION_NOT_FOUND",
795
+ message: "API connection not found"
796
+ }, 404);
797
+ return json(publicConnection(result.connection));
798
+ }
799
+ }),
800
+ defineRoute({
801
+ method: "DELETE",
802
+ path: "/connections/:slug",
803
+ errorCodes: ["CONNECTION_NOT_FOUND"],
804
+ handler: async function({ pathParams }, { empty, error }) {
805
+ if (!(await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("api_connection", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("secret", (b) => b.whereIndex("idx_secret_connection_kind", (eb) => eb("connectionId", "=", pathParams.slug))).find("oauthState", (b) => b.whereIndex("idx_oauth_state_connection", (eb) => eb("connectionId", "=", pathParams.slug)))).mutate(({ forSchema, retrieveResult: [connection, secrets, states] }) => {
806
+ if (!connection) return { deleted: false };
807
+ const uow = forSchema(apiSchema);
808
+ for (const secret of secrets) uow.delete("secret", secret.id);
809
+ for (const state of states) uow.delete("oauthState", state.id);
810
+ uow.delete("api_connection", connection.id);
811
+ uow.triggerHook("onConnectionDeleted", {
812
+ connectionId: pathParams.slug,
813
+ previous: connectionHookSnapshot(connection)
814
+ });
815
+ return { deleted: true };
816
+ }).execute()).deleted) return error({
817
+ code: "CONNECTION_NOT_FOUND",
818
+ message: "API connection not found"
819
+ }, 404);
820
+ return empty(204);
821
+ }
822
+ }),
823
+ defineRoute({
824
+ method: "GET",
825
+ path: "/connections/:slug/auth/status",
826
+ outputSchema: apiAuthStatusSchema,
827
+ errorCodes: ["CONNECTION_NOT_FOUND"],
828
+ handler: async function({ pathParams }, { json, error }) {
829
+ const [connection, secret, pendingState] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("api_connection", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).findFirst("secret", (b) => b.whereIndex("idx_secret_connection_kind", (eb) => eb.and(eb("connectionId", "=", pathParams.slug), eb("kind", "=", "auth")))).findFirst("oauthState", (b) => b.whereIndex("idx_oauth_state_pending", (eb) => eb.and(eb("connectionId", "=", pathParams.slug), eb.isNull("consumedAt"), eb("expiresAt", ">", eb.now()))).orderByIndex("idx_oauth_state_pending", "desc"))).execute();
830
+ if (!connection) return error({
831
+ code: "CONNECTION_NOT_FOUND",
832
+ message: "API connection not found"
833
+ }, 404);
834
+ return json(projectApiAuthStatus({
835
+ authMode: connection.authMode,
836
+ authSecret: secret ?? void 0,
837
+ hasPendingOAuth: pendingOAuthLink(pendingState) !== null
838
+ }));
839
+ }
840
+ }),
841
+ defineRoute({
842
+ method: "GET",
843
+ path: "/connections/:slug/auth/oauth/pending",
844
+ outputSchema: apiOAuthPendingSchema,
845
+ errorCodes: ["CONNECTION_NOT_FOUND"],
846
+ handler: async function({ pathParams }, { json, error }) {
847
+ const [connection, pendingState] = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("api_connection", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).findFirst("oauthState", (b) => b.whereIndex("idx_oauth_state_pending", (eb) => eb.and(eb("connectionId", "=", pathParams.slug), eb.isNull("consumedAt"), eb("expiresAt", ">", eb.now()))).orderByIndex("idx_oauth_state_pending", "desc"))).execute();
848
+ if (!connection) return error({
849
+ code: "CONNECTION_NOT_FOUND",
850
+ message: "API connection not found"
851
+ }, 404);
852
+ return json({ pending: pendingOAuthLink(pendingState) });
853
+ }
854
+ }),
855
+ defineRoute({
856
+ method: "POST",
857
+ path: "/connections/:slug/auth/token",
858
+ inputSchema: tokenAuthInputSchema,
859
+ outputSchema: apiAuthStatusSchema,
860
+ errorCodes: ["CONNECTION_NOT_FOUND"],
861
+ handler: async function({ input, pathParams }, { json, error }) {
862
+ const { token } = await input.valid();
863
+ const payload = JSON.stringify({
864
+ type: "bearer",
865
+ token
866
+ });
867
+ if (!(await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("api_connection", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).findFirst("secret", (b) => b.whereIndex("idx_secret_connection_kind", (eb) => eb.and(eb("connectionId", "=", pathParams.slug), eb("kind", "=", "auth"))))).mutate(({ forSchema, retrieveResult: [connection, secret] }) => {
868
+ if (!connection) return { found: false };
869
+ const uow = forSchema(apiSchema);
870
+ uow.update("api_connection", connection.id, (b) => b.set({
871
+ authMode: "bearer",
872
+ updatedAt: b.now()
873
+ }).check());
874
+ if (secret) uow.update("secret", secret.id, (b) => b.set({
875
+ payload,
876
+ expiresAt: null,
877
+ updatedAt: b.now()
878
+ }).check());
879
+ else uow.create("secret", {
880
+ id: `${pathParams.slug}:auth`,
881
+ connectionId: pathParams.slug,
882
+ kind: "auth",
883
+ payload,
884
+ expiresAt: null
885
+ });
886
+ const changedConnection = connectionHookSnapshot({
887
+ ...connection,
888
+ authMode: "bearer"
889
+ });
890
+ uow.triggerHook("onConnectionChanged", {
891
+ connectionId: pathParams.slug,
892
+ connection: changedConnection
893
+ });
894
+ if (!isApiAuthReady(connection.authMode, secret ?? void 0)) uow.triggerHook("onConnectionReadinessChanged", {
895
+ connectionId: pathParams.slug,
896
+ ready: true
897
+ });
898
+ return { found: true };
899
+ }).execute()).found) return error({
900
+ code: "CONNECTION_NOT_FOUND",
901
+ message: "API connection not found"
902
+ }, 404);
903
+ return json({
904
+ mode: "bearer",
905
+ credentials: "present"
906
+ });
907
+ }
908
+ }),
909
+ defineRoute({
910
+ method: "POST",
911
+ path: "/connections/:slug/auth/oauth/start",
912
+ inputSchema: oauthStartInputSchema,
913
+ outputSchema: oauthStartOutputSchema,
914
+ queryParameters: [API_OAUTH_REDIRECT_URI_QUERY_PARAMETER],
915
+ errorCodes: [
916
+ "INVALID_OAUTH_REDIRECT_URI",
917
+ "OAUTH_REDIRECT_URI_NOT_ALLOWED",
918
+ "CONNECTION_NOT_FOUND",
919
+ "AUTH_NOT_CONFIGURED",
920
+ "AUTH_NOT_OAUTH",
921
+ "OAUTH_ERROR"
922
+ ],
923
+ handler: async function({ input, pathParams, query }, { json, error }) {
924
+ const body = await input.valid();
925
+ const redirectUri = oauthRedirectUriSchema.safeParse(query.get(API_OAUTH_REDIRECT_URI_QUERY_PARAMETER));
926
+ if (!redirectUri.success) return error({
927
+ code: "INVALID_OAUTH_REDIRECT_URI",
928
+ message: "OAuth redirect URI must be an HTTP or HTTPS URL"
929
+ }, 400);
930
+ const oauthRedirectUri = new URL(redirectUri.data);
931
+ if (!config.allowedOAuthRedirectUris?.(oauthRedirectUri)) return error({
932
+ code: "OAUTH_REDIRECT_URI_NOT_ALLOWED",
933
+ message: "OAuth redirect URI is not allowed by the fragment configuration"
934
+ }, 400);
935
+ const state = `${pathParams.slug}:${crypto.randomUUID()}`;
936
+ try {
937
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.startOAuth({
938
+ connectionId: pathParams.slug,
939
+ stateId: state,
940
+ redirectUri: oauthRedirectUri.toString(),
941
+ scopes: body.scopes,
942
+ extraAuthorizationParams: body.extraAuthorizationParams,
943
+ discardTokens: body.discardTokens
944
+ })]).execute();
945
+ if (!result.found) {
946
+ if (result.reason === "connection_not_found") return error({
947
+ code: "CONNECTION_NOT_FOUND",
948
+ message: "API connection not found"
949
+ }, 404);
950
+ if (result.reason === "auth_not_configured") return error({
951
+ code: "AUTH_NOT_CONFIGURED",
952
+ message: "API connection auth is not configured"
953
+ }, 400);
954
+ return error({
955
+ code: "AUTH_NOT_OAUTH",
956
+ message: "API connection auth is not OAuth"
957
+ }, 400);
958
+ }
959
+ return json({
960
+ authorizationUrl: result.authorizationUrl,
961
+ state
962
+ });
963
+ } catch (err) {
964
+ logApiRouteError("API OAuth start failed", err);
965
+ return error({
966
+ code: "OAUTH_ERROR",
967
+ message: "An authentication error occurred"
968
+ }, 502);
969
+ }
970
+ }
971
+ }),
972
+ defineRoute({
973
+ method: "GET",
974
+ path: "/oauth/callback",
975
+ outputSchema: oauthCallbackOutputSchema,
976
+ errorCodes: [
977
+ "CONNECTION_NOT_FOUND",
978
+ "AUTH_NOT_CONFIGURED",
979
+ "AUTH_NOT_OAUTH",
980
+ "OAUTH_ERROR",
981
+ "INVALID_OAUTH_STATE"
982
+ ],
983
+ handler: async function({ query }, { json, error }) {
984
+ const code = query.get("code");
985
+ const stateId = query.get("state");
986
+ if (!code || !stateId) return error({
987
+ code: "INVALID_OAUTH_STATE",
988
+ message: "Missing OAuth code or state"
989
+ }, 400);
990
+ try {
991
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.completeOAuthCallback({
992
+ stateId,
993
+ code
994
+ })]).execute();
995
+ if (!result.found) {
996
+ if (result.reason === "connection_not_found") return error({
997
+ code: "CONNECTION_NOT_FOUND",
998
+ message: "API connection not found"
999
+ }, 404);
1000
+ if (result.reason === "auth_not_configured") return error({
1001
+ code: "AUTH_NOT_CONFIGURED",
1002
+ message: "API connection auth is not configured"
1003
+ }, 400);
1004
+ if (result.reason === "auth_not_oauth") return error({
1005
+ code: "AUTH_NOT_OAUTH",
1006
+ message: "API connection auth is not OAuth"
1007
+ }, 400);
1008
+ return error({
1009
+ code: "INVALID_OAUTH_STATE",
1010
+ message: "Invalid OAuth state"
1011
+ }, 400);
1012
+ }
1013
+ return json({
1014
+ authenticated: true,
1015
+ mode: "oauth"
1016
+ });
1017
+ } catch (err) {
1018
+ logApiRouteError("API OAuth callback failed", err);
1019
+ return error({
1020
+ code: "OAUTH_ERROR",
1021
+ message: "An authentication error occurred"
1022
+ }, 502);
1023
+ }
1024
+ }
1025
+ }),
1026
+ defineRoute({
1027
+ method: "DELETE",
1028
+ path: "/connections/:slug/auth",
1029
+ outputSchema: apiAuthStatusSchema,
1030
+ errorCodes: ["CONNECTION_NOT_FOUND"],
1031
+ handler: async function({ pathParams }, { json, error }) {
1032
+ const result = await this.handlerTx().retrieve(({ forSchema }) => forSchema(apiSchema).findFirst("api_connection", (b) => b.whereIndex("primary", (eb) => eb("id", "=", pathParams.slug))).find("secret", (b) => b.whereIndex("idx_secret_connection_kind", (eb) => eb("connectionId", "=", pathParams.slug))).find("oauthState", (b) => b.whereIndex("idx_oauth_state_connection", (eb) => eb("connectionId", "=", pathParams.slug)))).mutate(({ forSchema, retrieveResult: [connection, secrets, states] }) => {
1033
+ if (!connection) return { found: false };
1034
+ const uow = forSchema(apiSchema);
1035
+ for (const secret of secrets) uow.delete("secret", secret.id);
1036
+ for (const state of states) uow.delete("oauthState", state.id);
1037
+ uow.update("api_connection", connection.id, (b) => b.set({ updatedAt: b.now() }).check());
1038
+ uow.triggerHook("onConnectionChanged", {
1039
+ connectionId: pathParams.slug,
1040
+ connection: connectionHookSnapshot(connection)
1041
+ });
1042
+ const authSecret = secrets.find((secret) => secret.kind === "auth");
1043
+ const ready = isApiAuthReady(connection.authMode, void 0);
1044
+ if (ready !== isApiAuthReady(connection.authMode, authSecret)) uow.triggerHook("onConnectionReadinessChanged", {
1045
+ connectionId: pathParams.slug,
1046
+ ready
1047
+ });
1048
+ return {
1049
+ found: true,
1050
+ status: projectApiAuthStatus({
1051
+ authMode: connection.authMode,
1052
+ authSecret: void 0,
1053
+ hasPendingOAuth: false
1054
+ })
1055
+ };
1056
+ }).execute();
1057
+ if (!result.found) return error({
1058
+ code: "CONNECTION_NOT_FOUND",
1059
+ message: "API connection not found"
1060
+ }, 404);
1061
+ return json(result.status);
1062
+ }
1063
+ }),
1064
+ defineRoute({
1065
+ method: "POST",
1066
+ path: "/connections/:slug/request",
1067
+ inputSchema: apiRequestInputSchema,
1068
+ outputSchema: apiRequestOutputSchema,
1069
+ handler: async function({ input, pathParams }, { json }) {
1070
+ const request = await input.valid();
1071
+ const [result] = await this.handlerTx().withServiceCalls(() => [services.executeApiRequest({
1072
+ connectionId: pathParams.slug,
1073
+ request
1074
+ })]).execute();
1075
+ if (!result.found) {
1076
+ if (result.reason === "connection_disabled") return json({
1077
+ ok: false,
1078
+ response: null,
1079
+ error: {
1080
+ code: "CONNECTION_DISABLED",
1081
+ message: "API connection is disabled"
1082
+ }
1083
+ });
1084
+ return json({
1085
+ ok: false,
1086
+ response: null,
1087
+ error: {
1088
+ code: "CONNECTION_NOT_FOUND",
1089
+ message: "API connection not found"
1090
+ }
1091
+ });
1092
+ }
1093
+ return json(result.response);
1094
+ }
1095
+ })
1096
+ ]);
1097
+ //#endregion
1098
+ export { apiRoutesFactory };
1099
+
1100
+ //# sourceMappingURL=routes.js.map