veryfront 0.1.459 → 0.1.461

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
+ }
@@ -872,6 +872,15 @@ export {
872
872
  type RuntimeProjectSkillCatalogOptions,
873
873
  type RuntimeProjectSteeringLookup,
874
874
  } from "./runtime-project-skill-catalog.js";
875
+ export {
876
+ createRuntimePromptBlock,
877
+ type RuntimePromptBlockOptions,
878
+ } from "./runtime-prompt-block.js";
879
+ export {
880
+ buildRuntimeAvailableSkillsPromptBlock,
881
+ formatRuntimeSkillMetadata,
882
+ MAX_RUNTIME_SKILL_PROMPT_ENTRIES,
883
+ } from "./runtime-skill-prompt.js";
875
884
  export {
876
885
  buildRuntimeLoadedSkillResponse,
877
886
  buildRuntimeSkillDefinition,
@@ -1298,3 +1307,22 @@ export {
1298
1307
  WaitConflictError,
1299
1308
  WaitNotPendingError,
1300
1309
  } from "./runtime/index.js";
1310
+
1311
+ export {
1312
+ createHostedServiceAuth,
1313
+ getHostedServiceTokenFromRequest,
1314
+ type HostedServiceAuth,
1315
+ type HostedServiceAuthConfig,
1316
+ type HostedServiceAuthenticatedRequest,
1317
+ HostedServiceAuthError,
1318
+ type HostedServiceAuthErrorCode,
1319
+ type HostedServiceAuthFetch,
1320
+ type HostedServiceAuthLogger,
1321
+ type HostedServiceAuthOptions,
1322
+ type HostedServiceAuthTrace,
1323
+ type HostedServiceJwtError,
1324
+ type HostedServiceJwtResult,
1325
+ type HostedServiceProjectAccessError,
1326
+ type HostedServiceProjectAccessResult,
1327
+ isHostedServiceAuthError,
1328
+ } from "./hosted-service-auth.js";
@@ -0,0 +1,19 @@
1
+ export type RuntimePromptBlockOptions = {
2
+ name: string;
3
+ content: string;
4
+ attrs?: Record<string, string>;
5
+ };
6
+
7
+ export function createRuntimePromptBlock({
8
+ name,
9
+ content,
10
+ attrs,
11
+ }: RuntimePromptBlockOptions): string {
12
+ const attrString = attrs
13
+ ? Object.entries(attrs)
14
+ .map(([key, value]) => ` ${key}="${value}"`)
15
+ .join("")
16
+ : "";
17
+
18
+ return `<${name}${attrString}>\n${content.trim()}\n</${name}>`;
19
+ }
@@ -0,0 +1,61 @@
1
+ import {
2
+ KEEP_ROOT_ASSISTANT_VISIBLE_OWNER,
3
+ LOAD_SKILL_CONTINUE_SAME_TURN,
4
+ LOAD_SKILL_DELEGATION_THRESHOLD,
5
+ LOAD_SKILL_OVERRIDE_FORWARDING,
6
+ NO_DELEGATION_NARRATION_UNLESS_ASKED,
7
+ } from "./conversation-delegation-policy.js";
8
+ import { createRuntimePromptBlock } from "./runtime-prompt-block.js";
9
+ import type { RuntimeSkillDefinition } from "./runtime-skill-metadata.js";
10
+
11
+ export const MAX_RUNTIME_SKILL_PROMPT_ENTRIES = 30;
12
+
13
+ export function formatRuntimeSkillMetadata(skill: RuntimeSkillDefinition): string {
14
+ const details: string[] = [];
15
+ const allowedTools = skill.allowedTools ?? [];
16
+
17
+ if (allowedTools.length > 0) {
18
+ details.push(`tools: ${allowedTools.join(", ")}`);
19
+ }
20
+
21
+ if (skill.model) {
22
+ details.push(`model: ${skill.model}`);
23
+ }
24
+
25
+ if (skill.thinking === false) {
26
+ details.push("thinking: off");
27
+ } else if (typeof skill.thinking === "number") {
28
+ details.push(`thinking: ${skill.thinking}`);
29
+ }
30
+
31
+ if (skill.maxSteps !== undefined) {
32
+ details.push(`max-steps: ${skill.maxSteps}`);
33
+ }
34
+
35
+ return details.length > 0 ? ` (${details.join("; ")})` : "";
36
+ }
37
+
38
+ export function buildRuntimeAvailableSkillsPromptBlock(
39
+ skills: readonly RuntimeSkillDefinition[],
40
+ ): string {
41
+ const displaySkills = skills.slice(0, MAX_RUNTIME_SKILL_PROMPT_ENTRIES);
42
+ const skillsList = displaySkills
43
+ .map((skill) => `- ${skill.id}: ${skill.description}${formatRuntimeSkillMetadata(skill)}`)
44
+ .join("\n");
45
+
46
+ const truncationNote = skills.length > MAX_RUNTIME_SKILL_PROMPT_ENTRIES
47
+ ? `\n\n(${
48
+ skills.length - MAX_RUNTIME_SKILL_PROMPT_ENTRIES
49
+ } more skills available — use load_skill to discover)`
50
+ : "";
51
+
52
+ return createRuntimePromptBlock({
53
+ name: "available_skills",
54
+ content:
55
+ `You have access to these skills. Use load_skill to load full instructions when needed. load_skill only loads instructions plus metadata. ${LOAD_SKILL_CONTINUE_SAME_TURN} ${KEEP_ROOT_ASSISTANT_VISIBLE_OWNER} If a skill specifies allowed tools, you MUST stay within the current-run intersection of those tools. When delegating, use the platform orchestration tool \`invoke_agent\`. ${LOAD_SKILL_DELEGATION_THRESHOLD} ${LOAD_SKILL_OVERRIDE_FORWARDING} ${NO_DELEGATION_NARRATION_UNLESS_ASKED}
56
+
57
+ Do NOT attempt tools that are absent from the current run just because they appear in loaded skill instructions.
58
+
59
+ ${skillsList}${truncationNote}`,
60
+ });
61
+ }
@@ -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.459";
3
+ export const VERSION = "0.1.461";