veryfront 0.1.458 → 0.1.460

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.
@@ -0,0 +1,397 @@
1
+ import { importSPKI, jwtVerify, type KeyLike } from "jose";
2
+
3
+ export type HostedServiceAuthErrorCode =
4
+ | "UNAUTHENTICATED"
5
+ | "FORBIDDEN"
6
+ | "NOT_FOUND"
7
+ | "SERVER_ERROR";
8
+
9
+ export class HostedServiceAuthError extends Error {
10
+ readonly statusCode: number;
11
+ readonly errorCode: HostedServiceAuthErrorCode;
12
+
13
+ constructor(statusCode: number, message: string) {
14
+ super(message);
15
+ this.name = "HostedServiceAuthError";
16
+ this.statusCode = statusCode;
17
+ this.errorCode = statusCode === 401 ? "UNAUTHENTICATED" : "FORBIDDEN";
18
+ }
19
+ }
20
+
21
+ export function isHostedServiceAuthError(
22
+ error: unknown,
23
+ ): error is HostedServiceAuthError {
24
+ return error instanceof HostedServiceAuthError;
25
+ }
26
+
27
+ export type HostedServiceAuthenticatedRequest = {
28
+ authToken: string;
29
+ userId: string;
30
+ };
31
+
32
+ export type HostedServiceJwtError = {
33
+ statusCode: number;
34
+ errorCode: HostedServiceAuthErrorCode;
35
+ message: string;
36
+ };
37
+
38
+ export type HostedServiceJwtResult =
39
+ | { success: true; userId: string; email: string; token: string }
40
+ | { success: false; error: HostedServiceJwtError };
41
+
42
+ export type HostedServiceProjectAccessError = {
43
+ statusCode: number;
44
+ errorCode: HostedServiceAuthErrorCode;
45
+ message: string;
46
+ };
47
+
48
+ export type HostedServiceProjectAccessResult =
49
+ | { success: true; projectId: string }
50
+ | { success: false; error: HostedServiceProjectAccessError };
51
+
52
+ export type HostedServiceAuthConfig = {
53
+ OAUTH_PUBLIC_KEY?: string | null;
54
+ NODE_ENV?: string | null;
55
+ VERYFRONT_API_URL: string;
56
+ };
57
+
58
+ export type HostedServiceAuthLogger = {
59
+ debug?: (message: string, metadata?: Record<string, unknown>) => void;
60
+ error?: (message: string, metadata?: Record<string, unknown>) => void;
61
+ };
62
+
63
+ export type HostedServiceAuthTrace = <TResult>(
64
+ operationName: string,
65
+ operation: () => Promise<TResult>,
66
+ ) => Promise<TResult>;
67
+
68
+ export type HostedServiceAuthFetch = (
69
+ input: string | URL | Request,
70
+ init?: RequestInit,
71
+ ) => Promise<Response>;
72
+
73
+ export type HostedServiceAuthOptions = {
74
+ getConfig: () => HostedServiceAuthConfig;
75
+ logger?: HostedServiceAuthLogger;
76
+ trace?: HostedServiceAuthTrace;
77
+ fetch?: HostedServiceAuthFetch;
78
+ projectAccessTimeoutMs?: number;
79
+ };
80
+
81
+ export type HostedServiceAuth = {
82
+ authenticateRequest: (
83
+ request: Request,
84
+ ) => Promise<HostedServiceAuthenticatedRequest | Response>;
85
+ getTokenFromRequest: typeof getHostedServiceTokenFromRequest;
86
+ verifyJwt: (token: string) => Promise<HostedServiceJwtResult>;
87
+ verifyProjectAccess: (
88
+ projectId: string,
89
+ token: string,
90
+ ) => Promise<HostedServiceProjectAccessResult>;
91
+ };
92
+
93
+ let cachedPublicKeyInput: string | undefined;
94
+ let cachedPublicKeyPromise: Promise<KeyLike> | undefined;
95
+
96
+ function defaultTrace<TResult>(
97
+ _operationName: string,
98
+ operation: () => Promise<TResult>,
99
+ ): Promise<TResult> {
100
+ return operation();
101
+ }
102
+
103
+ function getFetch(options: HostedServiceAuthOptions): HostedServiceAuthFetch {
104
+ return options.fetch ?? fetch;
105
+ }
106
+
107
+ function getProjectAccessTimeoutMs(options: HostedServiceAuthOptions): number {
108
+ return options.projectAccessTimeoutMs ?? 15_000;
109
+ }
110
+
111
+ function getPublicKey(publicKeyInput: string): Promise<KeyLike> {
112
+ if (cachedPublicKeyInput !== publicKeyInput || !cachedPublicKeyPromise) {
113
+ cachedPublicKeyInput = publicKeyInput;
114
+ cachedPublicKeyPromise = importSPKI(publicKeyInput, "RS256");
115
+ }
116
+
117
+ return cachedPublicKeyPromise;
118
+ }
119
+
120
+ export function getHostedServiceTokenFromRequest(request: Request): string | null {
121
+ const cookies = request.headers.get("cookie") || "";
122
+ const cookieMatch = cookies.match(/(?:^|;\s*)authToken=([^;]+)/);
123
+ if (cookieMatch?.[1]) return cookieMatch[1];
124
+
125
+ const authHeader = request.headers.get("authorization");
126
+ if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7);
127
+
128
+ return null;
129
+ }
130
+
131
+ function makeUnauthenticatedError(message: string): HostedServiceJwtError {
132
+ return {
133
+ statusCode: 401,
134
+ errorCode: "UNAUTHENTICATED",
135
+ message,
136
+ };
137
+ }
138
+
139
+ function decodeBase64Url(input: string): string {
140
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
141
+ const paddingLength = (4 - (normalized.length % 4)) % 4;
142
+ const padded = `${normalized}${"=".repeat(paddingLength)}`;
143
+
144
+ if (typeof atob !== "function") {
145
+ throw new Error("Base64URL decoding is not available in this runtime");
146
+ }
147
+
148
+ const binary = atob(padded);
149
+ const bytes = new Uint8Array(binary.length);
150
+ for (let index = 0; index < binary.length; index += 1) {
151
+ bytes[index] = binary.charCodeAt(index);
152
+ }
153
+
154
+ return new TextDecoder().decode(bytes);
155
+ }
156
+
157
+ function isRecord(value: unknown): value is Record<string, unknown> {
158
+ return typeof value === "object" && value !== null && !Array.isArray(value);
159
+ }
160
+
161
+ function parseJsonObject(json: string): Record<string, unknown> | null {
162
+ const parsed: unknown = JSON.parse(json);
163
+ return isRecord(parsed) ? parsed : null;
164
+ }
165
+
166
+ function decodeHostedServiceJwtWithoutVerify(
167
+ token: string,
168
+ ): HostedServiceJwtResult {
169
+ try {
170
+ const parts = token.split(".");
171
+ if (parts.length !== 3) {
172
+ return {
173
+ success: false,
174
+ error: makeUnauthenticatedError("Invalid token format"),
175
+ };
176
+ }
177
+
178
+ const payloadPart = parts[1];
179
+ if (!payloadPart) {
180
+ return {
181
+ success: false,
182
+ error: makeUnauthenticatedError("Invalid token format"),
183
+ };
184
+ }
185
+
186
+ const payload = parseJsonObject(decodeBase64Url(payloadPart));
187
+ if (!payload) {
188
+ return {
189
+ success: false,
190
+ error: makeUnauthenticatedError("Invalid token"),
191
+ };
192
+ }
193
+
194
+ if (typeof payload.exp === "number" && payload.exp * 1000 < Date.now()) {
195
+ return {
196
+ success: false,
197
+ error: makeUnauthenticatedError("Token expired"),
198
+ };
199
+ }
200
+
201
+ const userId = typeof payload.userId === "string" ? payload.userId : null;
202
+ if (!userId) {
203
+ return {
204
+ success: false,
205
+ error: makeUnauthenticatedError("Invalid token: missing userId"),
206
+ };
207
+ }
208
+
209
+ return {
210
+ success: true,
211
+ userId,
212
+ email: typeof payload.email === "string" ? payload.email : "",
213
+ token,
214
+ };
215
+ } catch {
216
+ return {
217
+ success: false,
218
+ error: makeUnauthenticatedError("Invalid token"),
219
+ };
220
+ }
221
+ }
222
+
223
+ export function createHostedServiceAuth(
224
+ options: HostedServiceAuthOptions,
225
+ ): HostedServiceAuth {
226
+ const trace = options.trace ?? defaultTrace;
227
+
228
+ async function verifyJwt(token: string): Promise<HostedServiceJwtResult> {
229
+ return await trace("auth.verifyJwt", async () => {
230
+ if (!token) {
231
+ return {
232
+ success: false,
233
+ error: makeUnauthenticatedError("Authentication token required"),
234
+ };
235
+ }
236
+
237
+ const config = options.getConfig();
238
+
239
+ if (!config.OAUTH_PUBLIC_KEY) {
240
+ if (config.NODE_ENV === "production") {
241
+ return {
242
+ success: false,
243
+ error: {
244
+ statusCode: 500,
245
+ errorCode: "SERVER_ERROR",
246
+ message: "JWT public key not configured",
247
+ },
248
+ };
249
+ }
250
+ return decodeHostedServiceJwtWithoutVerify(token);
251
+ }
252
+
253
+ try {
254
+ const publicKey = await getPublicKey(config.OAUTH_PUBLIC_KEY);
255
+ const { payload } = await jwtVerify(token, publicKey, {
256
+ algorithms: ["RS256"],
257
+ });
258
+
259
+ const userId = typeof payload.userId === "string" ? payload.userId : null;
260
+ if (!userId) {
261
+ return {
262
+ success: false,
263
+ error: makeUnauthenticatedError("Invalid token: missing userId"),
264
+ };
265
+ }
266
+
267
+ return {
268
+ success: true,
269
+ userId,
270
+ email: typeof payload.email === "string" ? payload.email : "",
271
+ token,
272
+ };
273
+ } catch (error) {
274
+ options.logger?.debug?.("JWT verification failed", {
275
+ error: error instanceof Error ? error.message : String(error),
276
+ });
277
+
278
+ const errorMessage = error instanceof Error ? error.message : String(error);
279
+ if (errorMessage.includes("expired")) {
280
+ return {
281
+ success: false,
282
+ error: makeUnauthenticatedError("Token expired"),
283
+ };
284
+ }
285
+
286
+ return {
287
+ success: false,
288
+ error: makeUnauthenticatedError("Invalid token"),
289
+ };
290
+ }
291
+ });
292
+ }
293
+
294
+ async function authenticateRequest(
295
+ request: Request,
296
+ ): Promise<HostedServiceAuthenticatedRequest | Response> {
297
+ const token = getHostedServiceTokenFromRequest(request);
298
+ if (!token) {
299
+ return Response.json({ errorCode: "UNAUTHENTICATED" }, { status: 401 });
300
+ }
301
+
302
+ const auth = await verifyJwt(token);
303
+ if (!auth.success) {
304
+ return Response.json({ errorCode: auth.error.errorCode }, { status: 401 });
305
+ }
306
+
307
+ return {
308
+ authToken: auth.token,
309
+ userId: auth.userId,
310
+ };
311
+ }
312
+
313
+ async function verifyProjectAccess(
314
+ projectId: string,
315
+ token: string,
316
+ ): Promise<HostedServiceProjectAccessResult> {
317
+ return await trace("auth.verifyProjectAccess", async () => {
318
+ const config = options.getConfig();
319
+
320
+ try {
321
+ const apiUrl = new URL(config.VERYFRONT_API_URL);
322
+ const restUrl = new URL(`/projects/${projectId}`, apiUrl.origin);
323
+
324
+ const headers = new Headers({ "Content-Type": "application/json" });
325
+ if (token) {
326
+ headers.set("Authorization", `Bearer ${token}`);
327
+ }
328
+
329
+ const response = await getFetch(options)(restUrl.toString(), {
330
+ method: "GET",
331
+ headers,
332
+ signal: AbortSignal.timeout(getProjectAccessTimeoutMs(options)),
333
+ });
334
+
335
+ if (response.status === 404) {
336
+ return {
337
+ success: false,
338
+ error: {
339
+ statusCode: 404,
340
+ errorCode: "NOT_FOUND",
341
+ message: "Project not found",
342
+ },
343
+ };
344
+ }
345
+
346
+ if (response.status === 401 || response.status === 403) {
347
+ return {
348
+ success: false,
349
+ error: {
350
+ statusCode: 403,
351
+ errorCode: "FORBIDDEN",
352
+ message: "No access to project",
353
+ },
354
+ };
355
+ }
356
+
357
+ if (!response.ok) {
358
+ const errorText = await response.text();
359
+ options.logger?.error?.("Project access check failed", {
360
+ error: errorText,
361
+ projectId,
362
+ });
363
+ return {
364
+ success: false,
365
+ error: {
366
+ statusCode: 403,
367
+ errorCode: "FORBIDDEN",
368
+ message: "No access to project",
369
+ },
370
+ };
371
+ }
372
+
373
+ return {
374
+ success: true,
375
+ projectId,
376
+ };
377
+ } catch (error) {
378
+ options.logger?.error?.("Project access check failed", { error, projectId });
379
+ return {
380
+ success: false,
381
+ error: {
382
+ statusCode: 403,
383
+ errorCode: "FORBIDDEN",
384
+ message: "No access to project",
385
+ },
386
+ };
387
+ }
388
+ });
389
+ }
390
+
391
+ return {
392
+ authenticateRequest,
393
+ getTokenFromRequest: getHostedServiceTokenFromRequest,
394
+ verifyJwt,
395
+ verifyProjectAccess,
396
+ };
397
+ }
@@ -681,6 +681,9 @@ export {
681
681
  type PrepareAgentRuntimeMessagesFromUiMessagesOptions,
682
682
  } from "./runtime-message-preparation.js";
683
683
  export {
684
+ type HostedChatExecutionPreparationInput,
685
+ type HostedChatExecutionPreparationResult,
686
+ type HostedChatExecutionPreparationRootRunOptions,
684
687
  type HostedChatRuntimeCreationPreparationInput,
685
688
  type HostedChatRuntimeCreationPreparationResult,
686
689
  type HostedChatRuntimeInstructionsInput,
@@ -688,6 +691,7 @@ export {
688
691
  type HostedChatRuntimePreparationSteering,
689
692
  type NormalizedHostedChatRequest,
690
693
  normalizeParsedHostedChatRequest,
694
+ prepareHostedChatExecution,
691
695
  prepareHostedChatRuntimeCreationOptions,
692
696
  prepareHostedChatRuntimeMessages,
693
697
  type PrepareHostedChatRuntimeMessagesOptions,
@@ -1294,3 +1298,22 @@ export {
1294
1298
  WaitConflictError,
1295
1299
  WaitNotPendingError,
1296
1300
  } from "./runtime/index.js";
1301
+
1302
+ export {
1303
+ createHostedServiceAuth,
1304
+ getHostedServiceTokenFromRequest,
1305
+ type HostedServiceAuth,
1306
+ type HostedServiceAuthConfig,
1307
+ type HostedServiceAuthenticatedRequest,
1308
+ HostedServiceAuthError,
1309
+ type HostedServiceAuthErrorCode,
1310
+ type HostedServiceAuthFetch,
1311
+ type HostedServiceAuthLogger,
1312
+ type HostedServiceAuthOptions,
1313
+ type HostedServiceAuthTrace,
1314
+ type HostedServiceJwtError,
1315
+ type HostedServiceJwtResult,
1316
+ type HostedServiceProjectAccessError,
1317
+ type HostedServiceProjectAccessResult,
1318
+ isHostedServiceAuthError,
1319
+ } from "./hosted-service-auth.js";
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.458";
3
+ export const VERSION = "0.1.460";