@nshipster/sosumi 1.0.6 → 1.0.8

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/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