@nshipster/sosumi 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -5,7 +5,24 @@ import { cache } from "hono/cache"
5
5
  import { cors } from "hono/cors"
6
6
  import { HTTPException } from "hono/http-exception"
7
7
  import { trimTrailingSlash } from "hono/trailing-slash"
8
- import { buildAgentCard } from "./lib/a2a"
8
+ import {
9
+ A2A_MEDIA_TYPE,
10
+ A2A_MESSAGE_PATH,
11
+ A2AError,
12
+ buildAgentCard,
13
+ capabilityError,
14
+ createA2AErrorResponse,
15
+ handleA2AMessage,
16
+ parseA2AListTasksQuery,
17
+ pushNotificationsNotSupported,
18
+ readA2ACapabilityText,
19
+ readA2AJsonBody,
20
+ taskNotFound,
21
+ unsupportedOperation,
22
+ validateA2ARequestHeaders,
23
+ } from "./lib/a2a"
24
+ import { AI_CATALOG_MEDIA_TYPE, buildAiCatalog } from "./lib/ard"
25
+ import { buildDidDocument, DID_DOCUMENT_MEDIA_TYPE } from "./lib/did"
9
26
  import {
10
27
  decodeExternalTargetPath,
11
28
  ExternalAccessError,
@@ -47,10 +64,47 @@ interface Env {
47
64
  EXTERNAL_DOC_HOST_BLOCKLIST?: string
48
65
  WEB_BOT_AUTH_KEY?: string
49
66
  SIGNATURE_AGENT?: string
67
+ MCP_REGISTRY_PUBLIC_KEY?: string
50
68
  }
51
69
 
52
70
  const app = new Hono<{ Bindings: Env }>()
53
71
 
72
+ function requestedA2AVersion(request: Request): string | null {
73
+ return request.headers.get("A2A-Version") ?? new URL(request.url).searchParams.get("A2A-Version")
74
+ }
75
+
76
+ function fixedA2AError(request: Request, error: A2AError): Response {
77
+ try {
78
+ validateA2ARequestHeaders(
79
+ request.method === "POST" ? request.headers.get("Content-Type") : null,
80
+ requestedA2AVersion(request),
81
+ )
82
+ return createA2AErrorResponse(error)
83
+ } catch (headerError) {
84
+ return createA2AErrorResponse(headerError)
85
+ }
86
+ }
87
+
88
+ function taskIdFromPath(path: string): string {
89
+ const taskPath = path.slice("/tasks/".length)
90
+ const encodedTaskId = taskPath.split(/[:/]/, 1)[0] ?? ""
91
+ try {
92
+ return decodeURIComponent(encodedTaskId) || "unknown"
93
+ } catch {
94
+ return encodedTaskId || "unknown"
95
+ }
96
+ }
97
+
98
+ function isMarkdownContentRoute(path: string): boolean {
99
+ return (
100
+ path.startsWith("/documentation/") ||
101
+ path.startsWith("/external/") ||
102
+ path === "/design/human-interface-guidelines" ||
103
+ path.startsWith("/design/human-interface-guidelines/") ||
104
+ path.startsWith("/videos/play/")
105
+ )
106
+ }
107
+
54
108
  app.use("*", async (c, next) => {
55
109
  // Prime Web Bot Auth signing before any route or outbound fetch runs.
56
110
  configureWebBotAuth(c.env)
@@ -63,6 +117,7 @@ app.use("*", async (c, next) => {
63
117
  c.header("X-XSS-Protection", "1; mode=block")
64
118
  c.header("Referrer-Policy", "strict-origin-when-cross-origin")
65
119
  c.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
120
+ c.header("Content-Usage", "train-ai=n, search=y")
66
121
 
67
122
  // Performance headers
68
123
  c.header("Vary", "Accept")
@@ -72,18 +127,30 @@ app.use("*", async (c, next) => {
72
127
  c.header("Cache-Control", "no-store")
73
128
  }
74
129
 
130
+ const links = ['</llms.txt>; rel="describedby"']
131
+
75
132
  if (c.req.path === "/") {
76
- c.header(
77
- "Link",
78
- [
79
- '</.well-known/api-catalog>; rel="api-catalog"',
80
- '</.well-known/agent-card.json>; rel="service-desc"; type="application/json"',
81
- '</.well-known/mcp/server-card.json>; rel="service-desc"; type="application/json"',
82
- '</SKILL.md>; rel="service-doc"',
83
- '</llms.txt>; rel="alternate"; type="text/markdown"',
84
- ].join(", "),
133
+ links.unshift(
134
+ '</.well-known/api-catalog>; rel="api-catalog"',
135
+ `</.well-known/ard.json>; rel="ard"; type="${AI_CATALOG_MEDIA_TYPE}"`,
136
+ `</.well-known/ai-catalog.json>; rel="ai-catalog"; type="${AI_CATALOG_MEDIA_TYPE}"`,
137
+ '</.well-known/agent-card.json>; rel="service-desc"; type="application/json"',
138
+ '</.well-known/mcp/server-card.json>; rel="service-desc"; type="application/json"',
139
+ '</SKILL.md>; rel="service-doc"',
140
+ '</llms.txt>; rel="alternate"; type="text/markdown"',
85
141
  )
86
142
  }
143
+
144
+ if (isMarkdownContentRoute(c.req.path)) {
145
+ links.push(`<${c.req.path}>; rel="alternate"; type="text/markdown"`)
146
+ }
147
+
148
+ const existingLink = c.res.headers.get("Link")
149
+ if (existingLink) {
150
+ links.unshift(existingLink)
151
+ }
152
+
153
+ c.header("Link", links.join(", "))
87
154
  })
88
155
 
89
156
  app.use("*", cors())
@@ -188,6 +255,67 @@ const discoveryHeaders = {
188
255
  "Cache-Control": "public, max-age=300, s-maxage=600",
189
256
  } as const
190
257
 
258
+ const SECURITY_TXT_EXPIRY_MS = 364 * 24 * 60 * 60 * 1000 // 364 days
259
+
260
+ app.get("/.well-known/security.txt", (c) => {
261
+ const expires = new Date(Date.now() + SECURITY_TXT_EXPIRY_MS).toISOString()
262
+ const origin = new URL(c.req.url).origin
263
+
264
+ return c.text(
265
+ [
266
+ "Contact: mailto:info@sosumi.ai",
267
+ `Expires: ${expires}`,
268
+ `Canonical: ${origin}/.well-known/security.txt`,
269
+ "Preferred-Languages: en",
270
+ "",
271
+ ].join("\n"),
272
+ 200,
273
+ {
274
+ ...discoveryHeaders,
275
+ "Content-Type": "text/plain; charset=utf-8",
276
+ },
277
+ )
278
+ })
279
+
280
+ // Proves control of the sosumi.ai namespace to the MCP Registry
281
+ // (`mcp-publisher login http`). The registry doesn't follow redirects.
282
+ app.get("/.well-known/mcp-registry-auth", (c) => {
283
+ const publicKey = c.env.MCP_REGISTRY_PUBLIC_KEY?.trim()
284
+ const headers = {
285
+ "Content-Type": "text/plain; charset=utf-8",
286
+ "Cache-Control": "no-store",
287
+ }
288
+
289
+ if (!publicKey) {
290
+ return c.text("Not Found", 404, headers)
291
+ }
292
+
293
+ return c.text(`v=MCPv1; k=ed25519; p=${publicKey}`, 200, headers)
294
+ })
295
+
296
+ app.on("GET", ["/.well-known/ard.json", "/.well-known/ai-catalog.json"], (c) => {
297
+ const origin = new URL(c.req.url).origin
298
+
299
+ return c.json(buildAiCatalog(origin), 200, {
300
+ ...discoveryHeaders,
301
+ "Content-Type": `${AI_CATALOG_MEDIA_TYPE}; charset=utf-8`,
302
+ })
303
+ })
304
+
305
+ app.get("/.well-known/did.json", async (c) => {
306
+ const document = await buildDidDocument()
307
+ if (!document) {
308
+ return c.json({ error: "DID signing key is unavailable." }, 404, {
309
+ "Cache-Control": "no-store",
310
+ })
311
+ }
312
+
313
+ return c.json(document, 200, {
314
+ ...discoveryHeaders,
315
+ "Content-Type": `${DID_DOCUMENT_MEDIA_TYPE}; charset=utf-8`,
316
+ })
317
+ })
318
+
191
319
  app.get("/.well-known/api-catalog", (c) => {
192
320
  const origin = new URL(c.req.url).origin
193
321
 
@@ -250,6 +378,73 @@ app.get("/.well-known/agent-card.json", (c) => {
250
378
  })
251
379
  })
252
380
 
381
+ app.post(A2A_MESSAGE_PATH, async (c) => {
382
+ try {
383
+ validateA2ARequestHeaders(c.req.header("Content-Type") ?? null, requestedA2AVersion(c.req.raw))
384
+
385
+ const input = await readA2AJsonBody(c.req.raw)
386
+
387
+ const response = await handleA2AMessage(input, async (endpoint, outputMode) => {
388
+ const capabilityResponse = await app.request(
389
+ new URL(endpoint, c.req.url).toString(),
390
+ { headers: { Accept: outputMode } },
391
+ c.env,
392
+ )
393
+ const text = await readA2ACapabilityText(capabilityResponse)
394
+ if (!capabilityResponse.ok) {
395
+ throw capabilityError(
396
+ capabilityResponse.status,
397
+ text || `Sosumi capability request failed with status ${capabilityResponse.status}.`,
398
+ )
399
+ }
400
+ return text
401
+ })
402
+
403
+ return c.json(response, 200, {
404
+ "Content-Type": A2A_MEDIA_TYPE,
405
+ "Cache-Control": "no-store",
406
+ })
407
+ } catch (error) {
408
+ if (!(error instanceof A2AError)) {
409
+ console.error("A2A message execution failed", error)
410
+ }
411
+ return createA2AErrorResponse(error)
412
+ }
413
+ })
414
+
415
+ app.post("/message:stream", (c) =>
416
+ fixedA2AError(c.req.raw, unsupportedOperation("Streaming messages")),
417
+ )
418
+
419
+ app.get("/tasks", (c) => {
420
+ try {
421
+ validateA2ARequestHeaders(null, requestedA2AVersion(c.req.raw))
422
+ const pageSize = parseA2AListTasksQuery(new URL(c.req.url).searchParams)
423
+ return c.json({ tasks: [], nextPageToken: "", pageSize, totalSize: 0 }, 200, {
424
+ "Content-Type": A2A_MEDIA_TYPE,
425
+ "Cache-Control": "no-store",
426
+ })
427
+ } catch (error) {
428
+ return createA2AErrorResponse(error)
429
+ }
430
+ })
431
+
432
+ app.all("/tasks", (c) => fixedA2AError(c.req.raw, unsupportedOperation("This task operation")))
433
+
434
+ app.all("/tasks/*", (c) => {
435
+ if (c.req.path.includes("/pushNotificationConfigs")) {
436
+ return fixedA2AError(c.req.raw, pushNotificationsNotSupported())
437
+ }
438
+ if (c.req.path.endsWith(":subscribe")) {
439
+ return fixedA2AError(c.req.raw, unsupportedOperation("Task subscriptions"))
440
+ }
441
+ return fixedA2AError(c.req.raw, taskNotFound(taskIdFromPath(c.req.path)))
442
+ })
443
+
444
+ app.get("/extendedAgentCard", (c) =>
445
+ fixedA2AError(c.req.raw, unsupportedOperation("Extended Agent Cards")),
446
+ )
447
+
253
448
  app.get("/webmcp/manifest.json", (c) =>
254
449
  c.json(buildWebMcpManifest(), 200, {
255
450
  ...discoveryHeaders,