@nshipster/sosumi 1.0.6 → 1.0.7
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/README.md +44 -1
- package/package.json +4 -2
- package/public/index.html +3 -1
- package/public/llms.txt +1 -1
- package/public/robots.txt +1 -0
- package/public/webmcp.js +71 -0
- package/src/index.ts +112 -1
- package/src/lib/a2a.ts +90 -0
- package/src/lib/external/fetch.ts +3 -0
- package/src/lib/hig/render.ts +94 -73
- package/src/lib/markdown.ts +115 -0
- package/src/lib/mcp.ts +150 -115
- package/src/lib/reference/render.ts +112 -156
- package/src/lib/search.ts +115 -55
- package/src/lib/types.ts +12 -0
- package/src/lib/url.ts +35 -0
- package/src/lib/webbotauth.ts +261 -0
- package/src/lib/webmcp.ts +38 -0
- package/wrangler.jsonc +1 -1
package/src/lib/url.ts
CHANGED
|
@@ -53,3 +53,38 @@ export function isValidAppleDocUrl(url: string): boolean {
|
|
|
53
53
|
|
|
54
54
|
return url.startsWith(APPLE_DOC_BASE_URL)
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derives a human-readable title from the last component of a documentation
|
|
59
|
+
* path or `doc://` identifier.
|
|
60
|
+
*
|
|
61
|
+
* @param identifier - The identifier or path (e.g., `doc://com.apple.SwiftUI/documentation/SwiftUI/View/body-8kl5o`)
|
|
62
|
+
* @returns The title, with any disambiguation suffix removed (e.g., `body`)
|
|
63
|
+
*/
|
|
64
|
+
export function extractTitleFromIdentifier(identifier: string): string {
|
|
65
|
+
const parts = identifier.split("/")
|
|
66
|
+
const lastPart = parts[parts.length - 1]
|
|
67
|
+
|
|
68
|
+
// Handle disambiguation suffixes (e.g., "body-8kl5o" -> "body", "init(exactly:)-63925" -> "init(exactly:)")
|
|
69
|
+
const disambiguationMatch = lastPart.match(/^(.+?)(?:-\w+)?$/)
|
|
70
|
+
if (disambiguationMatch) {
|
|
71
|
+
const baseName = disambiguationMatch[1]
|
|
72
|
+
|
|
73
|
+
// If it looks like a method signature (contains parentheses), preserve it
|
|
74
|
+
if (baseName.includes("(") && baseName.includes(")")) {
|
|
75
|
+
return baseName
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// For simple identifiers, convert camelCase to readable format
|
|
79
|
+
return baseName
|
|
80
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
81
|
+
.replace(/\s+/g, " ")
|
|
82
|
+
.trim()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fallback: convert camelCase to readable format
|
|
86
|
+
return lastPart
|
|
87
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
88
|
+
.replace(/\s+/g, " ")
|
|
89
|
+
.trim()
|
|
90
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Bot Auth (IETF Web Bot Auth working group) support.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. Publish a JWKS at `/.well-known/http-message-signatures-directory`
|
|
7
|
+
* so receiving sites can look up the public key(s)
|
|
8
|
+
* used to verify our signatures.
|
|
9
|
+
* 2. Sign the requests sosumi.ai makes to external Swift-DocC hosts
|
|
10
|
+
* with RFC 9421 HTTP Message Signatures,
|
|
11
|
+
* attaching `Signature-Agent`, `Signature-Input`, and `Signature` headers
|
|
12
|
+
* so those sites can verify the traffic comes from us.
|
|
13
|
+
*
|
|
14
|
+
* The Ed25519 private key is provided out-of-band
|
|
15
|
+
* as the `WEB_BOT_AUTH_KEY` secret (a JSON Web Key).
|
|
16
|
+
* The matching public key served in the directory is derived from it,
|
|
17
|
+
* so no key material lives in the repository.
|
|
18
|
+
*
|
|
19
|
+
* See https://datatracker.ietf.org/wg/webbotauth/about/
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
directoryResponseHeaders,
|
|
24
|
+
HTTP_MESSAGE_SIGNATURES_DIRECTORY,
|
|
25
|
+
jwkToKeyID,
|
|
26
|
+
MediaType,
|
|
27
|
+
type Signer,
|
|
28
|
+
signatureHeaders,
|
|
29
|
+
} from "web-bot-auth"
|
|
30
|
+
import { helpers, signerFromJWK } from "web-bot-auth/crypto"
|
|
31
|
+
|
|
32
|
+
export interface WebBotAuthEnv {
|
|
33
|
+
/**
|
|
34
|
+
* Ed25519 private key as a JSON Web Key (JSON string).
|
|
35
|
+
* Provided as a secret.
|
|
36
|
+
*/
|
|
37
|
+
WEB_BOT_AUTH_KEY?: string
|
|
38
|
+
/** Origin advertised in the `Signature-Agent` header and hosting the directory. */
|
|
39
|
+
SIGNATURE_AGENT?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Path of the published key directory. */
|
|
43
|
+
export const DIRECTORY_PATH = HTTP_MESSAGE_SIGNATURES_DIRECTORY
|
|
44
|
+
|
|
45
|
+
/** Content type for the key directory response. */
|
|
46
|
+
export const DIRECTORY_MEDIA_TYPE = MediaType.HTTP_MESSAGE_SIGNATURES_DIRECTORY
|
|
47
|
+
|
|
48
|
+
const DEFAULT_SIGNATURE_AGENT = "https://sosumi.ai"
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* How long an outbound request signature stays valid.
|
|
52
|
+
* Short, to limit replay.
|
|
53
|
+
*/
|
|
54
|
+
const OUTBOUND_SIGNATURE_TTL_MS = 5 * 60 * 1000
|
|
55
|
+
|
|
56
|
+
/** How long the directory self-signature stays valid. */
|
|
57
|
+
const DIRECTORY_SIGNATURE_TTL_MS = 60 * 60 * 1000
|
|
58
|
+
|
|
59
|
+
/** A public JSON Web Key as published in the directory (RFC 8037 Ed25519). */
|
|
60
|
+
export interface DirectoryKey {
|
|
61
|
+
kty?: string
|
|
62
|
+
crv?: string
|
|
63
|
+
x?: string
|
|
64
|
+
kid: string
|
|
65
|
+
use: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface WebBotAuthConfig {
|
|
69
|
+
signer: Signer
|
|
70
|
+
publicKey: DirectoryKey
|
|
71
|
+
agent: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Cached, parsed configuration.
|
|
76
|
+
* The signing key is application-global (the same for every request),
|
|
77
|
+
* so it is memoized across requests in the isolate
|
|
78
|
+
* and only rebuilt when the underlying secret or agent changes.
|
|
79
|
+
*/
|
|
80
|
+
let cache: { key: string; agent: string; config: Promise<WebBotAuthConfig> } | null = null
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Prime the signing configuration from the environment.
|
|
84
|
+
* Call this once per request,
|
|
85
|
+
* before any outbound fetch or directory response is produced.
|
|
86
|
+
*/
|
|
87
|
+
export function configureWebBotAuth(env: WebBotAuthEnv): void {
|
|
88
|
+
const key = env.WEB_BOT_AUTH_KEY?.trim()
|
|
89
|
+
if (!key) {
|
|
90
|
+
cache = null
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const agent = env.SIGNATURE_AGENT?.trim() || DEFAULT_SIGNATURE_AGENT
|
|
95
|
+
if (cache && cache.key === key && cache.agent === agent) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
cache = { key, agent, config: buildConfig(key, agent) }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface Ed25519PrivateJwk {
|
|
103
|
+
kty: "OKP"
|
|
104
|
+
crv: "Ed25519"
|
|
105
|
+
x: string
|
|
106
|
+
d: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse and validate the signing secret as an Ed25519 OKP private JWK.
|
|
111
|
+
* Fails closed on anything malformed
|
|
112
|
+
* so a misconfigured `WEB_BOT_AUTH_KEY` never yields a broken directory key
|
|
113
|
+
* or unverifiable signatures.
|
|
114
|
+
*/
|
|
115
|
+
function parseSigningJwk(key: string): Ed25519PrivateJwk {
|
|
116
|
+
let jwk: JsonWebKey
|
|
117
|
+
try {
|
|
118
|
+
jwk = JSON.parse(key) as JsonWebKey
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(`WEB_BOT_AUTH_KEY is not valid JSON: ${(error as Error).message}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
jwk.kty !== "OKP" ||
|
|
125
|
+
jwk.crv !== "Ed25519" ||
|
|
126
|
+
typeof jwk.x !== "string" ||
|
|
127
|
+
jwk.x.length === 0 ||
|
|
128
|
+
typeof jwk.d !== "string" ||
|
|
129
|
+
jwk.d.length === 0
|
|
130
|
+
) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"WEB_BOT_AUTH_KEY must be an Ed25519 private JSON Web Key (kty=OKP, crv=Ed25519, with x and d).",
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { kty: jwk.kty, crv: jwk.crv, x: jwk.x, d: jwk.d }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function buildConfig(key: string, agent: string): Promise<WebBotAuthConfig> {
|
|
140
|
+
const jwk = parseSigningJwk(key)
|
|
141
|
+
const signer = await signerFromJWK(jwk)
|
|
142
|
+
const kid = await jwkToKeyID(jwk, helpers.WEBCRYPTO_SHA256, helpers.BASE64URL_DECODE)
|
|
143
|
+
|
|
144
|
+
// Publish only the public half of the key (never the `d` parameter).
|
|
145
|
+
const publicKey: DirectoryKey = {
|
|
146
|
+
kty: jwk.kty,
|
|
147
|
+
crv: jwk.crv,
|
|
148
|
+
x: jwk.x,
|
|
149
|
+
kid,
|
|
150
|
+
use: "sig",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { signer, publicKey, agent }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function currentConfig(): Promise<WebBotAuthConfig | null> {
|
|
157
|
+
const entry = cache
|
|
158
|
+
if (!entry) {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
return await entry.config
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error("web-bot-auth: failed to load signing key", error)
|
|
166
|
+
// Drop the failed config so the next request rebuilds it,
|
|
167
|
+
// rather than serving the cached rejection until the isolate restarts.
|
|
168
|
+
// Guard against clobbering a newer config set by a concurrent reconfigure.
|
|
169
|
+
if (cache === entry) {
|
|
170
|
+
cache = null
|
|
171
|
+
}
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Serialize a value as an RFC 8941 structured-field string (a quoted string). */
|
|
177
|
+
function structuredString(value: string): string {
|
|
178
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface DirectoryResponse {
|
|
182
|
+
body: { keys: DirectoryKey[] }
|
|
183
|
+
headers: Record<string, string>
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build the signed key directory response,
|
|
188
|
+
* or `null` when no key is configured.
|
|
189
|
+
*
|
|
190
|
+
* The response body is a JWKS
|
|
191
|
+
* and carries `Signature`/`Signature-Input` headers
|
|
192
|
+
* signing over `@authority` with `tag="http-message-signatures-directory"`,
|
|
193
|
+
* demonstrating control of the published key.
|
|
194
|
+
*/
|
|
195
|
+
export async function webBotAuthDirectory(requestUrl: string): Promise<DirectoryResponse | null> {
|
|
196
|
+
const config = await currentConfig()
|
|
197
|
+
if (!config) {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const created = new Date()
|
|
202
|
+
const expires = new Date(created.getTime() + DIRECTORY_SIGNATURE_TTL_MS)
|
|
203
|
+
const message = {
|
|
204
|
+
response: { status: 200, headers: {} as Record<string, string> },
|
|
205
|
+
request: { method: "GET", url: requestUrl, headers: {} as Record<string, string> },
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const signature = await directoryResponseHeaders(message, [config.signer], { created, expires })
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
body: { keys: [config.publicKey] },
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": DIRECTORY_MEDIA_TYPE,
|
|
214
|
+
Signature: signature.Signature,
|
|
215
|
+
"Signature-Input": signature["Signature-Input"],
|
|
216
|
+
"Cache-Control": "public, max-age=300, s-maxage=300",
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Produce Web Bot Auth headers for an outbound request,
|
|
223
|
+
* or `{}` when no key is configured (e.g. local development or the CLI).
|
|
224
|
+
* Signing failures degrade to an unsigned request
|
|
225
|
+
* rather than breaking the fetch.
|
|
226
|
+
*/
|
|
227
|
+
export async function webBotAuthHeaders(
|
|
228
|
+
method: string,
|
|
229
|
+
url: string | URL,
|
|
230
|
+
): Promise<Record<string, string>> {
|
|
231
|
+
const config = await currentConfig()
|
|
232
|
+
if (!config) {
|
|
233
|
+
return {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const agent = structuredString(config.agent)
|
|
238
|
+
// `signature-agent` must be present on the message
|
|
239
|
+
// so it is covered by the signature;
|
|
240
|
+
// the same value is returned for the outbound request.
|
|
241
|
+
const headers = new Headers()
|
|
242
|
+
headers.set("Signature-Agent", agent)
|
|
243
|
+
|
|
244
|
+
const created = new Date()
|
|
245
|
+
const expires = new Date(created.getTime() + OUTBOUND_SIGNATURE_TTL_MS)
|
|
246
|
+
const signature = await signatureHeaders(
|
|
247
|
+
{ method, url: url.toString(), headers },
|
|
248
|
+
config.signer,
|
|
249
|
+
{ created, expires },
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
"Signature-Agent": agent,
|
|
254
|
+
"Signature-Input": signature["Signature-Input"],
|
|
255
|
+
Signature: signature.Signature,
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error("web-bot-auth: failed to sign outbound request", error)
|
|
259
|
+
return {}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { zodToJsonSchema } from "zod-to-json-schema"
|
|
3
|
+
|
|
4
|
+
import { TOOL_DEFINITIONS } from "./mcp"
|
|
5
|
+
|
|
6
|
+
export interface WebMcpToolManifest {
|
|
7
|
+
name: string
|
|
8
|
+
title: string
|
|
9
|
+
description: string
|
|
10
|
+
inputSchema: Record<string, unknown>
|
|
11
|
+
readOnly: boolean
|
|
12
|
+
http: {
|
|
13
|
+
path?: string
|
|
14
|
+
pathFrom?: string
|
|
15
|
+
pathPrefix?: string
|
|
16
|
+
query?: Record<string, string>
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildWebMcpManifest(): WebMcpToolManifest[] {
|
|
21
|
+
return Object.values(TOOL_DEFINITIONS).map((def) => {
|
|
22
|
+
const jsonSchema = zodToJsonSchema(z.object(def.inputSchema), {
|
|
23
|
+
$refStrategy: "none",
|
|
24
|
+
})
|
|
25
|
+
const { $schema: _, ...inputSchema } = jsonSchema as Record<string, unknown> & {
|
|
26
|
+
$schema?: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: def.name,
|
|
31
|
+
title: def.title,
|
|
32
|
+
description: def.description,
|
|
33
|
+
inputSchema,
|
|
34
|
+
readOnly: def.annotations.readOnlyHint,
|
|
35
|
+
http: def.http,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
package/wrangler.jsonc
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"assets": {
|
|
18
18
|
"binding": "ASSETS",
|
|
19
19
|
"directory": "./public",
|
|
20
|
-
"run_worker_first": ["/", "/.well-known/agent-skills/*"]
|
|
20
|
+
"run_worker_first": ["/", "/.well-known/agent-card.json", "/.well-known/agent-skills/*"]
|
|
21
21
|
},
|
|
22
22
|
"observability": {
|
|
23
23
|
"enabled": true
|