@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.
- package/LICENSE.md +16 -0
- package/README.md +249 -2
- package/dist/browser/client/client-3ZV9sw3i.js +2580 -0
- package/dist/browser/client/client-3ZV9sw3i.js.map +1 -0
- package/dist/browser/client/client-DPjmoaJQ.d.ts +711 -0
- package/dist/browser/client/client-DPjmoaJQ.d.ts.map +1 -0
- package/dist/browser/client/client.d.ts +1645 -0
- package/dist/browser/client/client.d.ts.map +1 -0
- package/dist/browser/client/client.js +2 -0
- package/dist/browser/client/react.d.ts +1665 -0
- package/dist/browser/client/react.d.ts.map +1 -0
- package/dist/browser/client/react.js +194 -0
- package/dist/browser/client/react.js.map +1 -0
- package/dist/browser/client/solid.d.ts +1668 -0
- package/dist/browser/client/solid.d.ts.map +1 -0
- package/dist/browser/client/solid.js +129 -0
- package/dist/browser/client/solid.js.map +1 -0
- package/dist/browser/client/svelte.d.ts +1668 -0
- package/dist/browser/client/svelte.d.ts.map +1 -0
- package/dist/browser/client/svelte.js +131 -0
- package/dist/browser/client/svelte.js.map +1 -0
- package/dist/browser/client/vanilla.d.ts +1695 -0
- package/dist/browser/client/vanilla.d.ts.map +1 -0
- package/dist/browser/client/vanilla.js +159 -0
- package/dist/browser/client/vanilla.js.map +1 -0
- package/dist/browser/client/vue.d.ts +1668 -0
- package/dist/browser/client/vue.d.ts.map +1 -0
- package/dist/browser/client/vue.js +127 -0
- package/dist/browser/client/vue.js.map +1 -0
- package/dist/browser/client-D4NnY-zV.d.ts +713 -0
- package/dist/browser/client-D4NnY-zV.d.ts.map +1 -0
- package/dist/browser/client-DQE0c7GI.d.ts +711 -0
- package/dist/browser/client-DQE0c7GI.d.ts.map +1 -0
- package/dist/browser/index.d.ts +3130 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/src-BrYPVbAN.js +2404 -0
- package/dist/browser/src-BrYPVbAN.js.map +1 -0
- package/dist/browser/src-t-4wp9-f.js +2400 -0
- package/dist/browser/src-t-4wp9-f.js.map +1 -0
- package/dist/node/api-types.d.ts +731 -0
- package/dist/node/api-types.d.ts.map +1 -0
- package/dist/node/api-types.js +281 -0
- package/dist/node/api-types.js.map +1 -0
- package/dist/node/crypto.d.ts +6 -0
- package/dist/node/crypto.d.ts.map +1 -0
- package/dist/node/crypto.js +103 -0
- package/dist/node/crypto.js.map +1 -0
- package/dist/node/definition.d.ts +638 -0
- package/dist/node/definition.d.ts.map +1 -0
- package/dist/node/definition.js +315 -0
- package/dist/node/definition.js.map +1 -0
- package/dist/node/oauth.js +124 -0
- package/dist/node/oauth.js.map +1 -0
- package/dist/node/routes.d.ts +1585 -0
- package/dist/node/routes.d.ts.map +1 -0
- package/dist/node/routes.js +1100 -0
- package/dist/node/routes.js.map +1 -0
- package/dist/node/schema.d.ts +99 -0
- package/dist/node/schema.d.ts.map +1 -0
- package/dist/node/schema.js +16 -0
- package/dist/node/schema.js.map +1 -0
- package/dist/node/server.d.ts +1585 -0
- package/dist/node/server.d.ts.map +1 -0
- package/dist/node/server.js +12 -0
- package/dist/node/server.js.map +1 -0
- package/dist/node/services.d.ts +10 -0
- package/dist/node/services.d.ts.map +1 -0
- package/dist/node/services.js +304 -0
- package/dist/node/services.js.map +1 -0
- package/dist/node/webhooks/auth.d.ts +133 -0
- package/dist/node/webhooks/auth.d.ts.map +1 -0
- package/dist/node/webhooks/auth.js +268 -0
- package/dist/node/webhooks/auth.js.map +1 -0
- package/dist/node/webhooks/verification.d.ts +102 -0
- package/dist/node/webhooks/verification.d.ts.map +1 -0
- package/dist/node/webhooks/verification.js +119 -0
- package/dist/node/webhooks/verification.js.map +1 -0
- package/package.json +136 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","names":[],"sources":["../../src/server.ts"],"mappings":";;;;;iBAQgB,kBACd,QAAQ,mBACR,cAAc,4DAA8B,gEAAA,0EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kGAA9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAQ+9O"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { apiFragmentDefinition } from "./definition.js";
|
|
2
|
+
import { apiRoutesFactory } from "./routes.js";
|
|
3
|
+
import { instantiate } from "@fragno-dev/core";
|
|
4
|
+
//#region src/server.ts
|
|
5
|
+
const apiRoutes = [apiRoutesFactory];
|
|
6
|
+
function createApiFragment(config, fragnoConfig) {
|
|
7
|
+
return instantiate(apiFragmentDefinition).withConfig(config).withRoutes(apiRoutes).withOptions(fragnoConfig).build();
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { createApiFragment };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../../src/server.ts"],"sourcesContent":["import { instantiate } from \"@fragno-dev/core\";\nimport type { FragnoPublicConfigWithDatabase } from \"@fragno-dev/db\";\n\nimport { apiFragmentDefinition, type ApiFragmentConfig } from \"./definition\";\nimport { apiRoutesFactory } from \"./routes\";\n\nconst apiRoutes = [apiRoutesFactory] as const;\n\nexport function createApiFragment(\n config: ApiFragmentConfig,\n fragnoConfig: FragnoPublicConfigWithDatabase,\n) {\n return instantiate(apiFragmentDefinition)\n .withConfig(config)\n .withRoutes(apiRoutes)\n .withOptions(fragnoConfig)\n .build();\n}\n"],"mappings":";;;;AAMA,MAAM,YAAY,CAAC,gBAAgB;AAEnC,SAAgB,kBACd,QACA,cACA;CACA,OAAO,YAAY,qBAAqB,CAAC,CACtC,WAAW,MAAM,CAAC,CAClB,WAAW,SAAS,CAAC,CACrB,YAAY,YAAY,CAAC,CACzB,MAAM;AACX"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./api-types.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/services.d.ts
|
|
4
|
+
interface AuthPersistenceChanges {
|
|
5
|
+
authPayload?: string;
|
|
6
|
+
authExpiresAt?: Date | null;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { AuthPersistenceChanges };
|
|
10
|
+
//# sourceMappingURL=services.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"services.d.ts","names":[],"sources":["../../src/services.ts"],"mappings":";;;UA+CiB;EACf;EACA,gBAAgB"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { bytesToBase64, randomBase64Url, utf8Bytes } from "./crypto.js";
|
|
2
|
+
import { authConfigSchema } from "./api-types.js";
|
|
3
|
+
import { createAuthorizationUrl, createCodeChallenge, exchangeAuthorizationCode, oauthTokensSchema, refreshOAuthToken, requestClientCredentialsToken, tokenExpiry } from "./oauth.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/services.ts
|
|
6
|
+
const storedAuthPayloadSchema = z.discriminatedUnion("type", [
|
|
7
|
+
z.object({
|
|
8
|
+
type: z.literal("bearer"),
|
|
9
|
+
token: z.string().min(1)
|
|
10
|
+
}),
|
|
11
|
+
z.object({
|
|
12
|
+
type: z.literal("basic"),
|
|
13
|
+
username: z.string().min(1),
|
|
14
|
+
password: z.string().min(1)
|
|
15
|
+
}),
|
|
16
|
+
authConfigSchema.options[3].extend({ tokens: oauthTokensSchema.optional() }),
|
|
17
|
+
authConfigSchema.options[4].extend({
|
|
18
|
+
tokens: oauthTokensSchema.optional(),
|
|
19
|
+
redirectUri: z.url().optional()
|
|
20
|
+
})
|
|
21
|
+
]);
|
|
22
|
+
const TOKEN_EXPIRY_SKEW_MS = 3e4;
|
|
23
|
+
function assertAllowedBaseUrl(baseUrl, config) {
|
|
24
|
+
const url = new URL(baseUrl);
|
|
25
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error("API base URL must be an HTTP(S) URL");
|
|
26
|
+
if (config.allowedBaseUrls && !config.allowedBaseUrls(url)) throw new Error("API base URL is not allowed");
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
function secretByKind(secrets, kind) {
|
|
30
|
+
return secrets.find((secret) => secret.kind === kind);
|
|
31
|
+
}
|
|
32
|
+
function secretExpiresInFuture(secret) {
|
|
33
|
+
if (!secret?.expiresAt) return false;
|
|
34
|
+
return new Date(secret.expiresAt).getTime() > Date.now() + TOKEN_EXPIRY_SKEW_MS;
|
|
35
|
+
}
|
|
36
|
+
function secretIsUnexpired(secret) {
|
|
37
|
+
return !secret?.expiresAt || secretExpiresInFuture(secret);
|
|
38
|
+
}
|
|
39
|
+
/** Projects stored auth without secrets; whether credentials work can only be learned by using them. */
|
|
40
|
+
function projectApiAuthStatus(args) {
|
|
41
|
+
const mode = args.authMode;
|
|
42
|
+
if (mode === "none") return { mode };
|
|
43
|
+
if (mode !== "oauth") return {
|
|
44
|
+
mode,
|
|
45
|
+
credentials: args.authSecret ? "present" : "missing"
|
|
46
|
+
};
|
|
47
|
+
if (!args.authSecret) return {
|
|
48
|
+
mode,
|
|
49
|
+
state: "client-missing"
|
|
50
|
+
};
|
|
51
|
+
const auth = storedAuthPayloadSchema.parse(JSON.parse(args.authSecret.payload));
|
|
52
|
+
const tokens = auth.type === "oauth" ? auth.tokens : void 0;
|
|
53
|
+
if (tokens?.accessToken && (tokens.refreshToken || secretIsUnexpired(args.authSecret))) return {
|
|
54
|
+
mode,
|
|
55
|
+
state: "authorized"
|
|
56
|
+
};
|
|
57
|
+
if (args.hasPendingOAuth) return {
|
|
58
|
+
mode,
|
|
59
|
+
state: "consent-pending"
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
mode,
|
|
63
|
+
state: tokens?.accessToken ? "expired" : "consent-required"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Whether stored auth can authorize requests without new consent or credentials. */
|
|
67
|
+
function isApiAuthReady(authMode, authSecret) {
|
|
68
|
+
const status = projectApiAuthStatus({
|
|
69
|
+
authMode,
|
|
70
|
+
authSecret,
|
|
71
|
+
hasPendingOAuth: false
|
|
72
|
+
});
|
|
73
|
+
switch (status.mode) {
|
|
74
|
+
case "none": return true;
|
|
75
|
+
case "oauth": return status.state === "authorized";
|
|
76
|
+
case "bearer":
|
|
77
|
+
case "basic":
|
|
78
|
+
case "client_credentials": return status.credentials === "present";
|
|
79
|
+
default: throw new Error("Unknown API auth mode.", { cause: status });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* When stored auth stops authorizing requests because its access token expires without renewal,
|
|
84
|
+
* whether or not that moment has passed. Null when a refresh token renews it or nothing expires.
|
|
85
|
+
*/
|
|
86
|
+
function apiAuthReadyUntil(authMode, authSecret) {
|
|
87
|
+
if (authMode !== "oauth" || !authSecret.expiresAt) return null;
|
|
88
|
+
const auth = storedAuthPayloadSchema.parse(JSON.parse(authSecret.payload));
|
|
89
|
+
if (auth.type !== "oauth" || !auth.tokens?.accessToken || auth.tokens.refreshToken) return null;
|
|
90
|
+
return /* @__PURE__ */ new Date(new Date(authSecret.expiresAt).getTime() - TOKEN_EXPIRY_SKEW_MS);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Projects the newest unexpired, unconsumed state; any such state completes the same connection.
|
|
94
|
+
* States started before links were retained have no URL and cannot be resumed.
|
|
95
|
+
*/
|
|
96
|
+
function pendingOAuthLink(state) {
|
|
97
|
+
return state?.authorizationUrl ? {
|
|
98
|
+
authorizationUrl: state.authorizationUrl,
|
|
99
|
+
expiresAt: state.expiresAt
|
|
100
|
+
} : null;
|
|
101
|
+
}
|
|
102
|
+
async function createOAuthStartSnapshot(args) {
|
|
103
|
+
const codeVerifier = randomBase64Url(32);
|
|
104
|
+
const codeChallenge = await createCodeChallenge(codeVerifier);
|
|
105
|
+
const scopes = args.scopes ?? args.auth.scopes;
|
|
106
|
+
const authorizationUrl = createAuthorizationUrl({
|
|
107
|
+
authorizationEndpoint: args.auth.authorizationEndpoint,
|
|
108
|
+
clientId: args.auth.clientId,
|
|
109
|
+
redirectUri: args.redirectUri,
|
|
110
|
+
state: args.stateId,
|
|
111
|
+
scopes,
|
|
112
|
+
codeChallenge,
|
|
113
|
+
extraParams: {
|
|
114
|
+
...args.auth.extraAuthorizationParams,
|
|
115
|
+
...args.extraAuthorizationParams
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
authorizationUrl,
|
|
120
|
+
oauthState: {
|
|
121
|
+
id: args.stateId,
|
|
122
|
+
connectionId: args.connection.id.toString(),
|
|
123
|
+
authorizationUrl: authorizationUrl.toString(),
|
|
124
|
+
codeVerifier,
|
|
125
|
+
redirectUri: args.redirectUri,
|
|
126
|
+
scope: scopes?.join(" ") ?? null,
|
|
127
|
+
expiresAt: new Date(Date.now() + 600 * 1e3)
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function createOAuthCallbackSnapshot(args) {
|
|
132
|
+
const tokens = await exchangeAuthorizationCode({
|
|
133
|
+
auth: args.auth,
|
|
134
|
+
code: args.code,
|
|
135
|
+
redirectUri: args.state.redirectUri,
|
|
136
|
+
codeVerifier: args.state.codeVerifier,
|
|
137
|
+
fetchImplementation: args.config.fetch
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
authPayload: JSON.stringify({
|
|
141
|
+
...args.auth,
|
|
142
|
+
redirectUri: args.state.redirectUri,
|
|
143
|
+
tokens
|
|
144
|
+
}),
|
|
145
|
+
authExpiresAt: tokenExpiry(tokens)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function resolveOAuthAuth(args) {
|
|
149
|
+
if (!args.authPayload.tokens?.accessToken) return {};
|
|
150
|
+
if (secretIsUnexpired(args.authSecret)) return { authorization: `Bearer ${args.authPayload.tokens.accessToken}` };
|
|
151
|
+
if (!args.authPayload.tokens.refreshToken) throw new Error("OAuth access token expired and no refresh token is available");
|
|
152
|
+
const tokens = await refreshOAuthToken({
|
|
153
|
+
auth: args.authPayload,
|
|
154
|
+
refreshToken: args.authPayload.tokens.refreshToken,
|
|
155
|
+
fetchImplementation: args.config.fetch
|
|
156
|
+
});
|
|
157
|
+
if (!tokens.accessToken) throw new Error("OAuth refresh did not return an access token");
|
|
158
|
+
return {
|
|
159
|
+
authorization: `Bearer ${tokens.accessToken}`,
|
|
160
|
+
authChanges: {
|
|
161
|
+
authPayload: JSON.stringify({
|
|
162
|
+
...args.authPayload,
|
|
163
|
+
tokens
|
|
164
|
+
}),
|
|
165
|
+
authExpiresAt: tokenExpiry(tokens)
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function resolveClientCredentialsAuth(args) {
|
|
170
|
+
if (args.authPayload.tokens?.accessToken && secretExpiresInFuture(args.authSecret)) return { authorization: `Bearer ${args.authPayload.tokens.accessToken}` };
|
|
171
|
+
const tokens = await requestClientCredentialsToken({
|
|
172
|
+
auth: args.authPayload,
|
|
173
|
+
fetchImplementation: args.config.fetch
|
|
174
|
+
});
|
|
175
|
+
if (!tokens.accessToken) throw new Error("Client credentials flow did not return an access token");
|
|
176
|
+
return {
|
|
177
|
+
authorization: `Bearer ${tokens.accessToken}`,
|
|
178
|
+
authChanges: {
|
|
179
|
+
authPayload: JSON.stringify({
|
|
180
|
+
...args.authPayload,
|
|
181
|
+
tokens
|
|
182
|
+
}),
|
|
183
|
+
authExpiresAt: tokenExpiry(tokens)
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async function resolveApiOperationAuth(args) {
|
|
188
|
+
const authSecret = secretByKind(args.secrets, "auth");
|
|
189
|
+
const authPayload = authSecret ? storedAuthPayloadSchema.parse(JSON.parse(authSecret.payload)) : void 0;
|
|
190
|
+
if (!authPayload) return {};
|
|
191
|
+
if (authPayload.type === "bearer") return { authorization: `Bearer ${authPayload.token}` };
|
|
192
|
+
if (authPayload.type === "basic") return { authorization: `Basic ${bytesToBase64(utf8Bytes(`${authPayload.username}:${authPayload.password}`))}` };
|
|
193
|
+
if (authPayload.type === "oauth") return resolveOAuthAuth({
|
|
194
|
+
config: args.config,
|
|
195
|
+
authSecret,
|
|
196
|
+
authPayload
|
|
197
|
+
});
|
|
198
|
+
return resolveClientCredentialsAuth({
|
|
199
|
+
config: args.config,
|
|
200
|
+
authSecret,
|
|
201
|
+
authPayload
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function resolveRequestUrl(baseUrl, path, query) {
|
|
205
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith("//")) throw new Error("API request path must be relative");
|
|
206
|
+
const url = new URL(path.startsWith("/") ? path.slice(1) : path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
207
|
+
for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);
|
|
208
|
+
return url;
|
|
209
|
+
}
|
|
210
|
+
function boundedErrorMessage(message) {
|
|
211
|
+
const normalized = message.trim();
|
|
212
|
+
return normalized.length > 500 ? `${normalized.slice(0, 500)}…` : normalized;
|
|
213
|
+
}
|
|
214
|
+
async function performApiRequest(args) {
|
|
215
|
+
const controller = args.request.timeoutMs ? new AbortController() : void 0;
|
|
216
|
+
const timeout = controller ? setTimeout(() => {
|
|
217
|
+
controller.abort();
|
|
218
|
+
}, args.request.timeoutMs) : void 0;
|
|
219
|
+
try {
|
|
220
|
+
const url = resolveRequestUrl(args.baseUrl, args.request.path, args.request.query);
|
|
221
|
+
const headers = new Headers(args.request.headers ?? {});
|
|
222
|
+
headers.delete("authorization");
|
|
223
|
+
headers.delete("Authorization");
|
|
224
|
+
if (args.authorization) headers.set("authorization", args.authorization);
|
|
225
|
+
let body;
|
|
226
|
+
if (args.request.body.type === "json") {
|
|
227
|
+
headers.set("content-type", headers.get("content-type") ?? "application/json");
|
|
228
|
+
body = JSON.stringify(args.request.body.value);
|
|
229
|
+
} else if (args.request.body.type === "text") body = args.request.body.value;
|
|
230
|
+
const response = await (args.config.fetch ?? fetch)(url, {
|
|
231
|
+
method: args.request.method,
|
|
232
|
+
headers,
|
|
233
|
+
body,
|
|
234
|
+
signal: controller?.signal
|
|
235
|
+
});
|
|
236
|
+
const responseHeaders = Object.fromEntries(response.headers.entries());
|
|
237
|
+
const responseText = await response.text();
|
|
238
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
239
|
+
let responseBody;
|
|
240
|
+
let responseDecodingFailed = false;
|
|
241
|
+
if (!responseText) responseBody = {
|
|
242
|
+
type: "empty",
|
|
243
|
+
value: null
|
|
244
|
+
};
|
|
245
|
+
else if (contentType.includes("application/json")) try {
|
|
246
|
+
responseBody = {
|
|
247
|
+
type: "json",
|
|
248
|
+
value: JSON.parse(responseText)
|
|
249
|
+
};
|
|
250
|
+
} catch {
|
|
251
|
+
responseBody = {
|
|
252
|
+
type: "text",
|
|
253
|
+
value: responseText
|
|
254
|
+
};
|
|
255
|
+
responseDecodingFailed = true;
|
|
256
|
+
}
|
|
257
|
+
else responseBody = {
|
|
258
|
+
type: "text",
|
|
259
|
+
value: responseText
|
|
260
|
+
};
|
|
261
|
+
const apiResponse = {
|
|
262
|
+
status: response.status,
|
|
263
|
+
statusText: response.statusText,
|
|
264
|
+
headers: responseHeaders,
|
|
265
|
+
body: responseBody
|
|
266
|
+
};
|
|
267
|
+
if (responseDecodingFailed) return {
|
|
268
|
+
ok: false,
|
|
269
|
+
response: apiResponse,
|
|
270
|
+
error: {
|
|
271
|
+
code: "RESPONSE_DECODING_ERROR",
|
|
272
|
+
message: "Upstream response declared JSON but could not be decoded"
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
if (response.ok) return {
|
|
276
|
+
ok: true,
|
|
277
|
+
response: apiResponse,
|
|
278
|
+
error: null
|
|
279
|
+
};
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
response: apiResponse,
|
|
283
|
+
error: {
|
|
284
|
+
code: "HTTP_ERROR",
|
|
285
|
+
message: responseBody.type === "json" && responseBody.value && typeof responseBody.value === "object" && "message" in responseBody.value ? boundedErrorMessage(String(responseBody.value.message)) : boundedErrorMessage(responseText) || response.statusText || `HTTP ${response.status}`
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
response: null,
|
|
292
|
+
error: {
|
|
293
|
+
code: "REQUEST_ERROR",
|
|
294
|
+
message: error instanceof Error && error.name === "AbortError" ? `API request timed out after ${args.request.timeoutMs}ms` : error instanceof Error ? error.message : String(error)
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
} finally {
|
|
298
|
+
if (timeout) clearTimeout(timeout);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
export { apiAuthReadyUntil, assertAllowedBaseUrl, createOAuthCallbackSnapshot, createOAuthStartSnapshot, isApiAuthReady, pendingOAuthLink, performApiRequest, projectApiAuthStatus, resolveApiOperationAuth, storedAuthPayloadSchema };
|
|
303
|
+
|
|
304
|
+
//# sourceMappingURL=services.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"services.js","names":[],"sources":["../../src/services.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport {\n authConfigSchema,\n type ApiAuthStatus,\n type ApiFragmentConfig,\n type ApiRequestInput,\n type ApiRequestOutput,\n type AuthConfig,\n} from \"./api-types\";\nimport { bytesToBase64, utf8Bytes } from \"./crypto\";\nimport {\n createAuthorizationUrl,\n createCodeChallenge,\n exchangeAuthorizationCode,\n randomBase64Url,\n refreshOAuthToken,\n requestClientCredentialsToken,\n tokenExpiry,\n oauthTokensSchema,\n} from \"./oauth\";\n\nexport type SecretRecord = {\n id: { toString(): string } | string;\n kind: string;\n payload: string;\n expiresAt?: Date | string | null;\n};\n\nexport const storedAuthPayloadSchema = z.discriminatedUnion(\"type\", [\n z.object({ type: z.literal(\"bearer\"), token: z.string().min(1) }),\n z.object({\n type: z.literal(\"basic\"),\n username: z.string().min(1),\n password: z.string().min(1),\n }),\n authConfigSchema.options[3].extend({ tokens: oauthTokensSchema.optional() }),\n authConfigSchema.options[4].extend({\n tokens: oauthTokensSchema.optional(),\n redirectUri: z.url().optional(),\n }),\n]);\n\ntype StoredAuthPayload = z.infer<typeof storedAuthPayloadSchema>;\n\nconst TOKEN_EXPIRY_SKEW_MS = 30_000;\n\nexport interface AuthPersistenceChanges {\n authPayload?: string;\n authExpiresAt?: Date | null;\n}\n\nexport function assertAllowedBaseUrl(baseUrl: string, config: ApiFragmentConfig) {\n const url = new URL(baseUrl);\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new Error(\"API base URL must be an HTTP(S) URL\");\n }\n if (config.allowedBaseUrls && !config.allowedBaseUrls(url)) {\n throw new Error(\"API base URL is not allowed\");\n }\n return url;\n}\n\nfunction secretByKind<TSecret extends { kind: string }>(secrets: TSecret[], kind: string) {\n return secrets.find((secret) => secret.kind === kind);\n}\n\nfunction secretExpiresInFuture(secret: Pick<SecretRecord, \"expiresAt\"> | undefined) {\n if (!secret?.expiresAt) {\n return false;\n }\n return new Date(secret.expiresAt).getTime() > Date.now() + TOKEN_EXPIRY_SKEW_MS;\n}\n\nfunction secretIsUnexpired(secret: Pick<SecretRecord, \"expiresAt\"> | undefined) {\n return !secret?.expiresAt || secretExpiresInFuture(secret);\n}\n\n/** Projects stored auth without secrets; whether credentials work can only be learned by using them. */\nexport function projectApiAuthStatus(args: {\n authMode: string;\n authSecret: Pick<SecretRecord, \"payload\" | \"expiresAt\"> | undefined;\n hasPendingOAuth: boolean;\n}): ApiAuthStatus {\n // Routes write authMode only from authConfigSchema discriminators.\n const mode = args.authMode as AuthConfig[\"type\"];\n if (mode === \"none\") {\n return { mode };\n }\n if (mode !== \"oauth\") {\n return { mode, credentials: args.authSecret ? \"present\" : \"missing\" };\n }\n if (!args.authSecret) {\n return { mode, state: \"client-missing\" };\n }\n const auth = storedAuthPayloadSchema.parse(JSON.parse(args.authSecret.payload));\n const tokens = auth.type === \"oauth\" ? auth.tokens : undefined;\n // Matches request-time resolution: a refresh token renews an expired access token.\n if (tokens?.accessToken && (tokens.refreshToken || secretIsUnexpired(args.authSecret))) {\n return { mode, state: \"authorized\" };\n }\n // Expired tokens without a refresh token are unusable, so a pending link is the actionable state.\n if (args.hasPendingOAuth) {\n return { mode, state: \"consent-pending\" };\n }\n return { mode, state: tokens?.accessToken ? \"expired\" : \"consent-required\" };\n}\n\n/** Whether stored auth can authorize requests without new consent or credentials. */\nexport function isApiAuthReady(\n authMode: string,\n authSecret: Pick<SecretRecord, \"payload\" | \"expiresAt\"> | undefined,\n): boolean {\n // Pending consent only distinguishes states that are not ready.\n const status = projectApiAuthStatus({ authMode, authSecret, hasPendingOAuth: false });\n switch (status.mode) {\n case \"none\":\n return true;\n case \"oauth\":\n return status.state === \"authorized\";\n case \"bearer\":\n case \"basic\":\n case \"client_credentials\":\n return status.credentials === \"present\";\n default:\n throw new Error(\"Unknown API auth mode.\", { cause: status satisfies never });\n }\n}\n\n/**\n * When stored auth stops authorizing requests because its access token expires without renewal,\n * whether or not that moment has passed. Null when a refresh token renews it or nothing expires.\n */\nexport function apiAuthReadyUntil(\n authMode: string,\n authSecret: Pick<SecretRecord, \"payload\" | \"expiresAt\">,\n): Date | null {\n if (authMode !== \"oauth\" || !authSecret.expiresAt) {\n return null;\n }\n const auth = storedAuthPayloadSchema.parse(JSON.parse(authSecret.payload));\n if (auth.type !== \"oauth\" || !auth.tokens?.accessToken || auth.tokens.refreshToken) {\n return null;\n }\n return new Date(new Date(authSecret.expiresAt).getTime() - TOKEN_EXPIRY_SKEW_MS);\n}\n\n/**\n * Projects the newest unexpired, unconsumed state; any such state completes the same connection.\n * States started before links were retained have no URL and cannot be resumed.\n */\nexport function pendingOAuthLink(\n state: { authorizationUrl: string | null; expiresAt: Date } | null,\n): { authorizationUrl: string; expiresAt: Date } | null {\n return state?.authorizationUrl\n ? { authorizationUrl: state.authorizationUrl, expiresAt: state.expiresAt }\n : null;\n}\n\nexport async function createOAuthStartSnapshot(args: {\n connection: { id: { toString(): string } | string };\n auth: Extract<AuthConfig, { type: \"oauth\" }>;\n stateId: string;\n redirectUri: string;\n scopes?: string[];\n extraAuthorizationParams?: Record<string, string>;\n}) {\n const codeVerifier = randomBase64Url(32);\n const codeChallenge = await createCodeChallenge(codeVerifier);\n const scopes = args.scopes ?? args.auth.scopes;\n const authorizationUrl = createAuthorizationUrl({\n authorizationEndpoint: args.auth.authorizationEndpoint,\n clientId: args.auth.clientId,\n redirectUri: args.redirectUri,\n state: args.stateId,\n scopes,\n codeChallenge,\n extraParams: {\n ...args.auth.extraAuthorizationParams,\n ...args.extraAuthorizationParams,\n },\n });\n return {\n authorizationUrl,\n oauthState: {\n id: args.stateId,\n connectionId: args.connection.id.toString(),\n // The link carries only the PKCE challenge, never the verifier.\n authorizationUrl: authorizationUrl.toString(),\n codeVerifier,\n redirectUri: args.redirectUri,\n scope: scopes?.join(\" \") ?? null,\n expiresAt: new Date(Date.now() + 10 * 60 * 1000),\n },\n };\n}\n\nexport async function createOAuthCallbackSnapshot(args: {\n config: ApiFragmentConfig;\n stateId: string;\n code: string;\n state: {\n codeVerifier: string;\n redirectUri: string;\n scope?: string | null;\n expiresAt: Date | string;\n consumedAt?: Date | string | null;\n };\n auth: Extract<AuthConfig, { type: \"oauth\" }>;\n}) {\n const tokens = await exchangeAuthorizationCode({\n auth: args.auth,\n code: args.code,\n redirectUri: args.state.redirectUri,\n codeVerifier: args.state.codeVerifier,\n fetchImplementation: args.config.fetch,\n });\n return {\n authPayload: JSON.stringify({\n ...args.auth,\n redirectUri: args.state.redirectUri,\n tokens,\n }),\n authExpiresAt: tokenExpiry(tokens),\n } satisfies AuthPersistenceChanges;\n}\n\nasync function resolveOAuthAuth(args: {\n config: ApiFragmentConfig;\n authSecret: SecretRecord;\n authPayload: Extract<StoredAuthPayload, { type: \"oauth\" }>;\n}) {\n if (!args.authPayload.tokens?.accessToken) {\n return {};\n }\n if (secretIsUnexpired(args.authSecret)) {\n return { authorization: `Bearer ${args.authPayload.tokens.accessToken}` };\n }\n if (!args.authPayload.tokens.refreshToken) {\n throw new Error(\"OAuth access token expired and no refresh token is available\");\n }\n const tokens = await refreshOAuthToken({\n auth: args.authPayload,\n refreshToken: args.authPayload.tokens.refreshToken,\n fetchImplementation: args.config.fetch,\n });\n if (!tokens.accessToken) {\n throw new Error(\"OAuth refresh did not return an access token\");\n }\n return {\n authorization: `Bearer ${tokens.accessToken}`,\n authChanges: {\n authPayload: JSON.stringify({ ...args.authPayload, tokens }),\n authExpiresAt: tokenExpiry(tokens),\n },\n };\n}\n\nasync function resolveClientCredentialsAuth(args: {\n config: ApiFragmentConfig;\n authSecret: SecretRecord;\n authPayload: Extract<StoredAuthPayload, { type: \"client_credentials\" }>;\n}) {\n if (args.authPayload.tokens?.accessToken && secretExpiresInFuture(args.authSecret)) {\n return { authorization: `Bearer ${args.authPayload.tokens.accessToken}` };\n }\n const tokens = await requestClientCredentialsToken({\n auth: args.authPayload,\n fetchImplementation: args.config.fetch,\n });\n if (!tokens.accessToken) {\n throw new Error(\"Client credentials flow did not return an access token\");\n }\n return {\n authorization: `Bearer ${tokens.accessToken}`,\n authChanges: {\n authPayload: JSON.stringify({ ...args.authPayload, tokens }),\n authExpiresAt: tokenExpiry(tokens),\n },\n };\n}\n\nexport async function resolveApiOperationAuth(args: {\n config: ApiFragmentConfig;\n secrets: SecretRecord[];\n}): Promise<{ authorization?: string; authChanges?: AuthPersistenceChanges }> {\n const authSecret = secretByKind(args.secrets, \"auth\");\n const authPayload = authSecret\n ? storedAuthPayloadSchema.parse(JSON.parse(authSecret.payload))\n : undefined;\n if (!authPayload) {\n return {};\n }\n if (authPayload.type === \"bearer\") {\n return { authorization: `Bearer ${authPayload.token}` };\n }\n if (authPayload.type === \"basic\") {\n const credentials = bytesToBase64(utf8Bytes(`${authPayload.username}:${authPayload.password}`));\n return { authorization: `Basic ${credentials}` };\n }\n if (authPayload.type === \"oauth\") {\n return resolveOAuthAuth({ config: args.config, authSecret: authSecret!, authPayload });\n }\n return resolveClientCredentialsAuth({\n config: args.config,\n authSecret: authSecret!,\n authPayload,\n });\n}\n\nfunction resolveRequestUrl(baseUrl: string, path: string, query?: Record<string, string>) {\n if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith(\"//\")) {\n throw new Error(\"API request path must be relative\");\n }\n const url = new URL(\n path.startsWith(\"/\") ? path.slice(1) : path,\n baseUrl.endsWith(\"/\") ? baseUrl : `${baseUrl}/`,\n );\n for (const [key, value] of Object.entries(query ?? {})) {\n url.searchParams.set(key, value);\n }\n return url;\n}\n\nfunction boundedErrorMessage(message: string) {\n const normalized = message.trim();\n return normalized.length > 500 ? `${normalized.slice(0, 500)}…` : normalized;\n}\n\nexport async function performApiRequest(args: {\n config: ApiFragmentConfig;\n baseUrl: string;\n authorization?: string;\n request: ApiRequestInput;\n}): Promise<ApiRequestOutput> {\n const controller = args.request.timeoutMs ? new AbortController() : undefined;\n const timeout = controller\n ? setTimeout(() => {\n controller.abort();\n }, args.request.timeoutMs)\n : undefined;\n\n try {\n const url = resolveRequestUrl(args.baseUrl, args.request.path, args.request.query);\n const headers = new Headers(args.request.headers ?? {});\n headers.delete(\"authorization\");\n headers.delete(\"Authorization\");\n if (args.authorization) {\n headers.set(\"authorization\", args.authorization);\n }\n let body: BodyInit | undefined;\n if (args.request.body.type === \"json\") {\n headers.set(\"content-type\", headers.get(\"content-type\") ?? \"application/json\");\n body = JSON.stringify(args.request.body.value);\n } else if (args.request.body.type === \"text\") {\n body = args.request.body.value;\n }\n\n const response = await (args.config.fetch ?? fetch)(url, {\n method: args.request.method,\n headers,\n body,\n signal: controller?.signal,\n });\n const responseHeaders = Object.fromEntries(response.headers.entries());\n const responseText = await response.text();\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n let responseBody:\n | { type: \"json\"; value: unknown }\n | { type: \"text\"; value: string }\n | { type: \"empty\"; value: null };\n let responseDecodingFailed = false;\n if (!responseText) {\n responseBody = { type: \"empty\", value: null };\n } else if (contentType.includes(\"application/json\")) {\n try {\n responseBody = { type: \"json\", value: JSON.parse(responseText) as unknown };\n } catch {\n responseBody = { type: \"text\", value: responseText };\n responseDecodingFailed = true;\n }\n } else {\n responseBody = { type: \"text\", value: responseText };\n }\n const apiResponse = {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n body: responseBody,\n };\n if (responseDecodingFailed) {\n return {\n ok: false,\n response: apiResponse,\n error: {\n code: \"RESPONSE_DECODING_ERROR\",\n message: \"Upstream response declared JSON but could not be decoded\",\n },\n };\n }\n if (response.ok) {\n return { ok: true, response: apiResponse, error: null };\n }\n\n const responseMessage =\n responseBody.type === \"json\" &&\n responseBody.value &&\n typeof responseBody.value === \"object\" &&\n \"message\" in responseBody.value\n ? boundedErrorMessage(String(responseBody.value.message))\n : boundedErrorMessage(responseText) || response.statusText || `HTTP ${response.status}`;\n return {\n ok: false,\n response: apiResponse,\n error: { code: \"HTTP_ERROR\", message: responseMessage },\n };\n } catch (error) {\n const message =\n error instanceof Error && error.name === \"AbortError\"\n ? `API request timed out after ${args.request.timeoutMs}ms`\n : error instanceof Error\n ? error.message\n : String(error);\n return {\n ok: false,\n response: null,\n error: { code: \"REQUEST_ERROR\", message },\n };\n } finally {\n if (timeout) {\n clearTimeout(timeout);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { timingSafeEqualBytes, timingSafeEqualText } from "../crypto.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/webhooks/auth.d.ts
|
|
4
|
+
declare const webhookAuthConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
5
|
+
type: z.ZodLiteral<"none">;
|
|
6
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
7
|
+
type: z.ZodLiteral<"bearer">;
|
|
8
|
+
tokenRef: z.ZodString;
|
|
9
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
10
|
+
type: z.ZodLiteral<"apiKey">;
|
|
11
|
+
location: z.ZodEnum<{
|
|
12
|
+
header: "header";
|
|
13
|
+
query: "query";
|
|
14
|
+
}>;
|
|
15
|
+
name: z.ZodString;
|
|
16
|
+
secretRef: z.ZodString;
|
|
17
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
18
|
+
type: z.ZodLiteral<"basic">;
|
|
19
|
+
usernameRef: z.ZodString;
|
|
20
|
+
passwordRef: z.ZodString;
|
|
21
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
22
|
+
type: z.ZodLiteral<"hmac">;
|
|
23
|
+
secretRef: z.ZodString;
|
|
24
|
+
algorithm: z.ZodEnum<{
|
|
25
|
+
sha1: "sha1";
|
|
26
|
+
sha256: "sha256";
|
|
27
|
+
sha512: "sha512";
|
|
28
|
+
}>;
|
|
29
|
+
signature: z.ZodObject<{
|
|
30
|
+
location: z.ZodEnum<{
|
|
31
|
+
header: "header";
|
|
32
|
+
query: "query";
|
|
33
|
+
}>;
|
|
34
|
+
name: z.ZodString;
|
|
35
|
+
encoding: z.ZodEnum<{
|
|
36
|
+
hex: "hex";
|
|
37
|
+
base64: "base64";
|
|
38
|
+
base64url: "base64url";
|
|
39
|
+
}>;
|
|
40
|
+
prefix: z.ZodOptional<z.ZodString>;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
signedPayload: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
43
|
+
type: z.ZodLiteral<"rawBody">;
|
|
44
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
45
|
+
type: z.ZodLiteral<"timestampedBody">;
|
|
46
|
+
prefix: z.ZodString;
|
|
47
|
+
timestampHeader: z.ZodString;
|
|
48
|
+
delimiter: z.ZodString;
|
|
49
|
+
toleranceSeconds: z.ZodNumber;
|
|
50
|
+
}, z.core.$strip>], "type">;
|
|
51
|
+
}, z.core.$strip>], "type">;
|
|
52
|
+
type WebhookAuthConfig = z.infer<typeof webhookAuthConfigSchema>;
|
|
53
|
+
type WebhookAuthType = WebhookAuthConfig["type"];
|
|
54
|
+
type WebhookAuthSecretRef = string;
|
|
55
|
+
type WebhookSecretResolver = {
|
|
56
|
+
get(ref: WebhookAuthSecretRef): string | undefined | Promise<string | undefined>;
|
|
57
|
+
};
|
|
58
|
+
type WebhookAuthFailureReason = "missing_secret" | "missing_credential" | "invalid_credential" | "timestamp_out_of_range" | "bad_config";
|
|
59
|
+
type WebhookAuthResult = {
|
|
60
|
+
ok: true;
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
reason: WebhookAuthFailureReason;
|
|
64
|
+
};
|
|
65
|
+
type SensitiveRequestValue = {
|
|
66
|
+
location: "header" | "query";
|
|
67
|
+
name: string;
|
|
68
|
+
};
|
|
69
|
+
type WebhookAuthVerificationContext = {
|
|
70
|
+
request: Request;
|
|
71
|
+
now: Date;
|
|
72
|
+
secrets: WebhookSecretResolver;
|
|
73
|
+
rawBodyBytes(): Promise<Uint8Array>;
|
|
74
|
+
};
|
|
75
|
+
interface WebhookAuthVerifier<TConfig extends WebhookAuthConfig> {
|
|
76
|
+
type: TConfig["type"];
|
|
77
|
+
requiredSecretRefs(config: TConfig): readonly WebhookAuthSecretRef[];
|
|
78
|
+
sensitiveRequestValues(config: TConfig): readonly SensitiveRequestValue[];
|
|
79
|
+
verify(config: TConfig, context: WebhookAuthVerificationContext): Promise<WebhookAuthResult>;
|
|
80
|
+
}
|
|
81
|
+
type VerifyWebhookAuthInput = {
|
|
82
|
+
config: WebhookAuthConfig;
|
|
83
|
+
/** Pass the original web-standard Request or a clone. The body is read from request.clone(). */
|
|
84
|
+
request: Request;
|
|
85
|
+
secrets: WebhookSecretResolver;
|
|
86
|
+
now?: Date;
|
|
87
|
+
};
|
|
88
|
+
declare const webhookAuthVerifiers: {
|
|
89
|
+
none: WebhookAuthVerifier<{
|
|
90
|
+
type: "none";
|
|
91
|
+
}>;
|
|
92
|
+
bearer: WebhookAuthVerifier<{
|
|
93
|
+
type: "bearer";
|
|
94
|
+
tokenRef: string;
|
|
95
|
+
}>;
|
|
96
|
+
apiKey: WebhookAuthVerifier<{
|
|
97
|
+
type: "apiKey";
|
|
98
|
+
location: "header" | "query";
|
|
99
|
+
name: string;
|
|
100
|
+
secretRef: string;
|
|
101
|
+
}>;
|
|
102
|
+
basic: WebhookAuthVerifier<{
|
|
103
|
+
type: "basic";
|
|
104
|
+
usernameRef: string;
|
|
105
|
+
passwordRef: string;
|
|
106
|
+
}>;
|
|
107
|
+
hmac: WebhookAuthVerifier<{
|
|
108
|
+
type: "hmac";
|
|
109
|
+
secretRef: string;
|
|
110
|
+
algorithm: "sha1" | "sha256" | "sha512";
|
|
111
|
+
signature: {
|
|
112
|
+
location: "header" | "query";
|
|
113
|
+
name: string;
|
|
114
|
+
encoding: "hex" | "base64" | "base64url";
|
|
115
|
+
prefix?: string | undefined;
|
|
116
|
+
};
|
|
117
|
+
signedPayload: {
|
|
118
|
+
type: "rawBody";
|
|
119
|
+
} | {
|
|
120
|
+
type: "timestampedBody";
|
|
121
|
+
prefix: string;
|
|
122
|
+
timestampHeader: string;
|
|
123
|
+
delimiter: string;
|
|
124
|
+
toleranceSeconds: number;
|
|
125
|
+
};
|
|
126
|
+
}>;
|
|
127
|
+
};
|
|
128
|
+
declare function verifyWebhookAuth({ config, request, secrets, now }: VerifyWebhookAuthInput): Promise<WebhookAuthResult>;
|
|
129
|
+
declare function getWebhookAuthSecretRefs(config: WebhookAuthConfig): readonly WebhookAuthSecretRef[];
|
|
130
|
+
declare function getSensitiveWebhookAuthValues(config: WebhookAuthConfig): readonly SensitiveRequestValue[];
|
|
131
|
+
//#endregion
|
|
132
|
+
export { SensitiveRequestValue, VerifyWebhookAuthInput, WebhookAuthConfig, WebhookAuthFailureReason, WebhookAuthResult, WebhookAuthSecretRef, WebhookAuthType, WebhookAuthVerificationContext, WebhookAuthVerifier, WebhookSecretResolver, getSensitiveWebhookAuthValues, getWebhookAuthSecretRefs, timingSafeEqualBytes, timingSafeEqualText, verifyWebhookAuth, webhookAuthConfigSchema, webhookAuthVerifiers };
|
|
133
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","names":[],"sources":["../../../src/webhooks/auth.ts"],"mappings":";;;cAca,yBAAuB,EAAA,uBAAA,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqCxB,oBAAoB,EAAE,aAAa;KACnC,kBAAkB;KAClB;KAEA;EACV,IAAI,KAAK,4CAA4C;;KAG3C;KAOA;EAAsB;;EAAe;EAAW,QAAQ;;KAExD;EACV;EACA;;KAGU;EACV,SAAS;EACT,KAAK;EACL,SAAS;EACT,gBAAgB,QAAQ;;UAGT,oBAAoB,gBAAgB;EACnD,MAAM;EACN,mBAAmB,QAAQ,mBAAmB;EAC9C,uBAAuB,QAAQ,mBAAmB;EAClD,OAAO,QAAQ,SAAS,SAAS,iCAAiC,QAAQ;;KAOhE;EACV,QAAQ;;EAER,SAAS;EACT,SAAS;EACT,MAAM;;cAoLK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQS,oBACpB,QACA,SACA,SACA,OACC,yBAAyB,QAAQ;iBAepB,yBACd,QAAQ,6BACE;iBAII,8BACd,QAAQ,6BACE"}
|